repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
scripts/codegen/pi-ext-settings.test.ts
Rawimport { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, it } from "vitest";
import { sandboxEnv, withProcessEnv } from "../../test/harness";
import {
parseNumberSetting,
resolveSetting,
type SettingDeclaration,
} from "./templates/pi-ext-settings.ts";
let root: string | undefined;
afterEach(() => {
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
});
function resolve<T>(
declaration: SettingDeclaration<T>,
input: {
user?: unknown;
project?: unknown;
env?: Record<string, string>;
flags?: Record<string, string | boolean>;
trusted?: boolean;
},
) {
root = mkdtempSync(join(tmpdir(), "pi-ext-settings-"));
const env = sandboxEnv(root, { env: input.env });
const write = (dir: string, value: unknown) => {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "settings.json"), JSON.stringify(value));
};
if (input.user !== undefined)
write(join(env.HOME ?? "", ".pi/agent"), input.user);
if (input.project !== undefined) write(join(root, ".pi"), input.project);
const cwd = root;
return withProcessEnv(env, () =>
resolveSetting(
{ getFlag: (name) => input.flags?.[name] },
{ cwd, isProjectTrusted: () => input.trusted ?? true },
declaration,
),
);
}
const block: SettingDeclaration<Record<string, unknown>> = {
key: "demo",
parse: (raw) => {
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
return undefined;
const value = raw as Record<string, unknown>;
if ("bad" in value) throw new Error("demo.bad is not allowed");
return value;
},
default: {},
};
it("deep-merges objects project over user, replacing arrays and scalars", async () => {
const result = await resolve(block, {
user: {
demo: { a: 1, list: [1, 2], nested: { keep: true, over: "user" } },
},
project: { demo: { list: [3], nested: { over: "project" } } },
});
expect(result).toEqual({
ok: true,
source: "project",
value: {
a: 1,
list: [3],
nested: { keep: true, over: "project" },
},
});
});
it("keeps __proto__ keys as own properties when merging", async () => {
root = mkdtempSync(join(tmpdir(), "pi-ext-settings-"));
const env = sandboxEnv(root);
const agent = join(env.HOME ?? "", ".pi/agent");
mkdirSync(agent, { recursive: true });
mkdirSync(join(root, ".pi"), { recursive: true });
writeFileSync(join(agent, "settings.json"), '{"demo":{"a":1}}');
writeFileSync(
join(root, ".pi", "settings.json"),
'{"demo":{"__proto__":{"polluted":true}}}',
);
const cwd = root;
const result = await withProcessEnv(env, () =>
resolveSetting(
{ getFlag: () => undefined },
{ cwd, isProjectTrusted: () => true },
block,
),
);
if (!result.ok) throw new Error(result.error);
expect(Object.hasOwn(result.value, "__proto__")).toBe(true);
expect(Object.getPrototypeOf(result.value)).toBe(Object.prototype);
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
});
it("parses numbers from settings and numeric strings from env or flags only", () => {
expect(parseNumberSetting(70, "user")).toBe(70);
expect(parseNumberSetting("70", "env")).toBe(70);
expect(parseNumberSetting("2.5", "flag")).toBe(2.5);
expect(parseNumberSetting("70", "project")).toBeUndefined();
for (const raw of ["", " ", "abc", "Infinity", Number.NaN, true, null])
expect(parseNumberSetting(raw, "env"), String(raw)).toBeUndefined();
});
it("ignores untrusted project objects", async () => {
const result = await resolve(block, {
user: { demo: { a: 1 } },
project: { demo: { a: 2 } },
trusted: false,
});
expect(result).toEqual({ ok: true, source: "user", value: { a: 1 } });
});
it("reports thrown parser messages with the merged source", async () => {
const result = await resolve(block, {
user: { demo: { a: 1 } },
project: { demo: { bad: true } },
});
expect(result).toEqual({
ok: false,
source: "project",
error:
"Invalid demo from merged project and user settings: demo.bad is not allowed",
});
});
it("keeps a non-object project value from falling through or merging", async () => {
const result = await resolve(block, {
user: { demo: { a: 1 } },
project: { demo: "oops" },
});
expect(result).toEqual({
ok: false,
source: "project",
error: "Invalid demo from project settings",
});
});
it("prefers flag, then env, over settings", async () => {
const text: SettingDeclaration<string> = {
key: "demo.name",
parse: (raw) => (typeof raw === "string" ? raw : undefined),
default: "default",
flag: { name: "demo-name", type: "string", description: "demo" },
env: "PI_DEMO_NAME",
};
const settings = { user: { demo: { name: "user" } } };
expect(
await resolve(text, {
...settings,
env: { PI_DEMO_NAME: "env" },
flags: { "demo-name": "flag" },
}),
).toEqual({ ok: true, source: "flag", value: "flag" });
expect(
await resolve(text, { ...settings, env: { PI_DEMO_NAME: "env" } }),
).toEqual({ ok: true, source: "env", value: "env" });
expect(await resolve(text, {})).toEqual({
ok: true,
source: "default",
value: "default",
});
});