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>
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user