baa-conductor

git clone 

baa-conductor / plugins / baa-firefox
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});