mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix: tolerate qa cli json startup logs
This commit is contained in:
@@ -29,16 +29,26 @@ import {
|
||||
waitForMemorySearchMatch,
|
||||
} from "./suite-runtime-agent-process.js";
|
||||
|
||||
type MockEmitter = {
|
||||
emit: (eventName: string | symbol, ...args: unknown[]) => boolean;
|
||||
on: (eventName: string | symbol, listener: (...args: unknown[]) => void) => MockEmitter;
|
||||
once: (eventName: string | symbol, listener: (...args: unknown[]) => void) => MockEmitter;
|
||||
};
|
||||
|
||||
type MockChildProcess = MockEmitter & {
|
||||
stdout: MockEmitter;
|
||||
stderr: MockEmitter;
|
||||
kill: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
function createMockEmitter() {
|
||||
return new EventEmitter() as unknown as MockEmitter;
|
||||
}
|
||||
|
||||
function createSpawnedProcess() {
|
||||
const child = new EventEmitter() as EventEmitter & {
|
||||
stdout: EventEmitter;
|
||||
stderr: EventEmitter;
|
||||
kill: ReturnType<typeof vi.fn>;
|
||||
once: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
on: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
};
|
||||
child.stdout = new EventEmitter();
|
||||
child.stderr = new EventEmitter();
|
||||
const child = createMockEmitter() as MockChildProcess;
|
||||
child.stdout = createMockEmitter();
|
||||
child.stderr = createMockEmitter();
|
||||
child.kill = vi.fn();
|
||||
return child;
|
||||
}
|
||||
@@ -117,6 +127,68 @@ describe("qa suite runtime agent process helpers", () => {
|
||||
await expect(pending).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("parses json qa cli output after colored startup logs", async () => {
|
||||
const child = createSpawnedProcess();
|
||||
spawnMock.mockReturnValue(child);
|
||||
|
||||
const pending = runQaCli(
|
||||
{
|
||||
repoRoot: "/repo",
|
||||
gateway: {
|
||||
tempRoot: "/tmp/runtime",
|
||||
runtimeEnv: {},
|
||||
},
|
||||
primaryModel: "openai/gpt-5.4",
|
||||
alternateModel: "openai/gpt-5.4-mini",
|
||||
providerMode: "mock-openai",
|
||||
} as never,
|
||||
["memory", "search", "--json"],
|
||||
{ json: true },
|
||||
);
|
||||
|
||||
await waitForSpawnCount(1);
|
||||
child.stdout.emit(
|
||||
"data",
|
||||
Buffer.from(
|
||||
'\u001b[35m[plugins]\u001b[39m \u001b[36mcodex installed bundled runtime deps\u001b[39m\n{"results":[{"text":"ORBIT-10"}]}\n',
|
||||
),
|
||||
);
|
||||
child.emit("exit", 0);
|
||||
|
||||
await expect(pending).resolves.toEqual({ results: [{ text: "ORBIT-10" }] });
|
||||
});
|
||||
|
||||
it("parses pretty json qa cli output after startup logs", async () => {
|
||||
const child = createSpawnedProcess();
|
||||
spawnMock.mockReturnValue(child);
|
||||
|
||||
const pending = runQaCli(
|
||||
{
|
||||
repoRoot: "/repo",
|
||||
gateway: {
|
||||
tempRoot: "/tmp/runtime",
|
||||
runtimeEnv: {},
|
||||
},
|
||||
primaryModel: "openai/gpt-5.4",
|
||||
alternateModel: "openai/gpt-5.4-mini",
|
||||
providerMode: "mock-openai",
|
||||
} as never,
|
||||
["memory", "search", "--json"],
|
||||
{ json: true },
|
||||
);
|
||||
|
||||
await waitForSpawnCount(1);
|
||||
child.stdout.emit(
|
||||
"data",
|
||||
Buffer.from(
|
||||
'[plugins] memory-core installed bundled runtime deps\n{\n "results": [\n {\n "text": "ORBIT-10"\n }\n ]\n}\n',
|
||||
),
|
||||
);
|
||||
child.emit("exit", 0);
|
||||
|
||||
await expect(pending).resolves.toEqual({ results: [{ text: "ORBIT-10" }] });
|
||||
});
|
||||
|
||||
it("starts an agent run with transport-derived delivery metadata", async () => {
|
||||
const gatewayCall = vi.fn(async () => ({ runId: "run-1" }));
|
||||
const env = {
|
||||
|
||||
@@ -10,6 +10,50 @@ type QaMemorySearchResult = {
|
||||
results?: Array<{ snippet?: string; text?: string; path?: string }>;
|
||||
};
|
||||
|
||||
const ANSI_ESCAPE_PATTERN = new RegExp(String.raw`\x1B\[[0-?]*[ -/]*[@-~]`, "g");
|
||||
|
||||
function stripAnsiCodes(text: string) {
|
||||
return text.replace(ANSI_ESCAPE_PATTERN, "");
|
||||
}
|
||||
|
||||
function parseQaCliJsonOutput(text: string) {
|
||||
const cleaned = stripAnsiCodes(text).trim();
|
||||
if (!cleaned) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(cleaned) as unknown;
|
||||
} catch {
|
||||
// Some startup repair logs are emitted on stdout before command JSON.
|
||||
const lines = cleaned.split(/\r?\n/);
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const candidate = lines[index].trim();
|
||||
if (!candidate.startsWith("{") && !candidate.startsWith("[")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(lines.slice(index).join("\n")) as unknown;
|
||||
} catch {
|
||||
// Keep looking for the actual payload start.
|
||||
}
|
||||
}
|
||||
|
||||
// Keep a line-oriented fallback for compact payloads followed by diagnostics.
|
||||
for (const line of lines.toReversed()) {
|
||||
const candidate = line.trim();
|
||||
if (!candidate.startsWith("{") && !candidate.startsWith("[")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(candidate) as unknown;
|
||||
} catch {
|
||||
// Keep looking for the actual payload line.
|
||||
}
|
||||
}
|
||||
throw new Error(`qa cli returned non-JSON stdout: ${cleaned.slice(0, 240)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runQaCli(
|
||||
env: Pick<
|
||||
QaSuiteRuntimeEnv,
|
||||
@@ -55,7 +99,7 @@ async function runQaCli(
|
||||
if (!opts?.json) {
|
||||
return text;
|
||||
}
|
||||
return text ? (JSON.parse(text) as unknown) : {};
|
||||
return parseQaCliJsonOutput(text);
|
||||
}
|
||||
|
||||
async function startAgentRun(
|
||||
|
||||
Reference in New Issue
Block a user