-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest-multi-browser.mjs
More file actions
156 lines (137 loc) · 7.74 KB
/
Copy pathtest-multi-browser.mjs
File metadata and controls
156 lines (137 loc) · 7.74 KB
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
// Standalone smoke for Step 1-3 multi-browser routing.
// Bypasses Chrome (extension SW activation is fiddly in fresh user-data-dir).
// Spins up 2 fake "extensions" as raw WS clients, exercises every new code path:
// - hello with browserInstanceId + browserDisplayName captured into record
// - listClients exposes the new fields
// - pickClient resolves by display name AND by instance id
// - select_browser sets active, fallback chain uses it
// - list/select/switch tools work
import WebSocket from "ws";
const BRIDGE_HTTP = "http://127.0.0.1:17347";
const BRIDGE_WS = "ws://127.0.0.1:17346/extension";
function fakeExt(instanceId, displayName) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(BRIDGE_WS);
ws.on("open", () => {
ws.send(JSON.stringify({
type: "hello",
name: "personal-chrome",
userAgent: `Mozilla/5.0 (FakeTest ${displayName})`,
extensionVersion: "0.0.1-test",
browserInstanceId: instanceId,
browserDisplayName: displayName,
}));
ws.on("message", (raw) => {
const m = JSON.parse(raw.toString());
if (m.type !== "command") return;
// Real-extension-shaped responses for the new tools so we exercise the
// full HTTP→bridge→ws→ext-handler→reply→HTTP round-trip.
let result;
if (m.command === "chrome_read_page") {
result = {
ok: true,
pageContent: 'heading "Welcome" [ref_1]\n button "Sign in" [ref_2]\n',
url: "https://fake.example/",
title: "Fake page from " + displayName,
viewport: { width: 1366, height: 768 },
refCount: 2,
elementsScanned: 2,
truncated: false,
};
} else if (m.command === "chrome_click_ref") {
result = m.params?.ref === "ref_2"
? { ok: true, ref: "ref_2", tag: "button" }
: { ok: false, error: "unknown_ref" };
} else {
result = { echoedBy: displayName, command: m.command };
}
ws.send(JSON.stringify({ type: "result", id: m.id, ok: true, result }));
});
resolve({ ws, instanceId, displayName });
});
ws.on("error", reject);
});
}
async function tool(name, body = {}) {
const r = await fetch(`${BRIDGE_HTTP}/tool/${name}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const text = await r.text();
try { return { status: r.status, json: JSON.parse(text) }; } catch { return { status: r.status, text }; }
}
async function health() {
return (await fetch(`${BRIDGE_HTTP}/health`)).json();
}
function ok(cond, msg) {
console.log((cond ? "PASS " : "FAIL ") + msg);
if (!cond) process.exitCode = 1;
}
async function main() {
// Baseline: bridge may already have real-Chrome extensions / SW-restart ghosts
// connected. We measure Δ from this baseline, not absolute counts.
const baseline = await health();
const baselineClientCount = baseline.connected;
console.log(`(baseline: ${baselineClientCount} pre-existing client(s) on bridge — smoke verifies Δ from this)`);
console.log("\n=== Step 1: connect two fake extensions, verify identity captured ===");
const A = await fakeExt("uuid-AAA-bbb", "TestChrome-Victim");
const B = await fakeExt("uuid-CCC-ddd", "TestChrome-Attacker");
await new Promise((r) => setTimeout(r, 400));
const h = await health();
ok(h.connected === baselineClientCount + 2, `health.connected += 2 (baseline ${baselineClientCount} + 2 fake = ${baselineClientCount + 2}, got ${h.connected})`);
const byName = Object.fromEntries(h.clients.map((c) => [c.browserDisplayName, c]));
ok(byName["TestChrome-Victim"]?.browserInstanceId === "uuid-AAA-bbb", "Victim instanceId stored");
ok(byName["TestChrome-Attacker"]?.browserInstanceId === "uuid-CCC-ddd", "Attacker instanceId stored");
console.log("\n=== Step 2: list_browsers tool ===");
const lb = await tool("personal_chrome_list_browsers");
ok(lb.status === 200, `list_browsers HTTP 200 (got ${lb.status})`);
ok(lb.json?.browsers?.length === baselineClientCount + 2, `list returns ${baselineClientCount + 2} browsers (got ${lb.json?.browsers?.length})`);
// activeBrowser may carry over from earlier runs in same bridge process; only
// assert it's null when there's no carry-over baseline activity to inherit.
if (baselineClientCount === 0) {
ok(lb.json?.activeBrowser === null, "no active browser initially");
} else {
console.log(`(skipped 'no active browser initially' check — bridge has pre-existing state from baseline)`);
}
console.log("\n=== Step 3: select_browser by display name ===");
const sel = await tool("personal_chrome_select_browser", { browser: "TestChrome-Attacker" });
ok(sel.status === 200, "select HTTP 200");
ok(sel.json?.active?.browserInstanceId === "uuid-CCC-ddd", "active is Attacker");
const lb2 = await tool("personal_chrome_list_browsers");
ok(lb2.json?.activeBrowser === "uuid-CCC-ddd", `list.activeBrowser reflects selection (got ${lb2.json?.activeBrowser})`);
console.log("\n=== Step 4: route by display name without explicit clientId ===");
// call personal_chrome_status (real tool) — bridge will route via active hint
const s1 = await tool("personal_chrome_status");
ok(s1.status === 200 || (s1.json?.echoedBy === "TestChrome-Attacker"), `status routed to active (Attacker); raw=${JSON.stringify(s1).slice(0,200)}`);
console.log("\n=== Step 5: explicit browser= overrides active ===");
const s2 = await tool("personal_chrome_status", { browser: "TestChrome-Victim" });
ok(s2.status === 200 || (s2.json?.echoedBy === "TestChrome-Victim"), `explicit browser= routed to Victim; raw=${JSON.stringify(s2).slice(0,200)}`);
console.log("\n=== Step 6: select_browser by instance id (not name) ===");
const sel2 = await tool("personal_chrome_switch_browser", { browser: "uuid-AAA-bbb" });
ok(sel2.json?.active?.browserDisplayName === "TestChrome-Victim", "switch by instance id works");
console.log("\n=== Step 6b: read_page on active browser (Victim) ===");
const rp = await tool("personal_chrome_read_page", { maxChars: 4000 });
ok(rp.status === 200, `read_page HTTP 200 (got ${rp.status})`);
ok(typeof rp.json?.pageContent === "string" && rp.json.pageContent.includes("[ref_"), `pageContent has refs: ${JSON.stringify(rp.json).slice(0,200)}`);
ok(rp.json?.title?.includes("Victim"), "routed to Victim (currently active)");
console.log("\n=== Step 6c: click_ref using ref from read_page ===");
const cr = await tool("personal_chrome_click_ref", { ref: "ref_2" });
ok(cr.json?.ref === "ref_2" && cr.json?.tag === "button", `click_ref hit ref_2 button: ${JSON.stringify(cr.json).slice(0,200)}`);
const crBad = await tool("personal_chrome_click_ref", { ref: "ref_999" });
ok(crBad.json?.error === "unknown_ref", `unknown ref returns unknown_ref error: ${JSON.stringify(crBad.json).slice(0,200)}`);
console.log("\n=== Step 7: disconnect Victim, active hint should clear ===");
A.ws.close();
await new Promise((r) => setTimeout(r, 600));
const h2 = await health();
// Test asserts THIS smoke's fake Victim is gone — not total client count,
// because the bridge may also hold real-Chrome extension clients (and
// transient SW-restart ghost connections) we did not create here.
const victimGone = !h2.clients.some((c) => c.browserInstanceId === "uuid-AAA-bbb");
ok(victimGone, `Victim (uuid-AAA-bbb) removed from clients after disconnect`);
const lb3 = await tool("personal_chrome_list_browsers");
ok(lb3.json?.activeBrowser === null, `stale active selector auto-cleared (got ${lb3.json?.activeBrowser})`);
B.ws.close();
console.log(process.exitCode ? "\n=== FAIL ===" : "\n=== ALL PASS ===");
}
main().catch((e) => { console.error("CRASH:", e); process.exit(1); });