| 1 | import { JSDOM } from "jsdom"; |
| 2 | |
| 3 | import { __emitMockTerminalExit, __emitMockTerminalOutput } from "../lib/bridge"; |
| 4 | import { |
| 5 | __resetTerminalEventBus, |
| 6 | forgetTerminalSession, |
| 7 | registerTerminalSink, |
| 8 | startTerminalEventBridge, |
| 9 | terminalEventBufferLimit, |
| 10 | } from "../lib/terminalEvents"; |
| 11 | |
| 12 | function base64(bytes: Uint8Array): string { |
| 13 | return Buffer.from(bytes).toString("base64"); |
| 14 | } |
| 15 | |
| 16 | function check(condition: unknown, message: string): void { |
| 17 | if (!condition) throw new Error(message); |
| 18 | process.stdout.write(`PASS ${message}\n`); |
| 19 | } |
| 20 | |
| 21 | const dom = new JSDOM("<!doctype html><html><body></body></html>"); |
| 22 | const previousWindow = globalThis.window; |
| 23 | const previousAtob = globalThis.atob; |
| 24 | const previousBtoa = globalThis.btoa; |
| 25 | globalThis.window = dom.window as unknown as Window & typeof globalThis; |
| 26 | globalThis.atob = (value: string) => Buffer.from(value, "base64").toString("binary"); |
| 27 | globalThis.btoa = (value: string) => Buffer.from(value, "binary").toString("base64"); |
| 28 | |
| 29 | try { |
| 30 | __resetTerminalEventBus(); |
| 31 | startTerminalEventBridge(); |
| 32 | const first: number[] = []; |
| 33 | __emitMockTerminalOutput({ id: "utf8", data: base64(new Uint8Array([0xe4, 0xbd])) }); |
| 34 | __emitMockTerminalOutput({ id: "utf8", data: base64(new Uint8Array([0xa0])) }); |
| 35 | const unregister = registerTerminalSink("utf8", (bytes) => first.push(...bytes)); |
| 36 | unregister(); |
| 37 | check(JSON.stringify(first) === JSON.stringify([0xe4, 0xbd, 0xa0]), "split UTF-8 output is delivered as original bytes"); |
| 38 | |
| 39 | const live: number[] = []; |
| 40 | const unregisterLive = registerTerminalSink("replay", (bytes) => live.push(...bytes)); |
| 41 | __emitMockTerminalOutput({ id: "replay", data: base64(new Uint8Array([1, 2])) }); |
| 42 | unregisterLive(); |
| 43 | __emitMockTerminalOutput({ id: "replay", data: base64(new Uint8Array([3])) }); |
| 44 | const replayed: number[] = []; |
| 45 | const unregisterReplay = registerTerminalSink("replay", (bytes) => replayed.push(...bytes)); |
| 46 | unregisterReplay(); |
| 47 | check(JSON.stringify(live) === JSON.stringify([1, 2]), "attached terminal receives live output"); |
| 48 | check(JSON.stringify(replayed) === JSON.stringify([1, 2, 3]), "remounted terminal replays complete output history"); |
| 49 | |
| 50 | const chunk = new Uint8Array(64 * 1024); |
| 51 | chunk.fill(65); |
| 52 | for (let index = 0; index < 24; index += 1) { |
| 53 | __emitMockTerminalOutput({ id: "bounded", data: base64(chunk) }); |
| 54 | } |
| 55 | const bounded: number[] = []; |
| 56 | const unregisterBounded = registerTerminalSink("bounded", (bytes) => bounded.push(...bytes)); |
| 57 | unregisterBounded(); |
| 58 | check(bounded.length <= terminalEventBufferLimit, "terminal output history is bounded to one MiB per session"); |
| 59 | |
| 60 | __emitMockTerminalOutput({ id: "forgotten", data: base64(new Uint8Array([9])) }); |
| 61 | forgetTerminalSession("forgotten"); |
| 62 | const forgotten: number[] = []; |
| 63 | const unregisterForgotten = registerTerminalSink("forgotten", (bytes) => forgotten.push(...bytes)); |
| 64 | unregisterForgotten(); |
| 65 | check(forgotten.length === 0, "explicitly closed terminal history is discarded"); |
| 66 | |
| 67 | __emitMockTerminalOutput({ id: "natural-exit", data: base64(new Uint8Array([7])) }); |
| 68 | __emitMockTerminalExit({ id: "natural-exit", exitCode: 0 }); |
| 69 | const naturalExit: number[] = []; |
| 70 | const unregisterNaturalExit = registerTerminalSink("natural-exit", (bytes) => naturalExit.push(...bytes)); |
| 71 | unregisterNaturalExit(); |
| 72 | check(JSON.stringify(naturalExit) === JSON.stringify([7]), "natural exit preserves terminal history"); |
| 73 | |
| 74 | __emitMockTerminalOutput({ id: "removed-exit", data: base64(new Uint8Array([8])) }); |
| 75 | __emitMockTerminalExit({ id: "removed-exit", exitCode: 0, removed: true }); |
| 76 | const removedExit: number[] = []; |
| 77 | const unregisterRemovedExit = registerTerminalSink("removed-exit", (bytes) => removedExit.push(...bytes)); |
| 78 | unregisterRemovedExit(); |
| 79 | check(removedExit.length === 0, "removed terminal exit discards terminal history"); |
| 80 | } finally { |
| 81 | __resetTerminalEventBus(); |
| 82 | if (previousWindow) globalThis.window = previousWindow; |
| 83 | else delete (globalThis as { window?: unknown }).window; |
| 84 | if (previousAtob) globalThis.atob = previousAtob; |
| 85 | if (previousBtoa) globalThis.btoa = previousBtoa; |
| 86 | } |
| 87 |