| 1 | import { JSDOM } from "jsdom"; |
| 2 | |
| 3 | import { __emitMockTerminalExit, __emitMockTerminalOutput } from "../lib/bridge"; |
| 4 | import { |
| 5 | __resetTerminalEventBus, |
| 6 | forgetTerminalSession, |
| 7 | startTerminalEventBridge, |
| 8 | terminalEventBufferLimit, |
| 9 | } from "../lib/terminalEvents"; |
| 10 | import { registerTerminalSink } from "../lib/terminalSink"; |
| 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 utf8Subscription = registerTerminalSink("utf8", (bytes) => first.push(...bytes)); |
| 36 | utf8Subscription.dispose(); |
| 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 liveSubscription = registerTerminalSink("replay", (bytes) => live.push(...bytes)); |
| 41 | __emitMockTerminalOutput({ id: "replay", data: base64(new Uint8Array([1, 2])) }); |
| 42 | liveSubscription.dispose(); |
| 43 | __emitMockTerminalOutput({ id: "replay", data: base64(new Uint8Array([3])) }); |
| 44 | const replayed: number[] = []; |
| 45 | const replaySubscription = registerTerminalSink("replay", (bytes) => replayed.push(...bytes)); |
| 46 | replaySubscription.dispose(); |
| 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 boundedSubscription = registerTerminalSink("bounded", (bytes) => bounded.push(...bytes)); |
| 57 | boundedSubscription.dispose(); |
| 58 | check(bounded.length <= terminalEventBufferLimit, "terminal output history is bounded to one MiB per session"); |
| 59 | |
| 60 | const pausable: number[] = []; |
| 61 | const pausableSubscription = registerTerminalSink("pausable", (bytes) => pausable.push(...bytes)); |
| 62 | __emitMockTerminalOutput({ id: "pausable", data: base64(new Uint8Array([1, 2])) }); |
| 63 | pausableSubscription.setActive(false); |
| 64 | for (let index = 0; index < 24; index += 1) { |
| 65 | const pausedChunk = new Uint8Array(64 * 1024); |
| 66 | pausedChunk.fill(index + 10); |
| 67 | __emitMockTerminalOutput({ id: "pausable", data: base64(pausedChunk) }); |
| 68 | } |
| 69 | check(JSON.stringify(pausable) === JSON.stringify([1, 2]), "inactive terminal does not consume hidden PTY output"); |
| 70 | pausableSubscription.setActive(true); |
| 71 | check( |
| 72 | pausable.length === terminalEventBufferLimit + 2 && pausable[2] === 18 && pausable[pausable.length - 1] === 33, |
| 73 | "reactivated terminal replays only the retained missed output once", |
| 74 | ); |
| 75 | const replayedLength = pausable.length; |
| 76 | pausableSubscription.setActive(true); |
| 77 | check(pausable.length === replayedLength, "repeated activation does not duplicate terminal output"); |
| 78 | __emitMockTerminalOutput({ id: "pausable", data: base64(new Uint8Array([99])) }); |
| 79 | check(pausable[pausable.length - 1] === 99, "reactivated terminal resumes live output after replay"); |
| 80 | pausableSubscription.dispose(); |
| 81 | |
| 82 | __emitMockTerminalOutput({ id: "forgotten", data: base64(new Uint8Array([9])) }); |
| 83 | forgetTerminalSession("forgotten"); |
| 84 | const forgotten: number[] = []; |
| 85 | const forgottenSubscription = registerTerminalSink("forgotten", (bytes) => forgotten.push(...bytes)); |
| 86 | forgottenSubscription.dispose(); |
| 87 | check(forgotten.length === 0, "explicitly closed terminal history is discarded"); |
| 88 | |
| 89 | __emitMockTerminalOutput({ id: "natural-exit", data: base64(new Uint8Array([7])) }); |
| 90 | __emitMockTerminalExit({ id: "natural-exit", exitCode: 0 }); |
| 91 | const naturalExit: number[] = []; |
| 92 | const naturalExitSubscription = registerTerminalSink("natural-exit", (bytes) => naturalExit.push(...bytes)); |
| 93 | naturalExitSubscription.dispose(); |
| 94 | check(JSON.stringify(naturalExit) === JSON.stringify([7]), "natural exit preserves terminal history"); |
| 95 | |
| 96 | __emitMockTerminalOutput({ id: "removed-exit", data: base64(new Uint8Array([8])) }); |
| 97 | __emitMockTerminalExit({ id: "removed-exit", exitCode: 0, removed: true }); |
| 98 | const removedExit: number[] = []; |
| 99 | const removedExitSubscription = registerTerminalSink("removed-exit", (bytes) => removedExit.push(...bytes)); |
| 100 | removedExitSubscription.dispose(); |
| 101 | check(removedExit.length === 0, "removed terminal exit discards terminal history"); |
| 102 | |
| 103 | __resetTerminalEventBus(); |
| 104 | const releaseApp = startTerminalEventBridge(); |
| 105 | const releaseView = startTerminalEventBridge(); |
| 106 | const received: number[] = []; |
| 107 | const shared = registerTerminalSink("shared", (bytes) => received.push(...bytes)); |
| 108 | releaseApp(); |
| 109 | __emitMockTerminalOutput({ id: "shared", data: base64(new Uint8Array([1])) }); |
| 110 | check(received.length === 1, "one owner release cannot stop a still-mounted terminal view"); |
| 111 | releaseView(); |
| 112 | __emitMockTerminalOutput({ id: "shared", data: base64(new Uint8Array([2])) }); |
| 113 | check(received.length === 1, "the final owner release detaches terminal event delivery"); |
| 114 | const releaseReplacement = startTerminalEventBridge(); |
| 115 | releaseApp(); |
| 116 | releaseView(); |
| 117 | __emitMockTerminalOutput({ id: "shared", data: base64(new Uint8Array([3])) }); |
| 118 | check(JSON.stringify(received) === JSON.stringify([1, 3]), "old cleanups cannot release a replacement bridge"); |
| 119 | releaseReplacement(); |
| 120 | shared.dispose(); |
| 121 | } finally { |
| 122 | __resetTerminalEventBus(); |
| 123 | if (previousWindow) globalThis.window = previousWindow; |
| 124 | else delete (globalThis as { window?: unknown }).window; |
| 125 | if (previousAtob) globalThis.atob = previousAtob; |
| 126 | if (previousBtoa) globalThis.btoa = previousBtoa; |
| 127 | } |
| 128 |