@danypops/pi-packed 0.21.12 → 0.21.13
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/extension/src/tui.ts +124 -33
- package/package.json +1 -1
- 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 +20 -10
- package/service/test/pack-score.test.ts +8 -2
- 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 +14 -4
package/extension/src/tui.ts
CHANGED
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
* inline next to the row currently updating, settling into a real ✓/✗
|
|
30
30
|
* plus a bounded tail of that row's own actual captured stdout/stderr
|
|
31
31
|
* once it finishes, never a determinate bar (a single subprocess call has
|
|
32
|
-
* no knowable percentage). Rows render
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
* no knowable percentage). Rows render as a column-major grid of
|
|
33
|
+
* Malevich Cards (computeCardColumns/renderCardGrid, below) -- one card
|
|
34
|
+
* per package, as many side by side as `width` allows above a fixed
|
|
35
|
+
* minimum card width, rather than one full-width card per line -- inside
|
|
36
|
+
* this tab's own scroll-window slice. No pagination baked into the grid
|
|
37
|
+
* helpers themselves, so the visible-window-around-selectedIndex math
|
|
38
|
+
* stays here. u/x/d and the Enter action menu
|
|
38
39
|
* all run their whole approve+mutate+confirmReload flow inline, via the
|
|
39
40
|
* shared TabHost's inlineCtx -- every confirm() along the way renders as
|
|
40
41
|
* a real Malevich Dialog on this SAME overlay, dispatched by literal y/n
|
|
@@ -407,6 +408,87 @@ async function showActionMenu(ctx: ExtensionCommandContext, row: Row): Promise<"
|
|
|
407
408
|
);
|
|
408
409
|
}
|
|
409
410
|
|
|
411
|
+
/** A card narrower than this wastes more space on border/padding than it
|
|
412
|
+
* shows of a real scoped package name ("@scope/pi-longer-name") plus its
|
|
413
|
+
* version/status -- the floor `computeCardColumns` uses when deciding how
|
|
414
|
+
* many side-by-side columns actually fit `width`, rather than guessing a
|
|
415
|
+
* fixed row/page count independent of the panel's real width. */
|
|
416
|
+
const MIN_CARD_WIDTH = 28;
|
|
417
|
+
const CARD_GRID_GAP = 1;
|
|
418
|
+
|
|
419
|
+
/** How many side-by-side card columns fit `width` without any of them
|
|
420
|
+
* dropping below MIN_CARD_WIDTH, capped at `itemCount` -- never more
|
|
421
|
+
* columns than there are real items to fill them (a wide terminal with
|
|
422
|
+
* only 2 packages installed gets 2 columns, not 4 mostly-empty ones). */
|
|
423
|
+
function computeCardColumns(width: number, itemCount: number): number {
|
|
424
|
+
if (itemCount <= 0) return 1;
|
|
425
|
+
const fits = Math.floor((width + CARD_GRID_GAP) / (MIN_CARD_WIDTH + CARD_GRID_GAP));
|
|
426
|
+
return Math.max(1, Math.min(fits, itemCount));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Exact per-column widths for `columns` side-by-side cards inside `width`,
|
|
430
|
+
* every gap included -- sums (with CARD_GRID_GAP between each) to exactly
|
|
431
|
+
* `width`, distributing any remainder across the leading columns instead
|
|
432
|
+
* of leaving it as slack nobody renders into (the same exact-width
|
|
433
|
+
* contract every other line in this panel already holds, verified by this
|
|
434
|
+
* file's own "every rendered line at the exact same real width" test). */
|
|
435
|
+
function distributeColumnWidths(width: number, columns: number): number[] {
|
|
436
|
+
const available = width - CARD_GRID_GAP * (columns - 1);
|
|
437
|
+
const base = Math.floor(available / columns);
|
|
438
|
+
const remainder = available - base * columns;
|
|
439
|
+
return Array.from({ length: columns }, (_, i) => base + (i < remainder ? 1 : 0));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Lays `items` into a column-major grid of exactly `columns` columns
|
|
443
|
+
* (column 0 filled top-to-bottom first, then column 1, ...) -- the same
|
|
444
|
+
* fill order `ls -C` uses, so a flat Up/Down cycle through `items` moves
|
|
445
|
+
* down one column at a time instead of snaking left-to-right through a
|
|
446
|
+
* row-major grid, with no new keybinding needed (Left/Right stay reserved
|
|
447
|
+
* for the panel's own tab-cycling -- see this file's own header comment).
|
|
448
|
+
* `columns` is always the caller's own already-computed computeCardColumns
|
|
449
|
+
* result, never re-derived here from `items.length` -- a caller windowing
|
|
450
|
+
* `items` into a page shorter than its full list must still render that
|
|
451
|
+
* page at the same column count the rest of the list uses, or the grid
|
|
452
|
+
* would reflow mid-scroll. `selectedIndex` is an index into `items`
|
|
453
|
+
* itself, or -1 when nothing in this grid is selected. */
|
|
454
|
+
function renderCardGrid<T>(
|
|
455
|
+
items: T[],
|
|
456
|
+
columns: number,
|
|
457
|
+
width: number,
|
|
458
|
+
selectedIndex: number,
|
|
459
|
+
renderCard: (item: T, columnWidth: number, selected: boolean) => string[],
|
|
460
|
+
): string[] {
|
|
461
|
+
if (items.length === 0) return [];
|
|
462
|
+
const columnWidths = distributeColumnWidths(width, columns);
|
|
463
|
+
const rowsPerColumn = Math.ceil(items.length / columns);
|
|
464
|
+
const rendered: string[][] = [];
|
|
465
|
+
for (let c = 0; c < columns; c++) {
|
|
466
|
+
const columnWidth = columnWidths[c] ?? MIN_CARD_WIDTH;
|
|
467
|
+
const lines: string[] = [];
|
|
468
|
+
for (let r = 0; r < rowsPerColumn; r++) {
|
|
469
|
+
const index = c * rowsPerColumn + r;
|
|
470
|
+
const item = items[index];
|
|
471
|
+
if (item === undefined) break;
|
|
472
|
+
lines.push(...renderCard(item, columnWidth, index === selectedIndex));
|
|
473
|
+
}
|
|
474
|
+
rendered.push(lines);
|
|
475
|
+
}
|
|
476
|
+
const height = Math.max(0, ...rendered.map((lines) => lines.length));
|
|
477
|
+
const out: string[] = [];
|
|
478
|
+
for (let row = 0; row < height; row++) {
|
|
479
|
+
out.push(
|
|
480
|
+
rendered
|
|
481
|
+
.map((lines, c) => {
|
|
482
|
+
const line = lines[row] ?? "";
|
|
483
|
+
const pad = Math.max(0, (columnWidths[c] ?? MIN_CARD_WIDTH) - visibleWidth(line));
|
|
484
|
+
return line + " ".repeat(pad);
|
|
485
|
+
})
|
|
486
|
+
.join(" ".repeat(CARD_GRID_GAP)),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
|
|
410
492
|
/** Packages -- the panel's default/"home" tab. A real Component (not the
|
|
411
493
|
* panel's own top-level ctx.ui.custom owner anymore); the shared TabHost
|
|
412
494
|
* gives it inline approval/reload dialogs and a way to signal the overlay
|
|
@@ -509,33 +591,42 @@ export class PackagesTab implements Component {
|
|
|
509
591
|
lines.push(theme.fg("muted", " No packages"));
|
|
510
592
|
return lines;
|
|
511
593
|
}
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
594
|
+
// Column count is derived from `width` (and the total item count, so a
|
|
595
|
+
// short list never gets more columns than it has items) once per
|
|
596
|
+
// render, then held fixed across the whole page -- otherwise scrolling
|
|
597
|
+
// through a filtered list would reflow the grid mid-scroll. No
|
|
598
|
+
// scrolling of its own beyond that: the visible-window-around-
|
|
599
|
+
// selectedIndex math stays this tab's own job, same as before, just
|
|
600
|
+
// now counting grid rows (maxVisible) times columns instead of a flat
|
|
601
|
+
// one-card-per-line count.
|
|
602
|
+
const columns = computeCardColumns(width, this.filtered.length);
|
|
603
|
+
const pageSize = columns * this.maxVisible;
|
|
604
|
+
const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(pageSize / 2), this.filtered.length - pageSize));
|
|
605
|
+
const end = Math.min(start + pageSize, this.filtered.length);
|
|
606
|
+
const page = this.filtered.slice(start, end);
|
|
607
|
+
lines.push(
|
|
608
|
+
...renderCardGrid(page, columns, width, this.selectedIndex - start, (row, columnWidth, selected) => {
|
|
609
|
+
// A settled row shows its real outcome even for the instant before
|
|
610
|
+
// the next row's "start" event moves updatingRowName off it.
|
|
611
|
+
const rowSettled = this.settled.get(row.name);
|
|
612
|
+
const isUpdating = !rowSettled && this.updatingRowName === row.name;
|
|
613
|
+
const status = isUpdating
|
|
614
|
+
? theme.fg("accent", `${this.spinner.glyph()} updating…`)
|
|
615
|
+
: rowSettled
|
|
616
|
+
? theme.fg(rowSettled.ok ? "success" : "error", `${rowSettled.ok ? "✓" : "✗"}${rowSettled.tail ? ` ${rowSettled.tail}` : ""}`)
|
|
617
|
+
: row.hasUpdate
|
|
618
|
+
? theme.fg("warning", `↑${row.latest}`)
|
|
619
|
+
: theme.fg("muted", "installed");
|
|
620
|
+
const card = new Card({
|
|
621
|
+
title: theme.bold(row.name),
|
|
622
|
+
content: [`${theme.fg("dim", row.version)} · ${status}`],
|
|
623
|
+
selected,
|
|
624
|
+
theme: cardTheme(theme),
|
|
625
|
+
measure: this.measure,
|
|
626
|
+
});
|
|
627
|
+
return card.render(columnWidth);
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
539
630
|
const hasScroll = start > 0 || end < this.filtered.length;
|
|
540
631
|
lines.push(theme.fg("dim", ` ${hasScroll ? `${this.selectedIndex + 1}/${this.filtered.length} ` : ""}${this.mode}`));
|
|
541
632
|
return lines;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
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";
|
|
@@ -36,11 +36,18 @@ class NoopInstaller implements Installer {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
let server: Server<undefined> | undefined;
|
|
39
|
+
const roots: string[] = [];
|
|
39
40
|
afterEach(() => {
|
|
40
41
|
server?.stop(true);
|
|
41
42
|
server = undefined;
|
|
43
|
+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
42
44
|
});
|
|
43
45
|
|
|
46
|
+
function track(dir: string): string {
|
|
47
|
+
roots.push(dir);
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
const daysAgo = (n: number) => new Date(Date.now() - n * 24 * 60 * 60 * 1000).toISOString();
|
|
45
52
|
|
|
46
53
|
describe("fetchBulkAdvisories", () => {
|
|
@@ -221,6 +228,7 @@ describe("scanInstalledPackages", () => {
|
|
|
221
228
|
describe("advisories.scan operation (real daemon route)", () => {
|
|
222
229
|
it("resolves real installed npm packages' on-disk versions and routes through the authenticated operation registry", async () => {
|
|
223
230
|
const piHome = mkdtempSync(join(tmpdir(), "packed-advisories-daemon-"));
|
|
231
|
+
roots.push(piHome);
|
|
224
232
|
writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-vuln"] }));
|
|
225
233
|
const pkgDir = join(piHome, "npm", "node_modules", "pi-vuln");
|
|
226
234
|
mkdirSync(pkgDir, { recursive: true });
|
|
@@ -231,8 +239,8 @@ describe("advisories.scan operation (real daemon route)", () => {
|
|
|
231
239
|
reg: new NoopRegistry(),
|
|
232
240
|
inst: new NoopInstaller(),
|
|
233
241
|
token: "test-token",
|
|
234
|
-
stateDir: mkdtempSync(join(tmpdir(), "packed-advisories-state-")),
|
|
235
|
-
dataDir: mkdtempSync(join(tmpdir(), "packed-advisories-data-")),
|
|
242
|
+
stateDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-state-"))),
|
|
243
|
+
dataDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-data-"))),
|
|
236
244
|
piHome,
|
|
237
245
|
// injected exactly like pi.status's piVersion seam -- never a real
|
|
238
246
|
// network call to the live npm registry from an automated test.
|
|
@@ -258,9 +266,9 @@ describe("advisories.scan operation (real daemon route)", () => {
|
|
|
258
266
|
reg: new NoopRegistry(),
|
|
259
267
|
inst: new NoopInstaller(),
|
|
260
268
|
token: "test-token",
|
|
261
|
-
stateDir: mkdtempSync(join(tmpdir(), "packed-advisories-state-")),
|
|
262
|
-
dataDir: mkdtempSync(join(tmpdir(), "packed-advisories-data-")),
|
|
263
|
-
piHome: mkdtempSync(join(tmpdir(), "packed-advisories-pihome-")),
|
|
269
|
+
stateDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-state-"))),
|
|
270
|
+
dataDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-data-"))),
|
|
271
|
+
piHome: track(mkdtempSync(join(tmpdir(), "packed-advisories-pihome-"))),
|
|
264
272
|
});
|
|
265
273
|
const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
|
|
266
274
|
label: "Packed",
|
|
@@ -274,9 +282,9 @@ describe("advisories.scan operation (real daemon route)", () => {
|
|
|
274
282
|
reg: new NoopRegistry(),
|
|
275
283
|
inst: new NoopInstaller(),
|
|
276
284
|
token: "test-token",
|
|
277
|
-
stateDir: mkdtempSync(join(tmpdir(), "packed-advisories-state-")),
|
|
278
|
-
dataDir: mkdtempSync(join(tmpdir(), "packed-advisories-data-")),
|
|
279
|
-
piHome: mkdtempSync(join(tmpdir(), "packed-advisories-pihome-")),
|
|
285
|
+
stateDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-state-"))),
|
|
286
|
+
dataDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-data-"))),
|
|
287
|
+
piHome: track(mkdtempSync(join(tmpdir(), "packed-advisories-pihome-"))),
|
|
280
288
|
});
|
|
281
289
|
const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
|
|
282
290
|
label: "Packed",
|
|
@@ -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 pkg(manifest: Record<string, unknown>): string {
|
|
16
21
|
const root = mkdtempSync(join(tmpdir(), "packed-cleanup-"));
|
|
17
22
|
roots.push(root);
|
|
@@ -169,8 +174,8 @@ describe("packed remove applies pi.cleanup before delegating to pi remove (real
|
|
|
169
174
|
reg: new NoopRegistry(),
|
|
170
175
|
inst,
|
|
171
176
|
token: "test-token",
|
|
172
|
-
stateDir: mkdtempSync(join(tmpdir(), "packed-cleanup-state-")),
|
|
173
|
-
dataDir: mkdtempSync(join(tmpdir(), "packed-cleanup-data-")),
|
|
177
|
+
stateDir: track(mkdtempSync(join(tmpdir(), "packed-cleanup-state-"))),
|
|
178
|
+
dataDir: track(mkdtempSync(join(tmpdir(), "packed-cleanup-data-"))),
|
|
174
179
|
piHome,
|
|
175
180
|
});
|
|
176
181
|
return new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
|
package/service/test/cli.test.ts
CHANGED
|
@@ -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:
|
|
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(() =>
|
|
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
|
});
|
package/service/test/db.test.ts
CHANGED
|
@@ -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(() =>
|
|
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,8 +10,8 @@
|
|
|
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";
|
|
@@ -24,6 +24,16 @@ import { ExecInstaller } from "../src/packages/install.ts";
|
|
|
24
24
|
* baked into the script file itself (not an env var) so it is immune to
|
|
25
25
|
* Bun.spawn's default env snapshot not picking up late process.env writes.
|
|
26
26
|
*/
|
|
27
|
+
const roots: string[] = [];
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function track(dir: string): string {
|
|
33
|
+
roots.push(dir);
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
function writeFakePi(dir: string, rewrite?: { piHome: string; name: string; newVersion: string }): string {
|
|
28
38
|
const script = join(dir, "fake-pi");
|
|
29
39
|
const rewriteLine = rewrite
|
|
@@ -39,7 +49,7 @@ function writeFakePi(dir: string, rewrite?: { piHome: string; name: string; newV
|
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
function writePiHome(nodeModules: Record<string, string> = {}): string {
|
|
42
|
-
const dir = mkdtempSync(join(tmpdir(), "packed-exec-pihome-"));
|
|
52
|
+
const dir = track(mkdtempSync(join(tmpdir(), "packed-exec-pihome-")));
|
|
43
53
|
mkdirSync(join(dir, "npm"), { recursive: true });
|
|
44
54
|
for (const [name, version] of Object.entries(nodeModules)) {
|
|
45
55
|
const pkgDir = join(dir, "npm", "node_modules", name);
|
|
@@ -73,7 +83,7 @@ function writeFakeNpm(dir: string, logFile: string, rewrite?: { piHome: string;
|
|
|
73
83
|
|
|
74
84
|
describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguous exit-0 text", () => {
|
|
75
85
|
it("pinned source, version genuinely unchanged: alreadyUpToDate, reloadRequired false", async () => {
|
|
76
|
-
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
86
|
+
const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
|
|
77
87
|
const bin = writeFakePi(scriptDir);
|
|
78
88
|
const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
|
|
79
89
|
const piHome = writePiHome({ "@scope/pkg": "1.2.3" });
|
|
@@ -90,7 +100,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
|
|
|
90
100
|
});
|
|
91
101
|
|
|
92
102
|
it('unpinned source, already latest (pi still exits 0 and says "Updated"): alreadyUpToDate', async () => {
|
|
93
|
-
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
103
|
+
const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
|
|
94
104
|
const bin = writeFakePi(scriptDir);
|
|
95
105
|
const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
|
|
96
106
|
const piHome = writePiHome({ plain: "0.5.0" });
|
|
@@ -106,7 +116,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
|
|
|
106
116
|
});
|
|
107
117
|
|
|
108
118
|
it("unpinned source, a real version change happens: reloadRequired true", async () => {
|
|
109
|
-
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
119
|
+
const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
|
|
110
120
|
const piHome = writePiHome({ plain: "0.5.0" });
|
|
111
121
|
const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
|
|
112
122
|
const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
|
|
@@ -122,7 +132,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
|
|
|
122
132
|
});
|
|
123
133
|
|
|
124
134
|
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-"));
|
|
135
|
+
const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
|
|
126
136
|
const bin = writeFakePi(scriptDir);
|
|
127
137
|
const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
|
|
128
138
|
const piHome = writePiHome();
|
|
@@ -141,7 +151,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
|
|
|
141
151
|
|
|
142
152
|
describe("ExecInstaller — forces full dependency re-resolution, not just the target's own subtree", () => {
|
|
143
153
|
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-"));
|
|
154
|
+
const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
|
|
145
155
|
// Simulates the confirmed live defect: after `pi update npm:@scope/leaf`,
|
|
146
156
|
// the leaf's own version bumps, but a root-level sibling
|
|
147
157
|
// (@scope/shared) that the freshly-updated leaf now needs a newer
|
|
@@ -170,7 +180,7 @@ describe("ExecInstaller — forces full dependency re-resolution, not just the t
|
|
|
170
180
|
});
|
|
171
181
|
|
|
172
182
|
it("install() also forces a full re-resolution after a successful pi install", async () => {
|
|
173
|
-
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
183
|
+
const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
|
|
174
184
|
const piHome = writePiHome({ "@scope/shared": "1.0.0" });
|
|
175
185
|
const bin = writeFakePi(scriptDir);
|
|
176
186
|
const npmLog = join(scriptDir, "npm.log");
|
|
@@ -185,7 +195,7 @@ describe("ExecInstaller — forces full dependency re-resolution, not just the t
|
|
|
185
195
|
});
|
|
186
196
|
|
|
187
197
|
it("surfaces a failed re-resolution instead of silently reporting success", async () => {
|
|
188
|
-
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
198
|
+
const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
|
|
189
199
|
const piHome = writePiHome({ plain: "0.5.0" });
|
|
190
200
|
const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
|
|
191
201
|
const failingNpm = join(scriptDir, "fake-npm-fail");
|
|
@@ -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;
|
|
@@ -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(
|
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
import type { Installer, PkgInfo, Registry, SearchPage, UpdateOutcome } from "../src/packages/package.ts";
|
|
@@ -54,8 +54,18 @@ class GitFixture implements GitResolutionPort {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const roots: string[] = [];
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function track(dir: string): string {
|
|
63
|
+
roots.push(dir);
|
|
64
|
+
return dir;
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
function piHome(withPackage = true): string {
|
|
58
|
-
const root = mkdtempSync(join(tmpdir(), "packed-setup-home-"));
|
|
68
|
+
const root = track(mkdtempSync(join(tmpdir(), "packed-setup-home-")));
|
|
59
69
|
const packages = withPackage ? ["npm:pi-demo"] : [];
|
|
60
70
|
writeFileSync(join(root, "settings.json"), JSON.stringify({ packages }));
|
|
61
71
|
if (withPackage) {
|
|
@@ -73,7 +83,7 @@ function piHome(withPackage = true): string {
|
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
function project(): string {
|
|
76
|
-
const root = mkdtempSync(join(tmpdir(), "packed-setup-project-"));
|
|
86
|
+
const root = track(mkdtempSync(join(tmpdir(), "packed-setup-project-")));
|
|
77
87
|
mkdirSync(join(root, ".pi"), { recursive: true });
|
|
78
88
|
return root;
|
|
79
89
|
}
|