import { afterEach, describe, expect, it } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { OPERATION_NAMES } from "../src/daemon/service.ts"; import { createApp, type Deps } from "../src/daemon/service.ts"; import type { Installer, Pkg, PkgInfo, Registry, SearchPage, UpdateOutcome } from "../src/packages/package.ts"; const roots: string[] = []; afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); function temporaryRoot(prefix: string): string { const root = mkdtempSync(join(tmpdir(), prefix)); roots.push(root); return root; } class FakeRegistry implements Registry { constructor( private results: Pkg[] = [], private total = 0, ) {} async search(query: string, limit: number): Promise { return { results: this.results, total: this.total }; } async searchPage(): Promise { return { results: this.results, total: this.total }; } async searchAll(): Promise { return this.results; } async info(name: string): Promise { return { name, version: "1.0.0" }; } } class FakeInstaller implements Installer { async install(source: string): Promise { return `installed ${source}`; } async remove(source: string): Promise { return `removed ${source}`; } async update(source: string): Promise { return { output: `updated ${source}`, reloadRequired: true, alreadyUpToDate: false, pinned: false }; } } function deps(over: Partial = {}): Deps { return { reg: new FakeRegistry([{ name: "pi-lsp", version: "1.0.0", description: "An LSP package" }], 1), inst: new FakeInstaller(), token: "test-token", stateDir: temporaryRoot("packed-vehicle-"), ...over, }; } async function invoke(app: { fetch(request: Request): Promise }, name: string, input: Record, token = "test-token") { const response = await app.fetch( new Request("http://packed.internal/vehicle/invoke", { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ name, version: 1, input, permissions: ["packed:read", "packed:write"] }), }), ); return { status: response.status, body: (await response.json()) as { output?: unknown; error?: { category: string; message: string } } }; } describe("packed's daemon operation surface, through the real Vehicle wire protocol", () => { it("GET /vehicle/manifest lists all 29 operations plus the registry's own vehicle.approval.resolve/vehicle.approval.status, authenticated the same as /api/v1/ops", async () => { const app = createApp(deps()); const unauthorized = await app.fetch(new Request("http://packed.internal/vehicle/manifest")); expect(unauthorized.status).toBe(401); const response = await app.fetch(new Request("http://packed.internal/vehicle/manifest", { headers: { authorization: "Bearer test-token" } })); expect(response.status).toBe(200); const body = (await response.json()) as { operations: Array<{ name: string }> }; expect(body.operations.map((o) => o.name).sort()).toEqual( [...OPERATION_NAMES, "vehicle.approval.resolve", "vehicle.approval.status"].sort(), ); }); it("a mutation's approvalRequired reflects security.ts's own per-operation classification, not a coarse effect default -- restart_service is gated despite sharing external-write with the never-gated catalog.sync", async () => { const app = createApp(deps()); const response = await app.fetch(new Request("http://packed.internal/vehicle/manifest", { headers: { authorization: "Bearer test-token" } })); const body = (await response.json()) as { operations: Array<{ name: string; approvalRequired?: boolean }> }; const byName = new Map(body.operations.map((o) => [o.name, o.approvalRequired])); expect(byName.get("package.restart_service")).toBe(true); expect(byName.get("package.reconcile_services")).toBe(true); expect(byName.get("package.catalog.sync")).toBe(false); expect(byName.get("package.index.build")).toBe(false); expect(byName.get("resources.toggle")).toBe(true); expect(byName.get("package.security.set")).toBe(true); expect(byName.get("setup.export")).toBe(false); expect(byName.get("setup.update")).toBe(false); }); it("derives Vehicle permissions from authenticated server state", async () => { const app = createApp(deps()); const response = await app.fetch( new Request("http://packed.internal/vehicle/invoke", { method: "POST", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, body: JSON.stringify({ name: "package.search", version: 1, input: { query: "lsp", limit: 10 }, permissions: [], principal: { id: "forged-caller" }, }), }), ); expect(response.status).toBe(200); }); it("package.search round-trips through /vehicle/invoke, matching /api/v1/ops's own shape", async () => { const app = createApp(deps()); const result = await invoke(app, "package.search", { query: "lsp", limit: 10 }); expect(result.status).toBe(200); expect((result.body.output as { total: number }).total).toBe(1); expect((result.body.output as { results: Array<{ name: string }> }).results[0]?.name).toBe("pi-lsp"); }); it("package.installed and pi.status (daemon-only, never a Pi tool) are still reachable through /vehicle/invoke", async () => { const app = createApp(deps({ piHome: temporaryRoot("packed-vehicle-pihome-") })); const installed = await invoke(app, "package.installed", {}); expect(installed.status).toBe(200); expect(installed.body.output).toEqual([]); }); it("a denied mutation (default mutationApproval: always, no approved flag) surfaces Vehicle's authorization category -> HTTP 403, same as /api/v1/ops's own 403", async () => { const app = createApp(deps()); const result = await invoke(app, "package.remove", { name: "pi-lsp" }); expect(result.status).toBe(403); expect(result.body.error?.category).toBe("authorization"); }); it("a genuinely approved mutation (through Vehicle's own request/resolve/capability dance) succeeds exactly like an approved /api/v1/ops call -- an approved:true field in the input body alone is no longer enough, since Vehicle's own gate never reads it", async () => { const app = createApp(deps()); const stillDenied = await invoke(app, "package.remove", { name: "pi-lsp", approved: true }); expect(stillDenied.status).toBe(403); const denied = await invoke(app, "package.remove", { name: "pi-lsp" }); const requestId = (denied.body.error as unknown as { details?: { requestId?: string } }).details?.requestId; expect(typeof requestId).toBe("string"); const resolveResponse = await app.fetch( new Request("http://packed.internal/vehicle/invoke", { method: "POST", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, body: JSON.stringify({ name: "vehicle.approval.resolve", version: 1, input: { requestId, decision: "granted" }, permissions: ["vehicle:approvals:resolve"], }), }), ); const resolved = { status: resolveResponse.status, body: (await resolveResponse.json()) as { output?: { capability?: string } }, }; expect(resolved.status).toBe(200); const capability = (resolved.body.output as { capability?: string }).capability; expect(typeof capability).toBe("string"); const response = await app.fetch( new Request("http://packed.internal/vehicle/invoke", { method: "POST", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, body: JSON.stringify({ name: "package.remove", version: 1, input: { name: "pi-lsp" }, permissions: ["packed:read", "packed:write"], approvalCapability: capability, }), }), ); expect(response.status).toBe(200); const body = (await response.json()) as { output: { ok: boolean } }; expect(body.output.ok).toBe(true); }); it("mutationApproval: never disables the registry's own gate too, live, the instant /security POST changes it -- no daemon restart", async () => { const app = createApp(deps()); const deniedFirst = await invoke(app, "package.remove", { name: "pi-lsp" }); expect(deniedFirst.status).toBe(403); const securityPost = await app.fetch( new Request("http://packed.internal/security", { method: "POST", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, body: JSON.stringify({ mutationApproval: "never", approved: true }), }), ); expect(securityPost.status).toBe(200); const allowedNow = await invoke(app, "package.remove", { name: "pi-lsp" }); expect(allowedNow.status).toBe(200); expect((allowedNow.body.output as { ok: boolean }).ok).toBe(true); }); it("a daemon that boots with mutationApproval already never on disk starts with the gate already disabled, not just after the first /security POST", async () => { const stateDir = temporaryRoot("packed-vehicle-never-"); const bootstrap = createApp(deps({ stateDir })); const prep = await bootstrap.fetch( new Request("http://packed.internal/security", { method: "POST", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, body: JSON.stringify({ mutationApproval: "never", approved: true }), }), ); expect(prep.status).toBe(200); // A brand new app instance against the SAME on-disk state -- simulates a fresh daemon // process starting up after the setting was already changed by a previous run. const freshBoot = createApp(deps({ stateDir })); const result = await invoke(freshBoot, "package.remove", { name: "pi-lsp" }); expect(result.status).toBe(200); }); it("a handler-thrown validation error keeps its own real message, mapped to Vehicle's validation category (not a generic internal 500)", async () => { const app = createApp(deps()); const result = await invoke(app, "package.check", { path: "" }); expect(result.status).toBe(400); expect(result.body.error?.category).toBe("validation"); expect(result.body.error?.message).toContain("path must be a non-empty string"); }); it("the old /api/v1/ops route keeps working unchanged, alongside /vehicle/invoke", async () => { const app = createApp(deps()); const legacy = await app.fetch( new Request("http://packed.internal/api/v1/ops", { method: "POST", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, body: JSON.stringify({ op: "package.search", input: { query: "lsp", limit: 10 } }), }), ); expect(legacy.status).toBe(200); expect(((await legacy.json()) as { result: { total: number } }).result.total).toBe(1); }); });