import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestSession, type TestSession } from "../../../test/harness"; const extensionPath = path.resolve(import.meta.dirname, "../index.ts"); const CREDENTIALS = { PI_BAK_S3_ACCESS_KEY_ID: "id", PI_BAK_S3_SECRET_ACCESS_KEY: "secret", }; const INIT_WARNING = "warning: bak: run /bak init "; const settingWarnings = (notes: string[]) => notes.filter((note) => note.startsWith("warning") && note !== INIT_WARNING); type Case = { user?: unknown; project?: unknown; untrusted?: boolean; env?: Record; }; describe("bak settings", () => { let t: TestSession | undefined; let root: string | undefined; afterEach(async () => { await t?.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); t?.dispose(); t = undefined; if (root) rmSync(root, { recursive: true, force: true }); root = undefined; vi.unstubAllGlobals(); }); async function start(input: Case) { root = mkdtempSync(path.join(tmpdir(), "pi-ext-bak-settings-")); const write = (dir: string, bak: unknown) => { mkdirSync(dir, { recursive: true }); writeFileSync(path.join(dir, "settings.json"), JSON.stringify({ bak })); }; if ("user" in input) write(path.join(root, ".test-home/.pi/agent"), input.user); if ("project" in input) write(path.join(root, ".pi"), input.project); const requests: Request[] = []; vi.stubGlobal( "fetch", vi.fn(async (request: Request) => { requests.push(request); return new Response(null, { status: 404 }); }), ); t = await createTestSession({ cwd: root, extensions: [extensionPath], env: { ...CREDENTIALS, ...input.env }, }); if (input.untrusted) t.session.settingsManager.setProjectTrusted(false); const runner = t.session.extensionRunner; // The harness already emitted one session_start; dedupe its notes. await runner.emit({ type: "session_start", reason: "startup" }); const ctx = runner.createContext(); const notes = () => [ ...new Set( t?.events .uiCallsFor("notify") .map((call) => `${call.args[1]}: ${call.args[0]}`), ), ]; const bak = runner.extensions[0]?.commands.get("bak"); if (!bak) throw new Error("missing bak command"); return { notes, run: (args: string) => bak.handler(args, ctx), settle: async () => { await runner.emit({ type: "agent_settled" }); await new Promise((resolve) => setTimeout(resolve, 50)); }, target: () => { const request = requests[0]; if (!request) return undefined; const region = /Credential=id\/\d+\/([^/]+)\//.exec( request.headers.get("authorization") ?? "", )?.[1]; return `${new URL(request.url).origin}${new URL(request.url).pathname.split("/").slice(0, 2).join("/")} ${region}`; }, }; } it("keeps missing-endpoint default", async () => { const { notes } = await start({}); expect(notes()).toEqual([ "warning: bak: disabled; missing PI_BAK_S3_ENDPOINT", ]); }); it.each([ [ "env only with defaults", { env: { PI_BAK_S3_ENDPOINT: "https://env.test" } }, "https://env.test/pi-bak auto", ], [ "user settings", { user: { s3: { endpoint: "https://user.test", bucket: "user-bucket", region: "eu", }, }, }, "https://user.test/user-bucket eu", ], [ "env over settings", { user: { s3: { endpoint: "https://user.test", bucket: "user-bucket" } }, env: { PI_BAK_S3_ENDPOINT: "https://env.test", PI_BAK_S3_BUCKET: "env-bucket", PI_BAK_S3_REGION: "us", }, }, "https://env.test/env-bucket us", ], [ "trusted project merged over user", { user: { s3: { endpoint: "https://user.test", region: "eu" } }, project: { s3: { bucket: "project-bucket" } }, }, "https://user.test/project-bucket eu", ], [ "untrusted project ignored", { user: { s3: { endpoint: "https://user.test" } }, project: { s3: { endpoint: "https://project.test" } }, untrusted: true, }, "https://user.test/pi-bak auto", ], ] satisfies [string, Case, string][])( "applies %s", async (_name, input, expected) => { const { notes, run, target } = await start(input); await run("refresh"); expect(target()).toBe(expected); expect(settingWarnings(notes())).toEqual([]); }, ); it.each([ [ "non-string user bucket", { user: { s3: { endpoint: "https://user.test", bucket: 7 } }, env: {}, }, "Invalid bak.s3.bucket from user settings: expected a non-empty string", "https://user.test/pi-bak auto", ], [ "empty env region over valid user region", { user: { s3: { endpoint: "https://user.test", region: "eu" } }, env: { PI_BAK_S3_REGION: " " }, }, "Invalid bak.s3.region from environment variable PI_BAK_S3_REGION: expected a non-empty string", "https://user.test/pi-bak auto", ], [ "empty project bucket over valid user bucket", { user: { s3: { endpoint: "https://user.test", bucket: "user-bucket" } }, project: { s3: { bucket: "" } }, }, "Invalid bak.s3.bucket from project settings: expected a non-empty string", "https://user.test/pi-bak auto", ], ] satisfies [string, Case, string, string][])( "rejects %s without falling through", async (_name, input, error, expected) => { const { notes, run, target } = await start(input); await run("refresh"); expect(target()).toBe(expected); expect(settingWarnings(notes())).toEqual([ `warning: bak: ${error}; using default`, ]); }, ); it("rejects invalid endpoint and stays disabled", async () => { const { notes } = await start({ user: { s3: { endpoint: "https://user.test" } }, env: { PI_BAK_S3_ENDPOINT: "" }, }); expect(notes()).toEqual([ "warning: bak: Invalid bak.s3.endpoint from environment variable PI_BAK_S3_ENDPOINT: expected a non-empty string; using default", "warning: bak: disabled; missing PI_BAK_S3_ENDPOINT", ]); }); const configured = { s3: { endpoint: "https://user.test" } }; it.each([ ["default on", { user: configured }, true], ["user false", { user: { ...configured, autoBackup: false } }, false], [ "env over user", { user: { ...configured, autoBackup: false }, env: { PI_BAK_AUTO_BACKUP: "1" }, }, true, ], [ "trusted project over user", { user: configured, project: { autoBackup: false } }, false, ], [ "untrusted project ignored", { user: configured, project: { autoBackup: false }, untrusted: true }, true, ], ] satisfies [string, Case, boolean][])( "autoBackup %s", async (_name, input, automatic) => { const { notes, settle } = await start(input); await settle(); expect(notes()).toEqual(automatic ? [INIT_WARNING] : []); }, ); it.each([ [ "string user", { user: { ...configured, autoBackup: "false" } }, "Invalid bak.autoBackup from user settings", ], [ "non-boolean env", { user: { ...configured, autoBackup: false }, env: { PI_BAK_AUTO_BACKUP: "off" }, }, "Invalid bak.autoBackup from environment variable PI_BAK_AUTO_BACKUP", ], ] satisfies [string, Case, string][])( "autoBackup rejects %s and keeps default on", async (_name, input, error) => { const { notes, settle } = await start(input); await settle(); expect(notes()).toEqual([ `warning: bak: ${error}; using default`, INIT_WARNING, ]); }, ); it("keeps explicit backup when autoBackup is off", async () => { const { notes, run, settle } = await start({ user: { ...configured, autoBackup: false }, }); await settle(); expect(notes()).toEqual([]); await run("backup"); expect(notes()).toContain("info: bak: backed up 0; 0 indexed"); }); });