@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,7 +1,8 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
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) {
@@ -54,8 +102,18 @@ class GitFixture implements GitResolutionPort {
54
102
  }
55
103
  }
56
104
 
105
+ const roots: string[] = [];
106
+ afterEach(() => {
107
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
108
+ });
109
+
110
+ function track(dir: string): string {
111
+ roots.push(dir);
112
+ return dir;
113
+ }
114
+
57
115
  function piHome(withPackage = true): string {
58
- const root = mkdtempSync(join(tmpdir(), "packed-setup-home-"));
116
+ const root = track(mkdtempSync(join(tmpdir(), "packed-setup-home-")));
59
117
  const packages = withPackage ? ["npm:pi-demo"] : [];
60
118
  writeFileSync(join(root, "settings.json"), JSON.stringify({ packages }));
61
119
  if (withPackage) {
@@ -73,7 +131,7 @@ function piHome(withPackage = true): string {
73
131
  }
74
132
 
75
133
  function project(): string {
76
- const root = mkdtempSync(join(tmpdir(), "packed-setup-project-"));
134
+ const root = track(mkdtempSync(join(tmpdir(), "packed-setup-project-")));
77
135
  mkdirSync(join(root, ".pi"), { recursive: true });
78
136
  return root;
79
137
  }
@@ -373,3 +431,115 @@ describe("Pi setup manifest walking skeleton", () => {
373
431
  expect(existsSync(join(home, "profiles.json"))).toBe(false);
374
432
  });
375
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
+ });