refactor: align package folder names with npm package names
CI / check (pull_request) Failing after 8m30s
CI / check (pull_request) Failing after 8m30s
Rename packages/ subdirectories to match their @united-workforce/* scope: cli-workflow → cli workflow-agent-builtin → agent-builtin workflow-agent-claude-code → agent-claude-code workflow-agent-hermes → agent-hermes workflow-dashboard → dashboard workflow-protocol → protocol workflow-util-agent → util-agent workflow-util → util Updated all tsconfig references, scripts, and active docs. Historical docs (docs/plans/, docs/superpowers/) left as-is. Closes #21
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { ThreadId } from "@united-workforce/protocol";
|
||||
import type { AgentContext } from "@united-workforce/util-agent";
|
||||
import { buildClaudeCodePrompt } from "../src/claude-code.js";
|
||||
|
||||
function makeCtx(overrides: Partial<AgentContext> = {}): AgentContext {
|
||||
return {
|
||||
threadId: "01JTEST0000000000000000000" as ThreadId,
|
||||
edgePrompt: "Proceed with the assigned role.",
|
||||
isFirstVisit: true,
|
||||
workflow: {
|
||||
roles: {
|
||||
developer: {
|
||||
description: "TDD implementation per test spec",
|
||||
goal: "Write code",
|
||||
capabilities: ["coding"],
|
||||
procedure: "1. Read spec\n2. Write code",
|
||||
output: "List files changed",
|
||||
frontmatter: "",
|
||||
},
|
||||
},
|
||||
conditions: {},
|
||||
graph: {},
|
||||
},
|
||||
role: "developer",
|
||||
start: { prompt: "Fix the bug", workflowHash: "abc123", threadId: "t1" },
|
||||
steps: [],
|
||||
store: {} as AgentContext["store"],
|
||||
outputFormatInstruction: "Use YAML frontmatter",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildClaudeCodePrompt", () => {
|
||||
test("assembles outputFormatInstruction + role prompt + task prompt", () => {
|
||||
const result = buildClaudeCodePrompt(makeCtx());
|
||||
expect(result).toMatch(/^Use YAML frontmatter/);
|
||||
expect(result).toContain("Write code");
|
||||
expect(result).toContain("## Task\nFix the bug");
|
||||
});
|
||||
|
||||
test("includes previous steps with content on first visit", () => {
|
||||
const ctx = makeCtx({
|
||||
steps: [
|
||||
{
|
||||
role: "planner",
|
||||
output: '{"plan":"do X"}',
|
||||
agent: "hermes",
|
||||
detail: "detail-1",
|
||||
edgePrompt: "Create a plan.",
|
||||
content: "Here is my detailed plan for doing X.",
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = buildClaudeCodePrompt(ctx);
|
||||
expect(result).toContain("## What Happened Since Your Last Turn");
|
||||
expect(result).toContain("Step 1: planner");
|
||||
expect(result).toContain("do X");
|
||||
// First visit should include step content
|
||||
expect(result).toContain("Here is my detailed plan for doing X.");
|
||||
});
|
||||
|
||||
test("re-entry shows steps since last visit without content", () => {
|
||||
const ctx = makeCtx({
|
||||
isFirstVisit: false,
|
||||
steps: [
|
||||
{
|
||||
role: "developer",
|
||||
output: '{"status":"done"}',
|
||||
agent: "claude-code",
|
||||
detail: "detail-1",
|
||||
edgePrompt: "Implement.",
|
||||
content: "I implemented everything.",
|
||||
},
|
||||
{
|
||||
role: "reviewer",
|
||||
output: '{"approved":false}',
|
||||
agent: "claude-code",
|
||||
detail: "detail-2",
|
||||
edgePrompt: "Review.",
|
||||
content: "Rejected: complexity too high, refactor cmdStepRead.",
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = buildClaudeCodePrompt(ctx);
|
||||
expect(result).toContain("## What Happened Since Your Last Turn");
|
||||
expect(result).toContain("reviewer");
|
||||
expect(result).toContain("approved");
|
||||
});
|
||||
|
||||
test("omits history section when steps array is empty", () => {
|
||||
const result = buildClaudeCodePrompt(makeCtx({ steps: [] }));
|
||||
expect(result).not.toContain("## What Happened Since Your Last Turn");
|
||||
expect(result).toContain("## Current Instruction");
|
||||
});
|
||||
|
||||
test("works without outputFormatInstruction", () => {
|
||||
const result = buildClaudeCodePrompt(makeCtx({ outputFormatInstruction: "" }));
|
||||
expect(result).not.toMatch(/^\s*\n/);
|
||||
expect(result).toContain("Write code");
|
||||
expect(result).toContain("## Task");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,487 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createMemoryStore, walk } from "@ocas/core";
|
||||
import {
|
||||
parseClaudeCodeJsonOutput,
|
||||
parseClaudeCodeStreamOutput,
|
||||
storeClaudeCodeDetail,
|
||||
storeClaudeCodeRawOutput,
|
||||
} from "../src/session-detail.js";
|
||||
import type { ClaudeCodeParsedResult } from "../src/types.js";
|
||||
|
||||
describe("parseClaudeCodeJsonOutput", () => {
|
||||
test("parses valid claude -p --output-format json output", () => {
|
||||
const stdout = JSON.stringify({
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
result: "Done fixing bug",
|
||||
session_id: "75e2167f-abc",
|
||||
num_turns: 3,
|
||||
total_cost_usd: 0.08,
|
||||
duration_ms: 10276,
|
||||
stop_reason: "end_turn",
|
||||
usage: { input_tokens: 100, output_tokens: 50 },
|
||||
});
|
||||
const parsed = parseClaudeCodeJsonOutput(stdout);
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.type).toBe("result");
|
||||
expect(parsed!.subtype).toBe("success");
|
||||
expect(parsed!.result).toBe("Done fixing bug");
|
||||
expect(parsed!.sessionId).toBe("75e2167f-abc");
|
||||
expect(parsed!.numTurns).toBe(3);
|
||||
expect(parsed!.totalCostUsd).toBe(0.08);
|
||||
expect(parsed!.durationMs).toBe(10276);
|
||||
expect(parsed!.stopReason).toBe("end_turn");
|
||||
expect(parsed!.usage.inputTokens).toBe(100);
|
||||
expect(parsed!.usage.outputTokens).toBe(50);
|
||||
expect(parsed!.turns).toEqual([]);
|
||||
});
|
||||
|
||||
test("returns null for non-JSON output", () => {
|
||||
const parsed = parseClaudeCodeJsonOutput("Some random text\nwithout JSON");
|
||||
expect(parsed).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null when session_id is missing", () => {
|
||||
const stdout = JSON.stringify({ type: "result", result: "hi", subtype: "success" });
|
||||
const parsed = parseClaudeCodeJsonOutput(stdout);
|
||||
expect(parsed).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseClaudeCodeStreamOutput", () => {
|
||||
test("parses stream-json output with turns", () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
session_id: "sess-123",
|
||||
model: "claude-sonnet-4.5",
|
||||
tools: ["Bash", "Read"],
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "I'll list the files." },
|
||||
{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "ls" } },
|
||||
],
|
||||
},
|
||||
session_id: "sess-123",
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "user",
|
||||
message: {
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "tool_1", content: "file1.ts\nfile2.ts" }],
|
||||
},
|
||||
session_id: "sess-123",
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "There are 2 files." }],
|
||||
},
|
||||
session_id: "sess-123",
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
result: "There are 2 files.",
|
||||
session_id: "sess-123",
|
||||
num_turns: 2,
|
||||
total_cost_usd: 0.05,
|
||||
duration_ms: 5000,
|
||||
stop_reason: "end_turn",
|
||||
usage: {
|
||||
input_tokens: 200,
|
||||
output_tokens: 30,
|
||||
cache_read_input_tokens: 100,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
}),
|
||||
];
|
||||
const stdout = lines.join("\n");
|
||||
const parsed = parseClaudeCodeStreamOutput(stdout);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.model).toBe("claude-sonnet-4.5");
|
||||
expect(parsed!.sessionId).toBe("sess-123");
|
||||
expect(parsed!.result).toBe("There are 2 files.");
|
||||
expect(parsed!.stopReason).toBe("end_turn");
|
||||
expect(parsed!.usage.inputTokens).toBe(200);
|
||||
expect(parsed!.usage.outputTokens).toBe(30);
|
||||
expect(parsed!.usage.cacheReadInputTokens).toBe(100);
|
||||
|
||||
// Turns: assistant(text+tool), tool_result, assistant(text)
|
||||
expect(parsed!.turns).toHaveLength(3);
|
||||
expect(parsed!.turns[0]!.role).toBe("assistant");
|
||||
expect(parsed!.turns[0]!.content).toBe("I'll list the files.");
|
||||
expect(parsed!.turns[0]!.toolCalls).toHaveLength(1);
|
||||
expect(parsed!.turns[0]!.toolCalls![0]!.name).toBe("Bash");
|
||||
expect(parsed!.turns[1]!.role).toBe("tool_result");
|
||||
expect(parsed!.turns[1]!.content).toBe("file1.ts\nfile2.ts");
|
||||
expect(parsed!.turns[2]!.role).toBe("assistant");
|
||||
expect(parsed!.turns[2]!.content).toBe("There are 2 files.");
|
||||
expect(parsed!.turns[2]!.toolCalls).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null when no result line", () => {
|
||||
const stdout = JSON.stringify({ type: "system", model: "test" });
|
||||
expect(parseClaudeCodeStreamOutput(stdout)).toBeNull();
|
||||
});
|
||||
|
||||
test("skips invalid JSON lines gracefully", () => {
|
||||
const lines = [
|
||||
"not json",
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
result: "ok",
|
||||
session_id: "s1",
|
||||
num_turns: 1,
|
||||
total_cost_usd: 0.01,
|
||||
duration_ms: 1000,
|
||||
stop_reason: "end_turn",
|
||||
usage: {},
|
||||
}),
|
||||
];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.result).toBe("ok");
|
||||
expect(parsed!.turns).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseClaudeCodeStreamOutput — helper extraction", () => {
|
||||
test("processSystemLine sets model from system message", () => {
|
||||
const lines = [
|
||||
JSON.stringify({ type: "system", model: "claude-opus-4" }),
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
result: "ok",
|
||||
session_id: "s1",
|
||||
num_turns: 0,
|
||||
total_cost_usd: 0,
|
||||
duration_ms: 0,
|
||||
stop_reason: "end_turn",
|
||||
}),
|
||||
];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.model).toBe("claude-opus-4");
|
||||
});
|
||||
|
||||
test("processAssistantLine skips empty content", () => {
|
||||
const lines = [
|
||||
JSON.stringify({ type: "assistant", message: { role: "assistant", content: [] } }),
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
result: "ok",
|
||||
session_id: "s1",
|
||||
num_turns: 0,
|
||||
total_cost_usd: 0,
|
||||
duration_ms: 0,
|
||||
stop_reason: "end_turn",
|
||||
}),
|
||||
];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.turns).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("processUserLine skips when no tool_result items", () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
type: "user",
|
||||
message: { role: "user", content: [{ type: "text", text: "hi" }] },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
result: "ok",
|
||||
session_id: "s1",
|
||||
num_turns: 0,
|
||||
total_cost_usd: 0,
|
||||
duration_ms: 0,
|
||||
stop_reason: "end_turn",
|
||||
}),
|
||||
];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.turns).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("turn indices are sequential across mixed assistant and user lines", () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [{ type: "text", text: "A" }] },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "user",
|
||||
message: { role: "user", content: [{ type: "tool_result", content: "R" }] },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [{ type: "text", text: "B" }] },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
result: "ok",
|
||||
session_id: "s1",
|
||||
num_turns: 3,
|
||||
total_cost_usd: 0,
|
||||
duration_ms: 0,
|
||||
stop_reason: "end_turn",
|
||||
}),
|
||||
];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.turns).toHaveLength(3);
|
||||
expect(parsed!.turns.map((t) => t.index)).toEqual([0, 1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("storeClaudeCodeDetail", () => {
|
||||
const baseParsed: ClaudeCodeParsedResult = {
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
result: "The answer",
|
||||
sessionId: "abc-123",
|
||||
numTurns: 5,
|
||||
totalCostUsd: 0.12,
|
||||
durationMs: 15000,
|
||||
model: "claude-sonnet-4.5",
|
||||
stopReason: "end_turn",
|
||||
usage: {
|
||||
inputTokens: 100,
|
||||
outputTokens: 50,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
},
|
||||
turns: [
|
||||
{ index: 0, role: "assistant", content: "hello", toolCalls: null },
|
||||
{ index: 1, role: "tool_result", content: "world", toolCalls: null },
|
||||
],
|
||||
};
|
||||
|
||||
test("stores detail with per-turn CAS nodes", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { detailHash, output, sessionId } = await storeClaudeCodeDetail(store, baseParsed);
|
||||
|
||||
expect(detailHash).toHaveLength(13);
|
||||
expect(output).toBe("The answer");
|
||||
expect(sessionId).toBe("abc-123");
|
||||
|
||||
const node = await store.get(detailHash);
|
||||
expect(node).not.toBeNull();
|
||||
expect(node!.payload.model).toBe("claude-sonnet-4.5");
|
||||
expect(node!.payload.stopReason).toBe("end_turn");
|
||||
expect(node!.payload.usage.inputTokens).toBe(100);
|
||||
expect(node!.payload.turns).toHaveLength(2);
|
||||
|
||||
// Verify turn CAS nodes
|
||||
const turn0 = await store.get(node!.payload.turns[0]);
|
||||
expect(turn0).not.toBeNull();
|
||||
expect(turn0!.payload.role).toBe("assistant");
|
||||
expect(turn0!.payload.content).toBe("hello");
|
||||
});
|
||||
|
||||
test("detail node is walkable from root", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { detailHash } = await storeClaudeCodeDetail(store, baseParsed);
|
||||
const visited: string[] = [];
|
||||
walk(store, detailHash, (hash) => visited.push(hash));
|
||||
expect(visited.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseClaudeCodeStreamOutput — incomplete output (no result line)", () => {
|
||||
test("Test 1.1: parses stream with turns but no result line", () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
session_id: "sess-incomplete-1",
|
||||
model: "claude-sonnet-4.5",
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Starting work..." }],
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "This is the last assistant message." }],
|
||||
},
|
||||
}),
|
||||
];
|
||||
const stdout = lines.join("\n");
|
||||
const parsed = parseClaudeCodeStreamOutput(stdout);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.subtype).toBe("incomplete");
|
||||
expect(parsed!.result).toBe("This is the last assistant message.");
|
||||
expect(parsed!.sessionId).toBe("sess-incomplete-1");
|
||||
expect(parsed!.model).toBe("claude-sonnet-4.5");
|
||||
expect(parsed!.turns).toHaveLength(2);
|
||||
expect(parsed!.stopReason).toBe("incomplete_no_result_line");
|
||||
expect(parsed!.numTurns).toBe(2);
|
||||
expect(parsed!.durationMs).toBe(0);
|
||||
expect(parsed!.totalCostUsd).toBe(0);
|
||||
});
|
||||
|
||||
test("Test 1.2: parses stream with no turns and no result line", () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
type: "system",
|
||||
session_id: "sess-no-turns",
|
||||
model: "claude-opus-4",
|
||||
}),
|
||||
];
|
||||
const stdout = lines.join("\n");
|
||||
const parsed = parseClaudeCodeStreamOutput(stdout);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.subtype).toBe("incomplete");
|
||||
expect(parsed!.result).toBe("");
|
||||
expect(parsed!.sessionId).toBe("sess-no-turns");
|
||||
expect(parsed!.model).toBe("claude-opus-4");
|
||||
expect(parsed!.turns).toHaveLength(0);
|
||||
expect(parsed!.stopReason).toBe("incomplete_no_result_line");
|
||||
});
|
||||
|
||||
test("Test 1.3: returns null for completely empty output", () => {
|
||||
const parsed1 = parseClaudeCodeStreamOutput("");
|
||||
expect(parsed1).toBeNull();
|
||||
|
||||
const parsed2 = parseClaudeCodeStreamOutput(" \n \n ");
|
||||
expect(parsed2).toBeNull();
|
||||
});
|
||||
|
||||
test("Test 1.4: returns null for malformed JSON lines only", () => {
|
||||
const stdout = "not json\n{broken json\n[invalid";
|
||||
const parsed = parseClaudeCodeStreamOutput(stdout);
|
||||
expect(parsed).toBeNull();
|
||||
});
|
||||
|
||||
test("Test 6.1: extracts from last assistant text-only turn", () => {
|
||||
const lines = [
|
||||
JSON.stringify({ type: "system", session_id: "s1", model: "test" }),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [{ type: "text", text: "First message" }] },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [{ type: "text", text: "Last message" }] },
|
||||
}),
|
||||
];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.result).toBe("Last message");
|
||||
});
|
||||
|
||||
test("Test 6.2: extracts from last assistant turn with tool calls", () => {
|
||||
const lines = [
|
||||
JSON.stringify({ type: "system", session_id: "s1", model: "test" }),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "Text with tools" },
|
||||
{ type: "tool_use", name: "Bash", input: { command: "ls" } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.result).toBe("Text with tools");
|
||||
});
|
||||
|
||||
test("Test 6.3: returns empty string when no assistant turns", () => {
|
||||
const lines = [JSON.stringify({ type: "system", session_id: "s1", model: "test" })];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.result).toBe("");
|
||||
});
|
||||
|
||||
test("Test 6.4: extracts from most recent assistant turn before tool_result", () => {
|
||||
const lines = [
|
||||
JSON.stringify({ type: "system", session_id: "s1", model: "test" }),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [{ type: "text", text: "Before tool call" }] },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "user",
|
||||
message: { role: "user", content: [{ type: "tool_result", content: "tool output" }] },
|
||||
}),
|
||||
];
|
||||
const parsed = parseClaudeCodeStreamOutput(lines.join("\n"));
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.result).toBe("Before tool call");
|
||||
});
|
||||
});
|
||||
|
||||
describe("storeClaudeCodeDetail — incomplete results", () => {
|
||||
test("Test 4.1: stores incomplete result as detail", async () => {
|
||||
const store = createMemoryStore();
|
||||
const incompleteParsed: ClaudeCodeParsedResult = {
|
||||
type: "result",
|
||||
subtype: "incomplete",
|
||||
result: "Partial output",
|
||||
sessionId: "sess-incomplete",
|
||||
numTurns: 2,
|
||||
totalCostUsd: 0,
|
||||
durationMs: 0,
|
||||
model: "claude-sonnet-4.5",
|
||||
stopReason: "incomplete_no_result_line",
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadInputTokens: 0,
|
||||
cacheCreationInputTokens: 0,
|
||||
},
|
||||
turns: [
|
||||
{ index: 0, role: "assistant", content: "Turn 1", toolCalls: null },
|
||||
{ index: 1, role: "assistant", content: "Partial output", toolCalls: null },
|
||||
],
|
||||
};
|
||||
|
||||
const { detailHash, output, sessionId } = await storeClaudeCodeDetail(store, incompleteParsed);
|
||||
|
||||
expect(detailHash).toHaveLength(13);
|
||||
expect(output).toBe("Partial output");
|
||||
expect(sessionId).toBe("sess-incomplete");
|
||||
|
||||
const node = await store.get(detailHash);
|
||||
expect(node).not.toBeNull();
|
||||
expect(node!.payload.subtype).toBe("incomplete");
|
||||
expect(node!.payload.stopReason).toBe("incomplete_no_result_line");
|
||||
expect(node!.payload.turns).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("storeClaudeCodeRawOutput", () => {
|
||||
test("stores raw text when JSON parsing fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const rawText = "Claude produced plain text without JSON";
|
||||
const hash = await storeClaudeCodeRawOutput(store, rawText);
|
||||
expect(hash).toHaveLength(13);
|
||||
const node = await store.get(hash);
|
||||
expect(node).not.toBeNull();
|
||||
expect(node!.payload.text).toBe(rawText);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user