import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createAcpVisibleTextAccumulator, resolveFallbackRetryPrompt, sessionFileHasContent, } from "./attempt-execution.js"; describe("resolveFallbackRetryPrompt", () => { const originalBody = "Summarize the quarterly earnings report and highlight key trends."; it("returns original body on first attempt (isFallbackRetry=false)", () => { expect( resolveFallbackRetryPrompt({ body: originalBody, isFallbackRetry: false, }), ).toBe(originalBody); }); it("returns recovery prompt for fallback retry with existing session history", () => { expect( resolveFallbackRetryPrompt({ body: originalBody, isFallbackRetry: true, sessionHasHistory: true, }), ).toBe("Continue where you left off. The previous model attempt failed or timed out."); }); it("preserves original body for fallback retry when session has no history (subagent spawn)", () => { expect( resolveFallbackRetryPrompt({ body: originalBody, isFallbackRetry: true, sessionHasHistory: false, }), ).toBe(originalBody); }); it("preserves original body for fallback retry when sessionHasHistory is undefined", () => { expect( resolveFallbackRetryPrompt({ body: originalBody, isFallbackRetry: true, }), ).toBe(originalBody); }); it("returns original body on first attempt regardless of sessionHasHistory", () => { expect( resolveFallbackRetryPrompt({ body: originalBody, isFallbackRetry: false, sessionHasHistory: true, }), ).toBe(originalBody); expect( resolveFallbackRetryPrompt({ body: originalBody, isFallbackRetry: false, sessionHasHistory: false, }), ).toBe(originalBody); }); it("preserves original body on fallback retry without history", () => { expect( resolveFallbackRetryPrompt({ body: originalBody, isFallbackRetry: true, sessionHasHistory: false, }), ).toBe(originalBody); }); }); describe("sessionFileHasContent", () => { let tmpDir: string; beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "oc-test-")); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true, force: true }); }); it("returns false for undefined sessionFile", async () => { expect(await sessionFileHasContent(undefined)).toBe(false); }); it("returns false when session file does not exist", async () => { expect(await sessionFileHasContent(path.join(tmpDir, "nonexistent.jsonl"))).toBe(false); }); it("returns false when session file is empty", async () => { const file = path.join(tmpDir, "empty.jsonl"); await fs.writeFile(file, "", "utf-8"); expect(await sessionFileHasContent(file)).toBe(false); }); it("returns false when session file has only user message (no assistant flush)", async () => { const file = path.join(tmpDir, "user-only.jsonl"); await fs.writeFile( file, '{"type":"session","id":"s1"}\n{"type":"message","message":{"role":"user","content":"hello"}}\n', "utf-8", ); expect(await sessionFileHasContent(file)).toBe(false); }); it("returns true when session file has assistant message (flushed)", async () => { const file = path.join(tmpDir, "with-assistant.jsonl"); await fs.writeFile( file, '{"type":"session","id":"s1"}\n{"type":"message","message":{"role":"user","content":"hello"}}\n{"type":"message","message":{"role":"assistant","content":"hi"}}\n', "utf-8", ); expect(await sessionFileHasContent(file)).toBe(true); }); it("returns true when session file has spaced JSON (role : assistant)", async () => { const file = path.join(tmpDir, "spaced.jsonl"); await fs.writeFile( file, '{"type":"message","message":{"role": "assistant","content":"hi"}}\n', "utf-8", ); expect(await sessionFileHasContent(file)).toBe(true); }); it("returns true when assistant message appears after large user content", async () => { const file = path.join(tmpDir, "large-user.jsonl"); // Create a user message whose JSON line exceeds 256KB to ensure the // JSONL-based parser (CWE-703 fix) finds the assistant record that a // naive byte-prefix approach would miss. const bigContent = "x".repeat(300 * 1024); const lines = [ `{"type":"session","id":"s1"}`, `{"type":"message","message":{"role":"user","content":"${bigContent}"}}`, `{"type":"message","message":{"role":"assistant","content":"done"}}`, ].join("\n") + "\n"; await fs.writeFile(file, lines, "utf-8"); expect(await sessionFileHasContent(file)).toBe(true); }); it("returns false when session file is a symbolic link", async () => { const realFile = path.join(tmpDir, "real.jsonl"); await fs.writeFile( realFile, '{"type":"message","message":{"role":"assistant","content":"hi"}}\n', "utf-8", ); const link = path.join(tmpDir, "link.jsonl"); await fs.symlink(realFile, link); expect(await sessionFileHasContent(link)).toBe(false); }); }); describe("createAcpVisibleTextAccumulator", () => { it("preserves cumulative raw snapshots after stripping a glued NO_REPLY prefix", () => { const acc = createAcpVisibleTextAccumulator(); expect(acc.consume("NO_REPLYThe user")).toEqual({ text: "The user", delta: "The user", }); expect(acc.consume("NO_REPLYThe user is saying")).toEqual({ text: "The user is saying", delta: " is saying", }); expect(acc.finalize()).toBe("The user is saying"); expect(acc.finalizeRaw()).toBe("The user is saying"); }); it("keeps append-only deltas working after stripping a glued NO_REPLY prefix", () => { const acc = createAcpVisibleTextAccumulator(); expect(acc.consume("NO_REPLYThe user")).toEqual({ text: "The user", delta: "The user", }); expect(acc.consume(" is saying")).toEqual({ text: "The user is saying", delta: " is saying", }); }); it("preserves punctuation-start text that begins with NO_REPLY-like content", () => { const acc = createAcpVisibleTextAccumulator(); expect(acc.consume("NO_REPLY: explanation")).toEqual({ text: "NO_REPLY: explanation", delta: "NO_REPLY: explanation", }); expect(acc.finalize()).toBe("NO_REPLY: explanation"); }); });