@danypops/pi-packed 0.21.12 → 0.21.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/client.d.ts +16 -0
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +1 -1
  4. package/dist/protocol.d.ts +6 -0
  5. package/dist/protocol.d.ts.map +1 -1
  6. package/extension/src/index.ts +9 -1
  7. package/extension/src/model.ts +35 -0
  8. package/extension/src/package-inspector.ts +12 -0
  9. package/extension/src/tabs/discover.ts +14 -1
  10. package/extension/src/tools.ts +31 -20
  11. package/extension/src/tui.ts +124 -33
  12. package/extension/src/vehicle-target.ts +24 -0
  13. package/extension/src/vehicle-tools.ts +84 -0
  14. package/package.json +2 -2
  15. package/service/src/adoption/install-validation.ts +19 -1
  16. package/service/src/daemon/service.ts +17 -1
  17. package/service/src/packages/install.ts +94 -13
  18. package/service/src/packages/package.ts +35 -0
  19. package/service/src/public/client.ts +29 -0
  20. package/service/src/public/protocol.ts +6 -0
  21. package/service/src/registry/registry.ts +20 -1
  22. package/service/src/setup/setup.ts +50 -2
  23. package/service/test/advisories.test.ts +17 -9
  24. package/service/test/cleanup.test.ts +7 -2
  25. package/service/test/cli.test.ts +29 -13
  26. package/service/test/db.test.ts +8 -2
  27. package/service/test/doctor.test.ts +7 -2
  28. package/service/test/domain.test.ts +22 -12
  29. package/service/test/index.test.ts +22 -9
  30. package/service/test/install-validation.test.ts +14 -4
  31. package/service/test/install.test.ts +129 -12
  32. package/service/test/npm-metadata-e2e.test.ts +25 -3
  33. package/service/test/pack-score.test.ts +8 -2
  34. package/service/test/perf/multi-install.perf.test.ts +222 -0
  35. package/service/test/pi-version.test.ts +15 -5
  36. package/service/test/public-client.test.ts +16 -6
  37. package/service/test/publish.test.ts +13 -3
  38. package/service/test/registry-contract.test.ts +11 -2
  39. package/service/test/resources.test.ts +7 -2
  40. package/service/test/security.test.ts +15 -5
  41. package/service/test/service.test.ts +19 -9
  42. package/service/test/setup.test.ts +174 -4
@@ -9,6 +9,9 @@ import { HttpRegistry } from "../src/registry/registry.ts";
9
9
  import { createApp } from "../src/daemon/service.ts";
10
10
 
11
11
  const TOKEN = "e".repeat(64);
12
+ const PUBLISHED_MODIFIED = "2024-03-01T00:00:00.000Z";
13
+ const WEEKLY_DOWNLOADS = 12_345;
14
+ const MONTHLY_DOWNLOADS = 54_321;
12
15
  const PUBLISHED_README = `# @danypops/enigma
13
16
 
14
17
  Encrypted credential vault and supervisor daemon. Holds delegated OAuth/API
@@ -56,17 +59,28 @@ describe("published npm metadata E2E", () => {
56
59
  });
57
60
  }
58
61
  if (url.pathname === "/%40danypops/enigma") {
62
+ // One response body serves both callers that hit this exact path: HttpRegistry's own
63
+ // publishedReadme() (Accept: application/json, reads .readme) and modifiedAt() (Accept:
64
+ // application/vnd.npm.install-v1+json, reads .modified) -- real npm content-negotiates
65
+ // two different document shapes for the same URL; this fixture keeps it simple by
66
+ // returning both fields unconditionally, since each caller only ever reads its own.
59
67
  return Response.json({
60
68
  name: "@danypops/enigma",
61
69
  "dist-tags": { latest: "0.22.1" },
62
70
  readme: PUBLISHED_README,
71
+ modified: PUBLISHED_MODIFIED,
63
72
  });
64
73
  }
74
+ if (url.pathname === "/downloads/point/last-week/%40danypops/enigma") return Response.json({ downloads: WEEKLY_DOWNLOADS });
75
+ if (url.pathname === "/downloads/point/last-month/%40danypops/enigma") return Response.json({ downloads: MONTHLY_DOWNLOADS });
65
76
  return new Response("not found", { status: 404 });
66
77
  },
67
78
  });
68
79
  const app = createApp({
69
- reg: new HttpRegistry(`http://127.0.0.1:${npmServer.port}`, 20, 0, 1),
80
+ // downloadsBase points at the SAME fake server as the registry itself -- real npm splits
81
+ // these across two different hosts (registry.npmjs.org vs api.npmjs.org), but nothing here
82
+ // cares which host a request lands on, only that it's never the real internet.
83
+ reg: new HttpRegistry(`http://127.0.0.1:${npmServer.port}`, 20, 0, 1, `http://127.0.0.1:${npmServer.port}`),
70
84
  inst: new NoopInstaller(),
71
85
  token: TOKEN,
72
86
  stateDir,
@@ -80,15 +94,21 @@ describe("published npm metadata E2E", () => {
80
94
  rmSync(stateDir, { recursive: true, force: true });
81
95
  });
82
96
 
83
- it("preserves an npm-published README through HttpRegistry, daemon RPC, and PackedClient", async () => {
97
+ it("preserves an npm-published README through HttpRegistry, daemon RPC, and PackedClient, plus the daemon's own package.info release-date/downloads enrichment", async () => {
84
98
  const client = new PackedClient(`http://127.0.0.1:${daemonServer.port}`, TOKEN);
85
99
  const info = await client.info("@danypops/enigma");
86
100
 
87
- expect(npmRequests).toHaveLength(2);
101
+ // 2 from HttpRegistry.info() itself (latest + readme) + 1 from the daemon's own
102
+ // package.info enrichment calling modifiedAt() (same path as readme, different Accept) + 2
103
+ // from downloads() (last-week, last-month) -- see service.ts's "/info" handler.
104
+ expect(npmRequests).toHaveLength(5);
88
105
  expect(npmRequests).toEqual(
89
106
  expect.arrayContaining([
90
107
  { path: "/%40danypops/enigma/latest", accept: "application/json" },
91
108
  { path: "/%40danypops/enigma", accept: "application/json" },
109
+ { path: "/%40danypops/enigma", accept: "application/vnd.npm.install-v1+json" },
110
+ { path: "/downloads/point/last-week/%40danypops/enigma", accept: "*/*" },
111
+ { path: "/downloads/point/last-month/%40danypops/enigma", accept: "*/*" },
92
112
  ]),
93
113
  );
94
114
  expect(info).toMatchObject({
@@ -97,6 +117,8 @@ describe("published npm metadata E2E", () => {
97
117
  license: "MIT",
98
118
  readmeAvailable: true,
99
119
  readme: PUBLISHED_README,
120
+ modified: PUBLISHED_MODIFIED,
121
+ downloads: { weekly: WEEKLY_DOWNLOADS, monthly: MONTHLY_DOWNLOADS },
100
122
  });
101
123
  });
102
124
  });
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { createGithubLastCommitAt, type FetchGithubLastCommitAt } from "../src/adoption/commit-freshness.ts";
@@ -30,8 +30,14 @@ class FakeRegistry implements Registry {
30
30
  }
31
31
  }
32
32
 
33
+ const roots: string[] = [];
34
+ afterEach(() => {
35
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
36
+ });
37
+
33
38
  function fixture(manifest: Record<string, unknown>, readme = ""): string {
34
39
  const root = mkdtempSync(join(tmpdir(), "packed-pack-"));
40
+ roots.push(root);
35
41
  writeFileSync(join(root, "package.json"), JSON.stringify(manifest));
36
42
  if (readme) writeFileSync(join(root, "README.md"), readme);
37
43
  return root;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * A real, end-to-end multi-package install/update benchmark against a clean, isolated Pi + Packed
3
+ * environment -- the "install/update several packages together" question this exists to answer.
4
+ *
5
+ * As of this file's own history, SetupManager.apply() (service/src/setup/setup.ts) opts into
6
+ * BATCH MODE whenever its Installer implements validate()/installOnly()/reresolveDependencyTree()
7
+ * (ExecInstaller does): every package change is validate()d CONCURRENTLY first (each call stages
8
+ * into its own throwaway temp dir, zero shared state -- see Installer.validate's own doc comment),
9
+ * then installOnly() runs sequentially per package (an unlocked, shared piHome makes concurrent
10
+ * `pi install` calls genuinely unsafe -- confirmed empirically: two of three concurrent installs
11
+ * silently lost their own settings.json/package.json entries even though the CLI itself reported
12
+ * success), and reresolveDependencyTree() (a FULL `npm install` at piHome/npm) runs exactly ONCE
13
+ * for the whole batch instead of once per package. This file's own git history has the "before"
14
+ * numbers (N=10: ~35s, ~24% of it nine redundant reresolves) if you want the direct comparison.
15
+ *
16
+ * "Clean version of Pi with Packed": a freshly created temp directory stands in for ~/.pi --
17
+ * PI_CODING_AGENT_DIR (the real pi CLI's own env override, see @earendil-works/pi-coding-agent's
18
+ * config.js getAgentDir()) points the real `pi` binary at it, and the identical path is handed to
19
+ * SetupManager/ExecInstaller as piHome, so both sides agree on one on-disk state exactly the way a
20
+ * real `packed setup apply` run would. No pi-packed daemon process is started -- SetupManager and
21
+ * ExecInstaller are exactly what the daemon's own setup.apply Vehicle operation calls internally
22
+ * (see service/src/daemon/vehicle-registration.ts), so driving them directly here is the same real
23
+ * production code path minus the HTTP hop.
24
+ *
25
+ * Deliberately real npm packages, not a hand-rolled fake registry: ten tiny, long-published,
26
+ * zero-postinstall-script leaf packages (is-number, is-buffer, ...), pinned to exact versions with
27
+ * their real npm-published sha512 integrity below, so a run is deterministic in WHAT it installs
28
+ * even though the real npm registry's own latency is (honestly) not. None declares pi.extensions,
29
+ * so HeadlessInstallValidator's own extra per-extension load-check subprocesses never run for
30
+ * them -- this measures the floor every npm: install already pays (npm pack + npm install), not
31
+ * the additional cost a genuine Pi extension's own load validation adds on top.
32
+ *
33
+ * Skipped by default (like service/test/smoke.test.ts's own bwrap gate) -- this makes real,
34
+ * uncached network calls and takes real wall-clock seconds, unlike the rest of this suite. Run it
35
+ * on demand:
36
+ *
37
+ * PACKED_PERF=1 bun test service/test/perf/multi-install.perf.test.ts
38
+ * PACKED_PERF=1 PACKED_PERF_N=10 bun test service/test/perf/multi-install.perf.test.ts
39
+ */
40
+ import { describe, expect, it } from "bun:test";
41
+ import { mkdtempSync, rmSync } from "node:fs";
42
+ import { tmpdir } from "node:os";
43
+ import { join } from "node:path";
44
+ import { ExecInstaller } from "../../src/packages/install.ts";
45
+ import type { PkgInfo, Registry, SearchPage } from "../../src/packages/package.ts";
46
+ import { createLogger } from "../../src/shared/log.ts";
47
+ import { SetupManager } from "../../src/setup/setup.ts";
48
+
49
+ /** Real npm-published sha512 integrity for each pinned version below (`npm view <spec> dist.integrity`) -- SetupManifestSchema's NpmPackageSchema requires one, matching what SetupManager.resolvePackage() would itself fetch from the registry. */
50
+ const FIXTURE_PACKAGES: ReadonlyArray<{ name: string; version: string; integrity: string }> = [
51
+ { name: "is-number", version: "7.0.0", integrity: "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" },
52
+ { name: "is-buffer", version: "2.0.5", integrity: "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" },
53
+ {
54
+ name: "is-plain-object",
55
+ version: "5.0.0",
56
+ integrity: "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
57
+ },
58
+ { name: "is-callable", version: "1.2.7", integrity: "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" },
59
+ { name: "is-arrayish", version: "0.3.2", integrity: "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" },
60
+ {
61
+ name: "is-typedarray",
62
+ version: "1.0.0",
63
+ integrity: "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
64
+ },
65
+ {
66
+ name: "is-fullwidth-code-point",
67
+ version: "4.0.0",
68
+ integrity: "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
69
+ },
70
+ { name: "is-extglob", version: "2.1.1", integrity: "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" },
71
+ { name: "is-glob", version: "4.0.3", integrity: "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==" },
72
+ { name: "is-stream", version: "3.0.0", integrity: "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==" },
73
+ ];
74
+
75
+ /** Never exercised: resolvePackage() only needs Registry.info() when a package isn't ALREADY
76
+ * pinned in the manifest with a resolved version+integrity, which every fixture package here
77
+ * already is -- SetupManager.plan()/apply() never re-resolve an already-fully-specified entry. */
78
+ class UnusedRegistry implements Registry {
79
+ async search(): Promise<SearchPage> {
80
+ throw new Error("not exercised");
81
+ }
82
+ async searchPage(): Promise<SearchPage> {
83
+ throw new Error("not exercised");
84
+ }
85
+ async searchAll(): Promise<never[]> {
86
+ throw new Error("not exercised");
87
+ }
88
+ async info(): Promise<PkgInfo> {
89
+ throw new Error("not exercised");
90
+ }
91
+ }
92
+
93
+ interface PackageTiming {
94
+ source: string;
95
+ validateMs?: number;
96
+ installMs?: number;
97
+ }
98
+
99
+ function requestedPackageCount(): number {
100
+ const raw = Number(process.env.PACKED_PERF_N);
101
+ return Number.isFinite(raw) && raw > 0 ? Math.min(Math.floor(raw), FIXTURE_PACKAGES.length) : FIXTURE_PACKAGES.length;
102
+ }
103
+
104
+ function formatReport(timings: PackageTiming[], reresolveMs: number, wallClockMs: number): string {
105
+ const sumValidate = timings.reduce((sum, t) => sum + (t.validateMs ?? 0), 0);
106
+ const sumInstall = timings.reduce((sum, t) => sum + (t.installMs ?? 0), 0);
107
+ const lines = [
108
+ "",
109
+ `packed multi-install perf (N=${timings.length}, BATCH mode: validate() concurrent, installOnly() sequential, one reresolve)`,
110
+ "-".repeat(80),
111
+ ...timings.map(
112
+ (t, i) =>
113
+ ` #${i + 1} ${t.source.padEnd(28)} validate=${(t.validateMs ?? 0).toFixed(0).padStart(6)}ms installOnly=${(t.installMs ?? 0).toFixed(0).padStart(6)}ms`,
114
+ ),
115
+ "-".repeat(80),
116
+ ` sum of validateMs (ran CONCURRENTLY -- NOT how long this phase actually took wall-clock) : ${sumValidate.toFixed(0)}ms`,
117
+ ` sum of installOnly Ms (ran sequentially -- this IS roughly how long this phase took) : ${sumInstall.toFixed(0)}ms`,
118
+ ` single batch reresolveDependencyTree() call : ${reresolveMs.toFixed(0)}ms`,
119
+ ` observed wall clock (apply() total) : ${wallClockMs.toFixed(0)}ms`,
120
+ "",
121
+ ];
122
+ return lines.join("\n");
123
+ }
124
+
125
+ const runPerf = process.env.PACKED_PERF === "1" ? describe : describe.skip;
126
+
127
+ runPerf("real multi-package install against a clean, isolated Pi + Packed", () => {
128
+ it(
129
+ "installs N real npm packages in ONE batch (validate concurrently, commit sequentially, reresolve once) and reports where the time goes",
130
+ async () => {
131
+ const n = requestedPackageCount();
132
+ const fixtures = FIXTURE_PACKAGES.slice(0, n);
133
+
134
+ const root = mkdtempSync(join(tmpdir(), "packed-perf-clean-pi-"));
135
+ const agentDir = join(root, ".pi", "agent");
136
+ const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
137
+ process.env.PI_CODING_AGENT_DIR = agentDir; // the real `pi` binary's own home-dir override
138
+ try {
139
+ const timings = new Map<string, PackageTiming>();
140
+ let reresolveMs = 0;
141
+ let reresolveCalls = 0;
142
+ const logger = createLogger(
143
+ "perf",
144
+ (line) => {
145
+ try {
146
+ const entry = JSON.parse(line) as Record<string, unknown>;
147
+ if (entry.msg === "validate timing" && typeof entry.source === "string") {
148
+ timings.set(entry.source, { ...timings.get(entry.source), source: entry.source, validateMs: Number(entry.validateMs) });
149
+ } else if (entry.msg === "installOnly timing" && typeof entry.source === "string") {
150
+ timings.set(entry.source, { ...timings.get(entry.source), source: entry.source, installMs: Number(entry.installMs) });
151
+ } else if (entry.msg === "reresolve timing") {
152
+ reresolveCalls += 1;
153
+ reresolveMs = Number(entry.reresolveMs);
154
+ }
155
+ } catch {
156
+ // non-JSON log line -- ignore, this sink only cares about structured timing entries
157
+ }
158
+ },
159
+ "debug",
160
+ );
161
+
162
+ const installer = new ExecInstaller(undefined, agentDir, undefined, undefined, logger);
163
+ const manager = new SetupManager(new UnusedRegistry(), installer, agentDir);
164
+
165
+ const manifestPath = join(root, "pi-setup.json");
166
+ await Bun.write(
167
+ manifestPath,
168
+ JSON.stringify({
169
+ $schema: "./schema/pi-setup-v1.schema.json",
170
+ schemaVersion: 1,
171
+ packages: fixtures.map((pkg) => ({
172
+ kind: "npm",
173
+ scope: "global",
174
+ source: `npm:${pkg.name}@${pkg.version}`,
175
+ resolved: pkg.version,
176
+ integrity: pkg.integrity,
177
+ })),
178
+ profiles: {},
179
+ }),
180
+ );
181
+
182
+ const wallClockStart = performance.now();
183
+ const result = await manager.apply(manifestPath);
184
+ const wallClockMs = performance.now() - wallClockStart;
185
+
186
+ // Surfaced unconditionally (not just on failure): result.operations[].output carries
187
+ // ExecInstaller's own real stdout/stderr/error text -- result.diagnostics alone only
188
+ // ever says "package operation failed", which sent an earlier real run of this exact
189
+ // harness straight to ad hoc /tmp debugging instead of just reading the test's own
190
+ // output. Never do that again -- if this harness can't explain its own failure, that's
191
+ // a gap in the harness to fix, not a cue to reach for bash/heredocs outside it.
192
+ console.log(
193
+ [
194
+ "",
195
+ "per-operation outcomes:",
196
+ ...result.operations.map(
197
+ (op) => ` ${op.status.padEnd(9)} ${op.kind.padEnd(15)} ${op.target}${op.output ? ` -- ${op.output}` : ""}`,
198
+ ),
199
+ "",
200
+ ].join("\n"),
201
+ );
202
+ if (result.diagnostics.length > 0) console.log("diagnostics:", JSON.stringify(result.diagnostics, null, 2));
203
+
204
+ expect(result.diagnostics).toEqual([]);
205
+ expect(result.ok).toBe(true);
206
+ expect(result.operations.filter((op) => op.status === "succeeded")).toHaveLength(n);
207
+ expect(timings.size).toBe(n);
208
+ // The whole point of batch mode: exactly ONE reresolveDependencyTree() call for the
209
+ // entire batch, not one per package.
210
+ expect(reresolveCalls).toBe(1);
211
+
212
+ const orderedTimings = fixtures.map((pkg) => timings.get(`npm:${pkg.name}@${pkg.version}`)!);
213
+ console.log(formatReport(orderedTimings, reresolveMs, wallClockMs));
214
+ } finally {
215
+ if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
216
+ else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
217
+ rmSync(root, { recursive: true, force: true });
218
+ }
219
+ },
220
+ 10 * 60_000,
221
+ );
222
+ });
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync } from "node:fs";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
@@ -17,6 +17,16 @@ import {
17
17
  } from "../src/pi/pi-version.ts";
18
18
  import type { VersionCommand } from "../src/publish/publish.ts";
19
19
 
20
+ const roots: string[] = [];
21
+ afterEach(() => {
22
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
23
+ });
24
+
25
+ function track(dir: string): string {
26
+ roots.push(dir);
27
+ return dir;
28
+ }
29
+
20
30
  class NoopRegistry implements Registry {
21
31
  async search(): Promise<SearchPage> {
22
32
  return { results: [], total: 0 };
@@ -288,8 +298,8 @@ describe("pi.status operation", () => {
288
298
  reg: new NoopRegistry(),
289
299
  inst: new NoopInstaller(),
290
300
  token: "test-token",
291
- stateDir: mkdtempSync(join(tmpdir(), "packed-pi-version-state-")),
292
- dataDir: mkdtempSync(join(tmpdir(), "packed-pi-version-data-")),
301
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-pi-version-state-"))),
302
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-pi-version-data-"))),
293
303
  piVersion: { check: async () => ({ current: "0.82.1", latest: "0.83.0", upToDate: false }) },
294
304
  });
295
305
  const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -305,8 +315,8 @@ describe("pi.status operation", () => {
305
315
  reg: new NoopRegistry(),
306
316
  inst: new NoopInstaller(),
307
317
  token: "test-token",
308
- stateDir: mkdtempSync(join(tmpdir(), "packed-pi-version-state-")),
309
- dataDir: mkdtempSync(join(tmpdir(), "packed-pi-version-data-")),
318
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-pi-version-state-"))),
319
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-pi-version-data-"))),
310
320
  piVersion: { check: async () => ({}) },
311
321
  });
312
322
  const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -1,9 +1,19 @@
1
- import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { describe, expect, it } from "bun:test";
4
+ import { afterEach, describe, expect, it } from "bun:test";
5
5
  import { ensureClient, resolvePiBinForSpawn } from "../src/public/client.ts";
6
6
 
7
+ const roots: string[] = [];
8
+ afterEach(() => {
9
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
10
+ });
11
+
12
+ function track(dir: string): string {
13
+ roots.push(dir);
14
+ return dir;
15
+ }
16
+
7
17
  describe("ensureClient (packed daemon auto-spawn-or-wait decision)", () => {
8
18
  it("connects immediately without ever checking for a service or spawning, when already reachable", async () => {
9
19
  let spawnCalls = 0;
@@ -150,12 +160,12 @@ describe("resolvePiBinForSpawn", () => {
150
160
  });
151
161
 
152
162
  it("returns undefined when no PATH directory has an executable `pi`", () => {
153
- const dir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-"));
163
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-")));
154
164
  expect(resolvePiBinForSpawn({ PATH: dir })).toBeUndefined();
155
165
  });
156
166
 
157
167
  it("resolves the absolute path to an executable `pi` on PATH", () => {
158
- const dir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-"));
168
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-")));
159
169
  const piPath = join(dir, "pi");
160
170
  writeFileSync(piPath, "#!/bin/sh\necho pi\n");
161
171
  chmodSync(piPath, 0o755);
@@ -163,8 +173,8 @@ describe("resolvePiBinForSpawn", () => {
163
173
  });
164
174
 
165
175
  it("skips a non-executable `pi` earlier on PATH and resolves a later one", () => {
166
- const deadDir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-dead-"));
167
- const liveDir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-live-"));
176
+ const deadDir = track(mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-dead-")));
177
+ const liveDir = track(mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-live-")));
168
178
  writeFileSync(join(deadDir, "pi"), "not executable");
169
179
  chmodSync(join(deadDir, "pi"), 0o644);
170
180
  const livePi = join(liveDir, "pi");
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "bun:test";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -32,8 +32,18 @@ class RegistryFixture implements Registry {
32
32
  }
33
33
  }
34
34
 
35
+ const roots: string[] = [];
36
+ afterEach(() => {
37
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
38
+ });
39
+
40
+ function track(dir: string): string {
41
+ roots.push(dir);
42
+ return dir;
43
+ }
44
+
35
45
  function project(overrides: Record<string, unknown> = {}): string {
36
- const root = mkdtempSync(join(tmpdir(), "packed-publish-"));
46
+ const root = track(mkdtempSync(join(tmpdir(), "packed-publish-")));
37
47
  writeFileSync(
38
48
  join(root, "package.json"),
39
49
  JSON.stringify({
@@ -84,7 +94,7 @@ class MultiRegistryFixture implements Registry {
84
94
  /** A two-package Bun workspace: packages/core (published, depended on) and
85
95
  * packages/ext (the one under test, declaring a dependency on core). */
86
96
  function workspace(extDependencyRange = "^1.0.0"): { root: string; corePath: string; extPath: string } {
87
- const root = mkdtempSync(join(tmpdir(), "packed-workspace-"));
97
+ const root = track(mkdtempSync(join(tmpdir(), "packed-workspace-")));
88
98
  writeFileSync(join(root, "package.json"), JSON.stringify({ name: "demo-workspace", private: true, workspaces: ["packages/*"] }));
89
99
  writeFileSync(join(root, "bun.lock"), "{}");
90
100
  const corePath = join(root, "packages", "core");
@@ -1,5 +1,5 @@
1
1
  import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
- import { mkdtempSync } from "node:fs";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { Server } from "bun";
@@ -86,6 +86,14 @@ describe("HttpRegistry vs DaemonRegistry", () => {
86
86
  let httpServer: Server<undefined>;
87
87
  let daemonServer: Server<undefined>;
88
88
  const daemonToken = "c".repeat(64);
89
+ // createApp() is invoked fresh on every request below, so this can grow
90
+ // past one entry -- every one of them still needs cleanup.
91
+ const stateDirs: string[] = [];
92
+ function trackedStateDir(): string {
93
+ const dir = mkdtempSync(join(tmpdir(), "packed-registry-contract-"));
94
+ stateDirs.push(dir);
95
+ return dir;
96
+ }
89
97
 
90
98
  beforeAll(() => {
91
99
  httpServer = Bun.serve({
@@ -110,13 +118,14 @@ describe("HttpRegistry vs DaemonRegistry", () => {
110
118
  reg: new InMemoryRegistry(),
111
119
  inst: new NoopInstaller(),
112
120
  token: daemonToken,
113
- stateDir: mkdtempSync(join(tmpdir(), "packed-registry-contract-")),
121
+ stateDir: trackedStateDir(),
114
122
  }).fetch(req),
115
123
  });
116
124
  });
117
125
  afterAll(() => {
118
126
  httpServer.stop(true);
119
127
  daemonServer.stop(true);
128
+ for (const dir of stateDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
120
129
  });
121
130
 
122
131
  registryContract("HttpRegistry (real Bun.serve npm-mock)", () => new HttpRegistry(`http://127.0.0.1:${httpServer.port}`, 2, 0, 1_000));
@@ -12,6 +12,11 @@ afterEach(() => {
12
12
  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
13
13
  });
14
14
 
15
+ function track(dir: string): string {
16
+ roots.push(dir);
17
+ return dir;
18
+ }
19
+
15
20
  function piHome(settingsPackages: unknown[]): string {
16
21
  const root = mkdtempSync(join(tmpdir(), "packed-resources-"));
17
22
  roots.push(root);
@@ -149,8 +154,8 @@ function rpcClient(piHome: string) {
149
154
  reg: new NoopRegistry(),
150
155
  inst: new NoopInstaller(),
151
156
  token: "test-token",
152
- stateDir: mkdtempSync(join(tmpdir(), "packed-resources-state-")),
153
- dataDir: mkdtempSync(join(tmpdir(), "packed-resources-data-")),
157
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-resources-state-"))),
158
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-resources-data-"))),
154
159
  piHome,
155
160
  });
156
161
  return new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -1,12 +1,22 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { mkdtempSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { PACKAGE_OPERATIONS, packagePermissionDecision, readSecuritySettings, writeSecuritySettings } from "../src/security/security.ts";
6
6
 
7
+ const roots: string[] = [];
8
+ afterEach(() => {
9
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
10
+ });
11
+
12
+ function track(dir: string): string {
13
+ roots.push(dir);
14
+ return dir;
15
+ }
16
+
7
17
  describe("package permission policy", () => {
8
18
  it("defaults every arbitrary-code and settings/install-root mutation to approval", () => {
9
- const dir = mkdtempSync(join(tmpdir(), "packed-security-"));
19
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-security-")));
10
20
  const settings = readSecuritySettings(dir);
11
21
  expect(settings).toEqual({ mutationApproval: "always" });
12
22
  expect(PACKAGE_OPERATIONS).toEqual([
@@ -72,11 +82,11 @@ describe("package permission policy", () => {
72
82
  });
73
83
 
74
84
  it("persists an explicit unsafe opt-out and migrates the prior storage key", async () => {
75
- const dir = mkdtempSync(join(tmpdir(), "packed-security-"));
85
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-security-")));
76
86
  expect(await writeSecuritySettings(dir, { mutationApproval: "never" })).toEqual({ mutationApproval: "never" });
77
87
  expect(readSecuritySettings(dir)).toEqual({ mutationApproval: "never" });
78
88
 
79
- const legacyDir = mkdtempSync(join(tmpdir(), "packed-security-legacy-"));
89
+ const legacyDir = track(mkdtempSync(join(tmpdir(), "packed-security-legacy-")));
80
90
  writeFileSync(join(legacyDir, "security.json"), '{"installApproval":"never"}\n');
81
91
  expect(readSecuritySettings(legacyDir)).toEqual({ mutationApproval: "never" });
82
92
  });
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { ServiceSpec } from "@danypops/vehicle-server/service";
@@ -105,12 +105,22 @@ class FakeDaemonServiceInstaller implements DaemonServiceInstaller {
105
105
  }
106
106
  }
107
107
 
108
+ const roots: string[] = [];
109
+ afterEach(() => {
110
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
111
+ });
112
+
113
+ function track(dir: string): string {
114
+ roots.push(dir);
115
+ return dir;
116
+ }
117
+
108
118
  function deps(over: Partial<Deps> = {}): Deps {
109
119
  return {
110
120
  reg: new FakeRegistry(),
111
121
  inst: new FakeInstaller(),
112
122
  token: "test-token",
113
- stateDir: mkdtempSync(join(tmpdir(), "packed-")),
123
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-"))),
114
124
  ...over,
115
125
  };
116
126
  }
@@ -272,7 +282,7 @@ describe("service app", () => {
272
282
 
273
283
  it("POST /install configures an npm package's persistent Vehicle under the same approval", async () => {
274
284
  const svc = new FakeDaemonServiceInstaller();
275
- const piHome = mkdtempSync(join(tmpdir(), "packed-install-vehicle-"));
285
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-install-vehicle-")));
276
286
  const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
277
287
  const response = await app.fetch(
278
288
  new Request("http://x/install", {
@@ -328,7 +338,7 @@ describe("service app", () => {
328
338
 
329
339
  it("POST /install-service installs a real service once approved, reporting the resolved spec", async () => {
330
340
  const svc = new FakeDaemonServiceInstaller();
331
- const piHome = mkdtempSync(join(tmpdir(), "packed-pi-"));
341
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-")));
332
342
  const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
333
343
 
334
344
  const res = await app.fetch(
@@ -406,7 +416,7 @@ describe("service app", () => {
406
416
 
407
417
  it("POST /restart-service restarts a real service once approved, reporting the resolved spec", async () => {
408
418
  const svc = new FakeDaemonServiceInstaller();
409
- const piHome = mkdtempSync(join(tmpdir(), "packed-pi-"));
419
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-")));
410
420
  const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
411
421
 
412
422
  const res = await app.fetch(
@@ -492,7 +502,7 @@ describe("service app", () => {
492
502
 
493
503
  it("POST /update reconciles an installed Vehicle after a real package change", async () => {
494
504
  const svc = new FakeDaemonServiceInstaller();
495
- const piHome = mkdtempSync(join(tmpdir(), "packed-update-vehicle-"));
505
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-update-vehicle-")));
496
506
  const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
497
507
  const response = await app.fetch(
498
508
  new Request("http://x/update", {
@@ -542,7 +552,7 @@ describe("service app", () => {
542
552
  });
543
553
 
544
554
  it("GET /installed lists packages from pi settings", async () => {
545
- const piHome = mkdtempSync(join(tmpdir(), "packed-pi-"));
555
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-")));
546
556
  writeFileSync(
547
557
  join(piHome, "settings.json"),
548
558
  JSON.stringify({ packages: ["npm:pi-extension-manager@0.8.2", { source: "npm:obj@2.0.0" }] }),
@@ -582,7 +592,7 @@ describe("service app", () => {
582
592
  });
583
593
 
584
594
  it("POST /remove removes declared Vehicle state before deleting the package", async () => {
585
- const piHome = mkdtempSync(join(tmpdir(), "packed-remove-vehicle-"));
595
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-remove-vehicle-")));
586
596
  const packageDir = join(piHome, "npm", "node_modules", "probe");
587
597
  mkdirSync(packageDir, { recursive: true });
588
598
  writeFileSync(