Files
paperclip-adapter-claude-k8s/src/ui-parser.test.ts
T
Chris Farhood 545950daf2 Add CLI formatter, fix env forwarding, rename job prefix to agent-claude-
- Add src/cli/ with format-event.ts (printClaudeStreamEvent) exported from
  CLIAdapterModule
- Fix env var forwarding: read from pod spec container env dynamically instead
  of static allowlist; agent config env overrides pod values
- Rename K8s Job prefix from agent- to agent-claude-
- Add fsGroupChangePolicy: "OnRootMismatch" to skip PVC chown on subsequent runs
- Add comprehensive test coverage (159 tests across 5 test files)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 10:47:27 -04:00

252 lines
7.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { parseStdoutLine } from "./ui-parser.js";
const ts = "2026-04-12T00:00:00.000Z";
function parse(line: string) {
return parseStdoutLine(line, ts);
}
describe("parseStdoutLine", () => {
it("returns stdout entry for non-JSON input", () => {
const entries = parse("hello world");
expect(entries).toEqual([{ kind: "stdout", ts, text: "hello world" }]);
});
it("returns stdout entry for null parse result", () => {
const entries = parse(" ");
expect(entries[0]?.kind).toBe("stdout");
});
describe("system/init", () => {
it("parses init event", () => {
const entries = parse(JSON.stringify({
type: "system",
subtype: "init",
model: "claude-opus-4-6",
session_id: "sess_abc",
}));
expect(entries).toEqual([{
kind: "init",
ts,
model: "claude-opus-4-6",
sessionId: "sess_abc",
}]);
});
it("handles missing model with default", () => {
const entries = parse(JSON.stringify({ type: "system", subtype: "init" }));
expect(entries[0]).toMatchObject({ kind: "init", model: "unknown" });
});
});
describe("assistant messages", () => {
it("parses text block", () => {
const entries = parse(JSON.stringify({
type: "assistant",
message: { content: [{ type: "text", text: "Hello there" }] },
}));
expect(entries).toEqual([{ kind: "assistant", ts, text: "Hello there" }]);
});
it("skips empty text blocks", () => {
const entries = parse(JSON.stringify({
type: "assistant",
message: { content: [{ type: "text", text: "" }] },
}));
// Empty content arrays fall back to stdout
expect(entries[0]?.kind).toBe("stdout");
});
it("parses thinking block", () => {
const entries = parse(JSON.stringify({
type: "assistant",
message: { content: [{ type: "thinking", thinking: "Let me think about this" }] },
}));
expect(entries).toEqual([{ kind: "thinking", ts, text: "Let me think about this" }]);
});
it("skips empty thinking blocks", () => {
const entries = parse(JSON.stringify({
type: "assistant",
message: { content: [{ type: "thinking", thinking: "" }] },
}));
// Empty content arrays fall back to stdout
expect(entries[0]?.kind).toBe("stdout");
});
it("parses tool_use block", () => {
const entries = parse(JSON.stringify({
type: "assistant",
message: {
content: [{
type: "tool_use",
name: "Bash",
input: { command: "ls -la" },
id: "tool_123",
}],
},
}));
expect(entries).toEqual([{
kind: "tool_call",
ts,
name: "Bash",
input: { command: "ls -la" },
toolUseId: "tool_123",
}]);
});
it("parses tool_use with tool_use_id fallback", () => {
const entries = parse(JSON.stringify({
type: "assistant",
message: {
content: [{
type: "tool_use",
name: "Bash",
tool_use_id: "tool_fallback",
input: {},
}],
},
}));
const entry = entries[0] as { kind: string; toolUseId?: string };
expect(entry.kind).toBe("tool_call");
expect(entry.toolUseId).toBe("tool_fallback");
});
it("returns stdout as fallback for empty content", () => {
const entries = parse(JSON.stringify({
type: "assistant",
message: { content: [] },
}));
expect(entries[0]?.kind).toBe("stdout");
});
});
describe("user messages", () => {
it("parses user text block", () => {
const entries = parse(JSON.stringify({
type: "user",
message: { content: [{ type: "text", text: "Hello Claude" }] },
}));
expect(entries).toEqual([{ kind: "user", ts, text: "Hello Claude" }]);
});
it("parses tool_result block", () => {
const entries = parse(JSON.stringify({
type: "user",
message: {
content: [{
type: "tool_result",
tool_use_id: "tool_123",
content: "file1.txt\nfile2.txt",
}],
},
}));
expect(entries).toEqual([{
kind: "tool_result",
ts,
toolUseId: "tool_123",
content: "file1.txt\nfile2.txt",
isError: false,
}]);
});
it("marks tool_result as error when is_error is true", () => {
const entries = parse(JSON.stringify({
type: "user",
message: {
content: [{
type: "tool_result",
tool_use_id: "tool_123",
is_error: true,
content: "Permission denied",
}],
},
}));
expect(entries[0]).toMatchObject({ kind: "tool_result", isError: true });
});
it("handles text content array parts", () => {
const entries = parse(JSON.stringify({
type: "user",
message: {
content: [{
type: "tool_result",
tool_use_id: "tool_123",
content: [{ type: "text", text: "part1" }, { type: "text", text: "part2" }],
}],
},
}));
const entry = entries[0] as { kind: string; content: string };
expect(entry.kind).toBe("tool_result");
expect(entry.content).toBe("part1\npart2");
});
});
describe("result", () => {
it("parses result with usage and cost", () => {
const entries = parse(JSON.stringify({
type: "result",
result: "Task completed successfully",
subtype: "stop",
total_cost_usd: 0.0125,
usage: {
input_tokens: 500,
output_tokens: 300,
cache_read_input_tokens: 200,
},
}));
expect(entries[0]).toMatchObject({
kind: "result",
ts,
text: "Task completed successfully",
subtype: "stop",
costUsd: 0.0125,
inputTokens: 500,
outputTokens: 300,
cachedTokens: 200,
isError: false,
errors: [],
});
});
it("marks result as error when is_error is true", () => {
const entries = parse(JSON.stringify({
type: "result",
is_error: true,
errors: ["Something went wrong"],
}));
const entry = entries[0] as { kind: string; isError: boolean };
expect(entry.kind).toBe("result");
expect(entry.isError).toBe(true);
});
it("extracts errors array", () => {
const entries = parse(JSON.stringify({
type: "result",
errors: ["error one", "error two"],
}));
const entry = entries[0] as { kind: string; errors: string[] };
expect(entry.kind).toBe("result");
expect(entry.errors).toEqual(["error one", "error two"]);
});
it("handles non-string errors", () => {
const entries = parse(JSON.stringify({
type: "result",
errors: [{ message: "obj error" }],
}));
const entry = entries[0] as { kind: string; errors: string[] };
expect(entry.kind).toBe("result");
expect(entry.errors).toContain("obj error");
});
});
describe("stderr and system", () => {
it("passes through unknown types as stdout", () => {
const entries = parse(JSON.stringify({ type: "unknown", data: "stuff" }));
expect(entries[0]?.kind).toBe("stdout");
});
});
});