@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|