Files
PaperClipAI/tests/runner-e2e/question-resume.test.ts
T
Dotta 3937215a05 Qualify explicit native written references in the basic question journey (#18459)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Basic task journeys must prove that people can answer an agent and
receive saved work.
> - The question journey asks for a choice, then a typed reference, then
a saved note.
> - A native choice card can offer a real written-answer control as well
as predefined options.
> - The old observer rejected that form before a person could type the
reference.
> - This pull request accepts that explicit native control and keeps
exact answer and work evidence.
> - The separate text-only and named-tool qualifications keep their
stricter contracts.

## Linked Issues or Issue Description

Refs #18346 and #18456. The integration inherits the reviewed fixture
prerequisite #18387 and Agent Chat recovery #18465 through their actual
merged parent. The producer fix in #18456 is separate from this basic
journey contract correction. Public duplicate PR and issue searches
found no other correction for this form equivalence.

**What happened?**
The basic question journey requires the reference card to have
`answerMode: text`. It rejects a native single-select card even when its
canonical form explicitly enables a written answer. This makes the
observer test a widget shape in addition to the requested behavior.

**Expected behavior**
The person must make a real choice and type the complete reference. A
native card with an explicit written-answer control can satisfy the
second step. Selecting a predefined option cannot satisfy it. The agent
must resume the bound work and save both actual answers in its own task
document. The document must survive reload.

**Steps to reproduce**
Use the focused recording and real QuestionForm browser test in this PR.
Keep the reference as a single-select native question with one
predefined option and `customAnswer.enabled: true`. The previous grader
rejects `question-resume.forms`. The previous browser helper rejects
`single_select` before it uses the written-answer control.

**Paperclip version / commit**
Original source base `f2db909393de124caf6a5380a52d4e0e1d621661`. Current
integration `a417fd827347ed6f7998774a9de6aee70b76d867` merges exactly
`12b0cc630471ee151ec5bc8e41a34f09236d6723` into reviewed question source
`faec4ce6d775a34d303d200987e4e84d6dc26caf`. The scheduled failure on
`82d15946c5844d3ae41c6ade62fa224053f20a9a` remains a failure. Its saved
card had no canonical written-answer capability and would still fail
this change. The retained normalized events do not prove whether the
original provider flag was omitted or false.

**Deployment mode**
Attached native task journeys, including QA Cloud. This PR changes the
observer and grader. It does not change the app, provider prompt, model,
budget, timeouts, cleanup, or runtime deployment.

## What Changed

- Accept one provider-bound single-select reference card with explicit
canonical custom answers and valid, distinct options. Keep pure-text
reference support.
- Make the browser select the actual written-answer control and fill the
real rich input. Opt in only for the basic question journey.
- Keep the exact no-option typed response, immutable card, board actor,
native request and continuation, saved agent document, reload, model,
environment, checkpoints, and existing one-to-three-run checks.
- Reject missing or disabled custom capability, inferred legacy
capability, malformed or extra questions, foreign native identities,
changed cards, substituted options, and wrong or missing reference text.
- Add a real production QuestionForm and TaskChatRichInput Chromium
probe. It first selects a predefined option, then proves the helper
clears it and submits only the exact custom text.
- Document the contract correction and preserve the historical failed
attempt and its separate cleanup outcome. Keep necessary-question and
named-tool documentation qualification unchanged.

## Verification

- Before the observer change, the focused grader regression failed only
the new valid-written-reference case; the other 108 cases passed.
- Before the browser helper change, real Chromium passed the text
control and four rejection controls but failed the valid native
written-answer control at the old pure-text assertion.
- Initial focused Runner E2E unit selection at `af07f4de`: **256 passed
across 9 files**. This includes question/resumption, clarification
evidence/flow/collection/deadline, necessary question, documentation,
contracts, and report catalog tests.
- Initial real Chromium selection at `af07f4de`: **12 passed**. Six new
production QuestionForm/control checks and all six existing
document-readiness checks pass. No form or rich-input component is
mocked.
- Historical test-only head `faec4ce6d775a34d303d200987e4e84d6dc26caf`
adds a direct canonical-absent legacy `freeText` grader negative and
renders the actual legacy presentation in the browser. All four
production helper blobs remain identical to the reviewed head. A
temporary legacy inference mutant makes the new negative fail. After
restoring production source, **179 focused unit tests and all 6 real
QuestionForm checks pass**. The six document-readiness specs and their
helper are unchanged.
- Original [CI run
41205748589](https://github.com/paperclipai/paperclip/actions/runs/41205748589)
failed the staged-listen Runner handoff test; that failure is preserved
and was not retried. Its Runner source closure was identical to an
earlier passing canonical lane. The symptom also recurred on other heads
and is tracked independently; no infrastructure attribution or live pass
is claimed.
- Current integration `a417fd827347ed6f7998774a9de6aee70b76d867`
preserves **all 12 question files byte-for-byte** from faec. Every other
tracked path is identical to exact merged parent
`12b0cc630471ee151ec5bc8e41a34f09236d6723`, which contains actual
prerequisite `baf651c8d0122efc93f3ef03e891919eeb666215`. There is no
manual conflict resolution or additional production edit.
- On that integrated head: **257 focused Vitest tests across 9 files,
206 required Node source/reporter tests, and 12 real Chromium tests
pass**. Map/module-boundary checks and catalog discovery pass. The
actual `--preview` command passes with a synthetic question target and
an intentionally unavailable adapter; it performs no credential read,
adapter loading, provisioning or model call. It retains maxRuns3, a
600000ms campaign bound and zero retries.
- Exact-head [CI run
41210806950](https://github.com/paperclipai/paperclip/actions/runs/41210806950)
passes, including official typecheck, build, all eight browser shards,
both Runner Vitest lanes and aggregate verify. All 59 current PR
contexts are terminal with no failures. Fresh [Greptile
summary](https://github.com/paperclipai/paperclip/pull/18459#issuecomment-6543878408)
explicitly reviews `a417fd827347ed6f7998774a9de6aee70b76d867` at 5/5
with no findings; its exact-head check succeeds and all threads are
resolved. No fresh deployed journey pass is claimed yet.
- Catalog discovery lists the existing question journey. Map checks and
module-boundary checks pass. Diff checks pass.
- `tsc -p tests/runner-e2e/tsconfig.json` was attempted. The fresh
integrated checkout still lacks compiled Runner, plugin SDK, and adapter
declarations. The attempted local check reports 344 diagnostic lines
from dependency/type closure and zero diagnostics in all 12
question-owned paths. Official exact-head CI establishes the full
typecheck/build gates. Full repository build, typecheck, and test suites
were not repeated locally for this observer change.
- The first broad local report selection lacked the CLI loader and then
hit sandbox IPC restrictions. Locked dependency installation and an
authorized local IPC run resolved those setup errors; the full selected
256 tests pass. The initial localhost probe also needed the normal
local-listen permission.
- No fresh QA pass is claimed. The coordinator must qualify the final
reviewed runtime and harness together. Prior failed results remain
preserved.

## Risks

The basic journey accepts one additional form with explicit native
provenance. It still rejects the original closed card without a
written-answer capability. The separate producer default fix must reach
the selected remote provider pack before it can affect a deployed run.
Local browser and grader tests do not prove that artifact served or that
a model completed the live journey. Existing cleanup and physical-stop
obligations remain separate.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, source inspection, browser
testing, and code execution. The host does not expose the exact
underlying model version or context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used with available version and
capability details
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
related work above
- [x] I have described the issue in-PR following the bug template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket or instance-derived details
- [x] I have run the selected tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-10-31 13:16:40 -05:00

266 lines
18 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { gradeQuestionResume, questionPath, validResumeQuestionForm, type QuestionPath } from "./question-resume-scoring.js";
import { gradeContinuation } from "./continuation-scoring.js";
import { gradeLifecycleBaseline } from "./lifecycle-baseline.js";
import { continuationScenario } from "./continuation-cases.js";
import { runnerMatrix } from "./catalog.js";
type Row = Record<string, any>;
function recording(paths: QuestionPath[] = ["provider", "semantic"]) {
const runs: Row[] = [];
const cards: Row[] = [];
const points: any[] = [];
const newRun = (previous?: Row) => ({ id: `run-${runs.length + 1}`, status: "running", runtimeMode: "native",
companyId: "company", issueId: "task", agentId: "agent", resultJson: { semanticToolReceipts: {} },
runnerProfileJson: { nativeExecutionInput: { task: { prompt: "actual native task input" } } },
contextSnapshot: previous ? { issueId: "task", sourceRunId: previous.sourceRunId,
interactionId: previous.id, interactionKind: "ask_user_questions", interactionStatus: "answered" } : {},
});
runs.push(newRun());
const snap = (phase: string, documents: Row[] = []) => {
const active = runs.find(r => r.status === "running");
points.push(structuredClone({ phase, issue: { id: "task", companyId: "company", assigneeAgentId: "agent",
status: phase === "final" ? "done" : active ? "in_progress" : "in_review" },
lifecycle: { executionRunId: active?.id ?? null, scheduledRetry: null, activeRecoveryAction: null, monitorNextCheckAt: null },
runs, interactions: cards, children: [], attachments: [], comments: [], documents }));
};
for (let n = 0; n < 2; n++) {
const run = runs.at(-1)!;
const provider = paths[n] === "provider", api = paths[n] === "api";
const request = `request-${n}`;
const idempotencyKey = provider ? `paperclip-runner-question:${run.id}:${request}` : `question-${n}`;
const field = n === 0 ? { id: "field-1-question_0-hash", answerMode: "single_select", options: [
{ id: "am", label: "Morning" }, { id: "pm", label: "Afternoon" },
] } : { id: "reference", answerMode: "text" };
const questions: Row[] = [field];
if (provider && n === 0) questions.push({ id: "field-2-question_0_custom-hash", answerMode: "text", header: "Other", required: false });
const card: Row = { id: `card-${n}`, kind: "ask_user_questions", status: "pending",
companyId: "company", issueId: "task", createdByAgentId: "agent", sourceRunId: run.id, idempotencyKey,
continuationPolicy: provider ? "none" : "wake_assignee", createdByUserId: null,
payload: { ...(provider ? { runtimeRequestId: request } : {}), questionSet: { questions } } };
cards.push(card);
if (!provider) {
run.status = "succeeded";
if (api) run.resultJson.apiToolReceipts = { ["a".repeat(64)]: { state: "completed", digest: "b".repeat(64),
operationId: "POST /api/issues/{id}/interactions", result: { ok: true, status: 201,
operationId: "POST /api/issues/{id}/interactions", data: structuredClone(card) } } };
else run.resultJson.semanticToolReceipts[idempotencyKey] = { operationId: "request_human_input",
result: { disposition: "applied", interaction: structuredClone(card) } };
}
snap(n === 0 ? "initial" : "answered");
card.status = "answered"; card.resolvedByUserId = "local-board";
card.result = { answers: [n === 0 ? { questionId: field.id, optionIds: ["pm"] } :
{ questionId: field.id, optionIds: [], otherText: "Use AMBERnonce." }] };
if (!provider) runs.push(newRun(card));
}
runs.at(-1)!.status = "succeeded";
snap("final", [{ key: "welcome-note", latestRevisionId: "revision", body: "Welcome to the afternoon meetup. Use AMBERnonce." }]);
return points;
}
const failed = (r: any[]) => gradeQuestionResume(r, "AMBERnonce").filter(c => !c.passed).map(c => c.id);
const question = (r: any[], n = 0) => r[n].interactions[n];
function writtenReference(r: any[]) {
for (const point of r.slice(1)) Object.assign(point.interactions[1].payload.questionSet.questions[0], {
answerMode: "single_select", options: [{ id: "none", label: "No reference" }], customAnswer: { enabled: true },
});
return r;
}
describe("native question/resume path qualification", () => {
it("accepts an explicit provider written answer without selecting a predefined option", () => {
const r = writtenReference(recording(["provider", "provider"]));
expect(failed(r)).toEqual([]);
expect(validResumeQuestionForm(r[1], question(r, 1), false)).toBe(false);
});
it("rejects an inferred legacy freeText reference without a canonical question set", () => {
const r = writtenReference(recording(["provider", "provider"]));
for (const point of r.slice(1)) {
const payload = point.interactions[1].payload;
payload.questions = [{ id: "reference", prompt: "What reference should I include?", selectionMode: "single",
options: [{ id: "write", label: "Write reference", freeText: true }] }];
delete payload.questionSet;
}
expect(failed(r)).toContain("question-resume.forms");
});
it.each(["missing-custom", "disabled-custom", "missing-options", "duplicate-options", "missing-option-id", "multi-select", "extra-question",
"wrong-request", "foreign-source", "foreign-agent", "foreign-company", "semantic", "api"])("rejects an unverified written-reference form: %s", kind => {
const r = writtenReference(recording(["provider", kind === "semantic" || kind === "api" ? kind : "provider"]));
const card = question(r, 1), field = card.payload.questionSet.questions[0];
if (kind === "missing-custom") delete field.customAnswer;
if (kind === "disabled-custom") field.customAnswer.enabled = false;
if (kind === "missing-options") field.options = [];
if (kind === "duplicate-options") field.options.push(structuredClone(field.options[0]));
if (kind === "missing-option-id") delete field.options[0].id;
if (kind === "multi-select") field.answerMode = "multi_select";
if (kind === "extra-question") card.payload.questionSet.questions.push({ id: "extra", answerMode: "text" });
if (kind === "wrong-request") card.payload.runtimeRequestId = "other";
if (kind === "foreign-source") card.sourceRunId = "other";
if (kind === "foreign-agent") card.createdByAgentId = "other";
if (kind === "foreign-company") card.companyId = "other";
expect(failed(r)).toContain("question-resume.forms");
});
it.each(["predefined-option", "wrong-text", "missing-text", "foreign-user", "changed-form"])("rejects a corrupt written-reference answer: %s", kind => {
const r = writtenReference(recording(["provider", "provider"])), card = r[2].interactions[1], answer = card.result.answers[0];
if (kind === "predefined-option") answer.optionIds = ["none"];
if (kind === "wrong-text") answer.otherText = "Use OLD.";
if (kind === "missing-text") delete answer.otherText;
if (kind === "foreign-user") { card.resolvedByUserId = null; card.resolvedByAgentId = "agent"; }
if (kind === "changed-form") card.payload.questionSet.questions[0].customAnswer.label = "Changed";
expect(failed(r)).toContain("question-resume.text.answer");
});
it("accepts a native API creation receipt with the retained server response shape", () => {
const point = JSON.parse(readFileSync(new URL("./fixtures/native-api-question.json", import.meta.url), "utf8"));
expect(questionPath(point, point.interactions[0])).toBe("api");
expect(validResumeQuestionForm(point, point.interactions[0], true)).toBe(true);
});
it.each(["missing", "pending", "wrong-operation", "wrong-result-operation", "unknown-outcome", "wrong-status", "missing-digest", "wrong-key",
"foreign-card", "foreign-company", "foreign-issue", "foreign-agent", "foreign-source", "wrong-idempotency", "changed-form", "changed-compatibility-form",
"board-created", "resolved-creation", "wrong-policy", "runtime-request", "legacy-run", "active-source", "foreign-run", "duplicate-source", "spoofed-store"])("rejects unverified native API provenance: %s", kind => {
const point = JSON.parse(readFileSync(new URL("./fixtures/native-api-question.json", import.meta.url), "utf8"));
const card = point.interactions[0], run = point.runs[0], receipts = run.resultJson.apiToolReceipts;
const receipt = Object.values(receipts)[0] as Row, original = receipt.result.data;
if (kind === "missing") delete run.resultJson.apiToolReceipts;
if (kind === "pending") receipt.state = "pending";
if (kind === "wrong-operation") receipt.operationId = "POST /api/issues/{id}/comments";
if (kind === "wrong-result-operation") receipt.result.operationId = "POST /api/issues/{id}/comments";
if (kind === "unknown-outcome") receipt.result.ok = false;
if (kind === "wrong-status") receipt.result.status = 200;
if (kind === "missing-digest") delete receipt.digest;
if (kind === "wrong-key") run.resultJson.apiToolReceipts = { "model-authored-claim": receipt };
if (kind === "foreign-card") original.id = "other";
if (kind === "foreign-company") original.companyId = "other";
if (kind === "foreign-issue") original.issueId = "other";
if (kind === "foreign-agent") original.createdByAgentId = "other";
if (kind === "foreign-source") original.sourceRunId = "other";
if (kind === "wrong-idempotency") original.idempotencyKey = "other";
if (kind === "changed-form") original.payload.questionSet.questions[0].options[1].label = "Other";
if (kind === "changed-compatibility-form") original.payload.questions[0].prompt = "Other";
if (kind === "board-created") original.createdByUserId = "other";
if (kind === "resolved-creation") original.status = "answered";
if (kind === "wrong-policy") card.continuationPolicy = "none";
if (kind === "runtime-request") card.payload.runtimeRequestId = "fabricated-provider-request";
if (kind === "legacy-run") run.runtimeMode = "legacy";
if (kind === "active-source") run.status = "running";
if (kind === "foreign-run") run.agentId = "other";
if (kind === "duplicate-source") point.runs.push(structuredClone(run));
if (kind === "spoofed-store") { run.resultJson.modelClaim = receipts; delete run.resultJson.apiToolReceipts; }
expect(questionPath(point, card)).toBeNull();
});
it.each(["lost-receipt", "changed-receipt", "interactionId", "sourceRunId", "issueId", "interactionStatus", "duplicate-continuation"])("retains one immutable API receipt and one bound answer wake: %s", kind => {
const r = recording(["api", "api"]), source = r[1].runs[0], next = r[1].runs[1];
if (kind === "lost-receipt") delete source.resultJson.apiToolReceipts;
else if (kind === "changed-receipt") source.resultJson.apiToolReceipts["a".repeat(64)].digest = "c".repeat(64);
else if (kind === "duplicate-continuation") r[1].runs.push({ ...structuredClone(next), id: "extra" });
else next.contextSnapshot[kind] = "other";
expect(failed(r)).toContain("question-resume.choice.path");
});
it.each(["lost", "changed"])("rejects the first API receipt %s after the second answer", kind => {
const r = recording(["api", "api"]), source = r[2].runs[0];
if (kind === "lost") delete source.resultJson.apiToolReceipts;
else source.resultJson.apiToolReceipts["a".repeat(64)].digest = "c".repeat(64);
expect(failed(r)).toContain("question-resume.choice.path");
});
it("keeps the new journey explicit-only and the original documentation cells intact", () => {
const cells = runnerMatrix.filter(c => c.suite.id === "question-resume");
expect(cells.map(c => c.profile.id).sort()).toEqual(["runner-acpx-claude", "runner-codex"]);
for (const c of cells) {
expect(c.suite.manualOnly).toBe(true);
expect(c.task).toMatchObject({ expectedRunCount: 3, minimumExpectedRunCount: 1, automaticRetryPolicy: "single_attempt" });
expect(c.task.buildPrompt("nonce")).toBe(continuationScenario("question-tool-documentation", "nonce").prompt);
}
expect(runnerMatrix.filter(c => c.suite.id === "continuation")).toHaveLength(23);
});
it.each([
["provider", "provider"], ["provider", "semantic"], ["semantic", "provider"], ["semantic", "semantic"],
["api", "api"], ["api", "semantic"], ["semantic", "api"], ["provider", "api"], ["api", "provider"],
] as QuestionPath[][])("accepts exactly bound %s then %s journeys", (a, b) => {
const r = recording([a,b]);
expect(failed(r)).toEqual([]);
expect(gradeContinuation({ ...continuationScenario("question-answer-resume", "nonce"), runtimeMode: "native", checkpoints: r }).filter(c => !c.passed)).toEqual([]);
expect(gradeLifecycleBaseline(r).filter(c => !c.passed)).toEqual([]);
});
it.each([
"Welcome to the afternoon meetup, AMBERnonce.",
"Welcome to the afternoon meetup. Use AMBERnonce",
])("rejects a shortened supplied reference in the saved document: %s", body => {
const r = recording();
r[2].documents[0].body = body;
expect(failed(r)).toContain("question-resume.complete-evidence");
});
it("does not admit an optional Other field on the semantic path", () => {
const r = recording(["semantic", "semantic"]);
const i = question(r);
i.payload.questionSet.questions.push({ id: "field-2-question_0_custom-hash", answerMode: "text", header: "Other", required: false });
expect(validResumeQuestionForm(r[0], i, true)).toBe(false);
});
it.each(["sourceRunId", "issueId", "companyId", "createdByAgentId", "idempotencyKey"])("rejects a provider card with mismatched %s", key => {
const r = recording(); question(r)[key] = "unrelated";
expect(failed(r)).toContain("question-resume.choice.path");
});
it("rejects a consistent foreign-company card/run pair on this task", () => {
const r = recording(); question(r).companyId = "foreign"; r[0].runs[0].companyId = "foreign";
expect(questionPath(r[0], question(r))).toBeNull();
});
it.each(["missing-request", "wrong-request", "terminal-run", "wrong-policy", "legacy-run"])("rejects invalid provider binding: %s", kind => {
const r = recording(); const i = question(r);
if (kind === "missing-request") delete i.payload.runtimeRequestId;
if (kind === "wrong-request") i.payload.runtimeRequestId = "unrelated";
if (kind === "terminal-run") r[0].runs[0].status = "succeeded";
if (kind === "wrong-policy") i.continuationPolicy = "wake_assignee";
if (kind === "legacy-run") r[0].runs[0].runtimeMode = "legacy";
expect(questionPath(r[0], i)).toBeNull();
});
it.each(["missing", "wrong-operation", "rejected", "wrong-interaction", "wrong-source", "wrong-form"])("rejects missing or mismatched semantic receipt: %s", kind => {
const r = recording(["semantic", "semantic"]); const i = question(r);
const receipt = r[0].runs[0].resultJson.semanticToolReceipts[i.idempotencyKey];
if (kind === "missing") delete r[0].runs[0].resultJson.semanticToolReceipts[i.idempotencyKey];
if (kind === "wrong-operation") receipt.operationId = "write_document";
if (kind === "rejected") receipt.result.disposition = "rejected";
if (kind === "wrong-interaction") receipt.result.interaction.id = "unrelated";
if (kind === "wrong-source") receipt.result.interaction.sourceRunId = "unrelated";
if (kind === "wrong-form") receipt.result.interaction.payload.questionSet.questions = [];
expect(questionPath(r[0], i)).toBeNull();
});
it.each(["required-other", "extra-question", "text-with-options", "two-early-cards"])("rejects the wrong form: %s", kind => {
const r = recording(); const fields = question(r).payload.questionSet.questions;
if (kind === "required-other") fields[1].required = true;
if (kind === "extra-question") fields.push({ id: "extra", answerMode: "text" });
if (kind === "text-with-options") question(r,1).payload.questionSet.questions[0].options = [{ id: "other" }];
if (kind === "two-early-cards") r[0].interactions.push(structuredClone(question(r,1)));
expect(failed(r)).toContain("question-resume.forms");
});
it.each(["agent-answer", "wrong-choice", "wrong-text", "duplicate-answer", "replaced-card", "changed-form", "changed-answer", "unanswered"])("rejects corrupt answer evidence: %s", kind => {
const r = recording(); const first = r[1].interactions[0]; const last = r[2].interactions[1];
if (kind === "agent-answer") { first.resolvedByUserId = null; first.resolvedByAgentId = "agent"; }
if (kind === "wrong-choice") first.result.answers[0].optionIds = ["am"];
if (kind === "wrong-text") last.result.answers[0].otherText = "wrong";
if (kind === "duplicate-answer") first.result.answers.push(structuredClone(first.result.answers[0]));
if (kind === "replaced-card") last.id = "replacement";
if (kind === "changed-form") last.payload.questionSet.questions[0].prompt = "Changed question";
if (kind === "changed-answer") r[2].interactions[0].result.answers[0].optionIds = ["am"];
if (kind === "unanswered") last.status = "pending";
expect(failed(r).some(id => id.endsWith(".answer"))).toBe(true);
});
it("rejects replacing a paused provider run with a fresh run", () => {
const r = recording(); r[1].runs.push({ ...structuredClone(r[1].runs[0]), id: "replacement" });
expect(failed(r)).toContain("question-resume.choice.path");
});
it.each(["interactionId", "sourceRunId", "issueId", "interactionStatus"])("rejects the wrong semantic response wake %s", key => {
const r = recording(); r[2].runs[1].contextSnapshot[key] = "unrelated";
expect(failed(r)).toContain("question-resume.text.path");
});
it.each(["lost-run", "duplicate-run", "extra-run", "missing-input", "missing-final", "missing-middle", "missing-output", "wrong-output"])("rejects incomplete continuation evidence: %s", kind => {
const r = recording();
if (kind === "lost-run") r[2].runs.shift();
if (kind === "duplicate-run") r[2].runs.push(structuredClone(r[2].runs[0]));
if (kind === "extra-run") r[2].runs.push({ ...structuredClone(r[2].runs[0]), id: "extra" });
if (kind === "missing-input") delete r[2].runs[0].runnerProfileJson;
if (kind === "missing-output") r[2].documents = [];
if (kind === "wrong-output") r[2].documents[0].body = "Welcome AMBERnonce.";
if (kind === "missing-final") r.pop();
if (kind === "missing-middle") r.splice(1,1);
expect(failed(r).length).toBeGreaterThan(0);
});
});