@mochi.js/core 0.3.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Unit tests for the init-injector building blocks.
3
+ *
4
+ * Covers:
5
+ * - {@link rewriteCsp}: no-nonce / nonce / strict-dynamic / unsafe-inline
6
+ * idempotence / multiple directives.
7
+ * - {@link rewriteHeaders}: header-name case insensitive, content-length
8
+ * stripped, CSP and CSP-Report-Only both rewritten.
9
+ * - {@link rewriteMetaCsp}: HTML meta-tag rewriting + entity round-trip.
10
+ * - {@link injectIntoHead}: script splice ahead of first non-comment
11
+ * `<script>`; script-tag attributes (no defer/async/module).
12
+ * - {@link wrapSelfRemovingPayload}: first statement is the self-remove +
13
+ * marker; the inner payload is left intact.
14
+ *
15
+ */
16
+
17
+ import { describe, expect, it } from "bun:test";
18
+ import {
19
+ injectIntoHead,
20
+ MOCHI_INIT_MARKER,
21
+ MOCHI_INIT_SCRIPT_CLASS,
22
+ rewriteCsp,
23
+ rewriteHeaders,
24
+ rewriteMetaCsp,
25
+ wrapSelfRemovingPayload,
26
+ } from "../cdp/init-injector";
27
+
28
+ describe("rewriteCsp", () => {
29
+ it("no nonce, no unsafe-inline → adds 'unsafe-inline' to script-src", () => {
30
+ const out = rewriteCsp("script-src 'self' https://cdn.example.com");
31
+ expect(out.value).toContain("script-src 'self' https://cdn.example.com 'unsafe-inline'");
32
+ expect(out.nonce).toBeUndefined();
33
+ });
34
+
35
+ it("with-nonce → leaves directive intact and returns nonce string", () => {
36
+ const out = rewriteCsp("script-src 'self' 'nonce-abc123XYZ'");
37
+ expect(out.value).toBe("script-src 'self' 'nonce-abc123XYZ'");
38
+ expect(out.nonce).toBe("abc123XYZ");
39
+ });
40
+
41
+ it("strict-dynamic + nonce → leaves directive intact and surfaces nonce", () => {
42
+ const out = rewriteCsp("script-src 'strict-dynamic' 'nonce-XYZ' 'unsafe-eval'");
43
+ expect(out.value).toBe("script-src 'strict-dynamic' 'nonce-XYZ' 'unsafe-eval'");
44
+ expect(out.nonce).toBe("XYZ");
45
+ });
46
+
47
+ it("strict-dynamic without nonce → falls through to unsafe-inline (best-effort)", () => {
48
+ const out = rewriteCsp("script-src 'strict-dynamic'");
49
+ expect(out.value).toContain("'unsafe-inline'");
50
+ expect(out.nonce).toBeUndefined();
51
+ });
52
+
53
+ it("already has 'unsafe-inline' → idempotent (does not double-add)", () => {
54
+ const out = rewriteCsp("script-src 'self' 'unsafe-inline'");
55
+ expect(out.value).toBe("script-src 'self' 'unsafe-inline'");
56
+ expect(out.value.match(/'unsafe-inline'/g)?.length).toBe(1);
57
+ });
58
+
59
+ it("multiple directives → only script-src/script-src-elem/default-src mutated", () => {
60
+ const out = rewriteCsp(
61
+ "default-src 'self'; img-src https:; script-src 'self'; style-src 'self'",
62
+ );
63
+ expect(out.value).toContain("script-src 'self' 'unsafe-inline'");
64
+ expect(out.value).toContain("default-src 'self' 'unsafe-inline'");
65
+ expect(out.value).toContain("img-src https:");
66
+ expect(out.value).toContain("style-src 'self'");
67
+ });
68
+
69
+ it("script-src-elem also gets relaxed", () => {
70
+ const out = rewriteCsp("script-src-elem 'self'");
71
+ expect(out.value).toContain("script-src-elem 'self' 'unsafe-inline'");
72
+ });
73
+ });
74
+
75
+ describe("rewriteHeaders", () => {
76
+ it("rewrites Content-Security-Policy and surfaces nonce", () => {
77
+ const out = rewriteHeaders([
78
+ { name: "Content-Security-Policy", value: "script-src 'nonce-NN'" },
79
+ { name: "X-Frame-Options", value: "DENY" },
80
+ ]);
81
+ expect(out.scriptNonce).toBe("NN");
82
+ const csp = out.headers.find((h) => h.name === "Content-Security-Policy");
83
+ expect(csp?.value).toBe("script-src 'nonce-NN'");
84
+ expect(out.headers.find((h) => h.name === "X-Frame-Options")?.value).toBe("DENY");
85
+ });
86
+
87
+ it("rewrites Content-Security-Policy-Report-Only too", () => {
88
+ const out = rewriteHeaders([
89
+ { name: "content-security-policy-report-only", value: "script-src 'self'" },
90
+ ]);
91
+ const csp = out.headers.find(
92
+ (h) => h.name.toLowerCase() === "content-security-policy-report-only",
93
+ );
94
+ expect(csp?.value).toContain("'unsafe-inline'");
95
+ });
96
+
97
+ it("strips Content-Length so fulfillRequest recomputes", () => {
98
+ const out = rewriteHeaders([
99
+ { name: "Content-Length", value: "1234" },
100
+ { name: "Content-Type", value: "text/html" },
101
+ ]);
102
+ expect(out.headers.some((h) => h.name.toLowerCase() === "content-length")).toBe(false);
103
+ expect(out.headers.some((h) => h.name === "Content-Type")).toBe(true);
104
+ });
105
+
106
+ it("adopts the first nonce when multiple CSPs are present", () => {
107
+ const out = rewriteHeaders([
108
+ { name: "Content-Security-Policy", value: "script-src 'nonce-aaa'" },
109
+ { name: "Content-Security-Policy", value: "script-src 'nonce-bbb'" },
110
+ ]);
111
+ expect(out.scriptNonce).toBe("aaa");
112
+ });
113
+ });
114
+
115
+ describe("rewriteMetaCsp", () => {
116
+ it("rewrites a meta tag's CSP content attribute (encoded on the wire)", () => {
117
+ const html = `<head><meta http-equiv="Content-Security-Policy" content="script-src 'self'"></head>`;
118
+ const out = rewriteMetaCsp(html);
119
+ // Apostrophes in attribute values round-trip through entity encoding;
120
+ // assert the encoded form so we capture what Chromium will actually
121
+ // parse back into the document.
122
+ expect(out.html).toContain("&#39;unsafe-inline&#39;");
123
+ });
124
+
125
+ it("preserves other attribute order and unrelated meta tags", () => {
126
+ const html = `<head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="script-src 'self'"><meta name="viewport" content="width=device-width"></head>`;
127
+ const out = rewriteMetaCsp(html);
128
+ expect(out.html).toContain('<meta charset="utf-8">');
129
+ expect(out.html).toContain('<meta name="viewport"');
130
+ expect(out.html).toContain("&#39;unsafe-inline&#39;");
131
+ });
132
+
133
+ it("extracts nonce from a meta-tag CSP", () => {
134
+ const html = `<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-MMM'">`;
135
+ const out = rewriteMetaCsp(html);
136
+ expect(out.firstNonce).toBe("MMM");
137
+ });
138
+
139
+ it("ignores meta tags that are NOT CSP", () => {
140
+ const html = `<meta http-equiv="X-UA-Compatible" content="IE=edge">`;
141
+ const out = rewriteMetaCsp(html);
142
+ expect(out.html).toBe(html);
143
+ expect(out.firstNonce).toBeUndefined();
144
+ });
145
+
146
+ it("handles single-quoted attribute values too", () => {
147
+ const html = `<meta http-equiv='Content-Security-Policy' content='script-src \\'self\\''>`;
148
+ // Bun's regex is fine with the structure even though we don't decode \'
149
+ // here — the input shape is contrived; the production path always sees
150
+ // properly-escaped HTML from Chromium.
151
+ const out = rewriteMetaCsp(html);
152
+ expect(out.html).toContain("Content-Security-Policy");
153
+ });
154
+ });
155
+
156
+ describe("injectIntoHead", () => {
157
+ const SCRIPT = "console.log(1)";
158
+
159
+ it("inserts BEFORE the first non-comment <script> in head", () => {
160
+ const html = `<!doctype html><html><head><meta charset="utf-8"><script>window.first=true</script></head><body></body></html>`;
161
+ const out = injectIntoHead(html, SCRIPT, undefined);
162
+ const idxOurs = out.indexOf(`class="${MOCHI_INIT_SCRIPT_CLASS}"`);
163
+ const idxFirst = out.indexOf("window.first=true");
164
+ expect(idxOurs).toBeGreaterThan(-1);
165
+ expect(idxFirst).toBeGreaterThan(-1);
166
+ expect(idxOurs).toBeLessThan(idxFirst);
167
+ });
168
+
169
+ it("ignores HTML comments — does not splice before commented-out <script>", () => {
170
+ const html = `<head><!-- <script>window.fake=1</script> --><script>window.real=1</script></head>`;
171
+ const out = injectIntoHead(html, SCRIPT, undefined);
172
+ const idxOurs = out.indexOf(`class="${MOCHI_INIT_SCRIPT_CLASS}"`);
173
+ const idxReal = out.indexOf("window.real=1");
174
+ const idxFake = out.indexOf("window.fake=1");
175
+ expect(idxOurs).toBeLessThan(idxReal);
176
+ // Our script must also be after the comment block — splicing in the
177
+ // middle of a comment would be wrong.
178
+ expect(idxOurs).toBeGreaterThan(idxFake);
179
+ });
180
+
181
+ it("inserts at end-of-head when no <script> exists in head", () => {
182
+ const html = `<head><meta charset="utf-8"></head><body><script>window.bodyScript=1</script></body>`;
183
+ const out = injectIntoHead(html, SCRIPT, undefined);
184
+ const idxOurs = out.indexOf(`class="${MOCHI_INIT_SCRIPT_CLASS}"`);
185
+ const idxBody = out.indexOf("window.bodyScript=1");
186
+ expect(idxOurs).toBeGreaterThan(-1);
187
+ expect(idxOurs).toBeLessThan(idxBody);
188
+ // Script lands inside the head — i.e. before </head>.
189
+ const idxClose = out.indexOf("</head>");
190
+ expect(idxOurs).toBeLessThan(idxClose);
191
+ });
192
+
193
+ it("creates a <head> when missing", () => {
194
+ const html = `<html><body><h1>hi</h1></body></html>`;
195
+ const out = injectIntoHead(html, SCRIPT, undefined);
196
+ expect(out).toContain("<head>");
197
+ expect(out).toContain(`class="${MOCHI_INIT_SCRIPT_CLASS}"`);
198
+ });
199
+
200
+ it("does NOT add defer / async / type=module attributes (timing-critical)", () => {
201
+ const html = `<head></head>`;
202
+ const out = injectIntoHead(html, SCRIPT, undefined);
203
+ // The injected tag for timing-critical inject MUST be a parser-blocking
204
+ // classic script. The patchright finding hinges on this.
205
+ const tag = out.match(/<script[^>]*class="__mochi_init_script__"[^>]*>/);
206
+ expect(tag).not.toBeNull();
207
+ const tagSrc = tag?.[0] ?? "";
208
+ expect(tagSrc).not.toMatch(/\bdefer\b/);
209
+ expect(tagSrc).not.toMatch(/\basync\b/);
210
+ expect(tagSrc).not.toMatch(/type\s*=\s*"module"/);
211
+ expect(tagSrc).not.toMatch(/type\s*=\s*'module'/);
212
+ });
213
+
214
+ it("attaches nonce attribute when supplied", () => {
215
+ const html = `<head></head>`;
216
+ const out = injectIntoHead(html, SCRIPT, "abc123");
217
+ expect(out).toMatch(/<script[^>]+nonce="abc123"/);
218
+ });
219
+ });
220
+
221
+ describe("wrapSelfRemovingPayload", () => {
222
+ it("first statement removes document.currentScript", () => {
223
+ const wrapped = wrapSelfRemovingPayload("/* payload */");
224
+ // Self-remove must come BEFORE the marker assignment AND before the
225
+ // payload — otherwise a script that throws synchronously could leave a
226
+ // detectable orphan node in the DOM.
227
+ const idxSelfRemove = wrapped.indexOf("document.currentScript");
228
+ const idxMarker = wrapped.indexOf(MOCHI_INIT_MARKER);
229
+ const idxPayload = wrapped.indexOf("/* payload */");
230
+ expect(idxSelfRemove).toBeGreaterThan(-1);
231
+ expect(idxMarker).toBeGreaterThan(-1);
232
+ expect(idxPayload).toBeGreaterThan(-1);
233
+ expect(idxSelfRemove).toBeLessThan(idxMarker);
234
+ expect(idxMarker).toBeLessThan(idxPayload);
235
+ });
236
+
237
+ it("contains the post-load DOM walk (belt-and-suspenders)", () => {
238
+ const wrapped = wrapSelfRemovingPayload("0");
239
+ expect(wrapped).toContain(MOCHI_INIT_SCRIPT_CLASS);
240
+ expect(wrapped).toMatch(/load|complete/);
241
+ });
242
+
243
+ it("preserves the original payload bytes intact", () => {
244
+ const orig = "(function(){window.x=42;})();";
245
+ const wrapped = wrapSelfRemovingPayload(orig);
246
+ expect(wrapped).toContain(orig);
247
+ });
248
+ });
@@ -5,132 +5,23 @@
5
5
  * browser reports its bare, un-spoofed fingerprint.
6
6
  *
7
7
  * No real Chromium process is spawned; we drive `Session` against a fake
8
- * `ChromiumProcess` whose pipe reader/writer let us observe every CDP
9
- * request sent and inject canned responses. The §8.2 forbidden-method
10
- * assertions still gate every send through `MessageRouter`, so the test
11
- * implicitly enforces those too.
8
+ * `ChromiumProcess` via the shared `tests/helpers/cdp-fixture.ts` helper.
9
+ * The §8.2 forbidden-method assertions still gate every send through
10
+ * `MessageRouter`, so the test implicitly enforces those too.
12
11
  *
13
12
  * @see PLAN.md §12.1 — capture must run against bare Chromium.
14
- * @see tasks/0040-mochi-capture.md`bypassInject: true` requirement.
13
+ * @see tests/helpers/cdp-fixture.tsshared helper consolidating fake-pipe boilerplate.
15
14
  */
16
15
 
17
16
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
18
17
  import { deriveMatrix, type ProfileV1 } from "@mochi.js/consistency";
19
- import type { PipeReader, PipeWriter } from "../cdp/transport";
20
- import type { ChromiumProcess } from "../proc";
18
+ import {
19
+ type FakePipe,
20
+ fakeChromiumProcess,
21
+ makeFakePipe,
22
+ } from "../../../../tests/helpers/cdp-fixture";
21
23
  import { Session } from "../session";
22
24
 
23
- interface FakeBrowser {
24
- process: ChromiumProcess;
25
- /** All CDP requests written to the pipe, decoded as JSON-RPC objects. */
26
- written: Array<{ id?: number; method?: string; params?: unknown; sessionId?: string }>;
27
- /** Inject one inbound JSON frame (CDP response or event). */
28
- push(obj: unknown): void;
29
- /** Auto-respond to any request matching `methodPredicate`. Returns the unsubscribe. */
30
- autoRespond(methodPredicate: (m: string) => boolean, result: unknown): void;
31
- /** Resolve when the next `n` writes have completed. */
32
- waitForWrites(n: number): Promise<void>;
33
- }
34
-
35
- function makeFakeBrowser(): FakeBrowser {
36
- const written: FakeBrowser["written"] = [];
37
- let pumpController: ReadableStreamDefaultController<Uint8Array> | null = null;
38
- const stream = new ReadableStream<Uint8Array>({
39
- start(c) {
40
- pumpController = c;
41
- },
42
- });
43
- const enc = new TextEncoder();
44
- const dec = new TextDecoder();
45
- const autoResponders: Array<{ pred: (m: string) => boolean; result: unknown }> = [];
46
- const writeListeners: Array<() => void> = [];
47
-
48
- const reader: PipeReader = {
49
- getReader: () => stream.getReader(),
50
- };
51
-
52
- const push = (obj: unknown): void => {
53
- const bytes = enc.encode(JSON.stringify(obj));
54
- const out = new Uint8Array(bytes.length + 1);
55
- out.set(bytes, 0);
56
- out[bytes.length] = 0;
57
- pumpController?.enqueue(out);
58
- };
59
-
60
- const writer: PipeWriter = {
61
- write: (chunk) => {
62
- const last = chunk[chunk.length - 1] === 0 ? chunk.length - 1 : chunk.length;
63
- const json = dec.decode(chunk.subarray(0, last));
64
- try {
65
- const parsed = JSON.parse(json) as {
66
- id?: number;
67
- method?: string;
68
- params?: unknown;
69
- sessionId?: string;
70
- };
71
- written.push(parsed);
72
- // Notify listeners.
73
- const ls = writeListeners.splice(0, writeListeners.length);
74
- for (const fn of ls) fn();
75
- // Auto-respond if matched.
76
- if (typeof parsed.method === "string" && typeof parsed.id === "number") {
77
- const r = autoResponders.find((a) => a.pred(parsed.method ?? ""));
78
- if (r) {
79
- queueMicrotask(() => push({ id: parsed.id, result: r.result }));
80
- }
81
- }
82
- } catch {
83
- // ignore malformed
84
- }
85
- },
86
- flush: () => undefined,
87
- end: () => undefined,
88
- };
89
-
90
- let resolveExit: ((code: number) => void) | undefined;
91
- const exited = new Promise<number>((res) => {
92
- resolveExit = res;
93
- });
94
-
95
- let killed = false;
96
- const proc: ChromiumProcess = {
97
- userDataDir: "/tmp/fake-mochi-test",
98
- pid: 0,
99
- exited,
100
- reader,
101
- writer,
102
- close: async () => {
103
- if (killed) return;
104
- killed = true;
105
- try {
106
- pumpController?.close();
107
- } catch {
108
- // ignore
109
- }
110
- resolveExit?.(0);
111
- },
112
- };
113
-
114
- return {
115
- process: proc,
116
- written,
117
- push,
118
- autoRespond(pred, result) {
119
- autoResponders.push({ pred, result });
120
- },
121
- waitForWrites(n) {
122
- if (written.length >= n) return Promise.resolve();
123
- return new Promise<void>((resolve) => {
124
- const check = (): void => {
125
- if (written.length >= n) resolve();
126
- else writeListeners.push(check);
127
- };
128
- writeListeners.push(check);
129
- });
130
- },
131
- };
132
- }
133
-
134
25
  const TEST_PROFILE: ProfileV1 = {
135
26
  id: "bypass-inject-fixture",
136
27
  version: "0.0.0-test",
@@ -168,11 +59,18 @@ const TEST_PROFILE: ProfileV1 = {
168
59
  };
169
60
 
170
61
  describe("Session.bypassInject (PLAN.md §12.1, task 0040)", () => {
171
- let fake: FakeBrowser;
62
+ let pipe: FakePipe;
172
63
  let session: Session | undefined;
173
64
 
174
65
  beforeEach(() => {
175
- fake = makeFakeBrowser();
66
+ pipe = makeFakePipe({
67
+ responders: {
68
+ // Tests below assert on identifier shape — keep these stable.
69
+ "Target.createTarget": () => ({ targetId: "page-target-1" }),
70
+ "Target.attachToTarget": () => ({ sessionId: "session-1" }),
71
+ "Page.addScriptToEvaluateOnNewDocument": () => ({ identifier: "should-never-fire" }),
72
+ },
73
+ });
176
74
  session = undefined;
177
75
  });
178
76
 
@@ -186,47 +84,42 @@ describe("Session.bypassInject (PLAN.md §12.1, task 0040)", () => {
186
84
  }
187
85
  });
188
86
 
189
- it("with bypassInject:true — newPage() never sends Page.addScriptToEvaluateOnNewDocument", async () => {
87
+ it("with bypassInject:true — newPage() never sends Page.addScriptToEvaluateOnNewDocument and no Fetch.enable for inject", async () => {
190
88
  const matrix = deriveMatrix(TEST_PROFILE, "bypass-test");
191
89
  session = new Session({
192
- proc: fake.process,
90
+ proc: fakeChromiumProcess(pipe, { userDataDir: "/tmp/fake-mochi-test" }),
193
91
  matrix,
194
92
  seed: "bypass-test",
195
93
  bypassInject: true,
196
94
  });
197
95
 
198
- // Auto-respond to the small set of CDP calls Session/newPage drives:
199
- // Target.setAutoAttach (constructor), Target.createTarget, Target.attachToTarget,
200
- // and Page.enable. These are the *only* writes we expect.
201
- fake.autoRespond((m) => m === "Target.setAutoAttach", {});
202
- fake.autoRespond((m) => m === "Target.createTarget", { targetId: "page-target-1" });
203
- fake.autoRespond((m) => m === "Target.attachToTarget", { sessionId: "session-1" });
204
- fake.autoRespond((m) => m === "Page.enable", {});
205
- fake.autoRespond((m) => m === "Target.closeTarget", { success: true });
206
- fake.autoRespond((m) => m === "Page.removeScriptToEvaluateOnNewDocument", {});
207
- fake.autoRespond((m) => m === "Page.addScriptToEvaluateOnNewDocument", {
208
- identifier: "should-never-fire",
209
- });
210
-
211
96
  const page = await session.newPage();
212
97
  expect(page).toBeDefined();
98
+ // Allow the constructor's deferred init-injector promise to settle (it's
99
+ // a no-op in this case but the rejection-handler microtasks still queue).
100
+ await new Promise((r) => setTimeout(r, 5));
213
101
 
214
- const methods = fake.written
215
- .map((w) => w.method)
102
+ const methods = pipe.written
103
+ .map((w) => w.parsed.method)
216
104
  .filter((m): m is string => typeof m === "string");
217
105
  expect(methods).toContain("Target.createTarget");
218
106
  expect(methods).toContain("Target.attachToTarget");
219
107
  expect(methods).toContain("Page.enable");
220
- // The contract: ZERO addScriptToEvaluateOnNewDocument sends.
108
+ // Task 0266 contract: no Page.addScriptToEvaluateOnNewDocument under
109
+ // bypassInject — the session-level injector is also short-circuited.
221
110
  expect(methods).not.toContain("Page.addScriptToEvaluateOnNewDocument");
222
- // And no Runtime.evaluate either (worker injection is also bypassed).
111
+ // No Runtime.evaluate worker injection is also bypassed.
223
112
  expect(methods).not.toContain("Runtime.evaluate");
113
+ // No proxy creds, no payload to deliver — the unified injector
114
+ // short-circuits and does NOT send Fetch.enable. Capture flow keeps a
115
+ // zero-extra-protocol-surface posture.
116
+ expect(methods).not.toContain("Fetch.enable");
224
117
  });
225
118
 
226
119
  it("with bypassInject:true — _internalPayload() is null", () => {
227
120
  const matrix = deriveMatrix(TEST_PROFILE, "null-payload");
228
121
  session = new Session({
229
- proc: fake.process,
122
+ proc: fakeChromiumProcess(pipe, { userDataDir: "/tmp/fake-mochi-test" }),
230
123
  matrix,
231
124
  seed: "null-payload",
232
125
  bypassInject: true,
@@ -235,10 +128,20 @@ describe("Session.bypassInject (PLAN.md §12.1, task 0040)", () => {
235
128
  expect(session._internalBypassInject()).toBe(true);
236
129
  });
237
130
 
238
- it("with bypassInject omitted — _internalPayload() is non-null and newPage installs the inject script", async () => {
131
+ it("with bypassInject omitted — Session installs the unified Fetch-domain injector instead of Page.addScriptToEvaluateOnNewDocument", async () => {
132
+ // Override the script identifier for this test — it asserts that the
133
+ // dual-mechanism `addScriptToEvaluateOnNewDocument` call (commit 2 of
134
+ // 0266) carries the wrapped matrix payload.
135
+ const localPipe = makeFakePipe({
136
+ responders: {
137
+ "Target.createTarget": () => ({ targetId: "page-target-2" }),
138
+ "Target.attachToTarget": () => ({ sessionId: "session-2" }),
139
+ "Page.addScriptToEvaluateOnNewDocument": () => ({ identifier: "inj-1" }),
140
+ },
141
+ });
239
142
  const matrix = deriveMatrix(TEST_PROFILE, "default-inject");
240
143
  session = new Session({
241
- proc: fake.process,
144
+ proc: fakeChromiumProcess(localPipe, { userDataDir: "/tmp/fake-mochi-test" }),
242
145
  matrix,
243
146
  seed: "default-inject",
244
147
  });
@@ -247,34 +150,46 @@ describe("Session.bypassInject (PLAN.md §12.1, task 0040)", () => {
247
150
  expect(payload).not.toBeNull();
248
151
  expect(payload?.code.length ?? 0).toBeGreaterThan(0);
249
152
 
250
- fake.autoRespond((m) => m === "Target.setAutoAttach", {});
251
- fake.autoRespond((m) => m === "Target.createTarget", { targetId: "page-target-2" });
252
- fake.autoRespond((m) => m === "Target.attachToTarget", { sessionId: "session-2" });
253
- fake.autoRespond((m) => m === "Page.enable", {});
254
- // Task 0262: Session sends Emulation.setTimezoneOverride per page.
255
- fake.autoRespond((m) => m === "Emulation.setTimezoneOverride", {});
256
- // Task 0255: Session now sends Network.setUserAgentOverride per page.
257
- fake.autoRespond((m) => m === "Network.setUserAgentOverride", {});
258
- fake.autoRespond((m) => m === "Target.closeTarget", { success: true });
259
- fake.autoRespond((m) => m === "Page.removeScriptToEvaluateOnNewDocument", {});
260
- fake.autoRespond((m) => m === "Page.addScriptToEvaluateOnNewDocument", {
261
- identifier: "inj-1",
262
- });
263
-
264
153
  const page = await session.newPage();
265
154
  expect(page).toBeDefined();
155
+ // Yield once so the deferred installInitInjector promise settles.
156
+ await new Promise((r) => setTimeout(r, 10));
266
157
 
267
- const methods = fake.written
268
- .map((w) => w.method)
158
+ const methods = localPipe.written
159
+ .map((w) => w.parsed.method)
269
160
  .filter((m): m is string => typeof m === "string");
270
- // Default behavior: the inject script IS installed.
161
+ // Task 0266 dual-mechanism: Session uses BOTH Fetch.fulfillRequest body
162
+ // splice (HTTP/HTTPS Document responses — closes source-attribution
163
+ // leak) AND Page.addScriptToEvaluateOnNewDocument (per-page fallback for
164
+ // about:blank / data: / blob: where Fetch domain can't intercept). The
165
+ // wrapped payload's `__mochi_inject_marker` early-return prevents
166
+ // double-execution when both fire on the same realm.
271
167
  expect(methods).toContain("Page.addScriptToEvaluateOnNewDocument");
272
- // And the params carry the compiled payload code.
273
- const installCall = fake.written.find(
274
- (w) => w.method === "Page.addScriptToEvaluateOnNewDocument",
168
+ const addScriptCall = localPipe.written.find(
169
+ (w) => w.parsed.method === "Page.addScriptToEvaluateOnNewDocument",
275
170
  );
276
- const params = installCall?.params as { source?: string; runImmediately?: boolean } | undefined;
277
- expect(params?.source).toBe(payload?.code);
278
- expect(params?.runImmediately).toBe(true);
171
+ const addScriptParams = addScriptCall?.parsed.params as
172
+ | { source?: string; runImmediately?: boolean; worldName?: string }
173
+ | undefined;
174
+ expect(addScriptParams?.runImmediately).toBe(true);
175
+ expect(addScriptParams?.worldName).toBe(""); // PLAN.md §8.4 — main world
176
+ expect(addScriptParams?.source).toContain("__mochi_inject_marker"); // idempotency guard
177
+ // Fetch.enable is sent ONCE on session construction with the
178
+ // Document-first patterns. Auth is off because no proxyAuth was set.
179
+ expect(methods).toContain("Fetch.enable");
180
+ const enableCall = localPipe.written.find((w) => w.parsed.method === "Fetch.enable");
181
+ const enableParams = enableCall?.parsed.params as
182
+ | {
183
+ handleAuthRequests?: boolean;
184
+ patterns?: { urlPattern?: string; resourceType?: string }[];
185
+ }
186
+ | undefined;
187
+ expect(enableParams?.handleAuthRequests).toBe(false);
188
+ expect(enableParams?.patterns).toBeDefined();
189
+ expect(enableParams?.patterns?.[0]).toEqual({
190
+ urlPattern: "*",
191
+ resourceType: "Document",
192
+ });
193
+ expect(enableParams?.patterns?.[1]).toEqual({ urlPattern: "*" });
279
194
  });
280
195
  });