im_wower
·
2026-03-29
page-interceptor.test.cjs
1const assert = require("node:assert/strict");
2const fs = require("node:fs");
3const path = require("node:path");
4const test = require("node:test");
5const vm = require("node:vm");
6const { TextDecoder, TextEncoder } = require("node:util");
7
8const PAGE_INTERCEPTOR_SOURCE = fs.readFileSync(
9 path.join(__dirname, "page-interceptor.js"),
10 "utf8"
11);
12
13function createAbortingSseResponse(chunks, errorMessage = "The operation was aborted.") {
14 const headers = new Headers({
15 "content-type": "text/event-stream"
16 });
17 const encoder = new TextEncoder();
18
19 return {
20 headers,
21 status: 200,
22 clone() {
23 let index = 0;
24 return {
25 body: {
26 getReader() {
27 return {
28 async read() {
29 if (index < chunks.length) {
30 return {
31 done: false,
32 value: encoder.encode(chunks[index++])
33 };
34 }
35
36 throw new Error(errorMessage);
37 }
38 };
39 }
40 }
41 };
42 }
43 };
44}
45
46function createHarness(fetchImpl) {
47 const events = [];
48 const listeners = new Map();
49 const location = {
50 hostname: "chatgpt.com",
51 href: "https://chatgpt.com/c/conv_abort",
52 origin: "https://chatgpt.com"
53 };
54
55 function addEventListener(type, listener) {
56 if (!listeners.has(type)) {
57 listeners.set(type, new Set());
58 }
59 listeners.get(type).add(listener);
60 }
61
62 function removeEventListener(type, listener) {
63 listeners.get(type)?.delete(listener);
64 }
65
66 const window = {
67 __baaFirefoxIntercepted__: undefined,
68 addEventListener,
69 dispatchEvent(event) {
70 events.push({
71 detail: event.detail,
72 type: event.type
73 });
74
75 for (const listener of listeners.get(event.type) || []) {
76 listener.call(window, event);
77 }
78
79 return true;
80 },
81 fetch: fetchImpl,
82 removeEventListener
83 };
84 window.location = location;
85
86 function CustomEvent(type, init = {}) {
87 this.detail = init.detail;
88 this.type = type;
89 }
90
91 function FakeXMLHttpRequest() {}
92 FakeXMLHttpRequest.prototype.addEventListener = function addEventListenerNoop() {};
93 FakeXMLHttpRequest.prototype.getAllResponseHeaders = function getAllResponseHeaders() {
94 return "";
95 };
96 FakeXMLHttpRequest.prototype.getResponseHeader = function getResponseHeader() {
97 return "";
98 };
99 FakeXMLHttpRequest.prototype.open = function open() {};
100 FakeXMLHttpRequest.prototype.send = function send() {};
101 FakeXMLHttpRequest.prototype.setRequestHeader = function setRequestHeader() {};
102
103 const context = vm.createContext({
104 AbortController,
105 ArrayBuffer,
106 Blob,
107 CustomEvent,
108 FormData,
109 Headers,
110 Request,
111 Response,
112 TextDecoder,
113 URL,
114 URLSearchParams,
115 XMLHttpRequest: FakeXMLHttpRequest,
116 clearTimeout,
117 console: {
118 log() {}
119 },
120 location,
121 setTimeout,
122 window
123 });
124
125 vm.runInContext(PAGE_INTERCEPTOR_SOURCE, context, {
126 filename: "page-interceptor.js"
127 });
128
129 return {
130 events,
131 window
132 };
133}
134
135async function waitFor(predicate, timeoutMs = 200) {
136 const deadline = Date.now() + timeoutMs;
137
138 while (Date.now() < deadline) {
139 if (predicate()) {
140 return;
141 }
142
143 await new Promise((resolve) => setTimeout(resolve, 0));
144 }
145
146 throw new Error("Timed out waiting for page-interceptor events.");
147}
148
149test("page-interceptor flushes the buffered SSE tail before emitting the abort event", async () => {
150 const response = createAbortingSseResponse([
151 'data: {"message":{"id":"msg_abort","author":{"role":"assistant"},"content":{"parts":["partial"]}}}\n\n',
152 'data: {"message":{"author":{"role":"assistant"},"content":{"parts":["tail"]}}'
153 ]);
154 const { events, window } = createHarness(async () => response);
155
156 await window.fetch("https://chatgpt.com/backend-api/f/conversation", {
157 body: JSON.stringify({
158 conversation_id: "conv_abort"
159 }),
160 method: "POST"
161 });
162
163 await waitFor(() => events.some((event) => event.type === "__baa_sse__" && event.detail?.error));
164
165 const sseEvents = events.filter((event) => event.type === "__baa_sse__");
166 const errorEvent = sseEvents.find((event) => event.detail?.error);
167
168 assert.ok(
169 sseEvents.some((event) => event.detail?.chunk === 'data: {"message":{"author":{"role":"assistant"},"content":{"parts":["tail"]}}'),
170 "expected the unflushed tail chunk to be emitted before the error event"
171 );
172 assert.equal(errorEvent?.detail?.error, "The operation was aborted.");
173});