798b80f2f2
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>
227 lines
7.2 KiB
TypeScript
227 lines
7.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { parseOpenCodeJsonl, isOpenCodeUnknownSessionError, isOpenCodeStepLimitResult } from "./parse.js";
|
|
|
|
describe("parseOpenCodeJsonl", () => {
|
|
it("parses text messages", () => {
|
|
const stdout = [
|
|
JSON.stringify({ type: "text", part: { text: "Hello" }, sessionID: "ses_123" }),
|
|
JSON.stringify({ type: "text", part: { text: "World" }, sessionID: "ses_123" }),
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.sessionId).toBe("ses_123");
|
|
expect(result.summary).toBe("Hello\n\nWorld");
|
|
expect(result.errorMessage).toBeNull();
|
|
});
|
|
|
|
it("accumulates usage from step_finish events", () => {
|
|
const stdout = [
|
|
JSON.stringify({
|
|
type: "step_finish",
|
|
part: { tokens: { input: 100, output: 50, reasoning: 20, cache: { read: 80 } }, cost: 0.001 },
|
|
}),
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.usage.inputTokens).toBe(100);
|
|
expect(result.usage.cachedInputTokens).toBe(80);
|
|
expect(result.usage.outputTokens).toBe(70);
|
|
expect(result.costUsd).toBeCloseTo(0.001);
|
|
});
|
|
|
|
it("captures text from step_finish message field", () => {
|
|
const stdout = [
|
|
JSON.stringify({
|
|
type: "step_finish",
|
|
part: { message: "Final response text", tokens: { input: 10, output: 5 } },
|
|
}),
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.summary).toBe("Final response text");
|
|
});
|
|
|
|
it("captures errors from error type events", () => {
|
|
const stdout = [
|
|
JSON.stringify({ type: "error", error: { message: "Something went wrong" } }),
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.errorMessage).toBe("Something went wrong");
|
|
});
|
|
|
|
it("captures tool_use errors with error state", () => {
|
|
const stdout = [
|
|
JSON.stringify({
|
|
type: "tool_use",
|
|
part: { state: { status: "error", error: "Tool failed" } },
|
|
}),
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.errorMessage).toBe("Tool failed");
|
|
});
|
|
|
|
it("extracts sessionId from any event", () => {
|
|
const stdout = [
|
|
JSON.stringify({ type: "text", part: { text: "Hi" }, sessionID: "ses_abc" }),
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.sessionId).toBe("ses_abc");
|
|
});
|
|
|
|
it("handles empty stdout", () => {
|
|
const result = parseOpenCodeJsonl("");
|
|
|
|
expect(result.sessionId).toBeNull();
|
|
expect(result.summary).toBe("");
|
|
expect(result.errorMessage).toBeNull();
|
|
});
|
|
|
|
it("skips malformed JSON lines", () => {
|
|
const stdout = [
|
|
"not json at all",
|
|
JSON.stringify({ type: "text", part: { text: "Valid" }, sessionID: "ses_1" }),
|
|
"",
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.summary).toBe("Valid");
|
|
});
|
|
|
|
it("combines multiple errors", () => {
|
|
const stdout = [
|
|
JSON.stringify({ type: "error", error: { message: "Error 1" } }),
|
|
JSON.stringify({ type: "error", error: { message: "Error 2" } }),
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.errorMessage).toBe("Error 1\nError 2");
|
|
});
|
|
|
|
it("parses nested error message in data field", () => {
|
|
const stdout = [
|
|
JSON.stringify({ type: "error", error: { data: { message: "Nested error" } } }),
|
|
].join("\n");
|
|
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
|
|
expect(result.errorMessage).toBe("Nested error");
|
|
});
|
|
});
|
|
|
|
describe("isOpenCodeStepLimitResult", () => {
|
|
it("returns true for step_finish with reason max_turns", () => {
|
|
const stdout = JSON.stringify({ type: "step_finish", part: { reason: "max_turns", tokens: {} } });
|
|
expect(isOpenCodeStepLimitResult(stdout)).toBe(true);
|
|
});
|
|
|
|
it("returns true for step_finish with reason max_steps", () => {
|
|
const stdout = JSON.stringify({ type: "step_finish", part: { reason: "max_steps", tokens: {} } });
|
|
expect(isOpenCodeStepLimitResult(stdout)).toBe(true);
|
|
});
|
|
|
|
it("returns true for step_finish with reason step_limit", () => {
|
|
const stdout = JSON.stringify({ type: "step_finish", part: { reason: "step_limit", tokens: {} } });
|
|
expect(isOpenCodeStepLimitResult(stdout)).toBe(true);
|
|
});
|
|
|
|
it("returns false for step_finish with reason end_turn", () => {
|
|
const stdout = JSON.stringify({ type: "step_finish", part: { reason: "end_turn", tokens: {} } });
|
|
expect(isOpenCodeStepLimitResult(stdout)).toBe(false);
|
|
});
|
|
|
|
it("returns false with no step_finish events", () => {
|
|
const stdout = JSON.stringify({ type: "text", part: { text: "Hello" } });
|
|
expect(isOpenCodeStepLimitResult(stdout)).toBe(false);
|
|
});
|
|
|
|
it("returns false for empty stdout", () => {
|
|
expect(isOpenCodeStepLimitResult("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isOpenCodeUnknownSessionError", () => {
|
|
it("detects 'unknown session' in stdout", () => {
|
|
const stdout = "Error: unknown session";
|
|
expect(isOpenCodeUnknownSessionError(stdout, "")).toBe(true);
|
|
});
|
|
|
|
it("detects 'session not found' in stdout", () => {
|
|
const stdout = "session not found";
|
|
expect(isOpenCodeUnknownSessionError(stdout, "")).toBe(true);
|
|
});
|
|
|
|
it("detects 'resource not found' with session path in stdout", () => {
|
|
const stdout = "resource not found: /session/abc.json";
|
|
expect(isOpenCodeUnknownSessionError(stdout, "")).toBe(true);
|
|
});
|
|
|
|
it("detects 'no session' in combined output", () => {
|
|
const stdout = "";
|
|
const stderr = "no session available";
|
|
expect(isOpenCodeUnknownSessionError(stdout, stderr)).toBe(true);
|
|
});
|
|
|
|
it("returns false for normal errors", () => {
|
|
const stdout = "Something went wrong";
|
|
expect(isOpenCodeUnknownSessionError(stdout, "")).toBe(false);
|
|
});
|
|
|
|
it("handles case insensitivity", () => {
|
|
const stdout = "UNKNOWN SESSION";
|
|
expect(isOpenCodeUnknownSessionError(stdout, "")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("parseOpenCodeJsonl — errorText fallback paths", () => {
|
|
it("uses nested data.message when top-level message is missing", () => {
|
|
const stdout = JSON.stringify({
|
|
type: "error",
|
|
error: { data: { message: "nested issue" } },
|
|
sessionID: "ses_x",
|
|
});
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
expect(result.errorMessage).toContain("nested issue");
|
|
});
|
|
|
|
it("uses error.name when no message or nested message", () => {
|
|
const stdout = JSON.stringify({
|
|
type: "error",
|
|
error: { name: "ProviderAuthError" },
|
|
sessionID: "ses_x",
|
|
});
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
expect(result.errorMessage).toContain("ProviderAuthError");
|
|
});
|
|
|
|
it("uses error.code when no message/name", () => {
|
|
const stdout = JSON.stringify({
|
|
type: "error",
|
|
error: { code: "E_TIMEOUT" },
|
|
sessionID: "ses_x",
|
|
});
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
expect(result.errorMessage).toContain("E_TIMEOUT");
|
|
});
|
|
|
|
it("falls back to JSON.stringify of the error object when nothing matches", () => {
|
|
const stdout = JSON.stringify({
|
|
type: "error",
|
|
error: { unexpectedShape: { foo: "bar" } },
|
|
sessionID: "ses_x",
|
|
});
|
|
const result = parseOpenCodeJsonl(stdout);
|
|
expect(result.errorMessage).toContain("unexpectedShape");
|
|
});
|
|
});
|