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