@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
@@ -1,6 +1,6 @@
1
- import { afterAll, beforeAll, describe, expect, it } from "bun:test";
1
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { writeDaemonHandle } from "@danypops/vehicle-server/paths";
@@ -125,6 +125,16 @@ class FakeDaemonServiceInstaller {
125
125
  }
126
126
  }
127
127
 
128
+ const roots: string[] = [];
129
+ afterEach(() => {
130
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
131
+ });
132
+
133
+ function track(dir: string): string {
134
+ roots.push(dir);
135
+ return dir;
136
+ }
137
+
128
138
  function deps(over: Partial<CliDeps> = {}): CliDeps {
129
139
  return {
130
140
  reg: new FakeRegistry(),
@@ -138,8 +148,8 @@ function deps(over: Partial<CliDeps> = {}): CliDeps {
138
148
  return { mutationApproval };
139
149
  },
140
150
  },
141
- stateDir: mkdtempSync(join(tmpdir(), "packed-")),
142
- piHome: mkdtempSync(join(tmpdir(), "packed-pihome-")),
151
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-"))),
152
+ piHome: track(mkdtempSync(join(tmpdir(), "packed-pihome-"))),
143
153
  ...over,
144
154
  };
145
155
  }
@@ -389,7 +399,7 @@ describe("CLI", () => {
389
399
  it("updates --project also checks a project's own .pi/settings.json pins -- global-only misses them entirely", async () => {
390
400
  const d = deps();
391
401
  writeFileSync(join(d.piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-global@1.0.0"] }));
392
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-project-"));
402
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-project-")));
393
403
  const projectHome = join(projectRoot, ".pi");
394
404
  mkdirSync(projectHome, { recursive: true });
395
405
  writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus@0.21.2"] }));
@@ -938,7 +948,7 @@ describe("CLI", () => {
938
948
  });
939
949
 
940
950
  it("advisories runs standalone without a daemon and degrades to zero findings, never a real network call, when nothing is installed", async () => {
941
- const d = deps({ piHome: mkdtempSync(join(tmpdir(), "packed-advisories-cli-")) });
951
+ const d = deps({ piHome: track(mkdtempSync(join(tmpdir(), "packed-advisories-cli-"))) });
942
952
  const result = await cliRun(["advisories", "--json"], d);
943
953
  expect(result.code).toBe(0);
944
954
  expect(JSON.parse(result.out)).toEqual({ scanned: 0, findings: [], diagnostics: [], truncated: false });
@@ -956,7 +966,7 @@ describe("CLI", () => {
956
966
  });
957
967
 
958
968
  it("resources list and toggle run standalone without a daemon (CLI parity for the daemon-only resources.list/toggle operations)", async () => {
959
- const piHome = mkdtempSync(join(tmpdir(), "packed-resources-cli-"));
969
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-resources-cli-")));
960
970
  writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-demo"] }));
961
971
  const pkgDir = join(piHome, "npm", "node_modules", "pi-demo");
962
972
  mkdirSync(pkgDir, { recursive: true });
@@ -998,7 +1008,7 @@ describe("CLI", () => {
998
1008
  (bwrapUsable ? it : it.skip)(
999
1009
  "doctor runs standalone without a daemon and reproduces the jittor incident through the real CLI (CLI parity for the daemon-only doctor.run operation)",
1000
1010
  async () => {
1001
- const piHome = mkdtempSync(join(tmpdir(), "packed-doctor-cli-"));
1011
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-doctor-cli-")));
1002
1012
  writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-papyrus"] }));
1003
1013
  const globalPkg = join(piHome, "npm", "node_modules", "pi-papyrus");
1004
1014
  mkdirSync(join(globalPkg, "extension"), { recursive: true });
@@ -1007,7 +1017,7 @@ describe("CLI", () => {
1007
1017
  JSON.stringify({ name: "pi-papyrus", version: "1.0.0", pi: { extensions: ["extension/index.ts"] } }),
1008
1018
  );
1009
1019
  writeFileSync(join(globalPkg, "extension", "index.ts"), 'export default function (pi: any) { pi.registerTool({ name: "tasks" }); }');
1010
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-doctor-cli-project-"));
1020
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-doctor-cli-project-")));
1011
1021
  const projectHome = join(projectRoot, ".pi");
1012
1022
  mkdirSync(projectHome, { recursive: true });
1013
1023
  writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus"] }));
@@ -1063,12 +1073,14 @@ describe("CLI", () => {
1063
1073
  describe("daemon client", () => {
1064
1074
  let server: Server<undefined>;
1065
1075
  let daemonDir: string;
1076
+ let daemonPiHome: string;
1066
1077
  let daemonPaths: PackedPaths;
1067
1078
  let daemonInstaller: FakeInstaller;
1068
1079
  const daemonToken = "d".repeat(64);
1069
1080
 
1070
1081
  beforeAll(async () => {
1071
1082
  daemonDir = mkdtempSync(join(tmpdir(), "packed-daemon-"));
1083
+ daemonPiHome = mkdtempSync(join(tmpdir(), "packed-daemon-pi-"));
1072
1084
  daemonPaths = resolvePackedPaths({ env: { PI_PACKED_HOME: daemonDir } });
1073
1085
  writeFileSync(daemonPaths.token, `${daemonToken}\n`);
1074
1086
  daemonInstaller = new FakeInstaller();
@@ -1100,7 +1112,7 @@ describe("daemon client", () => {
1100
1112
  },
1101
1113
  token: daemonToken,
1102
1114
  stateDir: daemonDir,
1103
- piHome: mkdtempSync(join(tmpdir(), "packed-daemon-pi-")),
1115
+ piHome: daemonPiHome,
1104
1116
  packer: {
1105
1117
  async verify(path) {
1106
1118
  return {
@@ -1169,7 +1181,11 @@ describe("daemon client", () => {
1169
1181
  server = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: (req) => app.fetch(req) });
1170
1182
  writeDaemonHandle(daemonPaths.handle, { host: "127.0.0.1", port: server.port!, pid: process.pid });
1171
1183
  });
1172
- afterAll(() => server.stop(true));
1184
+ afterAll(() => {
1185
+ server.stop(true);
1186
+ rmSync(daemonDir, { recursive: true, force: true });
1187
+ rmSync(daemonPiHome, { recursive: true, force: true });
1188
+ });
1173
1189
 
1174
1190
  it("probe finds a live daemon", async () => {
1175
1191
  const found = await probe(daemonPaths);
@@ -1188,7 +1204,7 @@ describe("daemon client", () => {
1188
1204
  });
1189
1205
 
1190
1206
  it("probe rejects dead state", async () => {
1191
- const directory = mkdtempSync(join(tmpdir(), "packed-"));
1207
+ const directory = track(mkdtempSync(join(tmpdir(), "packed-")));
1192
1208
  expect(await probe(resolvePackedPaths({ env: { PI_PACKED_HOME: directory } }))).toBeUndefined();
1193
1209
  });
1194
1210
 
@@ -1260,7 +1276,7 @@ describe("daemon client", () => {
1260
1276
  it("resolveRegistry prefers daemon, falls back direct", async () => {
1261
1277
  const viaDaemon = await resolveRegistry(daemonPaths, "https://registry.npmjs.org");
1262
1278
  expect(viaDaemon).toBeInstanceOf(DaemonRegistry);
1263
- const directory = mkdtempSync(join(tmpdir(), "packed-"));
1279
+ const directory = track(mkdtempSync(join(tmpdir(), "packed-")));
1264
1280
  const direct = await resolveRegistry(resolvePackedPaths({ env: { PI_PACKED_HOME: directory } }), "https://registry.npmjs.org");
1265
1281
  expect(direct).toBeInstanceOf(HttpRegistry);
1266
1282
  });
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { existsSync, mkdtempSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { syncCatalog } from "../src/packages/catalog.ts";
@@ -127,9 +127,15 @@ class PagedRegistry implements Registry {
127
127
  }
128
128
  }
129
129
 
130
+ const roots: string[] = [];
131
+ afterEach(() => {
132
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
133
+ });
134
+
130
135
  describe("syncCatalog → SQLite", () => {
131
136
  it("accumulates pages into the DB and records sync meta", async () => {
132
137
  const dir = mkdtempSync(join(tmpdir(), "packed-"));
138
+ roots.push(dir);
133
139
  const reg = new PagedRegistry({ 0: PKGS.slice(0, 2), 2: PKGS.slice(2) }, 3);
134
140
  expect(await syncCatalog(reg, dir)).toBe(3);
135
141
  expect(existsSync(dbPath(dir))).toBe(true);
@@ -13,6 +13,11 @@ afterEach(() => {
13
13
  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
14
14
  });
15
15
 
16
+ function track(dir: string): string {
17
+ roots.push(dir);
18
+ return dir;
19
+ }
20
+
16
21
  // Same sandbox-availability probe as smoke.test.ts: binary presence alone
17
22
  // doesn't prove bwrap actually works under this host's user namespaces.
18
23
  function bwrapUsable(): boolean {
@@ -216,8 +221,8 @@ describeIfSandboxed("doctor.run (daemon RPC wiring)", () => {
216
221
  reg: new NoopRegistry(),
217
222
  inst: new NoopInstaller(),
218
223
  token: "test-token",
219
- stateDir: mkdtempSync(join(tmpdir(), "packed-doctor-state-")),
220
- dataDir: mkdtempSync(join(tmpdir(), "packed-doctor-data-")),
224
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-doctor-state-"))),
225
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-doctor-data-"))),
221
226
  piHome: home,
222
227
  });
223
228
  const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -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 { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
@@ -17,8 +17,18 @@ import {
17
17
  } from "../src/packages/installed.ts";
18
18
  import type { Installer, PkgInfo, Registry, SearchPage, UpdateOutcome, UpdatesSnapshot } from "../src/packages/package.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
  function writePiHome(settings: unknown, nodeModules: Record<string, string> = {}): string {
21
- const dir = mkdtempSync(join(tmpdir(), "packed-pihome-"));
31
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-pihome-")));
22
32
  writeFileSync(join(dir, "settings.json"), JSON.stringify(settings));
23
33
  for (const [name, version] of Object.entries(nodeModules)) {
24
34
  const pkgDir = join(dir, "npm", "node_modules", name);
@@ -99,7 +109,7 @@ describe("readInstalledPackages", () => {
99
109
  });
100
110
 
101
111
  it("missing settings → empty", () => {
102
- expect(readInstalledPackages(mkdtempSync(join(tmpdir(), "packed-")))).toEqual([]);
112
+ expect(readInstalledPackages(track(mkdtempSync(join(tmpdir(), "packed-"))))).toEqual([]);
103
113
  });
104
114
  });
105
115
 
@@ -113,7 +123,7 @@ describe("readInstalledPackagesAcrossScopes", () => {
113
123
 
114
124
  it("reproduces the real jittor gap: a stale project-scoped pin is invisible to a global-only read, visible once project scope is included", () => {
115
125
  const home = writePiHome({ packages: ["npm:pi-global@1.0.0"] });
116
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-project-"));
126
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-project-")));
117
127
  const projectHome = join(projectRoot, ".pi");
118
128
  mkdirSync(projectHome, { recursive: true });
119
129
  writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus@0.21.2"] }));
@@ -128,7 +138,7 @@ describe("readInstalledPackagesAcrossScopes", () => {
128
138
 
129
139
  it("is unaffected by a missing project settings file", () => {
130
140
  const home = writePiHome({ packages: ["npm:pi-global@1.0.0"] });
131
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-project-"));
141
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-project-")));
132
142
  expect(readInstalledPackagesAcrossScopes(home, projectRoot)).toEqual([
133
143
  { name: "pi-global", pinned: "1.0.0", installed: undefined, scope: "global" },
134
144
  ]);
@@ -214,13 +224,13 @@ class NoopInstaller implements Installer {
214
224
 
215
225
  describe("package.updates.project (daemon RPC wiring)", () => {
216
226
  it("computes a live, cross-scope drift check on demand, distinct from package.updates' own persisted global-only snapshot", async () => {
217
- const home = mkdtempSync(join(tmpdir(), "packed-updates-project-"));
227
+ const home = track(mkdtempSync(join(tmpdir(), "packed-updates-project-")));
218
228
  writeFileSync(join(home, "settings.json"), JSON.stringify({ packages: ["npm:pi-global@1.0.0"] }));
219
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-updates-project-root-"));
229
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-updates-project-root-")));
220
230
  const projectHome = join(projectRoot, ".pi");
221
231
  mkdirSync(projectHome, { recursive: true });
222
232
  writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus@0.21.2"] }));
223
- const stateDir = mkdtempSync(join(tmpdir(), "packed-updates-project-state-"));
233
+ const stateDir = track(mkdtempSync(join(tmpdir(), "packed-updates-project-state-")));
224
234
  const db = openDb(dbPath(stateDir));
225
235
  replaceAll(
226
236
  db,
@@ -249,7 +259,7 @@ describe("package.updates.project (daemon RPC wiring)", () => {
249
259
 
250
260
  describe("updates store", () => {
251
261
  it("roundtrips", async () => {
252
- const dir = mkdtempSync(join(tmpdir(), "packed-"));
262
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-")));
253
263
  const snap = { checkedAt: new Date().toISOString(), updates: [{ name: "a", installed: "1", latest: "2", detectedAt: "" }] };
254
264
  await saveUpdates(dir, snap);
255
265
  expect(await loadUpdates(dir)).toEqual(snap);
@@ -259,7 +269,7 @@ describe("updates store", () => {
259
269
 
260
270
  describe("watcher producer", () => {
261
271
  it("writes a snapshot on tick", async () => {
262
- const dir = mkdtempSync(join(tmpdir(), "packed-"));
272
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-")));
263
273
  let signalTick: ((snapshot: UpdatesSnapshot) => void) | undefined;
264
274
  const tick = new Promise<UpdatesSnapshot>((resolve) => {
265
275
  signalTick = resolve;
@@ -282,7 +292,7 @@ describe("watcher producer", () => {
282
292
 
283
293
  describe("catalog status", () => {
284
294
  it("stale when unsynced, fresh after sync", () => {
285
- const dir = mkdtempSync(join(tmpdir(), "packed-"));
295
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-")));
286
296
  expect(catalogStatus(dir, 6 * 3_600_000).stale).toBe(true);
287
297
  const db = openDb(`${dir}/packed.db`);
288
298
  replaceAll(db, [{ name: "a", version: "1" }], "test");
@@ -1,5 +1,5 @@
1
- import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, readFileSync } from "node:fs";
1
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, readFileSync, 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";
@@ -7,6 +7,16 @@ import { buildIndex, generateIndex, indexPath, indexStatus, readIndex, writeInde
7
7
  import { dbPath, openDb, replaceAll } from "../src/packages/db.ts";
8
8
  import { HttpRegistry } from "../src/registry/registry.ts";
9
9
 
10
+ const roots: string[] = [];
11
+ afterEach(() => {
12
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
13
+ });
14
+
15
+ function track(dir: string): string {
16
+ roots.push(dir);
17
+ return dir;
18
+ }
19
+
10
20
  // Guards the "never bulk GitHub calls" constraint against regression: a
11
21
  // future edit could easily start threading a GitHub-commit fetcher through
12
22
  // buildIndex the same way scoreTarget does. A source-level check catches
@@ -89,7 +99,10 @@ describe("buildIndex", () => {
89
99
  registry = new HttpRegistry(`http://127.0.0.1:${server.port}`, 250, 0, 1, `http://127.0.0.1:${server.port}`);
90
100
  });
91
101
 
92
- afterAll(() => server.stop(true));
102
+ afterAll(() => {
103
+ server.stop(true);
104
+ rmSync(catalogDir, { recursive: true, force: true });
105
+ });
93
106
 
94
107
  it("builds one entry per cataloged package, skipping a lookup failure rather than failing the whole run", async () => {
95
108
  const index = await buildIndex(registry, catalogDir, { delayMs: 0, currentPiVersion: async () => "0.83.0" });
@@ -123,7 +136,7 @@ describe("buildIndex", () => {
123
136
 
124
137
  describe("buildIndex bounds", () => {
125
138
  it("never calls downloads() -- confirmed live to trigger npm's 429s at real catalog scale -- and truncates past maxPackages, marking the result", async () => {
126
- const dir = mkdtempSync(join(tmpdir(), "packed-index-bounds-"));
139
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-bounds-")));
127
140
  const db = openDb(dbPath(dir));
128
141
  replaceAll(
129
142
  db,
@@ -160,7 +173,7 @@ describe("buildIndex bounds", () => {
160
173
  });
161
174
 
162
175
  it("collapses two concurrent callers into one run -- confirmed live to matter: the daemon's own maintenance tick and an on-demand CLI build overlapping compounded into a shutdown the daemon's SIGTERM grace period couldn't outlast", async () => {
163
- const dir = mkdtempSync(join(tmpdir(), "packed-index-concurrent-"));
176
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-concurrent-")));
164
177
  const db = openDb(dbPath(dir));
165
178
  replaceAll(db, [{ name: "pi-one", version: "1.0.0" }], "test");
166
179
  db.close();
@@ -203,7 +216,7 @@ describe("buildIndex bounds", () => {
203
216
 
204
217
  describe("buildIndex incremental delta scanning", () => {
205
218
  it("partitions the catalog into New/Changed/Unchanged against the prior index -- Unchanged makes zero live registry calls, New and Changed do", async () => {
206
- const dir = mkdtempSync(join(tmpdir(), "packed-index-delta-"));
219
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-delta-")));
207
220
  const db = openDb(dbPath(dir));
208
221
  replaceAll(
209
222
  db,
@@ -275,7 +288,7 @@ describe("buildIndex incremental delta scanning", () => {
275
288
  });
276
289
 
277
290
  it("maxPackages bounds only the New+Changed live-call queue -- Unchanged entries beyond that count still complete", async () => {
278
- const dir = mkdtempSync(join(tmpdir(), "packed-index-delta-bound-"));
291
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-delta-bound-")));
279
292
  const db = openDb(dbPath(dir));
280
293
  // 5 unchanged entries alone already exceed maxPackages: 1 below.
281
294
  replaceAll(
@@ -323,7 +336,7 @@ describe("buildIndex incremental delta scanning", () => {
323
336
 
324
337
  describe("index persistence", () => {
325
338
  it("writes, reads, and reports staleness", async () => {
326
- const dir = mkdtempSync(join(tmpdir(), "packed-index-store-"));
339
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-store-")));
327
340
  const path = indexPath(dir);
328
341
  expect(readIndex(path)).toBeUndefined();
329
342
  expect(indexStatus(path, 1_000).stale).toBe(true);
@@ -343,7 +356,7 @@ describe("index persistence", () => {
343
356
  });
344
357
 
345
358
  it("generateIndex builds and writes in one call", async () => {
346
- const dir = mkdtempSync(join(tmpdir(), "packed-index-generate-"));
359
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-generate-")));
347
360
  const db = openDb(dbPath(dir));
348
361
  replaceAll(db, [{ name: "pi-alpha", version: "1.0.0" }], "npm:keywords:pi-package");
349
362
  db.close();
@@ -7,8 +7,8 @@
7
7
  * a broken fixture must genuinely be refused, a healthy one must genuinely
8
8
  * pass.
9
9
  */
10
- import { describe, expect, it } from "bun:test";
11
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
10
+ import { afterEach, describe, expect, it } from "bun:test";
11
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { dirname, join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
@@ -23,6 +23,16 @@ const BROKEN = join(FIXTURES, "broken-package");
23
23
  const NO_MANIFEST = join(FIXTURES, "no-manifest-package");
24
24
  const DEP_PACKAGE = join(FIXTURES, "dep-package");
25
25
 
26
+ const roots: string[] = [];
27
+ afterEach(() => {
28
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
29
+ });
30
+
31
+ function track(dir: string): string {
32
+ roots.push(dir);
33
+ return dir;
34
+ }
35
+
26
36
  /** Writes a fresh package into `dir` whose extension entry point has a real
27
37
  * runtime import (`packed-fixture-dep`, a `file:` dependency resolvable
28
38
  * fully offline) -- otherwise healthy, structurally identical to HEALTHY,
@@ -145,7 +155,7 @@ describe("HeadlessInstallValidator (vehicle-client-pi pi-load-harness, non-gatin
145
155
 
146
156
  describe("HeadlessInstallValidator (bug repro, packed-headlessinstallvalidator-never-installs-the): staged tarball's own declared dependencies are never installed before the load check", () => {
147
157
  it("approves an otherwise-healthy package whose entry point needs its one declared file: dependency", async () => {
148
- const dir = mkdtempSync(join(tmpdir(), "packed-install-validation-dep-fixture-"));
158
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-install-validation-dep-fixture-")));
149
159
  writePackageWithRealDependency(dir);
150
160
 
151
161
  // Ground truth this isn't a broken fixture: a plain `bun install` in an
@@ -193,7 +203,7 @@ describe("ExecInstaller.install() -- refuses before ever spawning the real pi bi
193
203
  // step to cwd into -- /bin/true always exits 0 regardless, proving both
194
204
  // the real install spawn *and* the re-resolution spawn were reached (a
195
205
  // refused install never gets this far to find out).
196
- const piHome = mkdtempSync(join(tmpdir(), "packed-install-validation-pihome-"));
206
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-install-validation-pihome-")));
197
207
  mkdirSync(join(piHome, "npm"), { recursive: true });
198
208
  const installer = new ExecInstaller(
199
209
  "/bin/true",
@@ -10,20 +10,40 @@
10
10
  * always-"Updated"-regardless-of-outcome text, and assert the real on-disk
11
11
  * version diff is what actually drives reloadRequired/alreadyUpToDate.
12
12
  */
13
- import { describe, expect, it } from "bun:test";
14
- import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { afterEach, describe, expect, it } from "bun:test";
14
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
15
15
  import { tmpdir } from "node:os";
16
16
  import { join } from "node:path";
17
17
  import { ExecInstaller } from "../src/packages/install.ts";
18
+ import { createLogger } from "../src/shared/log.ts";
18
19
 
19
20
  /**
20
21
  * A fake `pi` binary: always prints "Updated <source>" and exits 0 (matching
21
22
  * real `pi`'s observed behavior for a no-op). When `rewrite` is given, it
22
23
  * additionally overwrites that exact package's on-disk version -- letting a
23
24
  * test simulate a genuine version change on demand. The rewrite target is
24
- * baked into the script file itself (not an env var) so it is immune to
25
- * Bun.spawn's default env snapshot not picking up late process.env writes.
25
+ * baked into the script file itself (not an env var) rather than read from
26
+ * process.env at spawn time, keeping these fixtures independent of whichever
27
+ * way ExecInstaller happens to thread its own env through -- see the
28
+ * `run()`/`reresolveDependencyTree() thread env explicitly` describe block
29
+ * below for a dedicated test of that env-threading behavior itself, added
30
+ * after service/test/perf/multi-install.perf.test.ts caught it live: a
31
+ * runtime process.env mutation (redirecting Pi's home directory) silently
32
+ * never reached the spawned `pi`/`npm` children because Bun.spawn's own
33
+ * default env inheritance doesn't pick up a mutation made after this
34
+ * process's own startup snapshot -- only an explicit `env: process.env`
35
+ * option re-reads the current object.
26
36
  */
37
+ const roots: string[] = [];
38
+ afterEach(() => {
39
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
40
+ });
41
+
42
+ function track(dir: string): string {
43
+ roots.push(dir);
44
+ return dir;
45
+ }
46
+
27
47
  function writeFakePi(dir: string, rewrite?: { piHome: string; name: string; newVersion: string }): string {
28
48
  const script = join(dir, "fake-pi");
29
49
  const rewriteLine = rewrite
@@ -39,7 +59,7 @@ function writeFakePi(dir: string, rewrite?: { piHome: string; name: string; newV
39
59
  }
40
60
 
41
61
  function writePiHome(nodeModules: Record<string, string> = {}): string {
42
- const dir = mkdtempSync(join(tmpdir(), "packed-exec-pihome-"));
62
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-exec-pihome-")));
43
63
  mkdirSync(join(dir, "npm"), { recursive: true });
44
64
  for (const [name, version] of Object.entries(nodeModules)) {
45
65
  const pkgDir = join(dir, "npm", "node_modules", name);
@@ -73,7 +93,7 @@ function writeFakeNpm(dir: string, logFile: string, rewrite?: { piHome: string;
73
93
 
74
94
  describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguous exit-0 text", () => {
75
95
  it("pinned source, version genuinely unchanged: alreadyUpToDate, reloadRequired false", async () => {
76
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
96
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
77
97
  const bin = writeFakePi(scriptDir);
78
98
  const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
79
99
  const piHome = writePiHome({ "@scope/pkg": "1.2.3" });
@@ -90,7 +110,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
90
110
  });
91
111
 
92
112
  it('unpinned source, already latest (pi still exits 0 and says "Updated"): alreadyUpToDate', async () => {
93
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
113
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
94
114
  const bin = writeFakePi(scriptDir);
95
115
  const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
96
116
  const piHome = writePiHome({ plain: "0.5.0" });
@@ -106,7 +126,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
106
126
  });
107
127
 
108
128
  it("unpinned source, a real version change happens: reloadRequired true", async () => {
109
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
129
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
110
130
  const piHome = writePiHome({ plain: "0.5.0" });
111
131
  const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
112
132
  const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
@@ -122,7 +142,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
122
142
  });
123
143
 
124
144
  it("git: source (no npm resolution possible either side): conservatively assumes it may have changed", async () => {
125
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
145
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
126
146
  const bin = writeFakePi(scriptDir);
127
147
  const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
128
148
  const piHome = writePiHome();
@@ -141,7 +161,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
141
161
 
142
162
  describe("ExecInstaller — forces full dependency re-resolution, not just the target's own subtree", () => {
143
163
  it("update() fixes a stale root-level sibling that the targeted pi update never touched", async () => {
144
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
164
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
145
165
  // Simulates the confirmed live defect: after `pi update npm:@scope/leaf`,
146
166
  // the leaf's own version bumps, but a root-level sibling
147
167
  // (@scope/shared) that the freshly-updated leaf now needs a newer
@@ -170,7 +190,7 @@ describe("ExecInstaller — forces full dependency re-resolution, not just the t
170
190
  });
171
191
 
172
192
  it("install() also forces a full re-resolution after a successful pi install", async () => {
173
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
193
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
174
194
  const piHome = writePiHome({ "@scope/shared": "1.0.0" });
175
195
  const bin = writeFakePi(scriptDir);
176
196
  const npmLog = join(scriptDir, "npm.log");
@@ -185,7 +205,7 @@ describe("ExecInstaller — forces full dependency re-resolution, not just the t
185
205
  });
186
206
 
187
207
  it("surfaces a failed re-resolution instead of silently reporting success", async () => {
188
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
208
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
189
209
  const piHome = writePiHome({ plain: "0.5.0" });
190
210
  const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
191
211
  const failingNpm = join(scriptDir, "fake-npm-fail");
@@ -196,3 +216,100 @@ describe("ExecInstaller — forces full dependency re-resolution, not just the t
196
216
  await expect(installer.update("npm:plain")).rejects.toThrow(/npm install failed to re-resolve/);
197
217
  });
198
218
  });
219
+
220
+ describe("ExecInstaller — timing instrumentation (see service/test/perf/multi-install.perf.test.ts for a real multi-package measurement)", () => {
221
+ it("install() logs a validateMs/installMs/reresolveMs/totalMs breakdown, not just a pass/fail result", async () => {
222
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
223
+ const bin = writeFakePi(scriptDir);
224
+ const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
225
+ const piHome = writePiHome();
226
+ const lines: string[] = [];
227
+ const logger = createLogger("test", (line) => lines.push(line), "debug");
228
+ const installer = new ExecInstaller(bin, piHome, { validate: async (source) => ({ ok: true, source, extensions: [] }) }, npmBin, logger);
229
+
230
+ await installer.install("npm:plain");
231
+
232
+ const timing = lines.map((line) => JSON.parse(line)).find((entry) => entry.msg === "install timing");
233
+ expect(timing).toBeDefined();
234
+ expect(timing.source).toBe("npm:plain");
235
+ for (const field of ["validateMs", "installMs", "reresolveMs", "totalMs"]) {
236
+ expect(typeof timing[field]).toBe("number");
237
+ expect(timing[field]).toBeGreaterThanOrEqual(0);
238
+ }
239
+ // totalMs is the whole call's own wall clock, not just one phase re-labeled --
240
+ // bounded above by itself plus a small scheduling-noise allowance, never equal to
241
+ // a single phase alone once every phase is genuinely counted once.
242
+ expect(timing.totalMs).toBeGreaterThanOrEqual(timing.validateMs + timing.installMs + timing.reresolveMs - 1);
243
+ });
244
+
245
+ it("update() logs an updateMs/reresolveMs/totalMs breakdown", async () => {
246
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
247
+ const bin = writeFakePi(scriptDir);
248
+ const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
249
+ const piHome = writePiHome({ plain: "0.5.0" });
250
+ const lines: string[] = [];
251
+ const logger = createLogger("test", (line) => lines.push(line), "debug");
252
+ const installer = new ExecInstaller(bin, piHome, undefined, npmBin, logger);
253
+
254
+ await installer.update("npm:plain");
255
+
256
+ const timing = lines.map((line) => JSON.parse(line)).find((entry) => entry.msg === "update timing");
257
+ expect(timing).toBeDefined();
258
+ expect(timing.source).toBe("npm:plain");
259
+ for (const field of ["updateMs", "reresolveMs", "totalMs"]) {
260
+ expect(typeof timing[field]).toBe("number");
261
+ expect(timing[field]).toBeGreaterThanOrEqual(0);
262
+ }
263
+ });
264
+ });
265
+
266
+ /**
267
+ * A fake binary that dumps one specific env var's CURRENT value to `logFile` -- proves a
268
+ * process.env mutation made at runtime (after this test process's own startup, the exact shape
269
+ * of a caller redirecting Pi's home directory) actually reaches the spawned child, rather than
270
+ * whatever snapshot Bun.spawn's own default env inheritance captured earlier.
271
+ */
272
+ function writeEnvDumpBinary(dir: string, name: string, varName: string, logFile: string): string {
273
+ const script = join(dir, name);
274
+ writeFileSync(script, ["#!/usr/bin/env bash", `printf '%s' "\$${varName}" > '${logFile}'`, "exit 0"].join("\n"));
275
+ chmodSync(script, 0o755);
276
+ return script;
277
+ }
278
+
279
+ describe("ExecInstaller — run()/reresolveDependencyTree() thread the CURRENT process.env through explicitly", () => {
280
+ const MARKER = "PACKED_TEST_ENV_MARKER";
281
+
282
+ afterEach(() => {
283
+ delete process.env[MARKER];
284
+ });
285
+
286
+ it("install()'s pi spawn sees an env var set on process.env after this process already started", async () => {
287
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-env-")));
288
+ const piLog = join(scriptDir, "pi-env.log");
289
+ const bin = writeEnvDumpBinary(scriptDir, "fake-pi-env", MARKER, piLog);
290
+ const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
291
+ const piHome = writePiHome();
292
+ const installer = new ExecInstaller(bin, piHome, { validate: async (source) => ({ ok: true, source, extensions: [] }) }, npmBin);
293
+
294
+ // Mutated well after this test process's own startup -- exactly what redirecting Pi's home
295
+ // via PI_CODING_AGENT_DIR at runtime looks like from ExecInstaller's own point of view.
296
+ process.env[MARKER] = "set-after-startup";
297
+ await installer.install("npm:plain");
298
+
299
+ expect(readFileSync(piLog, "utf8")).toBe("set-after-startup");
300
+ });
301
+
302
+ it("reresolveDependencyTree()'s npm spawn sees the same runtime env mutation", async () => {
303
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-env-")));
304
+ const bin = writeFakePi(scriptDir);
305
+ const npmLog = join(scriptDir, "npm-env.log");
306
+ const npmBin = writeEnvDumpBinary(scriptDir, "fake-npm-env", MARKER, npmLog);
307
+ const piHome = writePiHome();
308
+ const installer = new ExecInstaller(bin, piHome, { validate: async (source) => ({ ok: true, source, extensions: [] }) }, npmBin);
309
+
310
+ process.env[MARKER] = "reresolve-sees-this-too";
311
+ await installer.install("npm:plain");
312
+
313
+ expect(readFileSync(npmLog, "utf8")).toBe("reresolve-sees-this-too");
314
+ });
315
+ });