mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 12:07:09 +02:00
65 lines
5.5 KiB
TypeScript
65 lines
5.5 KiB
TypeScript
import { cursorDeniedCommand, type CursorToolNotice } from "./cursor-native-evidence.js";
|
|
import { describe, expect, it } from "vitest";
|
|
import { cursorNativeCaseDesigns, cursorNativePrompt, hasDeliveredCursorNativeRequest, hasCursorPlanDecision, hasCursorDenialBoundary, CURSOR_DENIAL_SAMPLE_PHASES, hasExactCursorNativeResponse } from "./cursor-native-cases.js";
|
|
|
|
const proof = () => {
|
|
const wrap = (eventType: string, sourceSeq: number, payload: unknown) => ({ runId: "run", protocolSchemaVersion: 1, payload: { prpEvent: {
|
|
schema: "paperclip.prp.event.v1", schemaVersion: 1, runId: "run", turnId: "turn", eventType, sourceSeq, payload,
|
|
} } });
|
|
return [wrap("runtime_request.created", 1, { request: { requestId: "request", turnId: "turn", type: "input", status: "pending", origin: { adapter: "acpx-runtime", provider: "cursor", method: "cursor/ask_question" } } }),
|
|
wrap("runtime_request.resolved", 2, { requestId: "request", turnId: "turn", action: "submit" })];
|
|
};
|
|
const grade = (events: unknown[]) => hasDeliveredCursorNativeRequest({ events, runId: "run", turnId: "turn", requestId: "request", method: "cursor/ask_question", action: "submit" });
|
|
describe("prepared Cursor native qualification", () => {
|
|
it("has four fixed native-only designs and bounded prompts", () => {
|
|
expect(cursorNativeCaseDesigns).toHaveLength(4);
|
|
for (const design of cursorNativeCaseDesigns) expect(cursorNativePrompt(design.id, "test-nonce")).toContain("Do not substitute");
|
|
expect(() => cursorNativePrompt("native-write-deny-reconnect", "../escape")).toThrow();
|
|
});
|
|
it("requires correlated native origin and response delivery evidence", () => { expect(grade(proof())).toBe(true); expect(grade([])).toBe(false); });
|
|
it("rejects semantic substitutions and permission fallbacks", () => {
|
|
for (const method of ["request_human_input", "session/request_permission", "cursor/create_plan"]) {
|
|
const rows = proof(); (rows[0]!.payload.prpEvent.payload as any).request.origin.method = method; expect(grade(rows)).toBe(false);
|
|
}
|
|
});
|
|
it("rejects missing, duplicated and expired delivery", () => {
|
|
const rows = proof(); expect(grade(rows.slice(0, 1))).toBe(false); expect(grade([...rows, rows[1]])).toBe(false);
|
|
rows[1]!.payload.prpEvent.eventType = "runtime_request.expired"; expect(grade(rows)).toBe(false);
|
|
});
|
|
it("rejects mismatched run, turn and event ordering", () => {
|
|
let rows = proof(); rows[1]!.runId = "foreign"; expect(grade(rows)).toBe(false);
|
|
rows = proof(); rows[1]!.payload.prpEvent.turnId = "stale"; expect(grade(rows)).toBe(false);
|
|
rows = proof(); rows[1]!.payload.prpEvent.sourceSeq = 0; expect(grade(rows)).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("binds plan decisions to the full revision and rejects stale answers", () => {
|
|
const id = `plan-${"a".repeat(64)}`;
|
|
const questionSet = { questions: [{ id }] };
|
|
const answer = { schema: "paperclip.question_response.v1", answers: { [id]: { selectedOptionIds: ["accept"] } } };
|
|
expect(hasCursorPlanDecision(questionSet, answer, "accept")).toBe(true);
|
|
expect(hasCursorPlanDecision({ questions: [{ id: `plan-${"b".repeat(64)}` }] }, answer, "accept")).toBe(false);
|
|
expect(hasCursorPlanDecision(questionSet, answer, "reject")).toBe(false);
|
|
});
|
|
it("requires supported denial choices, native IDs and complete no-effect boundaries", () => {
|
|
const input = { request: { requestId: "request", turnId: "turn", type: "permission", status: "pending", details: { toolCallId: "tool" },
|
|
origin: { adapter: "acpx-runtime", provider: "cursor", method: "session/request_permission" },
|
|
choices: [{ key: "accept" }, { key: "decline" }, { key: "cancel" }] }, expectedRequestId: "request", expectedToolCallId: "tool", path: "/fixture/denied.txt", runId: "run", turnId: "turn",
|
|
notices: ([{ stage: "tool", status: "pending" }, { stage: "permission_requested", requestId: "request", declineOffered: true }, { stage: "permission_delivered", requestId: "request", outcome: "reject_once" }, { stage: "tool", status: "failed" }].map((row, index) => ({ ...row, seq: index + 1, runId: "run", sessionId: "session", turnId: "turn", toolCallId: "tool", operation: "execute", commandSha256: cursorDeniedCommand("/fixture/denied.txt").commandSha256 })) as CursorToolNotice[]),
|
|
samples: CURSOR_DENIAL_SAMPLE_PHASES.map((phase, observedAt) => ({ phase, observedAt, absent: true, path: "/fixture/denied.txt" })) };
|
|
expect(hasCursorDenialBoundary(input)).toBe(true);
|
|
expect(hasCursorDenialBoundary({ ...input, samples: input.samples.slice(1) })).toBe(false);
|
|
expect(hasCursorDenialBoundary({ ...input, expectedToolCallId: "foreign" })).toBe(false);
|
|
input.samples[2]!.absent = false; expect(hasCursorDenialBoundary(input)).toBe(false); input.samples[2]!.absent = true;
|
|
input.request.choices.push({ key: "accept_for_session" }); expect(hasCursorDenialBoundary(input)).toBe(false);
|
|
});
|
|
|
|
it("requires exact delivered answer bytes rather than a saved or different answer", () => {
|
|
const rows = proof(); const response = { schema: "paperclip.question_response.v1", answers: { q: { selectedOptionIds: ["chosen"] } } };
|
|
(rows[1]!.payload.prpEvent.payload as any).response = response;
|
|
const input = { events: rows, runId: "run", turnId: "turn", requestId: "request", method: "cursor/ask_question" as const, action: "submit" as const, response };
|
|
expect(hasExactCursorNativeResponse(input)).toBe(true);
|
|
expect(hasExactCursorNativeResponse({ ...input, response: { ...response, answers: { q: { selectedOptionIds: ["other"] } } } })).toBe(false);
|
|
expect(hasExactCursorNativeResponse({ ...input, events: rows.slice(0, 1) })).toBe(false);
|
|
});
|