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,321 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { loadUnifiedConfig } from "#src/config-loader";
|
||||
import { registerPermissionSystemCommand } from "#src/config-modal";
|
||||
import type { CommandConfigStore } from "#src/config-store";
|
||||
import {
|
||||
DEFAULT_EXTENSION_CONFIG,
|
||||
normalizePermissionSystemConfig,
|
||||
type PermissionSystemExtensionConfig,
|
||||
} from "#src/extension-config";
|
||||
import type { Rule, Ruleset } from "#src/rule";
|
||||
|
||||
vi.mock("@earendil-works/pi-coding-agent", () => ({
|
||||
getSettingsListTheme: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("@earendil-works/pi-tui", () => ({
|
||||
SettingsList: class {
|
||||
handleInput(): void {}
|
||||
updateValue(): void {}
|
||||
render(): string[] {
|
||||
return [];
|
||||
}
|
||||
invalidate(): void {}
|
||||
},
|
||||
}));
|
||||
|
||||
type Notification = { message: string; level: "info" | "warning" | "error" };
|
||||
|
||||
type CommandContextStub = {
|
||||
hasUI: boolean;
|
||||
ui: {
|
||||
notify(message: string, level: "info" | "warning" | "error"): void;
|
||||
custom<T>(
|
||||
renderer: (...args: unknown[]) => unknown,
|
||||
options?: unknown,
|
||||
): Promise<T>;
|
||||
};
|
||||
};
|
||||
|
||||
function createCommandContext(hasUI: boolean): {
|
||||
ctx: CommandContextStub;
|
||||
notifications: Notification[];
|
||||
getCustomCalls(): number;
|
||||
} {
|
||||
const notifications: Notification[] = [];
|
||||
let customCalls = 0;
|
||||
|
||||
return {
|
||||
ctx: {
|
||||
hasUI,
|
||||
ui: {
|
||||
notify(message: string, level: "info" | "warning" | "error") {
|
||||
notifications.push({ message, level });
|
||||
},
|
||||
async custom<T>(
|
||||
_renderer: (...args: unknown[]) => unknown,
|
||||
_options?: unknown,
|
||||
): Promise<T> {
|
||||
customCalls += 1;
|
||||
return undefined as T;
|
||||
},
|
||||
},
|
||||
},
|
||||
notifications,
|
||||
getCustomCalls: () => customCalls,
|
||||
};
|
||||
}
|
||||
|
||||
function lastNotification(notifications: Notification[]): Notification {
|
||||
return notifications[notifications.length - 1];
|
||||
}
|
||||
|
||||
test("permission-system command completions expose top-level config actions", () => {
|
||||
const baseDir = mkdtempSync(
|
||||
join(tmpdir(), "pi-permission-system-command-completions-"),
|
||||
);
|
||||
const configPath = join(baseDir, "config.json");
|
||||
let config: PermissionSystemExtensionConfig = { ...DEFAULT_EXTENSION_CONFIG };
|
||||
|
||||
try {
|
||||
const configStore: CommandConfigStore = {
|
||||
current: () => config,
|
||||
save: (next) => {
|
||||
config = next;
|
||||
},
|
||||
};
|
||||
const controller = {
|
||||
config: configStore,
|
||||
configPath,
|
||||
getActiveAgentConfigRules: () => [] as Ruleset,
|
||||
};
|
||||
|
||||
let definition: {
|
||||
description: string;
|
||||
getArgumentCompletions?: (
|
||||
argumentPrefix: string,
|
||||
) => Array<{ value: string; label: string; description?: string }> | null;
|
||||
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
||||
} | null = null;
|
||||
|
||||
registerPermissionSystemCommand(
|
||||
{
|
||||
registerCommand(_name: string, nextDefinition: typeof definition) {
|
||||
definition = nextDefinition;
|
||||
},
|
||||
} as never,
|
||||
controller,
|
||||
);
|
||||
|
||||
expect(definition!.getArgumentCompletions).toBeTypeOf("function");
|
||||
|
||||
const topLevel = definition!.getArgumentCompletions?.("");
|
||||
expect(Array.isArray(topLevel)).toBeTruthy();
|
||||
expect(topLevel?.some((item) => item.value === "show")).toBeTruthy();
|
||||
expect(topLevel?.some((item) => item.value === "reset")).toBeTruthy();
|
||||
|
||||
const filtered = definition!.getArgumentCompletions?.("pa");
|
||||
expect(filtered?.map((item) => item.value)).toEqual(["path"]);
|
||||
expect(definition!.getArgumentCompletions?.("path extra")).toBe(null);
|
||||
expect(definition!.getArgumentCompletions?.("zzz")).toBe(null);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("permission-system command handlers manage config summary, persistence, and modal routing", async () => {
|
||||
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-command-"));
|
||||
const configPath = join(baseDir, "config.json");
|
||||
let config: PermissionSystemExtensionConfig = {
|
||||
debugLog: true,
|
||||
permissionReviewLog: false,
|
||||
yoloMode: true,
|
||||
doublePressToConfirm: true,
|
||||
};
|
||||
|
||||
try {
|
||||
writeFileSync(
|
||||
configPath,
|
||||
`${JSON.stringify(normalizePermissionSystemConfig(config), null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const configStore: CommandConfigStore = {
|
||||
current: () => config,
|
||||
save: (next) => {
|
||||
const currentConfig = normalizePermissionSystemConfig(
|
||||
loadUnifiedConfig(configPath).config,
|
||||
);
|
||||
const normalized = normalizePermissionSystemConfig(next);
|
||||
writeFileSync(
|
||||
configPath,
|
||||
`${JSON.stringify(normalized, null, 2)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
config = normalizePermissionSystemConfig(
|
||||
loadUnifiedConfig(configPath).config,
|
||||
);
|
||||
expect(config).not.toEqual(currentConfig);
|
||||
},
|
||||
};
|
||||
const controller = {
|
||||
config: configStore,
|
||||
configPath,
|
||||
getActiveAgentConfigRules: () => [] as Ruleset,
|
||||
};
|
||||
|
||||
let registeredName = "";
|
||||
let definition: {
|
||||
description: string;
|
||||
getArgumentCompletions?: (
|
||||
argumentPrefix: string,
|
||||
) => Array<{ value: string; label: string; description?: string }> | null;
|
||||
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
||||
} | null = null;
|
||||
|
||||
registerPermissionSystemCommand(
|
||||
{
|
||||
registerCommand(name: string, nextDefinition: typeof definition) {
|
||||
registeredName = name;
|
||||
definition = nextDefinition;
|
||||
},
|
||||
} as never,
|
||||
controller,
|
||||
);
|
||||
|
||||
expect(registeredName).toBe("permission-system");
|
||||
expect(definition!.description).toContain("Configure pi-permission-system");
|
||||
|
||||
const infoCtx = createCommandContext(true);
|
||||
await definition!.handler("show", infoCtx.ctx);
|
||||
expect(lastNotification(infoCtx.notifications).message).toContain(
|
||||
"yoloMode=on",
|
||||
);
|
||||
expect(lastNotification(infoCtx.notifications).message).toContain(
|
||||
"debugLog=on",
|
||||
);
|
||||
|
||||
await definition!.handler("path", infoCtx.ctx);
|
||||
expect(lastNotification(infoCtx.notifications).message).toBe(
|
||||
`permission-system config: ${configPath}`,
|
||||
);
|
||||
|
||||
await definition!.handler("help", infoCtx.ctx);
|
||||
expect(lastNotification(infoCtx.notifications).message).toContain(
|
||||
"Usage: /permission-system",
|
||||
);
|
||||
|
||||
await definition!.handler("reset", infoCtx.ctx);
|
||||
expect(config).toEqual(DEFAULT_EXTENSION_CONFIG);
|
||||
expect(lastNotification(infoCtx.notifications).message).toBe(
|
||||
"Permission system settings reset to defaults.",
|
||||
);
|
||||
|
||||
const persisted = JSON.parse(readFileSync(configPath, "utf8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(persisted).toEqual(DEFAULT_EXTENSION_CONFIG);
|
||||
|
||||
await definition!.handler("unknown", infoCtx.ctx);
|
||||
expect(lastNotification(infoCtx.notifications).level).toBe("warning");
|
||||
expect(lastNotification(infoCtx.notifications).message).toContain(
|
||||
"Usage: /permission-system",
|
||||
);
|
||||
|
||||
const headlessCtx = createCommandContext(false);
|
||||
await definition!.handler("", headlessCtx.ctx);
|
||||
expect(lastNotification(headlessCtx.notifications).message).toBe(
|
||||
"/permission-system requires interactive TUI mode.",
|
||||
);
|
||||
|
||||
const modalCtx = createCommandContext(true);
|
||||
await definition!.handler("", modalCtx.ctx);
|
||||
expect(modalCtx.getCustomCalls()).toBe(1);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("show output includes rule origins when getComposedRules is provided", async () => {
|
||||
const config = { ...DEFAULT_EXTENSION_CONFIG };
|
||||
const composedRules: Rule[] = [
|
||||
{
|
||||
surface: "read",
|
||||
pattern: "*",
|
||||
action: "allow",
|
||||
layer: "config",
|
||||
origin: "global",
|
||||
},
|
||||
{
|
||||
surface: "bash",
|
||||
pattern: "rm *",
|
||||
action: "deny",
|
||||
layer: "config",
|
||||
origin: "project",
|
||||
},
|
||||
];
|
||||
|
||||
const controller = {
|
||||
config: { current: () => config, save: () => {} } as CommandConfigStore,
|
||||
configPath: "/fake/config.json",
|
||||
getActiveAgentConfigRules: () => composedRules,
|
||||
};
|
||||
|
||||
let definition: {
|
||||
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
||||
} | null = null;
|
||||
|
||||
registerPermissionSystemCommand(
|
||||
{
|
||||
registerCommand(_name: string, nextDef: typeof definition) {
|
||||
definition = nextDef;
|
||||
},
|
||||
} as never,
|
||||
controller,
|
||||
);
|
||||
|
||||
const ctx = createCommandContext(true);
|
||||
await definition!.handler("show", ctx.ctx);
|
||||
const msg = lastNotification(ctx.notifications).message;
|
||||
|
||||
expect(msg).toContain("global");
|
||||
expect(msg).toContain("project");
|
||||
expect(msg).toContain("read");
|
||||
expect(msg).toContain("bash");
|
||||
});
|
||||
|
||||
test("show output omits rule summary when getComposedRules is not provided", async () => {
|
||||
const config = { ...DEFAULT_EXTENSION_CONFIG, yoloMode: true };
|
||||
|
||||
const controller = {
|
||||
config: { current: () => config, save: () => {} } as CommandConfigStore,
|
||||
configPath: "/fake/config.json",
|
||||
getActiveAgentConfigRules: () => [] as Ruleset,
|
||||
};
|
||||
|
||||
let definition: {
|
||||
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
||||
} | null = null;
|
||||
|
||||
registerPermissionSystemCommand(
|
||||
{
|
||||
registerCommand(_name: string, nextDef: typeof definition) {
|
||||
definition = nextDef;
|
||||
},
|
||||
} as never,
|
||||
controller,
|
||||
);
|
||||
|
||||
const ctx = createCommandContext(true);
|
||||
await definition!.handler("show", ctx.ctx);
|
||||
const msg = lastNotification(ctx.notifications).message;
|
||||
|
||||
// Config knobs still present.
|
||||
expect(msg).toContain("yoloMode=on");
|
||||
// No rule annotation lines.
|
||||
expect(msg).not.toContain("(global)");
|
||||
});
|
||||
Reference in New Issue
Block a user