返回 DeepSeek-Reasonix
terminal-events.test.ts
根目录 / desktop / frontend / src / __tests__ / terminal-events.test.ts
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
128 lines TYPESCRIPT