Files
paperclip-adapter-opencode-k8s/src/server/parse.test.ts
T
Chris Farhood 798b80f2f2 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>
2026-04-25 22:27:04 +00:00

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");
});
});