Files
PaperClipAI/server/src/services/native-runtime/native-cursor-plan-wait.test.ts
T
DottaandPaperclip c58a8881f2 fix: validate Cursor plan waits against canonical contract policy
Reuse the production completion-contract envelope hash and exercise real contract creation and reuse in accepted-plan persistence tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-30 02:54:16 -05:00

157 lines
14 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import * as runner from "../../vendor/paperclip-runner/index.js";
import { resolveQualifiedAcpxProfile, validatePrpStructuredRunResult } from "../../vendor/paperclip-runner/index.js";
import { buildQuestionResponseDeliveryEnvelope } from "../question-response-delivery.js";
import { NATIVE_COMPLETION_CONTRACT_SCHEMA, NATIVE_COMPLETION_POLICY_VERSION } from "./completion-contracts.js";
import { nativeSha256 } from "./canonical.js";
import { nativeCursorPlanWaitFromFacts, type CursorPlanWaitFacts } from "./native-cursor-plan-wait.js";
function fixture(): CursorPlanWaitFacts {
const b = { companyId: "company", issueId: "issue", agentId: "agent", runId: "run" };
const contract = { revision: "revision", criteria: [{ id: "criterion" }] };
const contractMetadata = { schemaVersion: NATIVE_COMPLETION_CONTRACT_SCHEMA, policyVersion: NATIVE_COMPLETION_POLICY_VERSION, risk: "low", completionAuthority: "agent_claim_policy" };
const contractSha = nativeSha256({ ...contractMetadata, contract });
const planId = `plan-${"a".repeat(64)}`;
const input = { schema: "paperclip.question_set.v1", title: "Plan", description: "Exact revised plan text", questions: [{ id: planId, prompt: "Proceed?", required: true, answerMode: "single_select", options: [{ id: "accept", label: "Accept" }, { id: "reject", label: "Reject" }, { id: "cancel", label: "Cancel" }] }] };
const i = { id: "interaction", ...b, sourceRunId: b.runId, createdByAgentId: b.agentId, resolvedByUserId: "board", resolvedByAgentId: null, resolvedAt: new Date(0),
kind: "ask_user_questions", status: "answered", continuationPolicy: "none", idempotencyKey: "paperclip-runner-question:run:request",
payload: { runtimeRequestId: "request", questionSet: input }, result: { version: 1, answers: [{ questionId: planId, optionIds: ["accept"] }] } };
const envelope = buildQuestionResponseDeliveryEnvelope(i as never);
const d = { id: "delivery", ...b, interactionId: i.id, sourceRunId: b.runId, targetRunId: b.runId, targetTurnId: null, status: "delivered", deliveryMode: "steered", acknowledgedAt: new Date(1), payloadSha256: nativeSha256(envelope) };
const event = (sourceSeq: number, eventType: string, payload: Record<string, unknown>) => {
const e = { schema: "paperclip.prp.event.v1", runId: b.runId, normalizedSessionId: "session", turnId: "turn", sourceInstanceId: "instance", sourceEventId: `instance:${sourceSeq}`, sourceSeq, sourceKind: "runner", schemaVersion: 1, eventType, payload };
return { ...b, seq: sourceSeq, eventType, payload: { prpEvent: e }, sourceInstanceId: e.sourceInstanceId, sourceEventId: e.sourceEventId, sourceSeq, sourcePayloadSha256: nativeSha256(e), protocolSchemaVersion: 1 };
};
return {
binding: b,
run: { id: b.runId, companyId: b.companyId, agentId: b.agentId, nativeIssueId: b.issueId, runtimeMode: "native", runnerInstanceId: "instance", status: "running", completionContractId: "contract", completionContractSha256: contractSha, runnerProfileJson: { nativeExecutionInput: { binding: b, provider: { kind: "acpx", agent: "cursor", cursorMode: "plan", model: "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]", profile: resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]") }, session: { normalizedSessionId: "session" }, completionContract: { id: "contract", sha256: contractSha, contract } } } },
contract: { id: "contract", ...contractMetadata, canonicalSha256: contractSha, contractJson: contract },
events: [
event(275, "runtime_request.created", { request: { schema: "paperclip.runtime_request.v2", status: "pending", type: "input", requestKind: "runtime", requestId: "request", turnId: "turn", itemId: "native-plan-tool", origin: { provider: "cursor", method: "cursor/create_plan", adapter: "acpx-runtime-sidecar" }, input } }),
event(294, "tool.execution.started", { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "running", name: "arbitrary display name" }),
event(300, "runtime_request.resolved", { requestId: "request", turnId: "turn", action: "submit", response: envelope.response }),
event(303, "tool.execution.completed", { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "completed" }),
event(304, "turn.completed", { status: "completed", error: null }),
],
interactions: [{ interaction: i, delivery: d }],
} as unknown as CursorPlanWaitFacts;
}
function editEvent(f: CursorPlanWaitFacts, index: number, edit: (e: any) => void) {
const row = f.events.filter(row => !row.eventType.startsWith("tool.execution."))[index]!;
const e = (row.payload as any).prpEvent;
edit(e); row.sourcePayloadSha256 = nativeSha256(e);
}
describe("accepted Cursor plan passive-wait authority", () => {
it("records the accepted revision and explicitly unfinished Plan-mode continuation", () => {
const value = nativeCursorPlanWaitFromFacts(fixture());
expect(value?.source).toMatchObject({ requestId: "request", planRevision: `plan-${"a".repeat(64)}`, terminalEventId: "instance:304", toolExecutionId: "native-plan-tool" });
expect(value?.result).toMatchObject({ reportedWorkDisposition: "yielded", completionClaim: { objectiveSatisfied: false, remainingWork: [{ blocksCompletion: true }] }, continuation: { kind: "response_wake" } });
expect(value?.result.summary).toContain("next message");
const validated = validatePrpStructuredRunResult(value!.result);
expect(validated.ok).toBe(true);
if (validated.ok) expect(validated.result).toEqual(value!.result);
});
it.each(["unrelated same-title tool", "cross turn", "cross session", "duplicate start", "duplicate completion", "missing start", "missing completion", "failed completion", "changed request tool", "uncommitted tool row"])("rejects correlated lifecycle corruption: %s", kind => {
const f = fixture();
const start = f.events.find(row => row.eventType === "tool.execution.started")!;
const end = f.events.find(row => row.eventType === "tool.execution.completed")!;
const event = (start.payload as any).prpEvent;
if (kind === "unrelated same-title tool") event.payload.executionId = "unrelated-tool";
if (kind === "cross turn") event.turnId = "other";
if (kind === "cross session") event.normalizedSessionId = "other";
if (kind === "duplicate start") f.events.splice(2, 0, structuredClone(start));
if (kind === "duplicate completion") f.events.splice(4, 0, structuredClone(end));
if (kind === "missing start") f.events = f.events.filter(row => row !== start);
if (kind === "missing completion") f.events = f.events.filter(row => row !== end);
if (kind === "failed completion") { (end.payload as any).prpEvent.payload.status = "failed"; end.sourcePayloadSha256 = nativeSha256((end.payload as any).prpEvent); }
if (kind === "changed request tool") editEvent(f, 0, e => { e.payload.request.itemId = "unrelated-tool"; });
start.sourcePayloadSha256 = kind === "uncommitted tool row" ? null : nativeSha256(event);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("rejects an additional same-title tool while the real plan tool remains correlated", () => {
const f = fixture();
const extra = structuredClone(f.events.find(row => row.eventType === "tool.execution.started")!);
extra.seq = 295; extra.sourceSeq = 295; extra.sourceEventId = "instance:295";
const e = (extra.payload as any).prpEvent;
Object.assign(e, { sourceSeq: 295, sourceEventId: extra.sourceEventId });
Object.assign(e.payload, { executionId: "unrelated", name: "Create Plan" });
extra.sourcePayloadSha256 = nativeSha256(e); f.events.splice(2, 0, extra);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("binds actual callback-before-tool-start order without trusting display names", () => {
const f = fixture(); const original = nativeCursorPlanWaitFromFacts(f)!;
expect(original.source.toolLifecycleSha256).toMatch(/^[a-f0-9]{64}$/);
const start = f.events.find(row => row.eventType === "tool.execution.started")!;
(start.payload as any).prpEvent.payload.name = "changed display text";
start.sourcePayloadSha256 = nativeSha256((start.payload as any).prpEvent);
const changed = nativeCursorPlanWaitFromFacts(f)!;
expect(changed).not.toBeNull();
expect(changed.source.authoritySha256).not.toBe(original.source.authoritySha256);
expect(changed.source.toolLifecycleSha256).not.toBe(original.source.toolLifecycleSha256);
});
it("does not create a new wait from an earlier profile after the catalog advances", () => {
const facts = fixture();
expect(nativeCursorPlanWaitFromFacts(facts)).not.toBeNull();
const current = resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]");
const resolver = vi.spyOn(runner, "resolveQualifiedAcpxProfile").mockReturnValue({ ...current, agentProfileVersion: 8, commandDigest: `sha256:${"b".repeat(64)}` });
try { expect(nativeCursorPlanWaitFromFacts(facts)).toBeNull(); }
finally { resolver.mockRestore(); }
});
it("projects only declared scope keys from a wider typed caller binding", () => {
const f = fixture(); Object.assign(f.binding, { wakeupRequestId: "unrelated-caller-metadata" });
const proof = nativeCursorPlanWaitFromFacts(f);
expect(proof).not.toBeNull();
expect(proof!.source).not.toHaveProperty("wakeupRequestId");
});
it.each([
["foreign runner", (f: CursorPlanWaitFacts) => { f.run.runnerInstanceId = "other"; }],
["changed admission binding", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.binding = { ...f.binding, runId: "other" }; }],
["wrong company", (f: CursorPlanWaitFacts) => { f.run.companyId = "other"; }],
["wrong task", (f: CursorPlanWaitFacts) => { f.run.nativeIssueId = "other"; }],
["failed run", (f: CursorPlanWaitFacts) => { f.run.status = "failed"; }],
["cancelled run", (f: CursorPlanWaitFacts) => { f.run.status = "cancelled"; }],
["Agent mode", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.provider.cursorMode = "agent"; }],
["stale profile", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.provider.profile = { ...resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]"), commandDigest: "old" }; }],
["changed contract policy", (f: CursorPlanWaitFacts) => { f.contract.policyVersion = "tampered-policy"; }],
["changed completion authority", (f: CursorPlanWaitFacts) => { f.contract.completionAuthority = "server_arbiter"; }],
["changed contract hash", (f: CursorPlanWaitFacts) => { f.contract.canonicalSha256 = "f".repeat(64); }],
["changed contract", (f: CursorPlanWaitFacts) => { f.contract.contractJson.revision = "new"; }],
["unknown origin", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.origin.provider = "acpx"; })],
["wrong method", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.origin.method = "cursor/ask_question"; })],
["untrusted adapter", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.origin.adapter = "other"; })],
["cross turn", (f: CursorPlanWaitFacts) => editEvent(f, 1, e => { e.turnId = "other"; })],
["cross session", (f: CursorPlanWaitFacts) => editEvent(f, 1, e => { e.normalizedSessionId = "other"; })],
["native error", (f: CursorPlanWaitFacts) => editEvent(f, 2, e => { e.payload.error = { message: "failure" }; })],
["changed plan", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.input.description = "other"; })],
["rejection", (f: CursorPlanWaitFacts) => editEvent(f, 1, e => { e.payload.response.answers[`plan-${"a".repeat(64)}`].selectedOptionIds = ["reject"]; })],
["cancellation", (f: CursorPlanWaitFacts) => editEvent(f, 1, e => { e.payload.response.answers[`plan-${"a".repeat(64)}`].selectedOptionIds = ["cancel"]; })],
["missing delivery", (f: CursorPlanWaitFacts) => { f.interactions = []; }],
["unacknowledged delivery", (f: CursorPlanWaitFacts) => { f.interactions[0]!.delivery.acknowledgedAt = null; }],
["fallback wake", (f: CursorPlanWaitFacts) => { f.interactions[0]!.delivery.deliveryMode = "wake_fallback"; }],
["wrong target", (f: CursorPlanWaitFacts) => { f.interactions[0]!.delivery.targetRunId = "other"; }],
["changed answer digest", (f: CursorPlanWaitFacts) => { f.interactions[0]!.delivery.payloadSha256 = "other"; }],
["missing resolution time", (f: CursorPlanWaitFacts) => { f.interactions[0]!.interaction.resolvedAt = null; }],
["agent resolved", (f: CursorPlanWaitFacts) => { f.interactions[0]!.interaction.resolvedByAgentId = "agent"; }],
["duplicate receipt", (f: CursorPlanWaitFacts) => { f.events.splice(1, 0, structuredClone(f.events[0]!)); }],
["duplicate delivery", (f: CursorPlanWaitFacts) => { f.interactions.push(structuredClone(f.interactions[0]!)); }],
["uncommitted event", (f: CursorPlanWaitFacts) => { f.events[1]!.sourcePayloadSha256 = null; }],
["changed row identity", (f: CursorPlanWaitFacts) => { f.events[1]!.sourceEventId = "other"; }],
["terminal before answer", (f: CursorPlanWaitFacts) => { f.events.reverse(); }],
] as const)("rejects %s", (_name, mutate) => {
const f = fixture(); mutate(f); expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("does not reuse an older acceptance after a new request, later work, or conflicting terminal", () => {
for (const kind of ["runtime_request.created", "tool.execution.started", "turn.failed", "turn.cancelled", "turn.completed"]) {
const f = fixture(); const row = structuredClone(f.events[0]!);
row.seq = 2.5; row.sourceSeq = 3; row.sourceEventId = "instance:later"; row.eventType = kind;
const e = (row.payload as any).prpEvent; Object.assign(e, { sourceSeq: 3, sourceEventId: row.sourceEventId, eventType: kind });
if (kind === "runtime_request.created") e.payload.request.requestId = "new-plan";
row.sourcePayloadSha256 = nativeSha256(e);
editEvent(f, 2, e => { e.sourceSeq = 4; }); f.events[2]!.sourceSeq = 4;
f.events.splice(2, 0, row);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
}
});
});