repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/horst/__tests__/settings.test.ts
Rawimport { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os, { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
createPiSession,
type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
const extensionPath = path.resolve(import.meta.dirname, "../index.ts");
const host = os.hostname().toLowerCase();
interface UiCall {
args: unknown[];
}
/**
* Install a recording UI context so `ctx.ui.notify` calls are captured.
*/
function installRecordingNotify(session: PiSession): UiCall[] {
const notifyCalls: UiCall[] = [];
session.session.extensionRunner.setUIContext(
{
setStatus: () => {},
notify: (...args: unknown[]) => notifyCalls.push({ args }),
} as never,
"print",
);
return notifyCalls;
}
type Case = { user?: unknown; project?: unknown; untrusted?: boolean };
describe("horst settings resolution", () => {
let session: PiSession | undefined;
let root: string | undefined;
afterEach(async () => {
await session?.dispose();
session = undefined;
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
});
async function start(input: Case) {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-horst-settings-"));
if ("project" in input) {
mkdirSync(path.join(root, ".pi"), { recursive: true });
writeFileSync(
path.join(root, ".pi", "settings.json"),
JSON.stringify({ horst: input.project }),
);
}
session = await createPiSession({
cwd: root,
extensionPaths: [extensionPath],
...("user" in input ? { userSettings: { horst: input.user } } : {}),
});
if (input.untrusted)
session.session.settingsManager.setProjectTrusted(false);
const notifyCalls = installRecordingNotify(session);
const runner = session.session.extensionRunner;
await runner.emit({ type: "session_start", reason: "startup" });
const { horst } = (
await runner.emitBeforeAgentStart(
{ messageType: "user", prompt: "prompt" },
{
cwd: session.cwd,
},
)
).systemPromptOptions.sections;
const warnings = notifyCalls
.filter((call) => call.args[1] === "warning")
.map((call) => call.args[0]);
return {
shell: /preferred shell: (.*)/.exec(horst ?? "")?.[1],
active: session.session.getActiveToolNames(),
warnings: [...new Set(warnings)],
};
}
it.each([
["default", {}, undefined],
["user", { user: { preferredShell: "nu" } }, "nu"],
[
"project over user",
{ user: { preferredShell: "nu" }, project: { preferredShell: "fish" } },
"fish",
],
[
"user kept when project omits key",
{ user: { preferredShell: "nu" }, project: { hosts: {} } },
"nu",
],
[
"untrusted project ignored",
{
user: { preferredShell: "nu" },
project: { preferredShell: "fish" },
untrusted: true,
},
"nu",
],
[
"untrusted project ignored without user",
{ project: { preferredShell: "fish" }, untrusted: true },
undefined,
],
] satisfies [string, Case, string | undefined][])(
"preferredShell: %s",
async (_name, input, expected) => {
const { shell, warnings } = await start(input);
expect(shell).toBe(expected);
expect(warnings).toEqual([]);
},
);
it("composes user OS and project host modifiers in order", async () => {
const osKey = { linux: "lnx", darwin: "mac", win32: "win32" }[
process.platform as "linux" | "darwin" | "win32"
];
const { active, warnings } = await start({
user: { os: { [osKey]: { tools: ["+grep", "+find", "-write"] } } },
project: { hosts: { [host]: { tools: ["-grep", "+write"] } } },
});
expect(active).not.toContain("grep");
expect(active).toContain("find");
expect(active).toContain("write");
expect(warnings).toEqual([]);
});
it("replaces tool arrays instead of merging them", async () => {
const { active } = await start({
user: { hosts: { [host]: { tools: ["+grep"] } } },
project: { hosts: { [host]: { tools: ["+find"] } } },
});
expect(active).toContain("find");
expect(active).not.toContain("grep");
});
it("allows an empty project array to clear inherited host modifiers", async () => {
const { active, warnings } = await start({
user: { hosts: { [host]: { tools: ["-write"] } } },
project: { hosts: { [host]: { tools: [] } } },
});
expect(active).toContain("write");
expect(warnings).toEqual([]);
});
it("ignores unknown names with one warning per tool", async () => {
const { active, warnings } = await start({
user: { hosts: { [host]: { tools: ["+absent", "-absent", "+grep"] } } },
});
expect(active).toContain("grep");
expect(active).not.toContain("absent");
expect(warnings).toEqual(["horst: unknown tools: absent"]);
});
it.each([
[
"invalid project",
{
user: {
preferredShell: "nu",
hosts: { [host]: { tools: ["+grep"] } },
},
project: { preferredShell: 42 },
},
"horst: Invalid horst from merged project and user settings: preferredShell must be a non-empty string; settings ignored",
],
[
"non-object project",
{ user: { preferredShell: "nu" }, project: "nu" },
"horst: Invalid horst from project settings: expected an object; settings ignored",
],
[
"invalid user",
{ user: { hosts: { [host]: { tools: ["grep"] } } } },
`horst: Invalid horst from user settings: hosts.${host}.tools must contain only +name/-name modifiers with non-empty exact tool names, not patterns; settings ignored`,
],
] satisfies [string, Case, string][])(
"rejects %s without falling through",
async (_name, input, warning) => {
const { shell, active, warnings } = await start(input);
expect(shell).toBeUndefined();
expect(active).not.toContain("grep");
expect(warnings).toEqual([warning]);
},
);
});