|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { assertInterruptedChat, prepareChatBrief } from "./chat-stories.js"; |
| 3 | +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; |
| 4 | +import { tmpdir } from "node:os"; |
| 5 | +import path from "node:path"; |
| 6 | +import { execFile } from "node:child_process"; |
| 7 | +import { promisify } from "node:util"; |
| 8 | +import { runnerMatrix } from "./catalog.js"; |
| 9 | +import { buildRunnerE2EProcessEnvironment } from "./harness-env.js"; |
| 10 | + |
| 11 | +const run = { id: "run", companyId: "company", agentId: "agent", status: "succeeded", runtimeMode: "native", contextSnapshot: { issueId: "chat" } }; |
| 12 | +const valid = { |
| 13 | + first: "Read the brief", followup: "Change the launch day", reference: "BRIEF123", marker: "UPDATED123", issueId: "chat", |
| 14 | + boundaryRun: { ...run, status: "running" }, activeAtFollowup: { ...run, status: "running" }, |
| 15 | + comments: [{ id: "first", body: "Read the brief" }, { id: "followup", body: "Change the launch day" }, |
| 16 | + { id: "answer", authorAgentId: "agent", createdByRunId: "run", body: "BRIEF123 UPDATED123" }], |
| 17 | + runs: [run], revisedPlan: JSON.stringify({ launchDay: "Friday", reference: "BRIEF123", revision: "UPDATED123" }), |
| 18 | +}; |
| 19 | + |
| 20 | +describe("active chat follow-up oracle", () => { |
| 21 | + it("creates the missing fixture directory and waits for the host-supplied brief", async () => { |
| 22 | + const root = await mkdtemp(path.join(tmpdir(), "chat-brief-test-")); |
| 23 | + try { |
| 24 | + const { gate, ready, scriptPath } = await prepareChatBrief(path.join(root, "new workspace"), "fixture"); |
| 25 | + const command = promisify(execFile)(process.execPath, [scriptPath], { timeout: 5_000 }); |
| 26 | + try { |
| 27 | + await expect.poll(() => readFile(ready, "utf8").catch(() => "")).toBe("waiting"); |
| 28 | + } finally { |
| 29 | + await writeFile(gate, "BRIEF fixture result"); |
| 30 | + expect((await command).stdout.trim()).toBe("BRIEF fixture result"); |
| 31 | + } |
| 32 | + } finally { |
| 33 | + await rm(root, { recursive: true, force: true }); |
| 34 | + } |
| 35 | + }); |
| 36 | + it("accepts either steering or one queued successor, with the saved correction", () => { |
| 37 | + expect(() => assertInterruptedChat(valid)).not.toThrow(); |
| 38 | + expect(() => assertInterruptedChat({ ...valid, runs: [run, { ...run, id: "successor" }], comments: [...valid.comments.slice(0, 2), { ...valid.comments[2]!, createdByRunId: "successor" }] })).not.toThrow(); |
| 39 | + }); |
| 40 | + it("rejects follow-ups sent after the active boundary", () => { |
| 41 | + expect(() => assertInterruptedChat({ ...valid, boundaryRun: run })).toThrow(); |
| 42 | + expect(() => assertInterruptedChat({ ...valid, activeAtFollowup: run })).toThrow(); |
| 43 | + expect(() => assertInterruptedChat({ ...valid, activeAtFollowup: { ...valid.activeAtFollowup, id: "other" } })).toThrow(); |
| 44 | + }); |
| 45 | + it("rejects lost or duplicated input, missing file evidence, and stale plans", () => { |
| 46 | + expect(() => assertInterruptedChat({ ...valid, comments: [...valid.comments, { ...valid.comments[2]!, id: "duplicate-reply" }] })).toThrow(); |
| 47 | + expect(() => assertInterruptedChat({ ...valid, comments: [...valid.comments.slice(0, 2), { ...valid.comments[2]!, createdByRunId: "unrelated" }] })).toThrow(); |
| 48 | + expect(() => assertInterruptedChat({ ...valid, comments: valid.comments.filter(c => c.id !== "followup") })).toThrow(); |
| 49 | + expect(() => assertInterruptedChat({ ...valid, comments: [...valid.comments, { id: "duplicate", body: valid.followup }] })).toThrow(); |
| 50 | + expect(() => assertInterruptedChat({ ...valid, comments: [...valid.comments.slice(0, 2), { id: "answer", authorAgentId: "agent", body: "UPDATED123" }] })).toThrow(); |
| 51 | + expect(() => assertInterruptedChat({ ...valid, revisedPlan: valid.revisedPlan.replace("Friday", "Monday") })).toThrow(); |
| 52 | + expect(() => assertInterruptedChat({ ...valid, revisedPlan: "The updated plan is saved." })).toThrow(); |
| 53 | + }); |
| 54 | + it("rejects failed, unfinished, duplicated, or unrelated execution", () => { |
| 55 | + for (const status of ["running", "queued", "failed", "cancelled"]) |
| 56 | + expect(() => assertInterruptedChat({ ...valid, runs: [{ ...run, status }] })).toThrow(); |
| 57 | + expect(() => assertInterruptedChat({ ...valid, runs: [] })).toThrow(); |
| 58 | + expect(() => assertInterruptedChat({ ...valid, runs: [run, run, run] })).toThrow(); |
| 59 | + expect(() => assertInterruptedChat({ ...valid, runs: [run, run] })).toThrow(); |
| 60 | + expect(() => assertInterruptedChat({ ...valid, runs: [{ ...run, id: "other" }] })).toThrow(); |
| 61 | + expect(() => assertInterruptedChat({ ...valid, runs: [{ ...run, contextSnapshot: { issueId: "other" } }] })).toThrow(); |
| 62 | + }); |
| 63 | + it("keeps setup and interruption stories native, local, opt-in, and outside API-tool overrides", () => { |
| 64 | + const cells = runnerMatrix.filter(cell => cell.suite.id === "agent-chat-stories"); |
| 65 | + expect(cells).toHaveLength(6); |
| 66 | + for (const cell of cells) { |
| 67 | + expect(cell.suite.manualOnly).toBe(true); |
| 68 | + expect(cell.profile.generation).toBe("native"); |
| 69 | + expect(cell.environment.id).toBe("local"); |
| 70 | + expect(buildRunnerE2EProcessEnvironment({}, [cell]).PAPERCLIP_RUNNER_API_TOOLS_ENABLED).toBeUndefined(); |
| 71 | + } |
| 72 | + }); |
| 73 | +}); |
0 commit comments