@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.
- package/dist/client.d.ts +16 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.d.ts.map +1 -1
- package/extension/src/index.ts +9 -1
- package/extension/src/model.ts +35 -0
- package/extension/src/package-inspector.ts +12 -0
- package/extension/src/tabs/discover.ts +14 -1
- package/extension/src/tools.ts +31 -20
- package/extension/src/tui.ts +124 -33
- package/extension/src/vehicle-target.ts +24 -0
- package/extension/src/vehicle-tools.ts +84 -0
- package/package.json +2 -2
- package/service/src/adoption/install-validation.ts +19 -1
- package/service/src/daemon/service.ts +17 -1
- package/service/src/packages/install.ts +94 -13
- package/service/src/packages/package.ts +35 -0
- package/service/src/public/client.ts +29 -0
- package/service/src/public/protocol.ts +6 -0
- package/service/src/registry/registry.ts +20 -1
- package/service/src/setup/setup.ts +50 -2
- package/service/test/advisories.test.ts +17 -9
- package/service/test/cleanup.test.ts +7 -2
- package/service/test/cli.test.ts +29 -13
- package/service/test/db.test.ts +8 -2
- package/service/test/doctor.test.ts +7 -2
- package/service/test/domain.test.ts +22 -12
- package/service/test/index.test.ts +22 -9
- package/service/test/install-validation.test.ts +14 -4
- package/service/test/install.test.ts +129 -12
- package/service/test/npm-metadata-e2e.test.ts +25 -3
- package/service/test/pack-score.test.ts +8 -2
- package/service/test/perf/multi-install.perf.test.ts +222 -0
- package/service/test/pi-version.test.ts +15 -5
- package/service/test/public-client.test.ts +16 -6
- package/service/test/publish.test.ts +13 -3
- package/service/test/registry-contract.test.ts +11 -2
- package/service/test/resources.test.ts +7 -2
- package/service/test/security.test.ts +15 -5
- package/service/test/service.test.ts +19 -9
- 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
|
+
});
|