mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Integration tests verifying that sequential tool calls to the same
|
||||
* external path only prompt once — the session-approval recorded by the
|
||||
* first call covers the second.
|
||||
*
|
||||
* Uses real PermissionSession + PermissionResolver + SessionRules so the
|
||||
* stateful approval-tracking path is exercised end-to-end.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
makeApprovingPrompter,
|
||||
makeDeduplicatingHandler,
|
||||
makeDedupWiring,
|
||||
makeExtDirBashEvent,
|
||||
makeExtDirToolEvent,
|
||||
} from "#test/helpers/external-directory-fixtures";
|
||||
import { makeCtx } from "#test/helpers/handler-fixtures";
|
||||
|
||||
// ── SDK stub ───────────────────────────────────────────────────────────────
|
||||
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
|
||||
return { ...original };
|
||||
});
|
||||
|
||||
// ── tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("external-directory session dedup", () => {
|
||||
describe("path-bearing tools (read, write, edit)", () => {
|
||||
it("does not re-prompt for the same external path after session approval", async () => {
|
||||
const { handler, prompter } = makeDeduplicatingHandler();
|
||||
const ctx = makeCtx();
|
||||
const externalPath = "/outside/project/data.txt";
|
||||
|
||||
// First call — should prompt
|
||||
const event1 = makeExtDirToolEvent("read", externalPath, "tc-1");
|
||||
const result1 = await handler.handleToolCall(event1, ctx);
|
||||
expect(result1).toEqual({ action: "allow" });
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call — same path, should hit session rule, no prompt
|
||||
const event2 = makeExtDirToolEvent("read", externalPath, "tc-2");
|
||||
const result2 = await handler.handleToolCall(event2, ctx);
|
||||
expect(result2).toEqual({ action: "allow" });
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not re-prompt for a different file in the same external directory", async () => {
|
||||
const { handler, prompter } = makeDeduplicatingHandler();
|
||||
const ctx = makeCtx();
|
||||
|
||||
// First call — prompt for /outside/project/a.txt
|
||||
const event1 = makeExtDirToolEvent(
|
||||
"read",
|
||||
"/outside/project/a.txt",
|
||||
"tc-1",
|
||||
);
|
||||
await handler.handleToolCall(event1, ctx);
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call — /outside/project/b.txt is in the same directory
|
||||
const event2 = makeExtDirToolEvent(
|
||||
"read",
|
||||
"/outside/project/b.txt",
|
||||
"tc-2",
|
||||
);
|
||||
await handler.handleToolCall(event2, ctx);
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does prompt for a file in a different external directory", async () => {
|
||||
const { handler, prompter } = makeDeduplicatingHandler();
|
||||
const ctx = makeCtx();
|
||||
|
||||
// First call — /outside/alpha/file.txt
|
||||
const event1 = makeExtDirToolEvent(
|
||||
"read",
|
||||
"/outside/alpha/file.txt",
|
||||
"tc-1",
|
||||
);
|
||||
await handler.handleToolCall(event1, ctx);
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call — /outside/beta/file.txt is a different directory
|
||||
const event2 = makeExtDirToolEvent(
|
||||
"read",
|
||||
"/outside/beta/file.txt",
|
||||
"tc-2",
|
||||
);
|
||||
await handler.handleToolCall(event2, ctx);
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("re-prompts when user approved once (not for session)", async () => {
|
||||
const approveOnce = makeApprovingPrompter();
|
||||
const { handler, prompter } = makeDeduplicatingHandler(approveOnce);
|
||||
const ctx = makeCtx();
|
||||
const externalPath = "/outside/project/data.txt";
|
||||
|
||||
// First call — prompt, approved once
|
||||
const event1 = makeExtDirToolEvent("read", externalPath, "tc-1");
|
||||
await handler.handleToolCall(event1, ctx);
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call — no session rule recorded, should prompt again
|
||||
const event2 = makeExtDirToolEvent("read", externalPath, "tc-2");
|
||||
await handler.handleToolCall(event2, ctx);
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bash commands with external paths", () => {
|
||||
it("does not re-prompt for a bash command referencing the same external path after session approval", async () => {
|
||||
const { handler, prompter } = makeDeduplicatingHandler();
|
||||
const ctx = makeCtx();
|
||||
|
||||
// First call — bash referencing /tmp/out.txt
|
||||
const event1 = makeExtDirBashEvent("echo hello > /tmp/out.txt", "tc-1");
|
||||
const result1 = await handler.handleToolCall(event1, ctx);
|
||||
expect(result1).toEqual({ action: "allow" });
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call — different bash command, same external path
|
||||
const event2 = makeExtDirBashEvent("cat /tmp/out.txt", "tc-2");
|
||||
const result2 = await handler.handleToolCall(event2, ctx);
|
||||
expect(result2).toEqual({ action: "allow" });
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not re-prompt for read after bash already approved the same directory", async () => {
|
||||
const { handler, prompter } = makeDeduplicatingHandler();
|
||||
const ctx = makeCtx();
|
||||
|
||||
// First call — bash writes to /tmp/out.txt
|
||||
const event1 = makeExtDirBashEvent("echo hello > /tmp/out.txt", "tc-1");
|
||||
await handler.handleToolCall(event1, ctx);
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call — read from /tmp/out.txt (same directory, different tool)
|
||||
const event2 = makeExtDirToolEvent("read", "/tmp/out.txt", "tc-2");
|
||||
await handler.handleToolCall(event2, ctx);
|
||||
expect(prompter.escalate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Moved from permission-system.test.ts catch-all (#342)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("session shutdown clears external-directory approvals", () => {
|
||||
it("re-prompts for the same path after session shutdown", async () => {
|
||||
const { handler, prompter, session } = makeDedupWiring();
|
||||
|
||||
const externalPath = "/tmp/sibling/foo.ts";
|
||||
const ctx = makeCtx();
|
||||
const event = makeExtDirToolEvent("read", externalPath, "tc-1");
|
||||
|
||||
// First access: prompt fires and records session approval.
|
||||
await handler.handleToolCall(event, ctx);
|
||||
expect(vi.mocked(prompter.escalate)).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second access: covered by session approval — no re-prompt.
|
||||
await handler.handleToolCall({ ...event, toolCallId: "tc-2" }, ctx);
|
||||
expect(vi.mocked(prompter.escalate)).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Shutdown clears session approvals.
|
||||
session.shutdown();
|
||||
|
||||
// Third access: session rules cleared — must re-prompt.
|
||||
await handler.handleToolCall({ ...event, toolCallId: "tc-3" }, ctx);
|
||||
expect(vi.mocked(prompter.escalate)).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user