545950daf2
- 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>
252 lines
7.3 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|