























1-import { appendFileSync } from "node:fs";
21import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
32import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
43import { tmpdir } from "node:os";
54import path from "node:path";
6-import * as nodePty from "@lydell/node-pty";
7-import type { PtyExitEvent, PtyHandle } from "@lydell/node-pty";
85import { afterEach, describe, expect, it } from "vitest";
96import type { OpenClawConfig } from "../config/types.openclaw.js";
10-11-type NodePtyRuntimeModule = typeof nodePty & {
12-default?: Partial<typeof nodePty>;
13-};
14-15-type KillablePtyHandle = PtyHandle & {
16-kill?: (signal?: string) => void;
17-};
18-19-type PtyRun = {
20-output: () => string;
21-write: (data: string, opts?: { delay?: boolean }) => Promise<void>;
22-waitForOutput: (needle: string, timeoutMs?: number) => Promise<string>;
23-waitForExit: (timeoutMs?: number) => Promise<PtyExitEvent>;
24-dispose: () => void;
25-};
7+import { startPty, waitFor, type PtyRun } from "./tui-pty-test-support.js";
268279type MockModelServer = {
2810baseUrl: string;
@@ -36,146 +18,6 @@ const LOCAL_OUTPUT_TIMEOUT_MS = 60_000;
3618const LOCAL_EXIT_TIMEOUT_MS = 4_000;
3719const LOCAL_TEST_TIMEOUT_MS = 90_000;
382039-function resolveSpawnPty() {
40-const runtime = nodePty as NodePtyRuntimeModule;
41-if (typeof runtime.spawn === "function") {
42-return runtime.spawn;
43-}
44-if (typeof runtime.default?.spawn === "function") {
45-return runtime.default.spawn;
46-}
47-throw new TypeError("@lydell/node-pty spawn export is unavailable");
48-}
49-50-const spawnPty = resolveSpawnPty();
51-52-function sleep(ms: number) {
53-return new Promise((resolve) => setTimeout(resolve, ms));
54-}
55-56-function readPositiveIntegerEnv(name: string): number | null {
57-const value = Number.parseInt(process.env[name] ?? "", 10);
58-return Number.isFinite(value) && value > 0 ? value : null;
59-}
60-61-function readPtyDimensionEnv(name: string, fallback: number): number {
62-return readPositiveIntegerEnv(name) ?? fallback;
63-}
64-65-async function writePtyInput(
66-pty: PtyHandle,
67-data: string,
68-opts: { delay?: boolean } = {},
69-): Promise<void> {
70-const delayMs = readPositiveIntegerEnv("OPENCLAW_TUI_PTY_TYPE_DELAY_MS");
71-if (!delayMs || opts.delay === false) {
72-pty.write(data);
73-return;
74-}
75-const chunkSize = readPositiveIntegerEnv("OPENCLAW_TUI_PTY_TYPE_CHUNK_SIZE") ?? 1;
76-for (let idx = 0; idx < data.length; idx += chunkSize) {
77-pty.write(data.slice(idx, idx + chunkSize));
78-if (idx + chunkSize < data.length) {
79-await sleep(delayMs);
80-}
81-}
82-}
83-84-function waitFor<T>(params: {
85-timeoutMs: number;
86-read: () => T | null;
87-onTimeout: () => Error;
88-}): Promise<T> {
89-const start = Date.now();
90-return new Promise((resolve, reject) => {
91-const tick = () => {
92-let result: T | null;
93-try {
94-result = params.read();
95-} catch (error) {
96-reject(error);
97-return;
98-}
99-if (result !== null) {
100-resolve(result);
101-return;
102-}
103-if (Date.now() - start >= params.timeoutMs) {
104-reject(params.onTimeout());
105-return;
106-}
107-setTimeout(tick, 25);
108-};
109-tick();
110-});
111-}
112-113-function mirrorPtyOutput(data: string) {
114-const mirrorPath = process.env.OPENCLAW_TUI_PTY_MIRROR_PATH;
115-if (!mirrorPath) {
116-return;
117-}
118-appendFileSync(mirrorPath, data, "utf8");
119-}
120-121-function startPty(command: string, args: string[], opts: { cwd: string; env: NodeJS.ProcessEnv }) {
122-let output = "";
123-let exitEvent: PtyExitEvent | null = null;
124-const pty = spawnPty(command, args, {
125-name: "xterm-256color",
126-cols: readPtyDimensionEnv("OPENCLAW_TUI_PTY_COLS", 100),
127-rows: readPtyDimensionEnv("OPENCLAW_TUI_PTY_ROWS", 30),
128-cwd: opts.cwd,
129-env: {
130- ...process.env,
131- ...opts.env,
132-TERM: "xterm-256color",
133-} as Record<string, string>,
134-}) as KillablePtyHandle;
135-136-pty.onData((data) => {
137-output += data;
138-mirrorPtyOutput(data);
139-});
140-pty.onExit((event) => {
141-exitEvent = event;
142-});
143-144-const run: PtyRun = {
145-output: () => output,
146-write: async (data, writeOpts) => await writePtyInput(pty, data, writeOpts),
147-waitForOutput: async (needle, timeoutMs = LOCAL_OUTPUT_TIMEOUT_MS) =>
148-await waitFor({
149- timeoutMs,
150-read: () => {
151-if (output.includes(needle)) {
152-return output;
153-}
154-if (exitEvent) {
155-throw new Error(
156-`PTY exited before ${JSON.stringify(needle)}\nexit=${JSON.stringify(exitEvent)}\n${output}`,
157-);
158-}
159-return null;
160-},
161-onTimeout: () => new Error(`timed out waiting for ${JSON.stringify(needle)}\n${output}`),
162-}),
163-waitForExit: async (timeoutMs = LOCAL_EXIT_TIMEOUT_MS) =>
164-await waitFor({
165- timeoutMs,
166-read: () => exitEvent,
167-onTimeout: () => new Error(`timed out waiting for PTY exit\n${output}`),
168-}),
169-dispose: () => {
170-if (!exitEvent) {
171-pty.kill?.("SIGTERM");
172-}
173-},
174-};
175-activeRuns.push(run);
176-return run;
177-}
178-17921async function readRequestBody(req: IncomingMessage): Promise<string> {
18022const chunks: Buffer[] = [];
18123for await (const chunk of req) {
@@ -368,6 +210,7 @@ async function startLocalModeTui() {
368210]);
369211370212const run = startPty(process.execPath, ["scripts/run-node.mjs", "tui", "--local"], {
213+ activeRuns,
371214cwd: process.cwd(),
372215env: {
373216HOME: homeDir,
@@ -381,6 +224,8 @@ async function startLocalModeTui() {
381224OPENCLAW_CODEX_DISCOVERY_LIVE: "0",
382225NO_COLOR: undefined,
383226},
227+exitTimeoutMs: LOCAL_EXIT_TIMEOUT_MS,
228+outputTimeoutMs: LOCAL_OUTPUT_TIMEOUT_MS,
384229});
385230386231return {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。