mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
62 lines
3.0 KiB
TypeScript
62 lines
3.0 KiB
TypeScript
import type { Db } from "@paperclipai/db";
|
|
import type { PrpEvent } from "../../vendor/paperclip-runner/index.js";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { projectNativeRuntimeRequest } from "./native-question-bridge.js";
|
|
|
|
const binding = {
|
|
companyId: "company-1", issueId: "issue-1", runId: "run-1", agentId: "agent-1",
|
|
normalizedSessionId: "session-1", runnerSourceInstanceId: "runner-1",
|
|
};
|
|
|
|
function permissionEvent(): PrpEvent {
|
|
return {
|
|
schema: "paperclip.prp.event.v1", schemaVersion: 1, sourceEventId: "permission-1",
|
|
sourceSeq: 1, sourceKind: "runner", sourceInstanceId: "runner-1", runId: "run-1",
|
|
normalizedSessionId: "session-1", turnId: "turn-1", itemId: "item-1",
|
|
eventType: "runtime_request.created", priority: 0, emittedAt: "2026-09-28T12:00:00Z",
|
|
payload: { request: {
|
|
schema: "paperclip.runtime_request.v2", requestKind: "permission_approval", type: "permission",
|
|
requestId: "permission-1", turnId: "turn-1", itemId: "item-1", status: "pending",
|
|
prompt: "Allow editing src/example.ts?",
|
|
choices: [{ key: "accept", label: "Allow once" }, { key: "decline", label: "Deny" }],
|
|
details: { toolCallId: "tool-1" },
|
|
origin: { adapter: "acpx-runtime-sidecar", provider: "acpx", method: "session/request_permission" },
|
|
} },
|
|
};
|
|
}
|
|
|
|
describe("native permission projection", () => {
|
|
it("admits the existing privileged runtime card without a human-only question mutation", async () => {
|
|
const event = permissionEvent();
|
|
const original = structuredClone(event);
|
|
const select = vi.fn();
|
|
await expect(projectNativeRuntimeRequest({ db: { select } as unknown as Db, binding, event })).resolves.toBeNull();
|
|
expect(select).not.toHaveBeenCalled();
|
|
expect(event).toEqual(original);
|
|
});
|
|
|
|
it("retains permissions with no provider item identity", async () => {
|
|
const event = permissionEvent();
|
|
delete event.itemId;
|
|
(event.payload.request as Record<string, unknown>).itemId = null;
|
|
await expect(projectNativeRuntimeRequest({ db: {} as Db, binding, event })).resolves.toBeNull();
|
|
});
|
|
|
|
it.each([
|
|
{ requestKind: "runtime" }, { type: "input" }, { turnId: "stale-turn" }, { itemId: "wrong-item" },
|
|
{ requestId: "../../forged" }, { prompt: "" }, { details: [] },
|
|
{ choices: [] }, { choices: [{ key: "allow_forever", label: "Always" }] },
|
|
{ choices: [{ key: "accept", label: "One" }, { key: "accept", label: "Two" }] },
|
|
])("rejects malformed or cross-bound permission data %j", async (override) => {
|
|
const event = permissionEvent();
|
|
Object.assign(event.payload.request as object, override);
|
|
await expect(projectNativeRuntimeRequest({ db: {} as Db, binding, event })).rejects.toThrow(/native_runtime_/);
|
|
});
|
|
|
|
it.each(["runId", "normalizedSessionId", "sourceInstanceId"] as const)("rejects a foreign %s before dispatch", async (field) => {
|
|
const event = permissionEvent();
|
|
event[field] = "foreign";
|
|
await expect(projectNativeRuntimeRequest({ db: {} as Db, binding, event })).rejects.toThrow("native_runtime_request_binding_mismatch");
|
|
});
|
|
});
|