Files
PaperClipAI/server/src/services/native-runtime/native-cursor-plan-wait.test.ts
T
DottaandPaperclip 56ef7c2b2c Use provider-neutral modes in Runner contracts
Carry opaque mode identifiers through native execution, ACPX sidecar and Rust recovery. Delegate supported values and acknowledgement to provider adapters. Preserve exact historical plan waits and fence incompatible pre-release sessions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-05 21:26:52 -05:00

264 lines
20 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 { hasCommittedNativeCursorPlanWait, 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", mode: "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);
}
function resequence(f: CursorPlanWaitFacts) {
f.events.forEach((row, index) => {
const sourceSeq = index + 1;
Object.assign(row, { seq: sourceSeq, sourceSeq, sourceEventId: `instance:${sourceSeq}` });
Object.assign((row.payload as any).prpEvent, { sourceSeq, sourceEventId: row.sourceEventId });
row.sourcePayloadSha256 = nativeSha256((row.payload as any).prpEvent);
});
}
function withProgress(count: number) {
const f = fixture();
const start = f.events.find(row => row.eventType === "tool.execution.started")!;
const rows = Array.from({ length: count }, () => {
const row = structuredClone(start);
row.eventType = "tool.execution.progressed";
(row.payload as any).prpEvent.eventType = row.eventType;
return row;
});
f.events.splice(3, 0, ...rows);
resequence(f);
return f;
}
describe("accepted Cursor plan passive-wait authority", () => {
it("settles a long plan while binding every progress event into its proof", () => {
const f = withProgress(2_000);
const proof = nativeCursorPlanWaitFromFacts(f);
expect(proof).not.toBeNull();
const progress = f.events[1_200]!;
(progress.payload as any).prpEvent.payload.name = "updated diagnostic text";
progress.sourcePayloadSha256 = nativeSha256((progress.payload as any).prpEvent);
const changed = nativeCursorPlanWaitFromFacts(f);
expect(changed).not.toBeNull();
expect(changed!.source.toolLifecycleSha256).not.toBe(proof!.source.toolLifecycleSha256);
expect(changed!.source.authoritySha256).not.toBe(proof!.source.authoritySha256);
});
it.each(["foreign tool", "foreign session", "tampered digest", "late progress"])("rejects %s beyond the first thousand events", kind => {
const f = withProgress(2_000), row = f.events[1_200]!;
const event = (row.payload as any).prpEvent;
if (kind === "foreign tool") event.payload.executionId = "unrelated";
if (kind === "foreign session") event.normalizedSessionId = "other";
if (kind === "late progress") { f.events.splice(1_200, 1); f.events.push(row); resequence(f); }
row.sourcePayloadSha256 = kind === "tampered digest" ? "tampered" : nativeSha256(event);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("fails closed only at the separate progress and control-event budgets", () => {
expect(nativeCursorPlanWaitFromFacts(withProgress(20_000))).not.toBeNull();
expect(nativeCursorPlanWaitFromFacts(withProgress(20_001))).toBeNull();
const f = withProgress(1_200);
const turnStart = structuredClone(f.events[0]!);
turnStart.eventType = "turn.started";
Object.assign((turnStart.payload as any).prpEvent, { eventType: "turn.started", payload: {} });
f.events.unshift(...Array.from({ length: 995 }, () => structuredClone(turnStart)));
resequence(f);
expect(nativeCursorPlanWaitFromFacts(f)).not.toBeNull();
f.events.unshift(structuredClone(turnStart)); resequence(f);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("preserves an exact committed wait using the pre-release field without admitting new legacy waits", async () => {
const f = fixture();
f.run.status = "succeeded";
const proof = nativeCursorPlanWaitFromFacts(f)!;
const admission = (f.run.runnerProfileJson as any).nativeExecutionInput;
admission.provider.cursorMode = admission.provider.mode;
delete admission.provider.mode;
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
const payload = (type: string) => (f.events.find(e => e.eventType === type)!.payload as any).prpEvent;
const { interaction, delivery } = f.interactions[0]!;
const toolBinding = { toolExecutionId: proof.source.toolExecutionId, toolLifecycleSha256: proof.source.toolLifecycleSha256 };
const source = { ...proof.source, authoritySha256: nativeSha256({ admission, contract: f.contract,
created: payload("runtime_request.created"), resolved: payload("runtime_request.resolved"), terminal: payload("turn.completed"),
interaction, delivery, resolvedAt: interaction.resolvedAt!.toISOString(), acknowledgedAt: delivery.acknowledgedAt!.toISOString(), toolBinding }) };
const resultJson = { result: proof.result, terminal: { schema: "paperclip.prp.terminal.v1", runTerminalState: "succeeded", turnTerminalState: "completed", reportedWorkDisposition: "yielded" } };
const canonicalSha256 = nativeSha256(resultJson);
const db = () => {
const rows = [
[{ decision: { decisionJson: { cursorPlanWait: source, cursorPlanWaitResult: { resultId: "result", resultSha256: canonicalSha256 } } },
result: { id: "result", canonicalSha256, completionContractId: source.contractId, turnId: source.turnId, resultJson } }],
[{ run: f.run, contract: f.contract }], f.events, f.interactions,
[{ ...f.run, createdAt: new Date(0) }], [interaction], [], [],
];
return { select: () => {
const result = rows.shift();
const query: any = { from: () => query, innerJoin: () => query, where: () => query, orderBy: () => query, limit: () => Promise.resolve(result) };
return query;
} } as never;
};
expect(await hasCommittedNativeCursorPlanWait(db(), f.binding)).toBe(true);
// History is immutable: neither changing the old mode nor rewriting its
// field name may preserve the original acceptance authority.
admission.provider.cursorMode = "agent";
expect(await hasCommittedNativeCursorPlanWait(db(), f.binding)).toBe(false);
delete admission.provider.cursorMode;
admission.provider.mode = "plan";
expect(await hasCommittedNativeCursorPlanWait(db(), f.binding)).toBe(false);
});
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("preserves a valid v11 plan wait saved before production promotion", () => {
const facts = fixture();
(facts.run.runnerProfileJson as any).nativeExecutionInput.provider.profile.qualificationStatus = "pending";
const saved = nativeCursorPlanWaitFromFacts(facts);
expect(saved).not.toBeNull();
expect(saved!.result.reportedWorkDisposition).toBe("yielded");
expect(saved!.result.completionClaim.objectiveSatisfied).toBe(false);
});
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]");
// Model a future catalog revision without tying this test to today's version.
const next = { ...current, agentProfileVersion: (current.agentProfileVersion + 1) as typeof current.agentProfileVersion, commandDigest: `sha256:${"b".repeat(64)}` } satisfies typeof current;
expect(next.agentProfileVersion).toBeGreaterThan(current.agentProfileVersion);
const resolver = vi.spyOn(runner, "resolveQualifiedAcpxProfile").mockReturnValue(next);
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.mode = "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();
}
});
});