





















@@ -0,0 +1,294 @@
1+import { spawn } from "node:child_process";
2+import { mkdir, readFile, writeFile } from "node:fs/promises";
3+import { createRequire } from "node:module";
4+import path from "node:path";
5+6+type Options = {
7+altScreen: boolean;
8+mirrorPath: string;
9+mode: "fake" | "local" | "all";
10+vitestArgs: string[];
11+};
12+13+const DEFAULT_MIRROR_PATH = path.join(process.cwd(), ".artifacts", "tui-pty-mirror", "latest.ansi");
14+const require = createRequire(import.meta.url);
15+const MODE_TEST_FILES = {
16+fake: ["src/tui/tui-pty-harness.e2e.test.ts"],
17+local: ["src/tui/tui-pty-local.e2e.test.ts"],
18+all: ["src/tui/tui-pty-harness.e2e.test.ts", "src/tui/tui-pty-local.e2e.test.ts"],
19+} as const;
20+const MIRROR_TERMINAL_QUERIES = ["\x1b[?u", "\x1b[16t"];
21+const DEFAULT_PTY_COLS = 100;
22+const DEFAULT_PTY_ROWS = 30;
23+24+function readOption(args: string[], name: string): string | undefined {
25+const idx = args.indexOf(name);
26+if (idx < 0) {
27+return undefined;
28+}
29+return args[idx + 1]?.trim() || undefined;
30+}
31+32+function readMode(args: string[]): Options["mode"] {
33+const mode = readOption(args, "--mode") ?? "fake";
34+if (mode === "fake" || mode === "local" || mode === "all") {
35+return mode;
36+}
37+throw new Error(`--mode must be fake, local, or all; got ${JSON.stringify(mode)}`);
38+}
39+40+function parseOptions(args = process.argv.slice(2)): Options {
41+const separator = args.indexOf("--");
42+const ownArgs = separator >= 0 ? args.slice(0, separator) : args;
43+const vitestArgs = separator >= 0 ? args.slice(separator + 1) : [];
44+const mirrorPath =
45+readOption(ownArgs, "--mirror-path") !== undefined
46+ ? path.resolve(readOption(ownArgs, "--mirror-path") ?? "")
47+ : DEFAULT_MIRROR_PATH;
48+return {
49+altScreen: !ownArgs.includes("--no-alt-screen"),
50+ mirrorPath,
51+mode: readMode(ownArgs),
52+ vitestArgs,
53+};
54+}
55+56+function delay(ms: number): Promise<void> {
57+return new Promise((resolve) => setTimeout(resolve, ms));
58+}
59+60+function shouldUseAltScreen(options: Options) {
61+return options.altScreen && process.stdout.isTTY;
62+}
63+64+function resolveVitestCliEntry(): string {
65+const vitestPackageJson = require.resolve("vitest/package.json");
66+return path.join(path.dirname(vitestPackageJson), "vitest.mjs");
67+}
68+69+function currentTerminalDimension(value: number | undefined, fallback: number): string {
70+return String(value && value > 0 ? value : fallback);
71+}
72+73+async function createMirrorFile(mirrorPath: string): Promise<void> {
74+await mkdir(path.dirname(mirrorPath), { recursive: true });
75+await writeFile(mirrorPath, "", "utf8");
76+}
77+78+async function readNewMirrorData(mirrorPath: string, offset: number) {
79+const data = await readFile(mirrorPath);
80+const nextOffset = data.byteLength;
81+if (nextOffset < offset) {
82+return { chunk: data, offset: nextOffset };
83+}
84+if (nextOffset === offset) {
85+return { chunk: Buffer.alloc(0), offset };
86+}
87+return { chunk: data.subarray(offset), offset: nextOffset };
88+}
89+90+async function main(): Promise<void> {
91+const options = parseOptions();
92+const useAltScreen = shouldUseAltScreen(options);
93+await createMirrorFile(options.mirrorPath);
94+95+const child = spawn(
96+process.execPath,
97+[
98+"--no-maglev",
99+resolveVitestCliEntry(),
100+"run",
101+"--config",
102+"test/vitest/vitest.tui-pty.config.ts",
103+ ...MODE_TEST_FILES[options.mode],
104+"--reporter=dot",
105+ ...options.vitestArgs,
106+],
107+{
108+cwd: process.cwd(),
109+env: {
110+ ...process.env,
111+OPENCLAW_TUI_PTY_MIRROR_PATH: options.mirrorPath,
112+OPENCLAW_TUI_PTY_INCLUDE_LOCAL: options.mode === "fake" ? "0" : "1",
113+OPENCLAW_TUI_PTY_COLS: currentTerminalDimension(process.stdout.columns, DEFAULT_PTY_COLS),
114+OPENCLAW_TUI_PTY_ROWS: currentTerminalDimension(process.stdout.rows, DEFAULT_PTY_ROWS),
115+OPENCLAW_TUI_PTY_TYPE_CHUNK_SIZE: process.env.OPENCLAW_TUI_PTY_TYPE_CHUNK_SIZE ?? "4",
116+OPENCLAW_TUI_PTY_TYPE_DELAY_MS: process.env.OPENCLAW_TUI_PTY_TYPE_DELAY_MS ?? "25",
117+},
118+stdio: ["ignore", "pipe", "pipe"],
119+},
120+);
121+122+let childStdout = "";
123+let childStderr = "";
124+let restored = false;
125+let mirrorOffset = 0;
126+let mirrorFilterPending = "";
127+let sawMirrorOutput = false;
128+const startedAt = Date.now();
129+130+const filterMirrorTerminalQueries = (chunk: Buffer) => {
131+const input = mirrorFilterPending + chunk.toString("utf8");
132+let output = "";
133+mirrorFilterPending = "";
134+for (let idx = 0; idx < input.length; idx += 1) {
135+const rest = input.slice(idx);
136+const fullMatch = MIRROR_TERMINAL_QUERIES.find((query) => rest.startsWith(query));
137+if (fullMatch) {
138+idx += fullMatch.length - 1;
139+continue;
140+}
141+const partialMatch = MIRROR_TERMINAL_QUERIES.find((query) => query.startsWith(rest));
142+if (partialMatch) {
143+mirrorFilterPending = rest;
144+break;
145+}
146+output += input[idx];
147+}
148+return output;
149+};
150+151+const writeMirrorChunk = (chunk: Buffer) => {
152+const filteredChunk = filterMirrorTerminalQueries(chunk);
153+if (filteredChunk.length === 0) {
154+return;
155+}
156+if (!sawMirrorOutput && useAltScreen) {
157+process.stdout.write("\x1b[2J\x1b[H");
158+}
159+sawMirrorOutput = true;
160+process.stdout.write(filteredChunk);
161+};
162+163+const restoreScreen = () => {
164+if (restored) {
165+return;
166+}
167+restored = true;
168+if (useAltScreen) {
169+process.stdout.write("\x1b[?1049l");
170+}
171+};
172+173+const stopChild = () => {
174+child.kill("SIGINT");
175+setTimeout(() => child.kill("SIGTERM"), 500).unref();
176+};
177+178+const ignoredInput = (chunk: Buffer) => {
179+if (chunk.includes(0x03)) {
180+stopChild();
181+}
182+};
183+const hadRawMode = process.stdin.isTTY && process.stdin.isRaw;
184+if (useAltScreen && process.stdin.isTTY) {
185+process.stdin.setRawMode(true);
186+process.stdin.resume();
187+process.stdin.on("data", ignoredInput);
188+}
189+190+const restoreInput = () => {
191+if (!process.stdin.isTTY) {
192+return;
193+}
194+process.stdin.off("data", ignoredInput);
195+process.stdin.setRawMode(hadRawMode);
196+if (!hadRawMode) {
197+process.stdin.pause();
198+}
199+};
200+201+const drainParentInput = async () => {
202+if (!useAltScreen || !process.stdin.isTTY) {
203+return;
204+}
205+await delay(100);
206+};
207+208+const renderWaitingStatus = () => {
209+if (!useAltScreen || sawMirrorOutput) {
210+return;
211+}
212+const elapsedSeconds = Math.floor((Date.now() - startedAt) / 1000);
213+process.stdout.write(
214+[
215+"\x1b[2J\x1b[H",
216+"openclaw TUI PTY tests",
217+"",
218+`Mode: ${options.mode}`,
219+`Waiting for the first TUI frame... ${elapsedSeconds}s`,
220+`Mirror: ${options.mirrorPath}`,
221+"",
222+"Vitest output is buffered and will print after the mirrored TUI run exits.",
223+].join("\n"),
224+);
225+};
226+227+if (useAltScreen) {
228+process.stdout.write("\x1b[?1049h\x1b[?25l");
229+renderWaitingStatus();
230+}
231+232+child.stdout?.on("data", (chunk: Buffer) => {
233+childStdout += chunk.toString("utf8");
234+});
235+child.stderr?.on("data", (chunk: Buffer) => {
236+childStderr += chunk.toString("utf8");
237+});
238+239+let childExit: { code: number | null; signal: NodeJS.Signals | null } | null = null;
240+child.on("exit", (code, signal) => {
241+childExit = { code, signal };
242+});
243+244+process.once("SIGINT", stopChild);
245+246+try {
247+for (;;) {
248+if (childExit) {
249+break;
250+}
251+const result = await readNewMirrorData(options.mirrorPath, mirrorOffset);
252+mirrorOffset = result.offset;
253+if (result.chunk.byteLength > 0) {
254+writeMirrorChunk(result.chunk);
255+} else {
256+renderWaitingStatus();
257+}
258+await delay(sawMirrorOutput ? 25 : 250);
259+}
260+261+const result = await readNewMirrorData(options.mirrorPath, mirrorOffset);
262+if (result.chunk.byteLength > 0) {
263+writeMirrorChunk(result.chunk);
264+}
265+} finally {
266+await drainParentInput();
267+restoreInput();
268+if (useAltScreen) {
269+process.stdout.write("\x1b[?2026l\x1b[?2004l\x1b[>4;0m\x1b[?25h");
270+}
271+restoreScreen();
272+}
273+274+if (childStdout) {
275+process.stdout.write(childStdout);
276+}
277+if (childStderr) {
278+process.stderr.write(childStderr);
279+}
280+281+if (childExit.signal) {
282+throw new Error(`TUI PTY tests exited with signal ${childExit.signal}`);
283+}
284+if (childExit.code !== 0) {
285+process.exitCode = childExit.code ?? 1;
286+}
287+}
288+289+main().catch((error) => {
290+process.stderr.write(
291+`${error instanceof Error ? error.stack || error.message : String(error)}\n`,
292+);
293+process.exit(1);
294+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。