@danypops/pi-packed 0.21.13 → 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.
@@ -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
+ });
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import type { InstallValidationResult } from "../src/adoption/install-validation.ts";
5
6
  import type { Installer, PkgInfo, Registry, SearchPage, UpdateOutcome } from "../src/packages/package.ts";
6
7
  import {
7
8
  bundledEcosystemManifestPath,
@@ -47,6 +48,53 @@ class InstallerFixture implements Installer {
47
48
  }
48
49
  }
49
50
 
51
+ /**
52
+ * Implements validate()/installOnly()/reresolveDependencyTree() (see Installer's own doc
53
+ * comment) in addition to the legacy install()/update()/remove() -- SetupManager.apply() only
54
+ * enters batch mode when all three are present. install() itself is kept real (not throwing) so
55
+ * a test can positively assert it's never called once the batch trio exists, rather than only
56
+ * ever seeing it absent from a call log.
57
+ */
58
+ class BatchInstallerFixture implements Installer {
59
+ events: string[] = [];
60
+ reresolveCalls = 0;
61
+ validateDelayMs = 0;
62
+ failValidate = new Set<string>();
63
+ failInstallOnly = new Set<string>();
64
+ failReresolve = false;
65
+
66
+ async install(source: string) {
67
+ this.events.push(`install:${source}`);
68
+ return `Installed ${source}`;
69
+ }
70
+ async update(source: string): Promise<UpdateOutcome> {
71
+ this.events.push(`update:${source}`);
72
+ return { output: `Updated ${source}`, reloadRequired: true, alreadyUpToDate: false, pinned: true };
73
+ }
74
+ async remove(source: string) {
75
+ this.events.push(`remove:${source}`);
76
+ return `Removed ${source}`;
77
+ }
78
+ async validate(source: string): Promise<InstallValidationResult> {
79
+ if (this.validateDelayMs > 0) await Bun.sleep(this.validateDelayMs);
80
+ this.events.push(`validate:${source}`);
81
+ return this.failValidate.has(source)
82
+ ? { ok: false, source, extensions: [], message: "validation refused" }
83
+ : { ok: true, source, extensions: [] };
84
+ }
85
+ async installOnly(source: string): Promise<string> {
86
+ this.events.push(`installOnly:${source}`);
87
+ if (this.failInstallOnly.has(source)) throw new Error(`installOnly failed for ${source}`);
88
+ return `Installed ${source}`;
89
+ }
90
+ async reresolveDependencyTree(): Promise<string> {
91
+ this.reresolveCalls++;
92
+ this.events.push("reresolve");
93
+ if (this.failReresolve) throw new Error("reresolve failed");
94
+ return "resolved";
95
+ }
96
+ }
97
+
50
98
  class GitFixture implements GitResolutionPort {
51
99
  constructor(private commit = "a".repeat(40)) {}
52
100
  async resolve(source: string) {
@@ -383,3 +431,115 @@ describe("Pi setup manifest walking skeleton", () => {
383
431
  expect(existsSync(join(home, "profiles.json"))).toBe(false);
384
432
  });
385
433
  });
434
+
435
+ function threePackageManifest(): SetupManifest {
436
+ return {
437
+ $schema: "./schema/pi-setup-v1.schema.json",
438
+ schemaVersion: 1,
439
+ packages: [
440
+ { kind: "npm", scope: "global", source: "npm:pkg-a@1.0.0", resolved: "1.0.0", integrity: "sha512-demo" },
441
+ { kind: "npm", scope: "global", source: "npm:pkg-b@1.0.0", resolved: "1.0.0", integrity: "sha512-demo" },
442
+ { kind: "npm", scope: "global", source: "npm:pkg-c@1.0.0", resolved: "1.0.0", integrity: "sha512-demo" },
443
+ ],
444
+ profiles: {},
445
+ };
446
+ }
447
+
448
+ describe("SetupManager.apply() batch mode -- validate() concurrently, installOnly() sequentially, reresolveDependencyTree() once", () => {
449
+ it("never calls the legacy install() once validate/installOnly/reresolveDependencyTree all exist", async () => {
450
+ const home = piHome(false);
451
+ const root = project();
452
+ writeFileSync(join(root, "pi-setup.json"), JSON.stringify(threePackageManifest()));
453
+ const installer = new BatchInstallerFixture();
454
+ const result = await new SetupManager(new RegistryFixture(), installer, home).apply(join(root, "pi-setup.json"));
455
+
456
+ expect(result.ok).toBe(true);
457
+ expect(installer.events.some((event) => event.startsWith("install:"))).toBe(false);
458
+ });
459
+
460
+ it("validates every package CONCURRENTLY, not one at a time -- wall clock proves overlap, not just call order", async () => {
461
+ const home = piHome(false);
462
+ const root = project();
463
+ writeFileSync(join(root, "pi-setup.json"), JSON.stringify(threePackageManifest()));
464
+ const installer = new BatchInstallerFixture();
465
+ installer.validateDelayMs = 50;
466
+ const manager = new SetupManager(new RegistryFixture(), installer, home);
467
+
468
+ const start = performance.now();
469
+ const result = await manager.apply(join(root, "pi-setup.json"));
470
+ const elapsedMs = performance.now() - start;
471
+
472
+ expect(result.ok).toBe(true);
473
+ // Three sequential 50ms validations would take >=150ms; three CONCURRENT ones take ~50ms
474
+ // plus scheduling noise. 100ms sits with a comfortable margin below the sequential floor
475
+ // without being tight enough to flake on a loaded CI box.
476
+ expect(elapsedMs).toBeLessThan(100);
477
+ });
478
+
479
+ it("validates every package before committing ANY of them, then commits sequentially, then reresolves exactly once", async () => {
480
+ const home = piHome(false);
481
+ const root = project();
482
+ writeFileSync(join(root, "pi-setup.json"), JSON.stringify(threePackageManifest()));
483
+ const installer = new BatchInstallerFixture();
484
+ const result = await new SetupManager(new RegistryFixture(), installer, home).apply(join(root, "pi-setup.json"));
485
+
486
+ expect(result.ok).toBe(true);
487
+ expect(installer.reresolveCalls).toBe(1);
488
+ const lastValidateIndex = installer.events.reduce((last, event, i) => (event.startsWith("validate:") ? i : last), -1);
489
+ const firstInstallOnlyIndex = installer.events.findIndex((event) => event.startsWith("installOnly:"));
490
+ expect(firstInstallOnlyIndex).toBeGreaterThan(lastValidateIndex);
491
+ // reresolve is genuinely last -- after every installOnly(), not interleaved between them.
492
+ expect(installer.events.at(-1)).toBe("reresolve");
493
+ expect(installer.events.filter((event) => event.startsWith("installOnly:"))).toHaveLength(3);
494
+ });
495
+
496
+ it("a validation failure stops the batch at that package -- earlier ones already committed, later ones never attempted, reresolve never runs", async () => {
497
+ const home = piHome(false);
498
+ const root = project();
499
+ writeFileSync(join(root, "pi-setup.json"), JSON.stringify(threePackageManifest()));
500
+ const installer = new BatchInstallerFixture();
501
+ installer.failValidate.add("npm:pkg-b@1.0.0");
502
+ const result = await new SetupManager(new RegistryFixture(), installer, home).apply(join(root, "pi-setup.json"));
503
+
504
+ expect(result.ok).toBe(false);
505
+ expect(result.diagnostics[0]?.code).toBe("SETUP_APPLY_FAILED");
506
+ expect(result.operations.map((op) => [op.target, op.status])).toEqual([
507
+ ["npm:pkg-a@1.0.0", "succeeded"],
508
+ ["npm:pkg-b@1.0.0", "failed"],
509
+ ]);
510
+ // Validation itself still runs concurrently for every package up front (pkg-c's validate
511
+ // call already happened before the commit loop reached pkg-b) -- what never happens is
512
+ // COMMITTING pkg-c once the loop hits pkg-b's own failure.
513
+ expect(installer.events).toContain("validate:npm:pkg-c@1.0.0");
514
+ expect(installer.events).not.toContain("installOnly:npm:pkg-c@1.0.0");
515
+ expect(installer.reresolveCalls).toBe(0);
516
+ });
517
+
518
+ it("an installOnly() failure (not a validation failure) also stops the batch and skips reresolve", async () => {
519
+ const home = piHome(false);
520
+ const root = project();
521
+ writeFileSync(join(root, "pi-setup.json"), JSON.stringify(threePackageManifest()));
522
+ const installer = new BatchInstallerFixture();
523
+ installer.failInstallOnly.add("npm:pkg-a@1.0.0");
524
+ const result = await new SetupManager(new RegistryFixture(), installer, home).apply(join(root, "pi-setup.json"));
525
+
526
+ expect(result.ok).toBe(false);
527
+ expect(result.operations.map((op) => op.status)).toEqual(["failed"]);
528
+ expect(installer.reresolveCalls).toBe(0);
529
+ });
530
+
531
+ it("reports a reresolveDependencyTree() failure distinctly, after every package already installed/updated successfully", async () => {
532
+ const home = piHome(false);
533
+ const root = project();
534
+ writeFileSync(join(root, "pi-setup.json"), JSON.stringify(threePackageManifest()));
535
+ const installer = new BatchInstallerFixture();
536
+ installer.failReresolve = true;
537
+ const result = await new SetupManager(new RegistryFixture(), installer, home).apply(join(root, "pi-setup.json"));
538
+
539
+ expect(result.ok).toBe(false);
540
+ expect(result.reloadRequired).toBe(true);
541
+ expect(result.operations.every((op) => op.status === "succeeded")).toBe(true);
542
+ expect(result.diagnostics[0]).toMatchObject({ code: "SETUP_APPLY_FAILED", path: "reresolveDependencyTree" });
543
+ expect(result.diagnostics[0]?.message).toContain("reresolve failed");
544
+ });
545
+ });