mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
test(runner): recognize pre-start Pi submission identity
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
9f6f020da6
commit
ad83416b03
4 files changed
+251
-1
No files matched your search
@@ -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"; }],
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user