31695e89a8
Claude Code CLI adapter for the workflow engine, mirroring workflow-agent-hermes architecture. Spawns `claude -p` with `--output-format json` for structured output parsing. Refs #391
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { createMemoryStore, walk } from "@uncaged/json-cas";
|
|
import {
|
|
parseClaudeCodeJsonOutput,
|
|
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,
|
|
});
|
|
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);
|
|
});
|
|
|
|
test("parses error_max_turns result", () => {
|
|
const stdout = JSON.stringify({
|
|
type: "result",
|
|
subtype: "error_max_turns",
|
|
result: "Ran out of turns",
|
|
session_id: "abc-def",
|
|
num_turns: 90,
|
|
total_cost_usd: 1.5,
|
|
duration_ms: 50000,
|
|
});
|
|
const parsed = parseClaudeCodeJsonOutput(stdout);
|
|
expect(parsed).not.toBeNull();
|
|
expect(parsed!.subtype).toBe("error_max_turns");
|
|
expect(parsed!.result).toBe("Ran out of turns");
|
|
});
|
|
|
|
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("storeClaudeCodeDetail", () => {
|
|
test("stores claude-code-detail CAS node and returns output + detailHash", async () => {
|
|
const store = createMemoryStore();
|
|
const parsed: ClaudeCodeParsedResult = {
|
|
type: "result",
|
|
subtype: "success",
|
|
result: "The answer",
|
|
sessionId: "abc-123",
|
|
numTurns: 5,
|
|
totalCostUsd: 0.12,
|
|
durationMs: 15000,
|
|
};
|
|
|
|
const { detailHash, output, sessionId } = await storeClaudeCodeDetail(store, parsed);
|
|
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.sessionId).toBe("abc-123");
|
|
expect(node!.payload.numTurns).toBe(5);
|
|
expect(node!.payload.totalCostUsd).toBe(0.12);
|
|
expect(node!.payload.durationMs).toBe(15000);
|
|
});
|
|
|
|
test("detail node is walkable from root", async () => {
|
|
const store = createMemoryStore();
|
|
const parsed: ClaudeCodeParsedResult = {
|
|
type: "result",
|
|
subtype: "success",
|
|
result: "walkable test",
|
|
sessionId: "walk-123",
|
|
numTurns: 1,
|
|
totalCostUsd: 0.01,
|
|
durationMs: 1000,
|
|
};
|
|
|
|
const { detailHash } = await storeClaudeCodeDetail(store, parsed);
|
|
const visited: string[] = [];
|
|
walk(store, detailHash, (hash) => visited.push(hash));
|
|
expect(visited.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|