From 90551414b12a5a55bf6581af7499f0aed7501920 Mon Sep 17 00:00:00 2001 From: liujing Date: Fri, 17 Jul 2026 14:20:59 +0800 Subject: [PATCH] fix: support Claude stream-json verbose mode --- src/workflow-executor.test.ts | 64 ++++++++++++++++++++++++++++------- src/workflow-executor.ts | 2 ++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/workflow-executor.test.ts b/src/workflow-executor.test.ts index eba6cb1..641deb8 100644 --- a/src/workflow-executor.test.ts +++ b/src/workflow-executor.test.ts @@ -154,7 +154,7 @@ describe("ClaudeAdapter", () => { beforeEach(() => { tmpDir = makeTmpDir(); }); afterEach(() => { cleanupDir(tmpDir); }); - it("start includes --session-id and -p flags", async () => { + it("start includes --session-id, -p, --verbose, and --output-format flags", async () => { const argsFile = path.join(tmpDir, "args.txt"); const fakeBin = writeFakeCli(tmpDir, { argsFile, @@ -170,10 +170,17 @@ describe("ClaudeAdapter", () => { config: { binary: fakeBin }, }); - const capturedArgs = fs.existsSync(argsFile) ? fs.readFileSync(argsFile, "utf8") : ""; + const capturedArgs = fs.existsSync(argsFile) + ? fs.readFileSync(argsFile, "utf8").split("\0").filter(Boolean) + : []; expect(capturedArgs).toContain("-p"); - expect(capturedArgs).toContain("--output-format"); - expect(capturedArgs).toContain("--session-id"); + expect(capturedArgs).toContain("--verbose"); + const outputFormatIdx = capturedArgs.indexOf("--output-format"); + expect(outputFormatIdx).toBeGreaterThanOrEqual(0); + expect(capturedArgs[outputFormatIdx + 1]).toBe("stream-json"); + const sessionIdIdx = capturedArgs.indexOf("--session-id"); + expect(sessionIdIdx).toBeGreaterThanOrEqual(0); + expect(capturedArgs[sessionIdIdx + 1]).toMatch(/^[0-9a-f-]{36}$/i); expect(result.sessionHandle?.kind).toBe("claude"); }); @@ -192,9 +199,17 @@ describe("ClaudeAdapter", () => { config: { binary: fakeBin }, }); - const capturedArgs = fs.existsSync(argsFile) ? fs.readFileSync(argsFile, "utf8") : ""; - expect(capturedArgs).toContain("--resume"); - expect(capturedArgs).toContain("captured-session-id"); + const capturedArgs = fs.existsSync(argsFile) + ? fs.readFileSync(argsFile, "utf8").split("\0").filter(Boolean) + : []; + expect(capturedArgs).toContain("-p"); + expect(capturedArgs).toContain("--verbose"); + const outputFormatIdx = capturedArgs.indexOf("--output-format"); + expect(outputFormatIdx).toBeGreaterThanOrEqual(0); + expect(capturedArgs[outputFormatIdx + 1]).toBe("stream-json"); + const resumeIdx = capturedArgs.indexOf("--resume"); + expect(resumeIdx).toBeGreaterThanOrEqual(0); + expect(capturedArgs[resumeIdx + 1]).toBe("captured-session-id"); expect(result.sessionHandle?.kind).toBe("claude"); }); @@ -338,10 +353,15 @@ printf '%s\\0' "$@" >> "${argsFile}" config: { binary: trackingBin }, }); - // 1) --output-format stream-json was passed - const capturedArgs = fs.existsSync(argsFile) ? fs.readFileSync(argsFile, "utf8") : ""; - expect(capturedArgs).toContain("--output-format"); - expect(capturedArgs).toContain("stream-json"); + // 1) --output-format stream-json, -p, --verbose were passed + const capturedArgs = fs.existsSync(argsFile) + ? fs.readFileSync(argsFile, "utf8").split("\0").filter(Boolean) + : []; + expect(capturedArgs).toContain("-p"); + expect(capturedArgs).toContain("--verbose"); + const outputFormatIdx = capturedArgs.indexOf("--output-format"); + expect(outputFormatIdx).toBeGreaterThanOrEqual(0); + expect(capturedArgs[outputFormatIdx + 1]).toBe("stream-json"); // 2) Exact session preservation if (result.sessionHandle?.kind === "claude") { @@ -384,11 +404,17 @@ printf '%s\\0' "$@" >> "${argsFile}" expect(sidecar!.phase).toBe("failed"); }); - it("resume: preserves session ID from the handle", async () => { + it("resume: preserves session ID from the handle and passes expected args", async () => { + const argsFile = path.join(tmpDir, "args.txt"); const fakeBin = makeFakeClaude([ '{"type":"system","subtype":"init","session_id":"resumed-session-999"}', '{"type":"result","result":"completed"}', ]); + const trackingBin = path.join(tmpDir, "tracking-claude.sh"); + fs.writeFileSync(trackingBin, `#!/bin/bash +printf '%s\\0' "$@" >> "${argsFile}" +"${fakeBin}" "$@" +`, { mode: 0o755 }); const adapter = createExecutorAdapter("claude"); const logFilePath = path.join(tmpDir, "claude-resume.log"); @@ -400,9 +426,21 @@ printf '%s\\0' "$@" >> "${argsFile}" runDir: tmpDir, cycleIndex: 2, logFilePath, - config: { binary: fakeBin }, + config: { binary: trackingBin }, }); + const capturedArgs = fs.existsSync(argsFile) + ? fs.readFileSync(argsFile, "utf8").split("\0").filter(Boolean) + : []; + expect(capturedArgs).toContain("-p"); + expect(capturedArgs).toContain("--verbose"); + const outputFormatIdx = capturedArgs.indexOf("--output-format"); + expect(outputFormatIdx).toBeGreaterThanOrEqual(0); + expect(capturedArgs[outputFormatIdx + 1]).toBe("stream-json"); + const resumeIdx = capturedArgs.indexOf("--resume"); + expect(resumeIdx).toBeGreaterThanOrEqual(0); + expect(capturedArgs[resumeIdx + 1]).toBe("resumed-session-999"); + if (result.sessionHandle?.kind === "claude") { expect(result.sessionHandle.sessionId).toBe("resumed-session-999"); } diff --git a/src/workflow-executor.ts b/src/workflow-executor.ts index 9371a61..4c3ad09 100644 --- a/src/workflow-executor.ts +++ b/src/workflow-executor.ts @@ -631,6 +631,7 @@ class ClaudeAdapter implements ExecutorAdapter { const args: string[] = [ "-p", + "--verbose", "--safe-mode", "--permission-mode", "bypassPermissions", "--output-format", "stream-json", @@ -703,6 +704,7 @@ class ClaudeAdapter implements ExecutorAdapter { const args: string[] = [ "-p", + "--verbose", "--safe-mode", "--permission-mode", "bypassPermissions", "--output-format", "stream-json",