test: grade current chat status independently of historical mentions

Require the actual current blocker and active-run count in a structured status response. Reject stale blockers, missing execution evidence, and task mutations; version the fixture contract. Apply Rust formatter to the readiness regression.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-21 09:51:13 -05:00
1 parent 6a98ef7435
commit 50448c228a
6 files changed
+33 -13

No files matched your search

@@ -2127,7 +2127,9 @@ mod tests {
assert_eq!(ready["warmAttachReady"], true);
assert_eq!(ready["warmAttachBlockers"], json!([]));
let blocked_states: [(&str, fn(&mut AcpxDurableState)); 4] = [
("durable_closed", |state| state.lifecycle = "closed".to_owned()),
("durable_closed", |state| {
state.lifecycle = "closed".to_owned()
}),
("provider_exit_unconfirmed", |state| {
state.lifecycle = "prepared".to_owned();
state.provider_exit_unconfirmed = true;
+3
View File
@@ -177,6 +177,9 @@ failed and cancelled runs must not disappear from campaign totals.
the ordinary public APIs to seed source documents and blockers. Keep the answer
out of the user's status/review request. Grade the exact source values, latest
blocker, preserved task identities, worker-authored output, and real executions.
The status request asks for JSON so the grader can distinguish the current
blocker from a historical mention and compare active-run count separately from
task status. The request must not reveal those expected values.
The lost-acknowledgement probe may interrupt only the fixture browser's own
comment request after the real server has committed it. Retain its request ID
and replay that same request through the public API after restarting the server.
+4
View File
@@ -172,6 +172,10 @@ Restart continuity requires the agent to recall a phrase after the server
restarts. The final prompt does not reveal that phrase. A generic successful
reply after restart cannot pass this check.
The blocker query requests a JSON status snapshot. It must name the current
recorded blocker and report zero active runs independently of the task's blocked
status. Mentioning the right blocker only as resolved history cannot pass.
The committed-send case drops the browser's acknowledgement after the server
saves its comment. It waits for the agent to save one backlog task, restarts
Paperclip, and replays the exact public request with the original client request ID. It
+1 -1
View File
@@ -959,7 +959,7 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
environments: [localEnvironment, daytonaWarmEnvironment], tasks: chatHardeningTasks, expectedMatrixSize: 18,
excludedExecutionIds: ["runner-codex", "runner-acpx-claude"].flatMap(profile =>
["stop-startup-new-resume", "hire-delegate-reuse", "blocked-status-review"].map(task => `agent-chat-hardening.${profile}.daytona.${task}`)),
definitionMetadata: { version: 2, permissions: "production-defaults", instructions: "production", grading: "durable-state-and-source-evidence", scheduling: "explicit-only", restartMemory: "required-after-restart" },
definitionMetadata: { version: 3, permissions: "production-defaults", instructions: "production", grading: "durable-state-and-source-evidence", scheduling: "explicit-only", restartMemory: "required-after-restart", statusEvidence: "structured-current-blocker-and-active-run-count" },
},
...(process.env.PAPERCLIP_RUNNER_E2E_CONNECTION_REVIEWS === "1" ? [connectionReviewSuite] : []),
{
+9 -2
View File
@@ -31,12 +31,19 @@ describe("agent chat hardening oracles", () => {
it("requires the current recorded blocker without silently starting or changing work", () => {
const before = { ...task, status: "blocked" };
const valid = { reply: "RUN-2 is blocked on VENUE123.", expectedIssueIdentifier: "RUN-2", blocker: "VENUE123", staleBlocker: "BUDGET123",
const status = { issueIdentifier: "RUN-2", status: "blocked", currentBlockerLabel: "VENUE123", activeRunCount: 0 };
const valid = { reply: JSON.stringify(status), expectedIssueIdentifier: "RUN-2", blocker: "VENUE123",
before, after: before, taskIdsBefore: ["task"], taskIdsAfter: ["task"], taskRuns: [] };
expect(() => assertGroundedChatStatus(valid)).not.toThrow();
expect(() => assertGroundedChatStatus({ ...valid, reply: "RUN-2 is blocked on VENUE123. BUDGET123 was resolved." })).not.toThrow();
expect(() => assertGroundedChatStatus({ ...valid, reply: JSON.stringify({ ...status, explanation: "BUDGET123 was resolved." }) })).not.toThrow();
expect(() => assertGroundedChatStatus({ ...valid, reply: JSON.stringify({ ...status, currentBlockerLabel: "BUDGET123", explanation: "VENUE123 was resolved." }) })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, reply: JSON.stringify({ ...status, activeRunCount: 1 }) })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, reply: JSON.stringify({ ...status, status: "in_progress", explanation: "Previously blocked." }) })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, reply: JSON.stringify({ ...status, activeRunCount: undefined }) })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, reply: "RUN-2 is blocked on BUDGET123." })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, after: task })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, after: { ...before, title: "Changed" } })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, after: { ...before, projectId: "different" } })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, taskRuns: [run] })).toThrow();
expect(() => assertGroundedChatStatus({ ...valid, taskIdsAfter: ["task", "replacement"] })).toThrow();
});
+13 -9
View File
@@ -37,15 +37,19 @@ export function assertChatHire(input: {
}
export function assertGroundedChatStatus(input: {
reply: string; expectedIssueIdentifier: string; blocker: string; staleBlocker: string;
reply: string; expectedIssueIdentifier: string; blocker: string;
before: ChatIssue; after: ChatIssue; taskIdsBefore: string[]; taskIdsAfter: string[]; taskRuns: ChatRun[];
}) {
expect(input.reply).toContain(input.expectedIssueIdentifier);
expect(input.reply).toMatch(/blocked/i);
expect(input.reply).toContain(input.blocker);
// A correct answer may explain that the old blocker was resolved. Require
// the current label rather than rejecting that useful historical context.
expect(input.after).toMatchObject({ id: input.before.id, status: "blocked", assigneeAgentId: input.before.assigneeAgentId });
// Mentioning a correct label is insufficient: the answer could falsely call
// it resolved, or confuse a blocked task with an actively running provider.
const json = input.reply.trim().replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
expect(JSON.parse(json)).toMatchObject({
issueIdentifier: input.expectedIssueIdentifier, status: "blocked",
currentBlockerLabel: input.blocker, activeRunCount: 0,
});
for (const field of ["id", "title", "status", "assigneeAgentId", "parentId", "projectId"] as const) {
expect(input.after[field], `Status reporting must preserve ${field}`).toEqual(input.before[field]);
}
expect([...input.taskIdsAfter].sort()).toEqual([...input.taskIdsBefore].sort());
expect(input.taskRuns).toHaveLength(0);
}
@@ -148,8 +152,8 @@ export async function runChatHardeningFlow(context: {
const sourceBrief = await api.get<Document>(`/api/issues/${source.id}/documents/brief`);
await api.post(`/api/issues/${source.id}/comments`, { body: `Earlier blocker: ${staleBlocker}. Waiting for the budget.` });
await api.post(`/api/issues/${source.id}/comments`, { body: `Budget is resolved. Current blocker: ${blocker}. Waiting for the venue confirmation. Keep this task blocked; no execution is active.` });
await turn(`What is the actual current blocker on ${source.identifier}? Read the latest recorded explanation, include its exact blocker label, and distinguish task status from active execution. Just report; do not change it or create work.`, 1);
assertGroundedChatStatus({ reply: await latestReply(), expectedIssueIdentifier: source.identifier!, blocker, staleBlocker,
await turn(`What is the actual current status of ${source.identifier}? Read its latest recorded blocker and execution state. Reply with only JSON containing issueIdentifier, status, currentBlockerLabel, and activeRunCount. You may include an explanation field. Just report; do not change it or create work.`, 1);
assertGroundedChatStatus({ reply: await latestReply(), expectedIssueIdentifier: source.identifier!, blocker,
before: source, after: await api.get(`/api/issues/${source.id}`), taskIdsBefore: [source.id],
taskIdsAfter: (await tasks()).map(task => task.id), taskRuns: (await allRuns()).filter(run => run.contextSnapshot?.issueId === source.id) });
const config = execution.profile.buildAgent({ environmentId: f.environment.id, environmentFixtureId: execution.environment.id,