test: verify necessary free-text questions through one real answer

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-21 08:48:07 -05:00
1 parent d942a5c874
commit 9d322bfa21
12 files changed
+216 -11

No files matched your search

+6
View File
@@ -1,5 +1,11 @@
# Paperclip evaluation guide
The explicit-only [necessary free-text question journey](../tests/runner-e2e/README.md#necessary-free-text-question)
checks one essential input, the rendered prompt and context, the real answer,
one bound continuation, and an exact saved confirmation. Its four development
cells use the recorded Codex and Claude models. Registration and calibration do
not establish Slack, staging authentication, warm lifecycle, or live qualification.
The explicit-only [case handoff workflow](../tests/runner-e2e/README.md#persistent-case-handoffs-explicit-only)
contains arithmetic and receipt smoke cells for a same-task round trip among three native agents using real case/task
APIs, saved documents, handoff receipts, and browser readback. Receipt smoke has
@@ -156,3 +156,13 @@ through the real service and prepares verified bytes on the reused task root,
with only remote clone transport replaced by an owned local repository. The
queued sixth campaign was cancelled during its build, before provider execution,
when this review finding made that candidate an uninformative paid retry.
The necessary-question journey now extends the existing continuation flow. Four
explicit development cells require a visible free-text prompt and context, one
real user answer retained on the unchanged question, an exactly bound provider
resume or semantic successor, and an independently read exact saved confirmation
with a visible link. Calibration preserves the existing continuation and question
suites. These cells use production instructions and the recorded models, but
retain explicit API-key/per-turn differences and do not count as Slack or staging
qualification. Actual provider measurement is still required.
+23
View File
@@ -64,6 +64,29 @@ pnpm test:e2e:runner -- --id legacy-conversion.runner-codex.local.convert-and-de
pnpm test:e2e:runner -- --id automatic-repository-configured-models.runner-codex.local.automatic-repository-resume --max-automatic-retries 0
```
## Necessary free-text question
The explicit-only `necessary-question` suite has four development cells: the
recorded Codex and Claude models, on local and Daytona environments. It requests
one essential booking reference that only the test user knows. The browser must
show the actual free-text prompt and its explanation, then submit the real answer.
The oracle checks the unchanged question, its server-owned provider or semantic
receipt, the exact stored user answer, and one bound continuation. The saved
confirmation must contain that answer exactly and have a visible document link.
A title, success claim, replacement card, duplicate wake, or uncommitted result
cannot establish a pass.
This extends the existing continuation flow and keeps the older question suites
unchanged. It uses production hire instructions, one attempt, one or two provider
admissions, and 1,000-cent company and agent budgets. API-key authentication and
per-turn lifecycle remain explicit development differences. It does not qualify
Slack, staging subscription credentials, warm reuse, or the complete checkpoint.
```sh
pnpm test:e2e:runner -- --list --suite necessary-question
pnpm test:e2e:runner -- --id necessary-question.runner-codex.local.necessary-free-text --max-automatic-retries 0
```
## Live provider connection journeys
See [PROVIDER-CONNECTIONS.md](PROVIDER-CONNECTIONS.md) for the explicit-only
+3 -3
View File
@@ -211,10 +211,10 @@ describe("runner E2E catalog", () => {
expect(localIntegrityTasks).toHaveLength(2);
expect(openRouterBreadthTasks).toHaveLength(3);
expect(runnerSuites.map((suite) => suite.expectedMatrixSize)).toEqual([
1, 3, 4, 4, 4, 4, 4, 8, 10, 6, 63, 12, 6, 4, 2, 4, 2, 30, 3, 16, 16, 2, 6, 8, 46, 2, 23, 15, 52, 6, 6, 20, 26, 52, 28, 18, 2, 6, 6, 13, 10, 48, 16, 10, 2, 1, 1, 3, 116, 2,
1, 3, 4, 4, 4, 4, 4, 4, 8, 10, 6, 63, 12, 6, 4, 2, 4, 2, 30, 3, 16, 16, 2, 6, 8, 46, 2, 23, 15, 52, 6, 6, 20, 26, 52, 28, 18, 2, 6, 6, 13, 10, 48, 16, 10, 2, 1, 1, 3, 116, 2,
]);
expect(validateRunnerCatalog()).toHaveLength(752);
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(752);
expect(validateRunnerCatalog()).toHaveLength(756);
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(756);
expect(
runnerMatrix.filter((entry) => entry.suite.id === "core-compatibility"),
).toHaveLength(48);
+11
View File
@@ -1,3 +1,4 @@
import { necessaryQuestionTask, necessaryQuestionDigest } from "./necessary-question.js";
import { caseHandoffTask, caseHandoffDefinitionDigest } from "./case-handoffs-cases.js";
import { caseReceiptTask, caseReceiptDefinitionDigest, CASE_RECEIPT_BUDGET_CENTS, CASE_RECEIPT_MAX_TURNS, CASE_RECEIPT_RUN_TIMEOUT_MS } from "./case-receipt-cases.js";
import { TASK_WORKSPACES_SUITE, taskWorkspaceTask, taskWorkspaceProfile, taskWorkspaceDefinitionDigest } from "./task-workspaces-cases.js";
@@ -1185,6 +1186,16 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
definitionMetadata: { version: 1, definitionDigest: taskWorkspaceDefinitionDigest(), scheduling: "explicit-only", providerTurns: 3,
attempts: 1, budgetMonthlyCents: 1000, project: "none", cwd: "unconfigured-task-default", oracle: "public-file-download-and-host-nested-git", remoteLifecycle: "per_turn" },
},
{
id: "necessary-question", label: "Necessary free-text question and verified confirmation", manualOnly: true,
description: "One required private input, a visible question and context, and one exactly bound answer continuation.",
groups: [], profiles: repositoryModelProfiles.map(({ native }) => productionDefaultHireProfile(native)),
environments: runnerEnvironments, tasks: [necessaryQuestionTask], expectedMatrixSize: 4,
definitionMetadata: { version: 1, definitionDigest: necessaryQuestionDigest(), scheduling: "explicit-only", attempts: 1,
providerTurns: "1-2; provider pause or one semantic successor", budgetMonthlyCents: 1000, instructions: "production-default",
surface: "paperclip", target: "isolated-development-instance", authentication: "api-key", stagingAuthentication: "subscription",
remoteLifecycle: "per_turn", stagingQualification: false },
},
...([false, true] as const).map(conversion => ({
id: conversion ? "legacy-conversion" : "configured-repository",
label: conversion ? "Legacy conversion with verified delivery" : "Configured repository with verified delivery",
+8 -2
View File
@@ -10,9 +10,13 @@ export const CONTINUATION_CASES = [
"question-tool-documentation",
"provider-question-bridge",
] as const;
export type ContinuationCase = (typeof CONTINUATION_CASES)[number] | "question-answer-resume";
export type ContinuationCase = (typeof CONTINUATION_CASES)[number] | "question-answer-resume" | "necessary-free-text";
export function necessaryQuestionContract() {
return { prompt: "What booking reference should appear on the confirmation?",
context: "Only you have the booking reference. I need it to write the confirmation correctly." };
}
export function continuationScenario(id: string, nonce: string) {
if (id !== "question-answer-resume" && !CONTINUATION_CASES.includes(id as (typeof CONTINUATION_CASES)[number]))
if (id !== "question-answer-resume" && id !== "necessary-free-text" && !CONTINUATION_CASES.includes(id as (typeof CONTINUATION_CASES)[number]))
throw new Error(`Unknown continuation case: ${id}`);
const marker = `AMBER${nonce.replace(/[^a-z0-9]/gi, "")}`;
const old = `COBALT${nonce.replace(/[^a-z0-9]/gi, "")}`;
@@ -22,7 +26,9 @@ export function continuationScenario(id: string, nonce: string) {
const document =
"Save the final note as a task document. No other deliverables or child tasks are needed.";
const questionPrompt = "Help me write a one-sentence welcome note for a club meetup. First let me choose Morning or Afternoon using clickable choices. After I choose, ask me for a reference to include using an open text field. Ask only one question at a time and wait for my answers. Then save the note as a task document, including the selected time and my reference exactly as supplied, and finish. Do not create any other tasks or deliverables.";
const necessary = necessaryQuestionContract();
const prompts: Record<ContinuationCase, string> = {
"necessary-free-text": `Prepare a booking confirmation. The booking reference is required, is known only to me, and has not been supplied. Ask one open text question with exactly this prompt: ${necessary.prompt} Explain this context in the question: ${necessary.context} Wait for my answer. Then save one task document with exactly the text Booking confirmed: followed by a space and my complete answer, unchanged. Deliver its accessible link here and finish. Do not create other tasks or deliverables.`,
"question-answer-resume": questionPrompt,
"provider-question-bridge": `Use your built-in AskUserQuestion tool (not Paperclip's request_human_input) to ask which reference to include, with two choices: ${marker} and ${old}. Wait for my real answer, then save a one-sentence welcome note including only my selected reference as a task document and finish. No other tasks or deliverables are needed.`,
"question-tool-documentation": questionPrompt,
+19 -2
View File
@@ -1,3 +1,5 @@
import { gradeNecessaryQuestion } from "./necessary-question.js";
import { necessaryQuestionContract } from "./continuation-cases.js";
import { validResumeQuestionForm } from "./question-resume-scoring.js";
import { gradeLifecycleBaseline, type LifecycleCheckpoint } from "./lifecycle-baseline.js";
import { lifecycleLiveCase, lifecycleLiveContinuation, gradeLifecycleNarrative } from "./lifecycle-live-cases.js";
@@ -107,7 +109,7 @@ export async function runContinuationFlow(input: {
return ready;
},
reject: (state) =>
state.runs.length > (scenario.id === "question-answer-resume" ? 3 : 12)
state.runs.length > (scenario.id === "necessary-free-text" ? 2 : scenario.id === "question-answer-resume" ? 3 : 12)
? "Bounded continuation run count exceeded"
: state.runs.some((r) =>
["failed", "timed_out", "cancelled"].includes(r.status),
@@ -171,6 +173,18 @@ export async function runContinuationFlow(input: {
});
await input.evidence("api-state.json", checkpoints.at(-1));
await open();
if (scenario.id === "necessary-free-text") {
if (phase === "initial") {
const contract = necessaryQuestionContract();
await expect(page.getByText(contract.prompt, { exact: true }).last()).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(contract.context, { exact: true }).last()).toBeVisible({ timeout: 30_000 });
const question = interactions.find(interaction => interaction.kind === "ask_user_questions" && interaction.status === "pending");
checkpoints.at(-1)!.questionPresentation = { interactionId: question?.id ?? "", promptVisible: true, contextVisible: true };
} else if (phase === "final") {
await expect(page.locator('a[href*="#document-"]').last()).toBeVisible({ timeout: 30_000 });
checkpoints.at(-1)!.outputLinkVisible = true;
}
}
await captureLoadedContinuation(page, String(issue!.title), () => input.capture(
phase,
`Continuation: ${phase}`,
@@ -186,7 +200,9 @@ export async function runContinuationFlow(input: {
);
expect(questions, "one real question must be shown").toHaveLength(1);
const set = chatQuestionPresentation(questions[0].payload);
if (scenario.id === "question-answer-resume") {
if (scenario.id === "necessary-free-text") {
expect(validResumeQuestionForm(checkpoints[0]!, questions[0], false), "a bound text-only question").toBe(true);
} else if (scenario.id === "question-answer-resume") {
expect(validResumeQuestionForm(checkpoints.at(-1)!, questions[0], !!choice), "a verified native question path and the requested form").toBe(true);
} else if (scenario.id === "provider-question-bridge") {
expect(isSingleClaudeQuestion(set.questions), "one choice question with only the optional provider Other field").toBe(true);
@@ -294,6 +310,7 @@ export async function runContinuationFlow(input: {
runtimeMode: execution.profile.expectedRuntimeMode,
});
checks.push(...gradeLifecycleBaseline(checkpoints));
if (scenario.id === "necessary-free-text") checks.push(...gradeNecessaryQuestion(checkpoints, scenario.answer));
if (lifecycleProbe) checks.push(gradeLifecycleNarrative({
narrative: lifecycleProbe.narrative,
agentId: fixtures.agent.id,
+3 -1
View File
@@ -14,6 +14,8 @@ export interface ContinuationCheckpoint {
documents: Array<{ key: string; body: string; latestRevisionId?: string }>;
attachments: unknown[];
comments: unknown[];
questionPresentation?: { interactionId: string; promptVisible: boolean; contextVisible: boolean };
outputLinkVisible?: boolean;
interactions: unknown[];
runs: Array<{ id: string; status: string; runtimeMode?: string }>;
}
@@ -50,7 +52,7 @@ export function gradeContinuation(input: {
const before = input.checkpoints.filter((c) => c.phase !== "final");
check(
"recorded-continuation",
before.length > 0 && !!final && final.runs.length >= (["provider-question-bridge", "question-answer-resume"].includes(input.id) ? 1 : 2),
before.length > 0 && !!final && final.runs.length >= (["provider-question-bridge", "question-answer-resume", "necessary-free-text"].includes(input.id) ? 1 : 2),
"Initial and final turns must both be recorded.",
);
for (const c of before) {
@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import { runnerMatrix } from "./catalog.js";
import { continuationScenario, necessaryQuestionContract } from "./continuation-cases.js";
import { gradeNecessaryQuestion } from "./necessary-question.js";
import { gradeContinuation, type ContinuationCheckpoint } from "./continuation-scoring.js";
import { gradeLifecycleBaseline } from "./lifecycle-baseline.js";
type Row = Record<string, any>;
function recording(provider = false): any[] {
const contract = necessaryQuestionContract(), scenario = continuationScenario("necessary-free-text", "nonce");
const card: Row = { id: "card", kind: "ask_user_questions", status: "pending", companyId: "company", issueId: "task", createdByAgentId: "agent",
sourceRunId: "first", continuationPolicy: provider ? "none" : "wake_assignee",
idempotencyKey: provider ? "paperclip-runner-question:first:request" : "question",
payload: { ...(provider ? { runtimeRequestId: "request" } : {}), questionSet: { schema: "paperclip.question_set.v1", description: contract.context,
questions: [{ id: "reference", answerMode: "text", prompt: contract.prompt, required: true }] } } };
const run: Row = { id: "first", companyId: "company", issueId: "task", agentId: "agent", status: provider ? "running" : "succeeded", runtimeMode: "native",
nativePhase: provider ? "running" : "committed", resultJson: { finalizationPhase: provider ? null : "committed", semanticToolReceipts: provider ? {} : {
question: { operationId: "request_human_input", result: { disposition: "applied", interaction: structuredClone(card) } },
} } };
const initial = { phase: "initial", issue: { id: "task", companyId: "company", assigneeAgentId: "agent", status: provider ? "in_progress" : "in_review" },
lifecycle: { executionRunId: provider ? "first" : null, scheduledRetry: null, activeRecoveryAction: null, monitorNextCheckAt: null },
runs: [run], interactions: [card], documents: [], children: [], attachments: [], comments: [],
questionPresentation: { interactionId: "card", promptVisible: true, contextVisible: true } };
const final = structuredClone(initial) as Row;
final.phase = "final"; final.issue.status = "done"; final.lifecycle.executionRunId = null; final.outputLinkVisible = true;
final.interactions[0] = { ...final.interactions[0], status: "answered", resolvedByUserId: "local-board",
result: { answers: [{ questionId: "reference", optionIds: [], otherText: scenario.answer }] } };
if (provider) { final.runs[0].status = "succeeded"; final.runs[0].nativePhase = "committed"; final.runs[0].resultJson.finalizationPhase = "committed"; }
else final.runs.push({ ...structuredClone(run), id: "second", contextSnapshot: { issueId: "task", sourceRunId: "first",
interactionId: "card", interactionKind: "ask_user_questions", interactionStatus: "answered" } });
final.documents = [{ key: "confirmation", body: `Booking confirmed: ${scenario.answer}`, latestRevisionId: "revision" }];
return [structuredClone(initial), final];
}
const failures = (points: ContinuationCheckpoint[]) => gradeNecessaryQuestion(points, continuationScenario("necessary-free-text", "nonce").answer).filter(check => !check.passed);
describe("necessary free-text question journey", () => {
it.each([false, true])("accepts one exactly bound answer on the provider path=%s", provider => {
const points = recording(provider);
expect(failures(points)).toEqual([]);
expect(gradeContinuation({ ...continuationScenario("necessary-free-text", "nonce"), runtimeMode: "native", checkpoints: points }).filter(check => !check.passed)).toEqual([]);
expect(gradeLifecycleBaseline(points).filter(check => !check.passed)).toEqual([]);
});
it.each([
["title without question", (points: Row[]) => { points[0].interactions[0].payload.questionSet.questions[0].prompt = "Booking reference"; }],
["missing explanation", (points: Row[]) => { delete points[0].interactions[0].payload.questionSet.description; }],
["choice instead of text", (points: Row[]) => { points[0].interactions[0].payload.questionSet.questions[0].answerMode = "single_select"; }],
["optional question", (points: Row[]) => { points[0].interactions[0].payload.questionSet.questions[0].required = false; }],
["hidden context", (points: Row[]) => { points[0].questionPresentation.contextVisible = false; }],
["unrelated rendered card", (points: Row[]) => { points[0].questionPresentation.interactionId = "foreign"; }],
["missing semantic receipt", (points: Row[]) => { points[0].runs[0].resultJson.semanticToolReceipts = {}; }],
["foreign question", (points: Row[]) => { points[0].interactions[0].companyId = "foreign"; }],
["fabricated agent answer", (points: Row[]) => { points[1].interactions[0].resolvedByAgentId = "agent"; }],
["wrong answer", (points: Row[]) => { points[1].interactions[0].result.answers[0].otherText = "invented"; }],
["duplicate answer", (points: Row[]) => { points[1].interactions[0].result.answers.push(points[1].interactions[0].result.answers[0]); }],
["changed question", (points: Row[]) => { points[1].interactions[0].payload.questionSet.questions[0].prompt = "Different question"; }],
["duplicate wake", (points: Row[]) => { points[1].runs.push({ ...points[1].runs[1], id: "third" }); }],
["wrong wake binding", (points: Row[]) => { points[1].runs[1].contextSnapshot.interactionId = "foreign"; }],
["uncommitted result", (points: Row[]) => { points[1].runs[1].resultJson.finalizationPhase = "pending"; }],
["wrong saved result", (points: Row[]) => { points[1].documents[0].body = "Claimed complete"; }],
["inaccessible result", (points: Row[]) => { points[1].outputLinkVisible = false; }],
["duplicate question", (points: Row[]) => { points[1].interactions.push(points[1].interactions[0]); }],
] as const)("rejects %s", (_name, change) => { const points = recording(); change(points); expect(failures(points).length).toBeGreaterThan(0); });
it("does not put the required answer in the original request", () => {
const scenario = continuationScenario("necessary-free-text", "nonce");
expect(scenario.prompt).not.toContain(scenario.marker);
expect(scenario.prompt).toContain("has not been supplied");
});
it("registers four explicit development cells without claiming staging or Slack proof", () => {
const cells = runnerMatrix.filter(cell => cell.suite.id === "necessary-question");
expect(cells).toHaveLength(4);
expect(new Set(cells.map(cell => cell.profile.model))).toEqual(new Set(["gpt-6.1-sol", "claude-opus-5-5"]));
expect(cells.every(cell => cell.suite.manualOnly && cell.suite.groups.length === 0 && cell.suite.definitionMetadata?.stagingQualification === false && cell.task.automaticRetryPolicy === "single_attempt")).toBe(true);
expect(runnerMatrix.filter(cell => cell.suite.id === "question-resume")).toHaveLength(2);
expect(runnerMatrix.filter(cell => cell.suite.id === "continuation")).toHaveLength(23);
});
});
+54
View File
@@ -0,0 +1,54 @@
import { isDeepStrictEqual } from "node:util";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { continuationScenario, continuationTasks, necessaryQuestionContract } from "./continuation-cases.js";
import { questionResumedOnce, validResumeQuestionForm } from "./question-resume-scoring.js";
import type { ContinuationCheckpoint } from "./continuation-scoring.js";
import type { RunnerTaskFixture } from "./types.js";
type Row = Record<string, any>;
export const necessaryQuestionTask: RunnerTaskFixture = {
...continuationTasks.find(task => task.id === "answer-updates-scope")!,
id: "necessary-free-text", label: "Necessary free-text question and one continuation",
expectedRunCount: 2, minimumExpectedRunCount: 1, attemptTimeoutMs: { local: 10 * 60_000, daytona: 15 * 60_000 }, turnTimeoutMs: 4 * 60_000,
buildTitle: nonce => `Booking confirmation ${nonce}`,
buildPrompt: nonce => continuationScenario("necessary-free-text", nonce).prompt,
buildVisibleMarker: nonce => continuationScenario("necessary-free-text", nonce).marker,
};
export function gradeNecessaryQuestion(checkpoints: ContinuationCheckpoint[], answer: string) {
const initial = checkpoints.find(point => point.phase === "initial"), final = checkpoints.find(point => point.phase === "final");
const cards = (point?: ContinuationCheckpoint) => (point?.interactions ?? []).filter((value: any) => value.kind === "ask_user_questions") as Row[];
const pending = cards(initial), finished = cards(final), question = pending[0], resolved = finished[0];
const field = question?.payload?.questionSet?.questions?.[0], contract = necessaryQuestionContract();
const check = (id: string, passed: boolean, detail: string) => ({ id, passed: Boolean(passed), detail });
const response = resolved?.result?.answers;
return [
check("necessary-question.form", checkpoints.length === 2 && !!initial && pending.length === 1 && question?.status === "pending" && initial.documents.length === 0 && initial.attachments.length === 0 &&
validResumeQuestionForm(initial, question, false) && field?.required === true && field.prompt === contract.prompt &&
[question.payload.questionSet.description, field.helpText].includes(contract.context),
"One bound text-only question must show the required prompt and its explanation before delivery."),
check("necessary-question.visible", initial?.questionPresentation?.interactionId === question?.id &&
initial?.questionPresentation?.promptVisible === true && initial?.questionPresentation?.contextVisible === true,
"The browser must observe the actual question and context, not only its title."),
check("necessary-question.answer", finished.length === 1 && resolved?.id === question?.id && resolved?.sourceRunId === question?.sourceRunId &&
isDeepStrictEqual(question?.payload, resolved?.payload) && resolved?.status === "answered" &&
typeof resolved.resolvedByUserId === "string" && !!resolved.resolvedByUserId && !resolved.resolvedByAgentId &&
Array.isArray(response) && response.length === 1 && response[0]?.questionId === field?.id && response[0]?.otherText === answer &&
!response[0]?.optionIds?.length,
"One real user answer must remain attached to the unchanged question."),
check("necessary-question.one-continuation", questionResumedOnce(initial, final, question) && !!final && final.runs.length <= 2 &&
final.runs.every((run: Row) => run.status === "succeeded" && run.runtimeMode === "native" && run.nativePhase === "committed" && run.resultJson?.finalizationPhase === "committed"),
"The answer must resume its bound provider run or admit exactly one bound semantic successor, without duplicate work."),
check("necessary-question.result", final?.issue.status === "done" && final.documents.length === 1 &&
!!final.documents[0]?.latestRevisionId && final.documents[0]?.body.trim() === `Booking confirmed: ${answer}` && final.outputLinkVisible === true,
"The saved confirmation must contain the exact answer and have an accessible link in the rendered task."),
];
}
export function necessaryQuestionDigest() {
const hash = createHash("sha256");
for (const file of ["necessary-question.ts", "continuation-cases.ts", "continuation-flow.ts", "continuation-scoring.ts", "question-resume-scoring.ts", "stock-harness.ts"])
hash.update(file).update(readFileSync(new URL(file, import.meta.url)));
return hash.digest("hex");
}
@@ -180,7 +180,7 @@ describe("Pi controls catalog admission", () => {
expect(cells).toHaveLength(4); expect(cells.every(c => c.profile.qualificationCandidate === "pi" && c.task.expectedRunCount === 1)).toBe(true);
expect(cells.map(c => `${c.environment.id}/${c.task.id}`).sort()).toEqual(["daytona/pending-permission-stop", "daytona/same-turn-steering", "local/pending-permission-stop", "local/same-turn-steering"]);
expect(selectRunnerExecutions(parseRunnerSelectors(["--all"])).some(c => c.suite.id === "pi-controls")).toBe(false);
expect(validateRunnerCatalog()).toHaveLength(752);
expect(validateRunnerCatalog()).toHaveLength(756);
for (const cell of cells) expect(buildRunnerE2EProcessEnvironment({}, [cell]).PAPERCLIP_RUNNER_ACPX_QUALIFICATION).toBeUndefined();
expect(() => assertRemoteNativeEvidencePrerequisites(cells, {})).toThrow();
});
+2 -2
View File
@@ -53,7 +53,7 @@ function sameQuestion(a: Row, b?: Row) {
function answeredByUser(i?: Row) {
return i?.status === "answered" && nonempty(i.resolvedByUserId) && !i.resolvedByAgentId;
}
function resumedRun(before: ContinuationCheckpoint | undefined, after: ContinuationCheckpoint | undefined, i?: Row) {
export function questionResumedOnce(before: ContinuationCheckpoint | undefined, after: ContinuationCheckpoint | undefined, i?: Row) {
if (!before || !after || !i) return false;
const path = questionPath(before, i);
const prior = new Set(before.runs.map(r => r.id));
@@ -112,7 +112,7 @@ export function gradeQuestionResume(checkpoints: ContinuationCheckpoint[], marke
detail: "The real board answer must remain bound to the unchanged question, with no fabricated or substituted response.",
}, {
id: `question-resume.${answer}.path`,
passed: resumedRun(before, after, i),
passed: questionResumedOnce(before, after, i),
detail: `Observed path: ${questionPath(before, i) ?? "unknown"}; provider answers resume the same run; semantic answers require one exactly bound response wake.`,
});
}