fix: detect mid-stream truncation and emit claude_truncated error code (FAR-95)

When Claude produces assistant content (output_tokens > 0) but the stream ends
without a result event, classify the run as truncated mid-stream rather than
falling through to the generic "did not produce a result — check API
credentials" message. The misleading hint pointed operators at auth/model
config when the real cause was pod termination, OOMKill, or CLI crash.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-26 01:54:35 +00:00
committed by Hugh Commit [agent]
parent 818aa0f1d6
commit a2874c0426
4 changed files with 104 additions and 0 deletions
+38
View File
@@ -994,6 +994,44 @@ describe("execute: happy path", () => {
expect(result.errorMessage).toContain("output_tokens: 0");
});
it("returns claude_truncated when assistant produced content but no result event arrived (FAR-95)", async () => {
const truncatedOutput = [
JSON.stringify({ type: "system", subtype: "init", model: "claude-opus-4-7", session_id: "sess_trunc" }),
JSON.stringify({
type: "assistant",
session_id: "sess_trunc",
message: {
id: "msg_trunc",
stop_reason: null,
usage: { input_tokens: 1, output_tokens: 35, cache_creation_input_tokens: 523, cache_read_input_tokens: 46295 },
content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "echo hi" } }],
},
}),
JSON.stringify({
type: "user",
message: { role: "user", content: [{ tool_use_id: "tool_1", type: "tool_result", content: "hi", is_error: false }] },
}),
].join("\n") + "\n";
mockLogFn.mockImplementation(
async (_ns: string, _pod: string, _ctr: string, writable: Writable) => {
writable.write(truncatedOutput);
},
);
mockCoreListPods.mockResolvedValue({
items: [{ metadata: { name: "pod-abc" }, status: { containerStatuses: [{ name: "claude", state: { terminated: { exitCode: 137 } } }] } }],
});
const executePromise = execute(makeCtx());
await vi.advanceTimersByTimeAsync(3_100);
const result = await executePromise;
expect(result.errorCode).toBe("claude_truncated");
expect(result.errorMessage).toContain("truncated mid-stream");
expect(result.errorMessage).toContain("claude-opus-4-7");
expect(result.errorMessage).toContain("exit code 137");
});
it("reconnects log stream and logs status when job completion takes > 3s", async () => {
// Make waitForJobCompletion take 4s so the 3s stream reconnect fires first.
// timeoutSec=4, graceSec=0 → completionTimeoutMs=4000.