feat: add workflow-agent-claude-code package
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
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user