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,296 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
|
||||
import {
|
||||
applyPermissionGate,
|
||||
type PermissionGateParams,
|
||||
} from "#src/permission-gate";
|
||||
import { DECIDED_BY_HUMAN } from "#test/helpers/decision-fixtures";
|
||||
|
||||
function makeParams(
|
||||
overrides: Partial<PermissionGateParams> = {},
|
||||
): PermissionGateParams {
|
||||
return {
|
||||
state: "allow",
|
||||
promptForApproval: vi.fn<() => Promise<PermissionPromptDecision>>(),
|
||||
writeLog: vi.fn(),
|
||||
logContext: { source: "test" },
|
||||
decidedByRule: {
|
||||
kind: "rule",
|
||||
surface: "bash",
|
||||
pattern: "*",
|
||||
origin: "global",
|
||||
},
|
||||
messages: {
|
||||
denyReason: "Denied by policy.",
|
||||
unavailableReason: (d) =>
|
||||
d.denialReason
|
||||
? `No interactive UI available. Reason: ${d.denialReason}`
|
||||
: "No interactive UI available.",
|
||||
userDeniedReason: (d) =>
|
||||
d.denialReason
|
||||
? `User denied. Reason: ${d.denialReason}.`
|
||||
: "User denied.",
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("applyPermissionGate", () => {
|
||||
describe("deny branch", () => {
|
||||
it("returns block with deny reason when state is deny", async () => {
|
||||
const params = makeParams({ state: "deny" });
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({
|
||||
action: "block",
|
||||
reason: "Denied by policy.",
|
||||
});
|
||||
});
|
||||
|
||||
it("calls writeLog with policy_denied resolution", async () => {
|
||||
const params = makeParams({
|
||||
state: "deny",
|
||||
logContext: { source: "tool_call", toolName: "bash" },
|
||||
});
|
||||
await applyPermissionGate(params);
|
||||
expect(params.writeLog).toHaveBeenCalledOnce();
|
||||
expect(params.writeLog).toHaveBeenCalledWith(
|
||||
"permission_request.blocked",
|
||||
{
|
||||
source: "tool_call",
|
||||
toolName: "bash",
|
||||
resolution: "policy_denied",
|
||||
decidedBy: {
|
||||
kind: "rule",
|
||||
surface: "bash",
|
||||
pattern: "*",
|
||||
origin: "global",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call promptForApproval when state is deny", async () => {
|
||||
const params = makeParams({ state: "deny" });
|
||||
await applyPermissionGate(params);
|
||||
expect(params.promptForApproval).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ask branch — confirmation unavailable", () => {
|
||||
const unavailableDecision: PermissionPromptDecision = {
|
||||
approved: false,
|
||||
state: "denied",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
confirmationUnavailable: true,
|
||||
};
|
||||
|
||||
it("returns block with unavailable reason when the decision is confirmation-unavailable", async () => {
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval: vi.fn().mockResolvedValue(unavailableDecision),
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({
|
||||
action: "block",
|
||||
reason: "No interactive UI available.",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call writeLog when confirmation is unavailable (logged by the prompter)", async () => {
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval: vi.fn().mockResolvedValue(unavailableDecision),
|
||||
logContext: { source: "skill_read", skillName: "foo" },
|
||||
});
|
||||
await applyPermissionGate(params);
|
||||
expect(params.writeLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes the decision's denial reason to the unavailable message", async () => {
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval: vi.fn().mockResolvedValue({
|
||||
...unavailableDecision,
|
||||
denialReason: "Session 'parent-1' did not answer within 600s.",
|
||||
}),
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({
|
||||
action: "block",
|
||||
reason:
|
||||
"No interactive UI available. Reason: Session 'parent-1' did not answer within 600s.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ask branch — user rejects", () => {
|
||||
it("returns block with user-denied reason when user rejects", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: false,
|
||||
state: "denied",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({ action: "block", reason: "User denied." });
|
||||
});
|
||||
|
||||
it("passes denial reason through userDeniedReason formatter", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: false,
|
||||
state: "denied_with_reason",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
denialReason: "not now",
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({
|
||||
action: "block",
|
||||
reason: "User denied. Reason: not now.",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call writeLog when user rejects (logged by promptPermission)", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: false,
|
||||
state: "denied",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
});
|
||||
await applyPermissionGate(params);
|
||||
expect(params.writeLog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ask branch — user approves", () => {
|
||||
it("returns allow when user approves", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: true,
|
||||
state: "approved",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({ action: "allow" });
|
||||
});
|
||||
|
||||
it("does not call writeLog when user approves", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: true,
|
||||
state: "approved",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
});
|
||||
await applyPermissionGate(params);
|
||||
expect(params.writeLog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ask branch — approved_for_session with sessionApproval", () => {
|
||||
it("attaches sessionApproval to result when decision is approved_for_session and param provided", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: true,
|
||||
state: "approved_for_session",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
sessionApproval: { surface: "bash", pattern: "git *" },
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({
|
||||
action: "allow",
|
||||
sessionApproval: { surface: "bash", pattern: "git *" },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not attach sessionApproval when decision is approved (once)", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: true,
|
||||
state: "approved",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
sessionApproval: { surface: "bash", pattern: "git *" },
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({ action: "allow" });
|
||||
});
|
||||
|
||||
it("does not attach sessionApproval when no sessionApproval param", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: true,
|
||||
state: "approved_for_session",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({ action: "allow" });
|
||||
});
|
||||
|
||||
it("does not attach sessionApproval when user denies", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: false,
|
||||
state: "denied",
|
||||
decidedBy: DECIDED_BY_HUMAN,
|
||||
};
|
||||
const promptForApproval = vi.fn().mockResolvedValue(decision);
|
||||
const params = makeParams({
|
||||
state: "ask",
|
||||
promptForApproval,
|
||||
sessionApproval: { surface: "bash", pattern: "git *" },
|
||||
});
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({ action: "block", reason: "User denied." });
|
||||
});
|
||||
});
|
||||
|
||||
describe("allow branch", () => {
|
||||
it("returns allow immediately when state is allow", async () => {
|
||||
const params = makeParams({ state: "allow" });
|
||||
const result = await applyPermissionGate(params);
|
||||
expect(result).toEqual({ action: "allow" });
|
||||
});
|
||||
|
||||
it("does not call writeLog when state is allow", async () => {
|
||||
const params = makeParams({ state: "allow" });
|
||||
await applyPermissionGate(params);
|
||||
expect(params.writeLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call promptForApproval when state is allow", async () => {
|
||||
const params = makeParams({ state: "allow" });
|
||||
await applyPermissionGate(params);
|
||||
expect(params.promptForApproval).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user