test: push coverage to 90%+ on lines for all files except execute.ts (FAR-85)
Overall before: 80.36% lines / 79.06% statements Overall after: 94.65% lines / 93.30% statements Per-file lines coverage (all targets ≥90% except execute.ts): | File | Before | After | |-------------------|--------|--------| | ui-parser.ts | 93.63% | 99.09% | | cli/format-event | 59.85% | 99.27% | | server/execute | 81.47% | 89.64% | | server/job-mfst | 90.30% | 98.78% | | server/k8s-client | 37.50% | 95.83% | | server/log-dedup | 97.77% | 97.77% | | server/parse | 89.85% | 98.55% | | server/skills | 100% | 100% | New tests added: - k8s-client.test.ts: getSelfPodInfo (env-var inheritance, secret volumes, PVC discovery, dnsConfig, all error paths) + kubeconfig file branch - format-event.test.ts: parseStdoutLine (cli) — full event-type matrix, tool_use status branches, errorText fallback paths - ui-parser.test.ts: errorText edge cases, empty event paths - parse.test.ts: errorText fallback to data.message, name, code, JSON - job-manifest.test.ts: workspace context env wiring, linkedIssueIds, paperclipWorkspaces/RuntimeServices JSON, authToken, inherited URLs, prompt-secret + data PVC + secret-volume mount paths - execute.test.ts: parseModelProvider, completionWithGrace, instructionsFilePath read failure, ensureAgentDbPvc throw paths, large-prompt secret create failure, step-limit detection, waitForPod no-pod messaging, init-container ImagePullBackOff / CrashLoopBackOff, main-container CrashLoopBackOff, all-inits-done happy path, skill bundle source loading (SKILL.md + flat-file fallback), SIGTERM handler full body via vi.resetModules() execute.ts remains at 89.64% lines — the residual gap is deep async/timer paths inside streamAndAwaitJob (grace poller, keepalive ticker, log-stream stop-signal/bail timer). Those need fake-timer scaffolding heavier than this batch warrants; tracking separately. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -248,3 +248,181 @@ describe("formatEvent", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
import { parseStdoutLine } from "./format-event.js";
|
||||
|
||||
describe("parseStdoutLine (cli)", () => {
|
||||
const TS = "2026-04-25T22:00:00.000Z";
|
||||
|
||||
it("returns empty for empty input", () => {
|
||||
expect(parseStdoutLine("", TS)).toEqual([]);
|
||||
expect(parseStdoutLine(" ", TS)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns stdout entry for non-JSON input", () => {
|
||||
expect(parseStdoutLine("plain log", TS)).toEqual([{ kind: "stdout", ts: TS, text: "plain log" }]);
|
||||
});
|
||||
|
||||
it("returns stdout entry when JSON parses to a non-object primitive", () => {
|
||||
expect(parseStdoutLine("42", TS)).toEqual([{ kind: "stdout", ts: TS, text: "42" }]);
|
||||
});
|
||||
|
||||
it("renders a text event as an assistant delta", () => {
|
||||
const line = JSON.stringify({ type: "text", part: { text: "Hello" } });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([{ kind: "assistant", ts: TS, text: "Hello", delta: true }]);
|
||||
});
|
||||
|
||||
it("returns empty for text event with empty text", () => {
|
||||
const line = JSON.stringify({ type: "text", part: { text: "" } });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([]);
|
||||
});
|
||||
|
||||
it("renders tool_use status=error as tool_result with isError", () => {
|
||||
const line = JSON.stringify({ type: "tool_use", part: { tool: "bash", id: "t1", state: { status: "error", error: "boom" } } });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([
|
||||
{ kind: "tool_result", ts: TS, toolUseId: "t1", toolName: "bash", content: "boom", isError: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses 'Tool error' fallback when error event has no error string", () => {
|
||||
const line = JSON.stringify({ type: "tool_use", part: { tool: "bash", id: "t1", state: { status: "error" } } });
|
||||
const result = parseStdoutLine(line, TS);
|
||||
expect((result[0] as { content: string }).content).toBe("Tool error");
|
||||
});
|
||||
|
||||
it("renders tool_use status=completed as tool_result with output", () => {
|
||||
const line = JSON.stringify({ type: "tool_use", part: { tool: "bash", id: "t1", state: { status: "completed", output: "ok" } } });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([
|
||||
{ kind: "tool_result", ts: TS, toolUseId: "t1", toolName: "bash", content: "ok", isError: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders tool_use status=done — falls back to description when no output", () => {
|
||||
const line = JSON.stringify({ type: "tool_use", part: { tool: "bash", id: "t1", state: { status: "done", description: "did it" } } });
|
||||
expect((parseStdoutLine(line, TS)[0] as { content: string }).content).toBe("did it");
|
||||
});
|
||||
|
||||
it("renders tool_use status=done — falls back to 'Done' when no output or description", () => {
|
||||
const line = JSON.stringify({ type: "tool_use", part: { tool: "bash", id: "t1", state: { status: "done" } } });
|
||||
expect((parseStdoutLine(line, TS)[0] as { content: string }).content).toBe("Done");
|
||||
});
|
||||
|
||||
it("renders tool_use pending status as tool_call", () => {
|
||||
const line = JSON.stringify({ type: "tool_use", part: { tool: "bash", id: "t1", state: { status: "running", description: "go" } } });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([
|
||||
{ kind: "tool_call", ts: TS, name: "bash", input: "go", toolUseId: "t1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back to part.type then 'tool' when no part.tool name", () => {
|
||||
const line = JSON.stringify({ type: "tool_use", part: { type: "edit", state: { status: "running" } } });
|
||||
expect((parseStdoutLine(line, TS)[0] as { name: string }).name).toBe("edit");
|
||||
const line2 = JSON.stringify({ type: "tool_use", part: { state: { status: "running" } } });
|
||||
expect((parseStdoutLine(line2, TS)[0] as { name: string }).name).toBe("tool");
|
||||
});
|
||||
|
||||
it("renders step_finish with token/cost metrics", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "step_finish",
|
||||
part: {
|
||||
message: "did the thing",
|
||||
reason: "stop",
|
||||
tokens: { input: 100, output: 50, reasoning: 10, cache: { read: 30 } },
|
||||
cost: 0.0123,
|
||||
},
|
||||
});
|
||||
const result = parseStdoutLine(line, TS);
|
||||
expect(result).toEqual([{
|
||||
kind: "result",
|
||||
ts: TS,
|
||||
text: "did the thing",
|
||||
inputTokens: 100,
|
||||
outputTokens: 60,
|
||||
cachedTokens: 30,
|
||||
costUsd: 0.0123,
|
||||
subtype: "stop",
|
||||
isError: false,
|
||||
errors: [],
|
||||
}]);
|
||||
});
|
||||
|
||||
it("renders step_finish with default text when no message", () => {
|
||||
const line = JSON.stringify({ type: "step_finish", part: { reason: "stop" } });
|
||||
expect((parseStdoutLine(line, TS)[0] as { text: string }).text).toBe("Step finished: stop");
|
||||
const line2 = JSON.stringify({ type: "step_finish", part: {} });
|
||||
expect((parseStdoutLine(line2, TS)[0] as { text: string }).text).toBe("Step finished: done");
|
||||
});
|
||||
|
||||
it("renders step_start as a system entry", () => {
|
||||
const line = JSON.stringify({ type: "step_start" });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([{ kind: "system", ts: TS, text: "Starting step…" }]);
|
||||
});
|
||||
|
||||
it("renders assistant event with nested text content", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "assistant",
|
||||
part: { message: { content: [{ type: "text", text: "hi there" }] } },
|
||||
});
|
||||
expect(parseStdoutLine(line, TS)).toEqual([{ kind: "assistant", ts: TS, text: "hi there" }]);
|
||||
});
|
||||
|
||||
it("handles assistant content as a single non-array object", () => {
|
||||
const line = JSON.stringify({
|
||||
type: "assistant",
|
||||
part: { message: { content: { type: "text", text: "single" } } },
|
||||
});
|
||||
expect(parseStdoutLine(line, TS)).toEqual([{ kind: "assistant", ts: TS, text: "single" }]);
|
||||
});
|
||||
|
||||
it("returns empty for assistant event with no extractable text", () => {
|
||||
const line = JSON.stringify({ type: "assistant", part: { message: { content: [{ type: "image" }] } } });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([]);
|
||||
const line2 = JSON.stringify({ type: "assistant", part: {} });
|
||||
expect(parseStdoutLine(line2, TS)).toEqual([]);
|
||||
});
|
||||
|
||||
it("renders error event with errorText", () => {
|
||||
const line = JSON.stringify({ type: "error", error: { message: "broken" } });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([{ kind: "stderr", ts: TS, text: "broken" }]);
|
||||
});
|
||||
|
||||
it("returns empty for error event with empty error string", () => {
|
||||
const line = JSON.stringify({ type: "error", error: "" });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses error.code fallback in errorText", () => {
|
||||
const line = JSON.stringify({ type: "error", error: { code: "E_X" } });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([{ kind: "stderr", ts: TS, text: "E_X" }]);
|
||||
});
|
||||
|
||||
it("uses nested data.message and name fallbacks in errorText", () => {
|
||||
const l1 = JSON.stringify({ type: "error", error: { data: { message: "nested" } } });
|
||||
expect((parseStdoutLine(l1, TS)[0] as { text: string }).text).toBe("nested");
|
||||
const l2 = JSON.stringify({ type: "error", error: { name: "ProviderErr" } });
|
||||
expect((parseStdoutLine(l2, TS)[0] as { text: string }).text).toBe("ProviderErr");
|
||||
});
|
||||
|
||||
it("falls back to JSON.stringify of the error object when nothing else matches", () => {
|
||||
const line = JSON.stringify({ type: "error", error: { weirdKey: "x" } });
|
||||
expect((parseStdoutLine(line, TS)[0] as { text: string }).text).toContain("weirdKey");
|
||||
});
|
||||
|
||||
it("returns empty array for unknown event types", () => {
|
||||
const line = JSON.stringify({ type: "totally_unknown" });
|
||||
expect(parseStdoutLine(line, TS)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatEvent — additional coverage", () => {
|
||||
it("returns empty for safeJsonParse of a non-object primitive", () => {
|
||||
// formatEvent treats a non-object as non-JSON and returns the trimmed line as-is
|
||||
const result = formatEvent("42", false);
|
||||
expect(result).toBe("42");
|
||||
});
|
||||
|
||||
it("returns empty for error event with empty error string", () => {
|
||||
const line = JSON.stringify({ type: "error", error: "" });
|
||||
expect(formatEvent(line, false)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user