test(runner): recognize pre-start Pi submission identity

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-02 06:38:22 -05:00
1 parent 9f6f020da6
commit ad83416b03
4 files changed
+251 -1

No files matched your search

+5
View File
@@ -314,6 +314,11 @@ resolution, one original successful turn and independently read exact workspace
JSON prove delivery. A replacement run, replay, cancellation, expiry, rewritten
request or merely reloaded browser cannot pass. Full states and PRP identities
stay in private snapshots under the existing publication allowlist.
The local runner's unique `turn.submitted` receipt may precede assignment of the
provider turn ID. The oracle accepts that missing ID only before the single
matching `turn.started` and request creation, with the same session and producer
and increasing durable/source sequence numbers. Later missing or changed turn
identities still fail.
### Pi file editing and registered artifacts
@@ -0,0 +1,182 @@
{
"description": "Sanitized identity-only projection of Pi 1.0.0 profile13 pending-input stream; original submission omits turnId before turn.started assigns it. No restart was reached in the original failing attempt. Prompt, reasoning, timestamps and private execution metadata omitted.",
"state": {
"issue": {
"id": "issue",
"companyId": "company",
"status": "in_progress"
},
"runs": [
{
"id": "run",
"companyId": "company",
"nativeIssueId": "issue",
"runtimeMode": "native",
"status": "running",
"nativeSessionId": "session",
"runnerInstanceId": "runner"
}
],
"interactions": [
{
"id": "interaction",
"companyId": "company",
"issueId": "issue",
"kind": "ask_user_questions",
"status": "pending",
"result": null,
"sourceRunId": "run",
"continuationPolicy": "none",
"resolverPolicy": "human_only",
"idempotencyKey": "paperclip-runner-question:run:request",
"payload": {
"version": 1,
"title": "Pi native restart",
"submitLabel": "Submit answers",
"supersedeOnUserComment": false,
"questions": [
{
"id": "answer",
"prompt": "Answer",
"helpText": "Pi native restart",
"selectionMode": "single",
"required": true,
"allowOther": true,
"options": [
{
"id": "paperclip_text_answer",
"label": "Pi native restart",
"description": "Expected text input",
"freeText": true
}
]
}
],
"questionSet": {
"schema": "paperclip.question_set.v1",
"title": "Pi native restart",
"submitLabel": "Submit answers",
"questions": [
{
"id": "answer",
"header": "Pi native restart",
"prompt": "Answer",
"required": true,
"answerMode": "text",
"textValidation": {
"inputType": "text"
}
}
]
},
"runtimeRequestId": "request"
}
}
]
},
"events": [
{
"companyId": "company",
"runId": "run",
"seq": 25,
"protocolSchemaVersion": 1,
"eventType": "turn.submitted",
"payload": {
"prpEvent": {
"schema": "paperclip.prp.event.v1",
"schemaVersion": 1,
"sourceKind": "runner",
"eventType": "turn.submitted",
"runId": "run",
"normalizedSessionId": "session",
"sourceInstanceId": "runner",
"sourceSeq": 6,
"sourceEventId": "runner:run:6",
"payload": {
"text": "Sanitized fixture request"
}
}
}
},
{
"companyId": "company",
"runId": "run",
"seq": 28,
"protocolSchemaVersion": 1,
"eventType": "turn.started",
"payload": {
"prpEvent": {
"schema": "paperclip.prp.event.v1",
"schemaVersion": 1,
"sourceKind": "runner",
"eventType": "turn.started",
"runId": "run",
"turnId": "turn",
"normalizedSessionId": "session",
"sourceInstanceId": "runner",
"sourceSeq": 7,
"sourceEventId": "runner:run:7",
"payload": {
"status": "inProgress"
}
}
}
},
{
"companyId": "company",
"runId": "run",
"seq": 222,
"protocolSchemaVersion": 1,
"eventType": "runtime_request.created",
"payload": {
"prpEvent": {
"schema": "paperclip.prp.event.v1",
"schemaVersion": 1,
"sourceKind": "runner",
"eventType": "runtime_request.created",
"runId": "run",
"turnId": "turn",
"itemId": "item-run",
"normalizedSessionId": "session",
"sourceInstanceId": "runner",
"sourceSeq": 195,
"sourceEventId": "runner:run:195",
"payload": {
"request": {
"type": "input",
"input": {
"title": "Pi native restart",
"schema": "paperclip.question_set.v1",
"questions": [
{
"id": "answer",
"header": "Pi native restart",
"prompt": "Answer",
"required": true,
"answerMode": "text",
"textValidation": {
"inputType": "text"
}
}
],
"submitLabel": "Submit answers"
},
"itemId": "item-run",
"origin": {
"method": "elicitation/create",
"adapter": "acpx-runtime-sidecar",
"provider": "pi"
},
"prompt": "Pi native restart",
"schema": "paperclip.runtime_request.v2",
"status": "pending",
"turnId": "turn",
"requestId": "request",
"requestKind": "runtime"
}
}
}
}
}
]
}
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { expect, it, vi } from "vitest";
import { gradePiRestartCompletion, observePiRestartPending, runPiPendingControllerRestart } from "./pi-native-restart-flow.js";
import { validatePrpStructuredRunResult } from "../../packages/paperclip-runner/src/protocol/replay-contract.js";
@@ -50,6 +51,56 @@ it.each(["acpx-runtime", "acpx-runtime-sidecar"])("accepts mixed runner/control-
expect(gradePiRestartCompletion(f.state, f.events, pending, "hidden", proof)).toBe(true);
});
function actualPendingPrefix() {
return JSON.parse(readFileSync(new URL("./fixtures/pi-native-restart-actual-prefix.json", import.meta.url), "utf8")) as {
state: { issue: Row; runs: Row[]; interactions: Row[] }; events: Row[];
};
}
it.each(["omitted", "null"])("accepts the actual pre-start submission with %s turn ID", representation => {
const f = actualPendingPrefix();
if (representation === "null") f.events[0]!.payload.prpEvent.turnId = null;
expect(observePiRestartPending(f.state, f.events, "company")).toMatchObject({
runId: "run", turnId: "turn", nativeSessionId: "session", sourceInstanceId: "runner", createdSourceSeq: 195,
});
});
it.each([
["missing start", (f: ReturnType<typeof actualPendingPrefix>) => { f.events.splice(1, 1); }],
["start without assigned turn", (f: ReturnType<typeof actualPendingPrefix>) => { delete f.events[1]!.payload.prpEvent.turnId; }],
["start with foreign turn", (f: ReturnType<typeof actualPendingPrefix>) => { f.events[1]!.payload.prpEvent.turnId = "other"; }],
["foreign submission turn", (f: ReturnType<typeof actualPendingPrefix>) => { f.events[0]!.payload.prpEvent.turnId = "other"; }],
["foreign submission session", (f: ReturnType<typeof actualPendingPrefix>) => { f.events[0]!.payload.prpEvent.normalizedSessionId = "other"; }],
["foreign submission producer", (f: ReturnType<typeof actualPendingPrefix>) => {
Object.assign(f.events[0]!.payload.prpEvent, { sourceInstanceId: "other", sourceEventId: "other:run:6" });
}],
["submission after start", (f: ReturnType<typeof actualPendingPrefix>) => {
f.events[0]!.seq = 29;
Object.assign(f.events[0]!.payload.prpEvent, { sourceSeq: 8, sourceEventId: "runner:run:8" });
}],
["start after request", (f: ReturnType<typeof actualPendingPrefix>) => {
f.events[1]!.seq = 223;
Object.assign(f.events[1]!.payload.prpEvent, { sourceSeq: 196, sourceEventId: "runner:run:196" });
}],
["duplicate submission", (f: ReturnType<typeof actualPendingPrefix>) => {
const row = structuredClone(f.events[0]!); row.seq = 26;
Object.assign(row.payload.prpEvent, { sourceSeq: 7, sourceEventId: "runner:run:7" });
Object.assign(f.events[1]!.payload.prpEvent, { sourceSeq: 8, sourceEventId: "runner:run:8" });
f.events.push(row);
}],
["duplicate start", (f: ReturnType<typeof actualPendingPrefix>) => {
const row = structuredClone(f.events[1]!); row.seq = 29;
Object.assign(row.payload.prpEvent, { sourceSeq: 8, sourceEventId: "runner:run:8" }); f.events.push(row);
}],
["later unbound turn event", (f: ReturnType<typeof actualPendingPrefix>) => {
const row = structuredClone(f.events[1]!); row.seq = 29; row.eventType = "turn.progress";
Object.assign(row.payload.prpEvent, { eventType: "turn.progress", turnId: null, sourceSeq: 8, sourceEventId: "runner:run:8" }); f.events.push(row);
}],
] as const)("rejects %s around an unbound submission", (_label, mutate) => {
const f = actualPendingPrefix(); mutate(f);
expect(() => observePiRestartPending(f.state, f.events, "company")).toThrow("Pi native restart");
});
it.each([
["no native event", (f: ReturnType<typeof fixture>) => { f.events.length = 0; }],
["wrong origin", (f: ReturnType<typeof fixture>) => { f.request.origin.provider = "cursor"; }],
+13 -1
View File
@@ -66,7 +66,19 @@ function inspect(state: State, events: readonly Row[], companyId: string, header
&& id(question.id) && question.answerMode === "text" && question.header === header
&& card.payload?.runtimeRequestId === request.requestId && digest(card.payload.questionSet) === digest(questionSet)
&& card.idempotencyKey === `paperclip-runner-question:${run.id}:${request.requestId}`, "canonical Pi input request identity missing");
requireProof(rows.filter(x => x.event.turnId != null || x.event.eventType.startsWith("turn.") || x.event.eventType.startsWith("runtime_request.")).every(({ event }) => event.turnId === request.turnId
const submitted = rows.filter(x => x.event.eventType === "turn.submitted");
const unboundSubmission = submitted.find(x => x.event.turnId == null);
if (unboundSubmission) {
// The local runner records submission before ACP assigns the provider turn ID.
// Only that unique, ordered pre-start receipt may omit the turn identity.
const started = rows.filter(x => x.event.eventType === "turn.started");
requireProof(submitted.length === 1 && started.length === 1
&& started[0]!.event.turnId === request.turnId
&& unboundSubmission.row.seq < started[0]!.row.seq && started[0]!.row.seq < opening.row.seq
&& unboundSubmission.event.sourceSeq < started[0]!.event.sourceSeq && started[0]!.event.sourceSeq < opening.event.sourceSeq,
"unbound submission is not the unique pre-start receipt");
}
requireProof(rows.filter(x => x.event.turnId != null || x.event.eventType.startsWith("turn.") || x.event.eventType.startsWith("runtime_request.")).every(({ row, event }) => (event.turnId === request.turnId || row === unboundSubmission?.row)
&& event.normalizedSessionId === run.nativeSessionId
&& event.sourceInstanceId === (event.sourceKind === "runner" ? run.runnerInstanceId : `${run.runnerInstanceId}:control`)), "turn, native session or producer was replaced");
requireProof(rows.filter(x => x.event.sourceKind === "control_plane").every(x => x.event.turnId === request.turnId), "control-plane result belongs to another turn");