mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
300 lines
9.1 KiB
TypeScript
300 lines
9.1 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
createDeniedPermissionDecision,
|
|
isPermissionDecisionState,
|
|
normalizePermissionDenialReason,
|
|
type PermissionDecisionUi,
|
|
requestPermissionDecisionFromUi,
|
|
} from "#src/authority/permission-dialog";
|
|
|
|
describe("isPermissionDecisionState", () => {
|
|
it("accepts approved", () => {
|
|
expect(isPermissionDecisionState("approved")).toBe(true);
|
|
});
|
|
|
|
it("accepts denied", () => {
|
|
expect(isPermissionDecisionState("denied")).toBe(true);
|
|
});
|
|
|
|
it("accepts denied_with_reason", () => {
|
|
expect(isPermissionDecisionState("denied_with_reason")).toBe(true);
|
|
});
|
|
|
|
it("accepts approved_for_session", () => {
|
|
expect(isPermissionDecisionState("approved_for_session")).toBe(true);
|
|
});
|
|
|
|
it("accepts approved_for_serving_session", () => {
|
|
expect(isPermissionDecisionState("approved_for_serving_session")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("rejects unknown strings", () => {
|
|
expect(isPermissionDecisionState("unknown")).toBe(false);
|
|
});
|
|
|
|
it("rejects non-strings", () => {
|
|
expect(isPermissionDecisionState(42)).toBe(false);
|
|
expect(isPermissionDecisionState(null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("requestPermissionDecisionFromUi", () => {
|
|
it("returns approved when user selects Yes", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi.fn().mockResolvedValue("Yes"),
|
|
input: vi.fn(),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
);
|
|
expect(result).toEqual({ approved: true, state: "approved" });
|
|
});
|
|
|
|
it("returns approved_for_session when user selects session option", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi.fn().mockResolvedValue("Yes, for this session"),
|
|
input: vi.fn(),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
);
|
|
expect(result).toEqual({ approved: true, state: "approved_for_session" });
|
|
});
|
|
|
|
it("returns denied when user selects No", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi.fn().mockResolvedValue("No"),
|
|
input: vi.fn(),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
);
|
|
expect(result).toEqual({ approved: false, state: "denied" });
|
|
});
|
|
|
|
it("returns denied_with_reason when user provides reason", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi.fn().mockResolvedValue("No, provide reason"),
|
|
input: vi.fn().mockResolvedValue("not now"),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
);
|
|
expect(result).toEqual({
|
|
approved: false,
|
|
state: "denied_with_reason",
|
|
denialReason: "not now",
|
|
});
|
|
});
|
|
|
|
it("returns denied when user selects deny-with-reason but gives empty input", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi.fn().mockResolvedValue("No, provide reason"),
|
|
input: vi.fn().mockResolvedValue(""),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
);
|
|
expect(result).toEqual({ approved: false, state: "denied" });
|
|
});
|
|
|
|
it("returns denied when user dismisses dialog (undefined)", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi.fn().mockResolvedValue(undefined),
|
|
input: vi.fn(),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
);
|
|
expect(result).toEqual({ approved: false, state: "denied" });
|
|
});
|
|
|
|
it("passes four options to ui.select", async () => {
|
|
const selectFn = vi.fn().mockResolvedValue("Yes");
|
|
const ui: PermissionDecisionUi = {
|
|
select: selectFn,
|
|
input: vi.fn(),
|
|
};
|
|
await requestPermissionDecisionFromUi(ui, "Title", "Message");
|
|
const options = selectFn.mock.calls[0][1] as string[];
|
|
expect(options).toEqual([
|
|
"Yes",
|
|
"Yes, for this session",
|
|
"No",
|
|
"No, provide reason",
|
|
]);
|
|
});
|
|
|
|
it("uses custom sessionLabel when provided", async () => {
|
|
const selectFn = vi.fn().mockResolvedValue("Yes");
|
|
const ui: PermissionDecisionUi = {
|
|
select: selectFn,
|
|
input: vi.fn(),
|
|
};
|
|
await requestPermissionDecisionFromUi(ui, "Title", "Message", {
|
|
sessionLabel: 'Yes, allow "git *" for this session',
|
|
});
|
|
const options = selectFn.mock.calls[0][1] as string[];
|
|
expect(options[1]).toBe('Yes, allow "git *" for this session');
|
|
});
|
|
|
|
it("still returns approved_for_session when user selects the custom session label", async () => {
|
|
const customLabel = 'Yes, allow "git *" for this session';
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi.fn().mockResolvedValue(customLabel),
|
|
input: vi.fn(),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
{ sessionLabel: customLabel },
|
|
);
|
|
expect(result).toEqual({ approved: true, state: "approved_for_session" });
|
|
});
|
|
|
|
it("falls back to default session label when no options provided", async () => {
|
|
const selectFn = vi.fn().mockResolvedValue("Yes");
|
|
const ui: PermissionDecisionUi = {
|
|
select: selectFn,
|
|
input: vi.fn(),
|
|
};
|
|
await requestPermissionDecisionFromUi(ui, "Title", "Message");
|
|
const options = selectFn.mock.calls[0][1] as string[];
|
|
expect(options[1]).toBe("Yes, for this session");
|
|
});
|
|
|
|
describe("sessionScope two-step (forwarded asks)", () => {
|
|
const sessionScope = {
|
|
subagentLabel: "This subagent only",
|
|
servingSessionLabel: "The whole session",
|
|
};
|
|
|
|
it("opens a second scope select after the session option is chosen", async () => {
|
|
const selectFn = vi
|
|
.fn()
|
|
.mockResolvedValueOnce("Yes, for this session")
|
|
.mockResolvedValueOnce("This subagent only");
|
|
const ui: PermissionDecisionUi = { select: selectFn, input: vi.fn() };
|
|
await requestPermissionDecisionFromUi(ui, "Title", "Message", {
|
|
sessionScope,
|
|
});
|
|
expect(selectFn).toHaveBeenCalledTimes(2);
|
|
const scopeOptions = selectFn.mock.calls[1][1] as string[];
|
|
expect(scopeOptions).toEqual(["This subagent only", "The whole session"]);
|
|
});
|
|
|
|
it("maps the subagent scope to approved_for_session", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi
|
|
.fn()
|
|
.mockResolvedValueOnce("Yes, for this session")
|
|
.mockResolvedValueOnce("This subagent only"),
|
|
input: vi.fn(),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
{ sessionScope },
|
|
);
|
|
expect(result).toEqual({ approved: true, state: "approved_for_session" });
|
|
});
|
|
|
|
it("maps the whole-session scope to approved_for_serving_session", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi
|
|
.fn()
|
|
.mockResolvedValueOnce("Yes, for this session")
|
|
.mockResolvedValueOnce("The whole session"),
|
|
input: vi.fn(),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
{ sessionScope },
|
|
);
|
|
expect(result).toEqual({
|
|
approved: true,
|
|
state: "approved_for_serving_session",
|
|
});
|
|
});
|
|
|
|
it("defaults to the least-privilege subagent scope when the scope select is cancelled", async () => {
|
|
const ui: PermissionDecisionUi = {
|
|
select: vi
|
|
.fn()
|
|
.mockResolvedValueOnce("Yes, for this session")
|
|
.mockResolvedValueOnce(undefined),
|
|
input: vi.fn(),
|
|
};
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
{ sessionScope },
|
|
);
|
|
expect(result).toEqual({ approved: true, state: "approved_for_session" });
|
|
});
|
|
|
|
it("does not open the scope select when the user picks plain Yes", async () => {
|
|
const selectFn = vi.fn().mockResolvedValueOnce("Yes");
|
|
const ui: PermissionDecisionUi = { select: selectFn, input: vi.fn() };
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
{ sessionScope },
|
|
);
|
|
expect(selectFn).toHaveBeenCalledTimes(1);
|
|
expect(result).toEqual({ approved: true, state: "approved" });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("normalizePermissionDenialReason", () => {
|
|
it("returns trimmed string for non-empty input", () => {
|
|
expect(normalizePermissionDenialReason(" reason ")).toBe("reason");
|
|
});
|
|
|
|
it("returns undefined for empty string", () => {
|
|
expect(normalizePermissionDenialReason("")).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined for non-string", () => {
|
|
expect(normalizePermissionDenialReason(42)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("createDeniedPermissionDecision", () => {
|
|
it("returns denied_with_reason when reason provided", () => {
|
|
expect(createDeniedPermissionDecision("nope")).toEqual({
|
|
approved: false,
|
|
state: "denied_with_reason",
|
|
denialReason: "nope",
|
|
});
|
|
});
|
|
|
|
it("returns denied when no reason", () => {
|
|
expect(createDeniedPermissionDecision()).toEqual({
|
|
approved: false,
|
|
state: "denied",
|
|
});
|
|
});
|
|
});
|