test/cdp/endpoint-test.ts (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
/**
* Unit tests for CDP target resolution — no browser and no network required.
*
* This is fork-specific logic (see FORK.md): upstream only ever drives a
* browser on localhost. The precedence here is deliberately asymmetric — an
* explicitly configured endpoint must never silently fall back to a different
* browser, while the built-in default must, so a machine off the wire network
* still gets the local browser the extension was originally written for.
*
* Run: npx tsx test/cdp/endpoint-test.ts
*/
import {
DEFAULT_REMOTE_CDP,
describeCdpTarget,
isValidTargetValue,
redactWsUrl,
resolveCdpTarget,
} from "../../src/cdp/endpoint";
import { getBrowserSystemPrompt } from "../../src/prompt";
let passed = 0;
let failed = 0;
const check = (cond: boolean, label: string): void => {
if (cond) {
passed++;
console.log(` ✓ ${label}`);
} else {
failed++;
console.error(` ✗ ${label}`);
}
};
const env = (value?: string): NodeJS.ProcessEnv => (value === undefined ? {} : { BU_CDP_HTTP: value });
function main(): void {
// E1: the built-in default, used when nothing is configured.
{
const t = resolveCdpTarget(env());
check(t.kind === "remote", "E1: unset BU_CDP_HTTP resolves to a remote target");
if (t.kind === "remote") {
check(t.source === "default", "E1: it is marked as the built-in default, not an instruction");
check(`${t.host}:${t.port}` === DEFAULT_REMOTE_CDP, "E1: it matches DEFAULT_REMOTE_CDP");
}
}
// E2: DEFAULT_REMOTE_CDP must itself be parseable, or the default silently
// degrades to local for everyone.
{
const t = resolveCdpTarget(env());
check(t.kind !== "local", "E2: the built-in default constant is well-formed");
}
// E3: an explicit endpoint is an instruction — source must reflect that, so
// discovery fails loudly rather than falling back.
{
const t = resolveCdpTarget(env("10.0.0.9:9333"));
check(t.kind === "remote", "E3: an explicit host:port resolves to a remote target");
if (t.kind === "remote") {
check(t.host === "10.0.0.9" && t.port === 9333, "E3: host and port are parsed");
check(t.source === "env", "E3: it is marked as explicitly configured");
}
}
// E4: opting back into upstream behaviour.
for (const alias of ["local", "LOCAL", "off", "none", "0", "no"]) {
const t = resolveCdpTarget(env(alias));
check(t.kind === "local", `E4: "${alias}" selects this machine's own browser`);
}
// E5: empty or whitespace is treated as unset rather than as an error — a
// shell exporting an empty var should not break the browser.
for (const blank of ["", " "]) {
const t = resolveCdpTarget(env(blank));
check(t.kind === "remote" && t.source === "default", `E5: ${JSON.stringify(blank)} falls back to the default`);
}
// E6: malformed values are rejected, not coerced. Silently reinterpreting
// these would drive an unexpected browser.
for (const bad of ["nonsense", "host:", ":9222", "host:0", "host:70000", "host:abc"]) {
const t = resolveCdpTarget(env(bad));
check(t.kind === "invalid", `E6: "${bad}" is rejected as malformed`);
}
// E7: IPv6 and hostnames still work — the port is taken from the LAST colon.
{
const t = resolveCdpTarget(env("castle.local:9223"));
check(t.kind === "remote" && t.host === "castle.local", "E7: hostnames are accepted");
const v6 = resolveCdpTarget(env("[::1]:9223"));
check(v6.kind === "remote" && v6.host === "[::1]" && v6.port === 9223, "E7: bracketed IPv6 keeps its host");
}
// E8: descriptions are for humans in /browser-status; they must distinguish
// a default from an instruction, since only one of them falls back.
{
const d = describeCdpTarget(resolveCdpTarget(env()));
const e = describeCdpTarget(resolveCdpTarget(env("10.0.0.9:9333")));
check(d.includes("default"), "E8: the default is labelled as such");
check(e.includes("BU_CDP_HTTP"), "E8: an explicit endpoint names the variable that set it");
check(describeCdpTarget(resolveCdpTarget(env("local"))).includes("own browser"), "E8: local is described plainly");
}
// E9: the stored choice (/browser-target) sits between env and the default.
{
const t = resolveCdpTarget({}, "local");
check(t.kind === "local" && t.source === "stored", "E9: a stored value is used when the env is unset");
const r = resolveCdpTarget({}, "10.2.3.4:9224");
check(r.kind === "remote" && r.source === "stored", "E9: a stored host:port resolves as a remote target");
}
// E10: the environment wins, so a one-off `BU_CDP_HTTP=… pi` is never
// silently overridden by a setting made days ago.
{
const t = resolveCdpTarget(env("local"), "10.2.3.4:9224");
check(t.kind === "local" && t.source === "env", "E10: env beats the stored value");
const r = resolveCdpTarget(env("10.9.9.9:1234"), "local");
check(r.kind === "remote" && r.host === "10.9.9.9" && r.source === "env", "E10: an env endpoint beats stored local");
}
// E11: clearing the stored value falls back to the built-in default rather
// than to local — clearing is "use the usual browser", not "go local".
{
const t = resolveCdpTarget({}, null);
check(t.kind === "remote" && t.source === "default", "E11: no stored value falls back to the default");
const blank = resolveCdpTarget({}, " ");
check(blank.kind === "remote" && blank.source === "default", "E11: a blank stored value is treated as cleared");
}
// E12: a corrupt stored value must be reported against the right source, or
// the operator goes hunting through their shell config for an env var that
// was never set.
{
const t = resolveCdpTarget({}, "garbage");
check(t.kind === "invalid" && t.source === "stored", "E12: a bad stored value is blamed on /browser-target");
if (t.kind === "invalid") {
check(describeCdpTarget(t).includes("/browser-target"), "E12: the description names the stored source");
}
const e = resolveCdpTarget(env("garbage"), null);
check(e.kind === "invalid" && e.source === "env", "E12: a bad env value is blamed on BU_CDP_HTTP");
}
// E13: the values /browser-target accepts must match what resolution can
// actually use, or the command would happily store something unusable.
{
for (const good of ["local", "off", "1.2.3.4:9222", "castle.local:9223"]) {
check(isValidTargetValue(good), `E13: "${good}" is accepted by the command`);
}
for (const bad of ["", " ", "garbage", "host:", "host:99999"]) {
check(!isValidTargetValue(bad), `E13: "${bad}" is rejected by the command`);
}
check(!isValidTargetValue("default"), "E13: \"default\" is handled as clear, not stored verbatim");
}
// E14: upstream's BU_CDP_WS is read by discoverEndpoint() before anything the
// fork resolves, so it must outrank BU_CDP_HTTP and the stored choice here
// too — otherwise status, /browser-target and the prompt name a browser the
// daemon is not driving.
{
const pinned = "ws://10.0.0.7:9229/devtools/browser/0b5c-uuid";
const t = resolveCdpTarget({ BU_CDP_WS: pinned, BU_CDP_HTTP: "10.0.0.9:9333" }, "10.0.0.8:9444");
check(t.kind === "remote", "E14: BU_CDP_WS resolves to a remote target");
if (t.kind === "remote") {
check(t.host === "10.0.0.7" && t.port === 9229, "E14: it wins over BU_CDP_HTTP and the stored choice");
check(t.source === "ws", "E14: it is marked as the pinned WebSocket");
}
const blank = resolveCdpTarget({ BU_CDP_WS: " ", BU_CDP_HTTP: "10.0.0.9:9333" });
check(blank.kind === "remote" && blank.source === "env", "E14: a blank BU_CDP_WS is ignored, as upstream does");
}
// E15: only host:port is ever shown. The path carries the browser id and a
// proxied endpoint may carry a token or credentials.
{
const secret = "wss://user:hunter2@cdp.example.com/devtools/browser/abc?token=s3cr3t";
const t = resolveCdpTarget({ BU_CDP_WS: secret });
const shown = describeCdpTarget(t);
check(t.kind === "remote" && t.port === 443, "E15: wss without a port means 443");
check(shown.startsWith("cdp.example.com:443 (BU_CDP_WS"), `E15: described as host:port — got ${shown}`);
check(!/hunter2|user|abc|token|s3cr3t|devtools/.test(shown), "E15: no path, query or credentials leak");
check(redactWsUrl(secret) === "cdp.example.com:443", "E15: redactWsUrl keeps host:port only");
check(redactWsUrl("ws://[::1]:9222/x") === "::1:9222", "E15: IPv6 hosts lose their brackets like elsewhere");
}
// E16: an unusable BU_CDP_WS is reported, still without echoing it.
for (const bad of ["not a url", "http://10.0.0.7:9229/devtools/browser/x"]) {
const t = resolveCdpTarget({ BU_CDP_WS: bad });
check(t.kind === "invalid" && t.source === "ws", `E16: "${bad}" is reported as an invalid BU_CDP_WS`);
check(!describeCdpTarget(t).includes("devtools"), "E16: the invalid value is not echoed");
}
// E17: the system prompt names the pinned endpoint, redacted.
{
const t = resolveCdpTarget({ BU_CDP_WS: "ws://10.0.0.7:9229/devtools/browser/0b5c-uuid" });
const prompt = getBrowserSystemPrompt(true, t);
check(prompt.includes("10.0.0.7:9229"), "E17: the prompt names the pinned host:port");
check(!prompt.includes("0b5c-uuid"), "E17: the prompt does not carry the browser id");
}
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
}
main();
|