import { afterEach, describe, expect, it } from "bun:test"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { VehicleRegistrationOutcome, VehicleSpec } from "@danypops/armada"; import type { ServiceSpec } from "@danypops/vehicle-server/service"; import type { DaemonServiceInstaller } from "../src/daemon/daemon-service.ts"; import { createApp, type Deps } from "../src/daemon/service.ts"; import { saveUpdates } from "../src/daemon/watcher.ts"; import { dbPath, openDb, replaceAll } from "../src/packages/db.ts"; import type { Installer, Pkg, PkgInfo, Registry, SearchPage, UpdateOutcome } from "../src/packages/package.ts"; class FakeRegistry implements Registry { searchCalls = 0; lastQuery = ""; lastLimit = 0; constructor( private results: Pkg[] = [], private total = 0, private failWith?: string, ) {} async search(query: string, limit: number): Promise { this.searchCalls++; this.lastQuery = query; this.lastLimit = limit; if (this.failWith) throw new Error(this.failWith); 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 { gotSource = ""; removed = ""; updated = ""; output = "ok"; fail = false; async install(source: string): Promise { this.gotSource = source; if (this.fail) throw new Error("exit 1\nnpm ERR! 404"); return this.output; } async remove(source: string): Promise { this.removed = source; return this.output; } updateOutcome: Partial = {}; gotTarget: string | undefined; updatedSources: string[] = []; updateOutcomeFor: Record> = {}; async update(source: string, options?: { target?: string }): Promise { this.updated = source; this.updatedSources.push(source); this.gotTarget = options?.target; return { output: this.output, reloadRequired: true, alreadyUpToDate: false, pinned: false, ...this.updateOutcome, ...this.updateOutcomeFor[source], }; } updateDaemonDependencyGotName = ""; updateDaemonDependencyGotVersion: string | undefined; updateDaemonDependencyOutcome: Partial = {}; async updateDaemonDependency(name: string, options?: { approved?: boolean; version?: string }): Promise { this.updateDaemonDependencyGotName = name; this.updateDaemonDependencyGotVersion = options?.version; return { output: this.output, reloadRequired: true, alreadyUpToDate: false, pinned: false, ...this.updateDaemonDependencyOutcome }; } } class FakeDaemonServiceInstaller implements DaemonServiceInstaller { gotPiHome = ""; gotSource = ""; resolveFailure: string | undefined; notADaemon = false; installFailure: string | undefined; removeGotPiHome = ""; removeGotSource = ""; restartGotPiHome = ""; restartGotSource = ""; restartResolveFailure: string | undefined; restartNotADaemon = false; restartReason: string | undefined; restarted = true; spec: ServiceSpec = { name: "probe", version: "1.0.0", binPath: "/opt/probe/cli.js", handlePath: "/tmp/probe.handle.json", }; async install( piHome: string, source: string, ): Promise<{ ok: true; result: { installed: true } | { installed: false; reason: string }; spec: ServiceSpec } | { ok: false; reason: string }> { this.gotPiHome = piHome; this.gotSource = source; if (this.resolveFailure) return { ok: false, reason: this.resolveFailure, ...(this.notADaemon ? { notADaemon: true } : {}) }; if (this.installFailure) return { ok: true, result: { installed: false, reason: this.installFailure }, spec: this.spec }; return { ok: true, result: { installed: true }, spec: this.spec }; } async remove(piHome: string, source: string): Promise<{ ok: true; result: { installed: true }; spec: ServiceSpec }> { this.removeGotPiHome = piHome; this.removeGotSource = source; return { ok: true, result: { installed: true }, spec: this.spec }; } async restart( piHome: string, source: string, ): Promise<{ ok: true; restarted: boolean; reason?: string; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean }> { this.restartGotPiHome = piHome; this.restartGotSource = source; if (this.restartResolveFailure) return { ok: false, reason: this.restartResolveFailure, ...(this.restartNotADaemon ? { notADaemon: true } : {}) }; return { ok: true, restarted: this.restarted, reason: this.restartReason, spec: this.spec }; } registeredVehicles: VehicleSpec[] = []; unregisterByNameCalls: string[] = []; async listRegisteredVehicles(): Promise { return this.registeredVehicles; } async unregisterVehicleByName(name: string): Promise { this.unregisterByNameCalls.push(name); this.registeredVehicles = this.registeredVehicles.filter((vehicle) => vehicle.name !== name); return { ok: true, manifestHash: "hash" as never, applied: [], diagnostics: [] }; } } const roots: string[] = []; afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); function track(dir: string): string { roots.push(dir); return dir; } function deps(over: Partial = {}): Deps { return { reg: new FakeRegistry(), inst: new FakeInstaller(), token: "test-token", stateDir: track(mkdtempSync(join(tmpdir(), "packed-"))), ...over, }; } const auth = { authorization: "Bearer test-token" }; describe("service app", () => { it("GET /health", async () => { const app = createApp(deps()); const res = await app.fetch(new Request("http://x/health", { headers: auth })); expect(res.status).toBe(200); expect(((await res.json()) as any).ok).toBe(true); }); it("requires bearer token", async () => { const app = createApp(deps()); for (const headers of [{}, { authorization: "Bearer wrong" }] as Record[]) { const res = await app.fetch(new Request("http://x/health", { headers })); expect(res.status).toBe(401); } }); it("reads and updates mutation approval with a secure default", async () => { const app = createApp(deps()); const initial = await app.fetch(new Request("http://x/security", { headers: auth })); expect(await initial.json()).toEqual({ mutationApproval: "always" }); const denied = await app.fetch( new Request("http://x/security", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ mutationApproval: "never" }), }), ); expect(denied.status).toBe(403); expect(await denied.json()).toMatchObject({ code: "approval_required", operation: "security.write" }); const updated = await app.fetch( new Request("http://x/security", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ mutationApproval: "never", approved: true }), }), ); expect(await updated.json()).toEqual({ mutationApproval: "never" }); const invalid = await app.fetch( new Request("http://x/security", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ mutationApproval: "sometimes", approved: true }), }), ); expect(invalid.status).toBe(400); }); it("GET /search scopes query and clamps limit", async () => { const reg = new FakeRegistry([{ name: "pi-lsp", version: "0.3.0" }], 42); const app = createApp(deps({ reg })); const res = await app.fetch(new Request("http://x/search?q=lsp&limit=999", { headers: auth })); expect(res.status).toBe(200); expect(reg.lastQuery).toBe("keywords:pi-package lsp"); expect(reg.lastLimit).toBe(50); const body = (await res.json()) as any; expect(body.total).toBe(42); expect(body.results[0].name).toBe("pi-lsp"); }); it("caches GET responses (second call skips upstream)", async () => { const reg = new FakeRegistry([{ name: "x", version: "1" }], 1); const app = createApp(deps({ reg })); for (let i = 0; i < 2; i++) { await app.fetch(new Request("http://x/search?q=cache", { headers: auth })); } expect(reg.searchCalls).toBe(1); }); it("upstream error → 502 with message", async () => { const reg = new FakeRegistry([], 0, "registry down"); const app = createApp(deps({ reg })); const res = await app.fetch(new Request("http://x/search?q=boom", { headers: auth })); expect(res.status).toBe(502); expect(await res.text()).toContain("registry down"); }); it("GET /info", async () => { const app = createApp(deps()); const res = await app.fetch(new Request("http://x/info?name=pi-lsp", { headers: auth })); expect(res.status).toBe(200); expect(((await res.json()) as any).name).toBe("pi-lsp"); }); it("POST /install rejects invalid sources", async () => { const inst = new FakeInstaller(); const app = createApp(deps({ inst })); for (const source of ["foo; rm -rf ~", "npm:foo && curl x|sh", "$(whoami)", "", "npm:"]) { const res = await app.fetch( new Request("http://x/install", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source }), }), ); expect(res.status).toBe(400); } expect(inst.gotSource).toBe(""); }); it("guards install and remove at the authenticated daemon boundary", async () => { const inst = new FakeInstaller(); const app = createApp(deps({ inst })); for (const [path, body] of [ ["/install", { source: "npm:foo" }], ["/remove", { name: "foo" }], ] as const) { const response = await app.fetch( new Request(`http://x${path}`, { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify(body), }), ); expect(response.status).toBe(403); expect(await response.json()).toMatchObject({ code: "approval_required" }); } expect(inst.gotSource).toBe(""); expect(inst.removed).toBe(""); }); it("POST /install accepts valid sources, reports failures in-band", async () => { const inst = new FakeInstaller(); const service = new FakeDaemonServiceInstaller(); service.resolveFailure = "not a Vehicle"; service.notADaemon = true; const app = createApp(deps({ inst, daemonServiceInstaller: service })); for (const source of ["npm:foo", "npm:@scope/pkg@1.2.3", "git:github.com/u/r@v1", "https://github.com/u/r"]) { const res = await app.fetch( new Request("http://x/install", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source, approved: true }), }), ); expect(res.status).toBe(200); expect(((await res.json()) as any).ok).toBe(true); } expect(inst.gotSource).toBe("https://github.com/u/r"); inst.fail = true; const res = await app.fetch( new Request("http://x/install", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:missing", approved: true }), }), ); expect(res.status).toBe(200); const body = (await res.json()) as any; expect(body.ok).toBe(false); expect(body.output).toContain("npm ERR! 404"); }); it("POST /install configures an npm package's persistent Vehicle under the same approval", async () => { const svc = new FakeDaemonServiceInstaller(); const piHome = track(mkdtempSync(join(tmpdir(), "packed-install-vehicle-"))); const app = createApp(deps({ daemonServiceInstaller: svc, piHome })); const response = await app.fetch( new Request("http://x/install", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:probe", approved: true }), }), ); expect(response.status).toBe(200); expect(await response.json()).toMatchObject({ ok: true, service: { name: "probe", binPath: "/opt/probe/cli.js" } }); expect(svc.gotPiHome).toBe(piHome); expect(svc.gotSource).toBe("npm:probe"); }); it("POST /install-service rejects invalid sources and requires approval, matching /install's own guard", async () => { const svc = new FakeDaemonServiceInstaller(); const app = createApp(deps({ daemonServiceInstaller: svc })); const invalid = await app.fetch( new Request("http://x/install-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:foo && curl x|sh" }), }), ); expect(invalid.status).toBe(400); expect(svc.gotSource).toBe(""); const unapproved = await app.fetch( new Request("http://x/install-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:web-spider-daemon" }), }), ); expect(unapproved.status).toBe(403); expect(await unapproved.json()).toMatchObject({ code: "approval_required" }); expect(svc.gotSource).toBe(""); // A structurally valid but non-npm source is approval-gated the same as // any other source -- it fails closed only once the resolver itself runs // (a resolveDaemonServiceSpec responsibility, not this route's own regex). const gitApproved = await app.fetch( new Request("http://x/install-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "git:github.com/u/r@v1", approved: true }), }), ); expect(gitApproved.status).toBe(200); expect(((await gitApproved.json()) as any).ok).toBe(true); // FakeDaemonServiceInstaller doesn't itself enforce the npm-only rule; resolveDaemonServiceSpec's own unit tests cover that. }); it("POST /install-service installs a real service once approved, reporting the resolved spec", async () => { const svc = new FakeDaemonServiceInstaller(); const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-"))); const app = createApp(deps({ daemonServiceInstaller: svc, piHome })); const res = await app.fetch( new Request("http://x/install-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:web-spider-daemon", approved: true }), }), ); expect(res.status).toBe(200); const body = (await res.json()) as any; expect(body.ok).toBe(true); expect(body.output).toContain("probe"); expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js" }); expect(svc.gotPiHome).toBe(piHome); expect(svc.gotSource).toBe("npm:web-spider-daemon"); }); it("POST /install-service reports a resolution failure (no manifest) or an install failure (e.g. unsupported init system) in-band, not as an HTTP error", async () => { const svc = new FakeDaemonServiceInstaller(); svc.resolveFailure = "web-spider-daemon does not declare a packed.daemonService manifest"; const app = createApp(deps({ daemonServiceInstaller: svc })); const resolutionFailure = await app.fetch( new Request("http://x/install-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:web-spider-daemon", approved: true }), }), ); expect(resolutionFailure.status).toBe(200); expect(((await resolutionFailure.json()) as any).ok).toBe(false); svc.resolveFailure = undefined; svc.installFailure = "no supported Linux init system was detected"; const installFailure = await app.fetch( new Request("http://x/install-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:web-spider-daemon", approved: true }), }), ); expect(installFailure.status).toBe(200); const body = (await installFailure.json()) as any; expect(body.ok).toBe(false); expect(body.output).toContain("no supported Linux init system"); expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js" }); }); it("POST /restart-service rejects invalid sources and requires approval, matching /install-service's own guard", async () => { const svc = new FakeDaemonServiceInstaller(); const app = createApp(deps({ daemonServiceInstaller: svc })); const invalid = await app.fetch( new Request("http://x/restart-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:foo && curl x|sh" }), }), ); expect(invalid.status).toBe(400); expect(svc.restartGotSource).toBe(""); const unapproved = await app.fetch( new Request("http://x/restart-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:web-spider-daemon" }), }), ); expect(unapproved.status).toBe(403); expect(await unapproved.json()).toMatchObject({ code: "approval_required" }); expect(svc.restartGotSource).toBe(""); }); it("POST /restart-service restarts a real service once approved, reporting the resolved spec", async () => { const svc = new FakeDaemonServiceInstaller(); const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-"))); const app = createApp(deps({ daemonServiceInstaller: svc, piHome })); const res = await app.fetch( new Request("http://x/restart-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:web-spider-daemon", approved: true }), }), ); expect(res.status).toBe(200); const body = (await res.json()) as any; expect(body.ok).toBe(true); expect(body.restarted).toBe(true); expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js" }); expect(svc.restartGotPiHome).toBe(piHome); expect(svc.restartGotSource).toBe("npm:web-spider-daemon"); }); it("POST /restart-service reports a resolution failure or a no-op (no registered service) in-band, not as an HTTP error", async () => { const svc = new FakeDaemonServiceInstaller(); svc.restartResolveFailure = "web-spider-daemon does not declare a packed.daemonService manifest"; const app = createApp(deps({ daemonServiceInstaller: svc })); const resolutionFailure = await app.fetch( new Request("http://x/restart-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:web-spider-daemon", approved: true }), }), ); expect(resolutionFailure.status).toBe(200); expect(((await resolutionFailure.json()) as any).ok).toBe(false); svc.restartResolveFailure = undefined; svc.restarted = false; svc.restartReason = "no persistent service is registered for web-spider-daemon"; const noop = await app.fetch( new Request("http://x/restart-service", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:web-spider-daemon", approved: true }), }), ); expect(noop.status).toBe(200); const body = (await noop.json()) as any; expect(body.ok).toBe(true); expect(body.restarted).toBe(false); expect(body.output).toContain("no persistent service is registered"); }); it("POST /update validates, authorizes, and delegates one Pi package source", async () => { const inst = new FakeInstaller(); const service = new FakeDaemonServiceInstaller(); service.restartResolveFailure = "not a Vehicle"; service.restartNotADaemon = true; const app = createApp(deps({ inst, daemonServiceInstaller: service })); const denied = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:pi-lsp" }), }), ); expect(denied.status).toBe(403); const allowed = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:pi-lsp", approved: true }), }), ); expect(allowed.status).toBe(200); expect(await allowed.json()).toEqual({ ok: true, source: "npm:pi-lsp", output: "ok", reloadRequired: true, alreadyUpToDate: false, pinned: false, }); expect(inst.updated).toBe("npm:pi-lsp"); }); it("POST /update reports package success separately from pending daemon adoption", async () => { const svc = new FakeDaemonServiceInstaller(); svc.restarted = false; svc.restartReason = "no persistent service is registered for probe"; const app = createApp(deps({ daemonServiceInstaller: svc })); const response = await app.fetch(new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:probe", approved: true }), })); expect(await response.json()).toMatchObject({ ok: false, packageUpdated: true, serviceReconciled: false, output: expect.stringContaining("Daemon adoption required"), }); }); it("POST /update reconciles an installed Vehicle after a real package change", async () => { const svc = new FakeDaemonServiceInstaller(); const piHome = track(mkdtempSync(join(tmpdir(), "packed-update-vehicle-"))); const app = createApp(deps({ daemonServiceInstaller: svc, piHome })); const response = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:probe", approved: true }), }), ); expect(await response.json()).toMatchObject({ ok: true, serviceReconciled: true }); expect(svc.restartGotPiHome).toBe(piHome); expect(svc.restartGotSource).toBe("npm:probe"); }); it("POST /update reports an honest no-op instead of trusting pi's always-0-exit-code text", async () => { const inst = new FakeInstaller(); inst.updateOutcome = { reloadRequired: false, alreadyUpToDate: true, pinned: true, previousVersion: "1.0.0", currentVersion: "1.0.0" }; const app = createApp(deps({ inst })); const res = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:pi-lsp@1.0.0", approved: true }), }), ); expect(await res.json()).toEqual({ ok: true, source: "npm:pi-lsp@1.0.0", output: "ok", reloadRequired: false, alreadyUpToDate: true, pinned: true, previousVersion: "1.0.0", currentVersion: "1.0.0", }); }); it("POST /update threads target through to the installer and validates it the same way as source", async () => { const inst = new FakeInstaller(); const app = createApp(deps({ inst })); const invalidTarget = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:@scope/pkg@1.0.0", target: "not a valid source!", approved: true }), }), ); expect(invalidTarget.status).toBe(400); expect(inst.updated).toBe(""); const valid = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:@scope/pkg@1.0.0", target: "npm:@scope/pkg@2.0.0", approved: true }), }), ); expect(valid.status).toBe(200); expect(inst.updated).toBe("npm:@scope/pkg@1.0.0"); expect(inst.gotTarget).toBe("npm:@scope/pkg@2.0.0"); }); it("POST /update reconciles the Vehicle under the NEW (target) source after a replace, not the removed one", async () => { const svc = new FakeDaemonServiceInstaller(); const inst = new FakeInstaller(); const piHome = track(mkdtempSync(join(tmpdir(), "packed-update-vehicle-replace-"))); const app = createApp(deps({ inst, daemonServiceInstaller: svc, piHome })); const response = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:@scope/pkg@1.0.0", target: "npm:@scope/pkg@2.0.0", approved: true }), }), ); expect(await response.json()).toMatchObject({ ok: true, serviceReconciled: true }); expect(svc.restartGotSource).toBe("npm:@scope/pkg@2.0.0"); }); it("POST /update surfaces pinnedSourceRequiresTarget/replaced/before/after/rollback through the wire response", async () => { const inst = new FakeInstaller(); inst.updateOutcome = { replaced: true, before: { source: "npm:@scope/pkg@1.0.0", version: "1.0.0" }, after: { source: "npm:@scope/pkg@2.0.0", version: "2.0.0" }, rollback: { attempted: false, ok: true }, }; const app = createApp(deps({ inst })); const response = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:@scope/pkg@1.0.0", target: "npm:@scope/pkg@2.0.0", approved: true }), }), ); expect(await response.json()).toMatchObject({ replaced: true, before: { source: "npm:@scope/pkg@1.0.0", version: "1.0.0" }, after: { source: "npm:@scope/pkg@2.0.0", version: "2.0.0" }, rollback: { attempted: false, ok: true }, }); }); it("POST /update routes a real Vehicle-shaped daemon dependency with no pi: manifest of its own through updateDaemonDependency, never pi update --extension -- packed-package-update-restart-service-cant-manage", async () => { const inst = new FakeInstaller(); const svc = new FakeDaemonServiceInstaller(); const piHome = track(mkdtempSync(join(tmpdir(), "packed-update-daemon-dep-"))); writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:@danypops/pi-lector@0.12.7"] })); const lectorDir = join(piHome, "npm", "node_modules", "@danypops", "lector"); mkdirSync(lectorDir, { recursive: true }); writeFileSync( join(lectorDir, "package.json"), JSON.stringify({ name: "@danypops/lector", version: "0.18.9", bin: { lector: "src/cli.ts" }, dependencies: { "@danypops/vehicle-server": "^0.18.2" }, }), ); const app = createApp(deps({ inst, daemonServiceInstaller: svc, piHome })); const response = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:@danypops/lector", approved: true }), }), ); expect(response.status).toBe(200); expect(await response.json()).toMatchObject({ ok: true, source: "npm:@danypops/lector", serviceReconciled: true }); expect(inst.updateDaemonDependencyGotName).toBe("@danypops/lector"); // Never fell through to pi update --extension -- the exact call that fails // live with pi-core's own "No matching package found". expect(inst.updated).toBe(""); expect(svc.restartGotSource).toBe("npm:@danypops/lector"); }); it("POST /update still routes a pi:-configured extension through the ordinary pi update --extension path, unchanged, even when it also happens to resolve as a Vehicle daemon itself", async () => { const inst = new FakeInstaller(); const svc = new FakeDaemonServiceInstaller(); const piHome = track(mkdtempSync(join(tmpdir(), "packed-update-extension-"))); writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:@danypops/papyrus"] })); const papyrusDir = join(piHome, "npm", "node_modules", "@danypops", "papyrus"); mkdirSync(papyrusDir, { recursive: true }); writeFileSync( join(papyrusDir, "package.json"), JSON.stringify({ name: "@danypops/papyrus", version: "1.0.0", bin: { papyrus: "src/cli.ts" }, dependencies: { "@danypops/vehicle-server": "^0.18.2" }, }), ); const app = createApp(deps({ inst, daemonServiceInstaller: svc, piHome })); const response = await app.fetch( new Request("http://x/update", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ source: "npm:@danypops/papyrus", approved: true }), }), ); expect(response.status).toBe(200); expect(inst.updated).toBe("npm:@danypops/papyrus"); expect(inst.updateDaemonDependencyGotName).toBe(""); }); it("POST /update-all validates, authorizes, and delegates every explicitly given source", async () => { const inst = new FakeInstaller(); const app = createApp(deps({ inst })); const denied = await app.fetch( new Request("http://x/update-all", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ sources: ["npm:pi-lsp"] }), }), ); expect(denied.status).toBe(403); const allowed = await app.fetch( new Request("http://x/update-all", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ sources: ["npm:pi-lsp", "npm:pi-tickets"], approved: true }), }), ); expect(allowed.status).toBe(200); const body = (await allowed.json()) as any; expect(body.ok).toBe(true); expect(inst.updatedSources).toEqual(["npm:pi-lsp", "npm:pi-tickets"]); expect(body.results.map((r: any) => r.source)).toEqual(["npm:pi-lsp", "npm:pi-tickets"]); expect(body.results.every((r: any) => r.ok)).toBe(true); }); it("POST /update-all defaults to every currently-stale global package from the mirror when sources is omitted", async () => { const inst = new FakeInstaller(); const d = deps({ inst }); await saveUpdates(d.stateDir, { checkedAt: new Date().toISOString(), updates: [ { name: "pi-lsp", installed: "1.0.0", latest: "1.1.0" }, { name: "pi-tickets", installed: "2.0.0", latest: "2.1.0" }, ], }); const app = createApp(d); const response = await app.fetch( new Request("http://x/update-all", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ approved: true }), }), ); expect(response.status).toBe(200); expect(inst.updatedSources).toEqual(["npm:pi-lsp", "npm:pi-tickets"]); }); it("POST /update-all reports one failure without blocking the rest of the batch", async () => { const inst = new FakeInstaller(); inst.updateOutcomeFor["npm:broken"] = undefined as unknown as Partial; const originalUpdate = inst.update.bind(inst); inst.update = async (source: string, options?: { target?: string }) => { if (source === "npm:broken") throw new Error("No matching package found"); return originalUpdate(source, options); }; const app = createApp(deps({ inst })); const response = await app.fetch( new Request("http://x/update-all", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ sources: ["npm:broken", "npm:pi-lsp"], approved: true }), }), ); expect(response.status).toBe(200); const body = (await response.json()) as any; expect(body.ok).toBe(false); expect(body.results[0]).toMatchObject({ source: "npm:broken", ok: false }); expect(body.results[1]).toMatchObject({ source: "npm:pi-lsp", ok: true }); }); it("POST /update-all rejects an invalid source the same way /update does", async () => { const app = createApp(deps({ inst: new FakeInstaller() })); const response = await app.fetch( new Request("http://x/update-all", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ sources: ["not-a-real-source"], approved: true }), }), ); expect(response.status).toBe(400); }); it("GET /updates serves the watcher snapshot", async () => { const d = deps(); await saveUpdates(d.stateDir, { checkedAt: new Date().toISOString(), updates: [{ name: "a", installed: "1", latest: "2", detectedAt: "" }], }); const app = createApp(d); const res = await app.fetch(new Request("http://x/updates", { headers: auth })); expect(res.status).toBe(200); expect(((await res.json()) as any).updates[0].latest).toBe("2"); }); it("GET /installed lists packages from pi settings", async () => { const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-"))); writeFileSync( join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-extension-manager@0.8.2", { source: "npm:obj@2.0.0" }] }), ); const d = deps({ piHome }); const app = createApp(d); const res = await app.fetch(new Request("http://x/installed", { headers: auth })); expect(res.status).toBe(200); const body = (await res.json()) as any; expect(body.map((p: { name: string }) => p.name)).toEqual(["pi-extension-manager", "obj"]); }); it("GET /installed also surfaces a real Vehicle-shaped daemon dependency that has no pi: manifest of its own -- packed-package-update-restart-service-cant-manage", async () => { const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-"))); writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:@danypops/pi-lector@0.12.7"] })); const piLectorDir = join(piHome, "npm", "node_modules", "@danypops", "pi-lector"); mkdirSync(piLectorDir, { recursive: true }); writeFileSync( join(piLectorDir, "package.json"), JSON.stringify({ name: "@danypops/pi-lector", version: "0.12.7", dependencies: { "@danypops/lector": "^0.18.0" } }), ); const lectorDir = join(piHome, "npm", "node_modules", "@danypops", "lector"); mkdirSync(lectorDir, { recursive: true }); writeFileSync( join(lectorDir, "package.json"), JSON.stringify({ name: "@danypops/lector", version: "0.18.9", bin: { lector: "src/cli.ts" }, dependencies: { "@danypops/vehicle-server": "^0.18.2" }, }), ); const d = deps({ piHome }); const app = createApp(d); const res = await app.fetch(new Request("http://x/installed", { headers: auth })); expect(res.status).toBe(200); const body = (await res.json()) as any; expect(body).toEqual([ { name: "@danypops/pi-lector", pinned: "0.12.7", installed: undefined, scope: "global", kind: "extension" }, { name: "@danypops/lector", installed: "0.18.9", scope: "global", kind: "daemon-dependency" }, ]); }); it("POST /remove validates bare names and reports in-band", async () => { const inst = new FakeInstaller(); const app = createApp(deps({ inst })); for (const name of ["npm:foo", "foo; rm -rf ~", ""]) { const res = await app.fetch( new Request("http://x/remove", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ name, approved: true }), }), ); expect(res.status).toBe(400); } expect(inst.removed).toBe(""); const res = await app.fetch( new Request("http://x/remove", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ name: "pi-lsp", approved: true }), }), ); expect(res.status).toBe(200); expect(inst.removed).toBe("npm:pi-lsp"); }); it("POST /remove removes declared Vehicle state before deleting the package", async () => { const piHome = track(mkdtempSync(join(tmpdir(), "packed-remove-vehicle-"))); const packageDir = join(piHome, "npm", "node_modules", "probe"); mkdirSync(packageDir, { recursive: true }); writeFileSync( join(packageDir, "package.json"), JSON.stringify({ name: "probe", version: "1.0.0", packed: { daemonService: { binPath: "cli.js" } } }), ); const svc = new FakeDaemonServiceInstaller(); const app = createApp(deps({ piHome, daemonServiceInstaller: svc })); const response = await app.fetch( new Request("http://x/remove", { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ name: "probe", approved: true }), }), ); expect((await response.json()) as unknown).toMatchObject({ ok: true, serviceRemoved: true }); expect(svc.removeGotPiHome).toBe(piHome); expect(svc.removeGotSource).toBe("npm:probe"); }); it("GET /catalog serves the SQLite mirror", async () => { const d = deps(); const db = openDb(dbPath(d.stateDir)); replaceAll(db, [{ name: "a", version: "1" }], "test"); db.close(); const app = createApp(d); const res = await app.fetch(new Request("http://x/catalog", { headers: auth })); expect(res.status).toBe(200); const body = (await res.json()) as any; expect(body.packages[0].name).toBe("a"); expect(body.sha256).toMatch(/^[0-9a-f]{64}$/); }); it("GET /search?offline=1 queries the mirror", async () => { const d = deps(); const db = openDb(dbPath(d.stateDir)); replaceAll(db, [{ name: "pi-lsp", version: "1", description: "LSP tools" }], "test"); db.close(); const app = createApp(d); const res = await app.fetch(new Request("http://x/search?q=lsp&offline=1", { headers: auth })); expect(res.status).toBe(200); const body = (await res.json()) as any; expect(body.offline).toBe(true); expect(body.results[0].name).toBe("pi-lsp"); }); });