mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
配置统一:权限策略、硬防御、Tool Search、提示词封面以及新迁入的 RTK、pi-compaction、pi-worktree、pi-hermes-memory 配置全部由扩展 直接读取已安装组合包内的 config/,pi install / pi update 即完成部署; 删除 rebuild-bundle-config.mjs 与 bundle-manifest.json,my-pi/ 只保留 日志与缓存。update.sh 成功升级后将默认 git 来源的已安装副本 reset 回发布版本。AGENTS.md 新增「组合包配置原则」。 脚本整合:search_config.sh / ssh_config.sh 合并为 configure.sh search|ssh;install.sh / update.sh 共享函数抽到 scripts/lib.sh,远程 安装器改为先下载到临时文件再执行,下载失败可被检测。 权限:hard-guards 新增 protectedWrites,写入 Pi 凭据/设置、已安装 组合包与扩展目录、pi-ssh 凭据库时一律人工确认(跳过 AutoReview、 yolo 与 Session grant,无 UI 时拒绝);放行 ls、LSP 查询、ctx 只读、 web_search、Chrome 只读与 git 只读命令,skill_manage 改为 ask。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
331 lines
10 KiB
TypeScript
331 lines
10 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("hideSessionOption (human-only asks)", () => {
|
|
it("omits the session option when hideSessionOption is set", async () => {
|
|
const selectFn = vi.fn().mockResolvedValue("Yes");
|
|
const ui: PermissionDecisionUi = { select: selectFn, input: vi.fn() };
|
|
await requestPermissionDecisionFromUi(ui, "Title", "Message", {
|
|
hideSessionOption: true,
|
|
});
|
|
const options = selectFn.mock.calls[0][1] as string[];
|
|
expect(options).toEqual(["Yes", "No", "No, provide reason"]);
|
|
});
|
|
|
|
it("does not open the scope select when the session option is hidden", async () => {
|
|
const selectFn = vi.fn().mockResolvedValue("Yes");
|
|
const ui: PermissionDecisionUi = { select: selectFn, input: vi.fn() };
|
|
const result = await requestPermissionDecisionFromUi(
|
|
ui,
|
|
"Title",
|
|
"Message",
|
|
{
|
|
hideSessionOption: true,
|
|
sessionScope: {
|
|
subagentLabel: "This subagent only",
|
|
servingSessionLabel: "The whole session",
|
|
},
|
|
},
|
|
);
|
|
expect(selectFn).toHaveBeenCalledTimes(1);
|
|
expect(result).toEqual({ approved: true, state: "approved" });
|
|
});
|
|
});
|
|
|
|
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",
|
|
});
|
|
});
|
|
});
|