mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(e2e): keep parallels json output parseable
This commit is contained in:
@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Release/CI/E2E: fail secret-provider proof startup immediately when the gateway exits by signal instead of waiting for the readiness timeout.
|
||||
- Release/CI/E2E: report plugin gateway gauntlet command-log write failures as failed rows instead of crashing the harness from child-process callbacks.
|
||||
- Release/CI/E2E: abort stalled Kitchen Sink RPC readiness probes as soon as the gateway exits so proof failures return promptly.
|
||||
- Release/CI/E2E: keep Parallels JSON-mode progress on stderr so macOS, Linux, Windows, and aggregate update smoke summaries stay parseable on stdout.
|
||||
|
||||
## 2026.6.1
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ const HOST_COMMAND_WRAPPER_BACKSTOP_MS = 5_000;
|
||||
const HOST_COMMAND_CHILD_PID_PREFIX = "__OPENCLAW_HOST_COMMAND_CHILD_PID__";
|
||||
const HOST_COMMAND_SPAWN_ERROR_PREFIX = "__OPENCLAW_HOST_COMMAND_SPAWN_ERROR__";
|
||||
const HOST_COMMAND_TIMEOUT_PREFIX = "__OPENCLAW_HOST_COMMAND_TIMEOUT__";
|
||||
let progressStderrDepth = 0;
|
||||
|
||||
type HostCommandInvocation = {
|
||||
args: string[];
|
||||
@@ -42,13 +43,23 @@ function hostInvocationFromRunner(runner: HostCommandInvocation): HostCommandInv
|
||||
}
|
||||
|
||||
export function say(message: string): void {
|
||||
process.stdout.write(`==> ${message}\n`);
|
||||
const stream = progressStderrDepth > 0 ? process.stderr : process.stdout;
|
||||
stream.write(`==> ${message}\n`);
|
||||
}
|
||||
|
||||
export function warn(message: string): void {
|
||||
process.stderr.write(`warn: ${message}\n`);
|
||||
}
|
||||
|
||||
export async function withProgressOnStderr<T>(fn: () => Promise<T>): Promise<T> {
|
||||
progressStderrDepth++;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
progressStderrDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
export function die(message: string): never {
|
||||
process.stderr.write(`error: ${message}\n`);
|
||||
process.exit(1);
|
||||
@@ -68,9 +79,7 @@ function signalHostCommandProcess(pid: number | undefined, signal: NodeJS.Signal
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code !== "ESRCH") {
|
||||
warn(
|
||||
`failed to send ${signal} to timed host command process ${pid}: ${
|
||||
code ?? String(error)
|
||||
}`,
|
||||
`failed to send ${signal} to timed host command process ${pid}: ${code ?? String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -279,21 +288,20 @@ export function run(command: string, args: string[], options: RunOptions = {}):
|
||||
const env = { ...process.env, ...options.env };
|
||||
const invocation = resolveHostCommandInvocation(command, args, { env });
|
||||
const usesPosixTimedWrapper = process.platform !== "win32" && options.timeoutMs !== undefined;
|
||||
const result =
|
||||
usesPosixTimedWrapper
|
||||
? runPosixTimedCommandSync(invocation, env, options)
|
||||
: spawnSync(invocation.command, invocation.args, {
|
||||
cwd: options.cwd ?? repoRoot,
|
||||
encoding: "utf8",
|
||||
env: invocation.env ?? env,
|
||||
input: options.input,
|
||||
killSignal: "SIGKILL",
|
||||
maxBuffer: HOST_COMMAND_MAX_BUFFER_BYTES,
|
||||
stdio: options.quiet ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
|
||||
shell: invocation.shell,
|
||||
timeout: options.timeoutMs,
|
||||
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
||||
});
|
||||
const result = usesPosixTimedWrapper
|
||||
? runPosixTimedCommandSync(invocation, env, options)
|
||||
: spawnSync(invocation.command, invocation.args, {
|
||||
cwd: options.cwd ?? repoRoot,
|
||||
encoding: "utf8",
|
||||
env: invocation.env ?? env,
|
||||
input: options.input,
|
||||
killSignal: "SIGKILL",
|
||||
maxBuffer: HOST_COMMAND_MAX_BUFFER_BYTES,
|
||||
stdio: options.quiet ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
|
||||
shell: invocation.shell,
|
||||
timeout: options.timeoutMs,
|
||||
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
||||
});
|
||||
|
||||
let wrapperTimedOut = false;
|
||||
if (usesPosixTimedWrapper) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
say,
|
||||
shellQuote,
|
||||
warn,
|
||||
withProgressOnStderr,
|
||||
writeJson,
|
||||
writeSummaryMarkdown,
|
||||
type Mode,
|
||||
@@ -828,5 +829,6 @@ fi`,
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
await mkdir(repoRoot, { recursive: true });
|
||||
await new LinuxSmoke(options).run();
|
||||
const runSmoke = () => new LinuxSmoke(options).run();
|
||||
await (options.json ? withProgressOnStderr(runSmoke) : runSmoke());
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
shellQuote,
|
||||
startHostServer,
|
||||
warn,
|
||||
withProgressOnStderr,
|
||||
writeJson,
|
||||
writeSummaryMarkdown,
|
||||
type HostServer,
|
||||
@@ -1213,7 +1214,10 @@ fi`,
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
await new MacosSmoke(parseArgs(process.argv.slice(2))).run().catch((error: unknown) => {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const runSmoke = () => new MacosSmoke(options).run();
|
||||
const runPromise = options.json ? withProgressOnStderr(runSmoke) : runSmoke();
|
||||
await runPromise.catch((error: unknown) => {
|
||||
die(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
say,
|
||||
shellQuote,
|
||||
startHostServer,
|
||||
withProgressOnStderr,
|
||||
writeSummaryMarkdown,
|
||||
writeJson,
|
||||
type HostServer,
|
||||
@@ -1307,7 +1308,10 @@ export class NpmUpdateSmoke {
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
await new NpmUpdateSmoke(parseArgs(process.argv.slice(2))).run().catch((error: unknown) => {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const runSmoke = () => new NpmUpdateSmoke(options).run();
|
||||
const runPromise = options.json ? withProgressOnStderr(runSmoke) : runSmoke();
|
||||
await runPromise.catch((error: unknown) => {
|
||||
die(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
run,
|
||||
say,
|
||||
warn,
|
||||
withProgressOnStderr,
|
||||
writeSummaryMarkdown,
|
||||
writeJson,
|
||||
type Mode,
|
||||
@@ -821,7 +822,10 @@ if (-not $agentOk) { throw 'openclaw agent finished without OK response' }`,
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
await new WindowsSmoke(parseArgs(process.argv.slice(2))).run().catch((error: unknown) => {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const runSmoke = () => new WindowsSmoke(options).run();
|
||||
const runPromise = options.json ? withProgressOnStderr(runSmoke) : runSmoke();
|
||||
await runPromise.catch((error: unknown) => {
|
||||
die(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
resolveWindowsProviderAuth,
|
||||
run,
|
||||
shellQuote,
|
||||
withProgressOnStderr,
|
||||
} from "../../scripts/e2e/parallels/common.ts";
|
||||
import { resolveHostCommandInvocation } from "../../scripts/e2e/parallels/host-command.ts";
|
||||
import { testing as hostServerTesting } from "../../scripts/e2e/parallels/host-server.ts";
|
||||
@@ -327,6 +328,26 @@ describe("Parallels smoke model selection", () => {
|
||||
expect(retained).toBe(`${"a".repeat(2)}${"b".repeat(10)}`);
|
||||
});
|
||||
|
||||
it("keeps JSON-mode progress off stdout", async () => {
|
||||
const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
||||
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
||||
try {
|
||||
await withProgressOnStderr(async () => {
|
||||
const { say } = await import("../../scripts/e2e/parallels/common.ts");
|
||||
say("progress");
|
||||
process.stdout.write('{"ok":true}\n');
|
||||
});
|
||||
|
||||
expect(stdoutWrite).toHaveBeenCalledTimes(1);
|
||||
expect(stdoutWrite).toHaveBeenCalledWith('{"ok":true}\n');
|
||||
expect(JSON.parse(String(stdoutWrite.mock.calls[0]?.[0]))).toEqual({ ok: true });
|
||||
expect(stderrWrite).toHaveBeenCalledWith("==> progress\n");
|
||||
} finally {
|
||||
stdoutWrite.mockRestore();
|
||||
stderrWrite.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("waits for host artifact server exit after SIGKILL before stop resolves", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user