mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
test: harden QA cleanup and update preflight
This commit is contained in:
@@ -48,8 +48,9 @@ export function createCodexAppServerAgentHarness(options?: {
|
||||
}
|
||||
},
|
||||
dispose: async () => {
|
||||
const { clearSharedCodexAppServerClient } = await import("./src/app-server/shared-client.js");
|
||||
clearSharedCodexAppServerClient();
|
||||
const { clearSharedCodexAppServerClientAndWait } =
|
||||
await import("./src/app-server/shared-client.js");
|
||||
await clearSharedCodexAppServerClientAndWait();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,6 +231,38 @@ describe("CodexAppServerClient", () => {
|
||||
expect(process.kill).toHaveBeenCalledWith("SIGKILL");
|
||||
expect(process.unref).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("waits for app-server transport exit during async shutdown", async () => {
|
||||
vi.useFakeTimers();
|
||||
const process = Object.assign(new EventEmitter(), {
|
||||
stdin: {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
unref: vi.fn(),
|
||||
},
|
||||
stdout: Object.assign(new PassThrough(), { unref: vi.fn() }),
|
||||
stderr: Object.assign(new PassThrough(), { unref: vi.fn() }),
|
||||
exitCode: null as number | null,
|
||||
signalCode: null as string | null,
|
||||
kill: vi.fn(),
|
||||
unref: vi.fn(),
|
||||
});
|
||||
|
||||
const closed = __testing.closeCodexAppServerTransportAndWait(process, {
|
||||
exitTimeoutMs: 100,
|
||||
forceKillDelayMs: 25,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(25);
|
||||
|
||||
expect(process.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(process.kill).toHaveBeenCalledWith("SIGKILL");
|
||||
process.signalCode = "SIGKILL";
|
||||
process.emit("exit");
|
||||
|
||||
await expect(closed).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("handles stdin write errors without crashing the process", async () => {
|
||||
const harness = createClientHarness();
|
||||
clients.push(harness.client);
|
||||
|
||||
@@ -16,7 +16,11 @@ import {
|
||||
} from "./protocol.js";
|
||||
import { createStdioTransport } from "./transport-stdio.js";
|
||||
import { createWebSocketTransport } from "./transport-websocket.js";
|
||||
import { closeCodexAppServerTransport, type CodexAppServerTransport } from "./transport.js";
|
||||
import {
|
||||
closeCodexAppServerTransport,
|
||||
closeCodexAppServerTransportAndWait,
|
||||
type CodexAppServerTransport,
|
||||
} from "./transport.js";
|
||||
|
||||
export const MIN_CODEX_APP_SERVER_VERSION = "0.125.0";
|
||||
const CODEX_APP_SERVER_PARSE_LOG_MAX = 500;
|
||||
@@ -225,15 +229,20 @@ export class CodexAppServerClient {
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.closed) {
|
||||
if (!this.markClosed(new Error("codex app-server client is closed"))) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
this.lines.close();
|
||||
this.rejectPendingRequests(new Error("codex app-server client is closed"));
|
||||
closeCodexAppServerTransport(this.child);
|
||||
}
|
||||
|
||||
async closeAndWait(options?: {
|
||||
exitTimeoutMs?: number;
|
||||
forceKillDelayMs?: number;
|
||||
}): Promise<void> {
|
||||
this.markClosed(new Error("codex app-server client is closed"));
|
||||
await closeCodexAppServerTransportAndWait(this.child, options);
|
||||
}
|
||||
|
||||
private writeMessage(message: RpcRequest | RpcResponse): void {
|
||||
if (this.closed) {
|
||||
return;
|
||||
@@ -325,13 +334,19 @@ export class CodexAppServerClient {
|
||||
}
|
||||
|
||||
private closeWithError(error: Error): void {
|
||||
if (this.markClosed(error)) {
|
||||
closeCodexAppServerTransport(this.child);
|
||||
}
|
||||
}
|
||||
|
||||
private markClosed(error: Error): boolean {
|
||||
if (this.closed) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this.closed = true;
|
||||
this.lines.close();
|
||||
this.rejectPendingRequests(error);
|
||||
closeCodexAppServerTransport(this.child);
|
||||
return true;
|
||||
}
|
||||
|
||||
private rejectPendingRequests(error: Error): void {
|
||||
@@ -485,5 +500,6 @@ function formatExitValue(value: unknown): string {
|
||||
|
||||
export const __testing = {
|
||||
closeCodexAppServerTransport,
|
||||
closeCodexAppServerTransportAndWait,
|
||||
redactCodexAppServerLinePreview,
|
||||
} as const;
|
||||
|
||||
@@ -120,6 +120,18 @@ export function clearSharedCodexAppServerClient(): void {
|
||||
client?.close();
|
||||
}
|
||||
|
||||
export async function clearSharedCodexAppServerClientAndWait(options?: {
|
||||
exitTimeoutMs?: number;
|
||||
forceKillDelayMs?: number;
|
||||
}): Promise<void> {
|
||||
const state = getSharedCodexAppServerClientState();
|
||||
const client = state.client;
|
||||
state.client = undefined;
|
||||
state.promise = undefined;
|
||||
state.key = undefined;
|
||||
await client?.closeAndWait(options);
|
||||
}
|
||||
|
||||
function clearSharedClientIfCurrent(client: CodexAppServerClient): void {
|
||||
const state = getSharedCodexAppServerClientState();
|
||||
if (state.client !== client) {
|
||||
|
||||
@@ -21,6 +21,7 @@ export type CodexAppServerTransport = {
|
||||
kill?: (signal?: NodeJS.Signals) => unknown;
|
||||
unref?: () => unknown;
|
||||
once: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
off?: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
};
|
||||
|
||||
export function closeCodexAppServerTransport(
|
||||
@@ -50,12 +51,55 @@ export function closeCodexAppServerTransport(
|
||||
child.stdin.unref?.();
|
||||
}
|
||||
|
||||
export async function closeCodexAppServerTransportAndWait(
|
||||
child: CodexAppServerTransport,
|
||||
options: { exitTimeoutMs?: number; forceKillDelayMs?: number } = {},
|
||||
): Promise<boolean> {
|
||||
if (!hasCodexAppServerTransportExited(child)) {
|
||||
closeCodexAppServerTransport(child, options);
|
||||
}
|
||||
return await waitForCodexAppServerTransportExit(child, options.exitTimeoutMs ?? 2_000);
|
||||
}
|
||||
|
||||
function hasCodexAppServerTransportExited(child: CodexAppServerTransport): boolean {
|
||||
return child.exitCode !== null && child.exitCode !== undefined
|
||||
? true
|
||||
: child.signalCode !== null && child.signalCode !== undefined;
|
||||
}
|
||||
|
||||
async function waitForCodexAppServerTransportExit(
|
||||
child: CodexAppServerTransport,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> {
|
||||
if (hasCodexAppServerTransportExited(child)) {
|
||||
return true;
|
||||
}
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
let settled = false;
|
||||
const onExit = () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
resolve(true);
|
||||
};
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
child.off?.("exit", onExit);
|
||||
resolve(false);
|
||||
},
|
||||
Math.max(1, timeoutMs),
|
||||
);
|
||||
timeout.unref?.();
|
||||
child.once("exit", onExit);
|
||||
});
|
||||
}
|
||||
|
||||
function signalCodexAppServerTransport(
|
||||
child: CodexAppServerTransport,
|
||||
signal: NodeJS.Signals,
|
||||
|
||||
Reference in New Issue
Block a user