mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
176 lines
7.2 KiB
TypeScript
176 lines
7.2 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|