@danypops/pi-packed 0.23.0 → 0.25.0
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/vehicle-tools.ts +4 -5
- package/package.json +3 -3
- package/service/src/daemon/daemon-service.ts +84 -3
- package/service/test/cli.test.ts +19 -1
- package/service/test/daemon-maintenance.test.ts +174 -0
- package/service/test/daemon-service.test.ts +1 -0
- package/service/test/service.test.ts +11 -0
|
@@ -74,11 +74,10 @@ export async function registerPackedVehicle(pi: ExtensionAPI): Promise<void> {
|
|
|
74
74
|
await registerVehicleTools(pi, client, {
|
|
75
75
|
permissions: ["packed:read"],
|
|
76
76
|
principal: { id: "pi-packed" },
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
shell: { coreOperations: CORE_OPERATIONS, broker: { ownVehicleName: "pi-packed" } },
|
|
77
|
+
// tools_list/tools_man are a neutral, process-wide singleton now (vehicle-client-pi's own
|
|
78
|
+
// ensureVehicleShellHandle) -- no ownVehicleName/broker option needed here anymore; every
|
|
79
|
+
// vehicle in the process (including packed's own) is discovered and namespaced uniformly.
|
|
80
|
+
shell: { coreOperations: CORE_OPERATIONS },
|
|
82
81
|
});
|
|
83
82
|
} catch {
|
|
84
83
|
// Daemon state is stale/unreachable -- degrade silently, matching
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-packed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"typecheck": "bunx tsc --noEmit"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@danypops/armada": "^0.4.
|
|
30
|
+
"@danypops/armada": "^0.4.6",
|
|
31
31
|
"@danypops/packed": "^0.7.0",
|
|
32
32
|
"@danypops/pi-extension-harness": "^0.7.0",
|
|
33
33
|
"@danypops/vehicle-client": "^0.8.1",
|
|
34
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
34
|
+
"@danypops/vehicle-client-pi": "^0.40.1",
|
|
35
35
|
"@danypops/vehicle-core": "^0.15.0",
|
|
36
36
|
"@danypops/vehicle-server": "^0.22.0",
|
|
37
37
|
"jiti": "^2.7.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { readdirSync, readFileSync } from "node:fs";
|
|
29
29
|
import { join } from "node:path";
|
|
30
|
-
import { createVehicleRegistrar, type VehicleRegistrar } from "@danypops/armada";
|
|
30
|
+
import { createVehicleRegistrar, type VehicleRegistrar, type VehicleRegistrationOutcome, type VehicleSpec } from "@danypops/armada";
|
|
31
31
|
import { resolveDaemonPaths } from "@danypops/vehicle-server/paths";
|
|
32
32
|
import {
|
|
33
33
|
isVehicleServiceRegistered,
|
|
@@ -408,6 +408,15 @@ export interface ReconcileAllResult {
|
|
|
408
408
|
reconciled: Array<{ packageName: string; vehicleName: string; installed: boolean; reason?: string }>;
|
|
409
409
|
skipped: number;
|
|
410
410
|
failed: Array<{ packageName: string; reason: string }>;
|
|
411
|
+
/**
|
|
412
|
+
* Every Vehicle unregistered because this same sweep could no longer
|
|
413
|
+
* discover it by any path (neither a configured extension's own
|
|
414
|
+
* dependency walk nor listUnconfiguredDaemonDependencies' root-pinned
|
|
415
|
+
* scan) -- see pruneStaleVehicles' own doc comment for why this is safe
|
|
416
|
+
* to do unconditionally rather than requiring a separate opt-in call.
|
|
417
|
+
*/
|
|
418
|
+
pruned: Array<{ vehicleName: string; executable: string }>;
|
|
419
|
+
pruneFailed: Array<{ vehicleName: string; reason: string }>;
|
|
411
420
|
}
|
|
412
421
|
|
|
413
422
|
/** Matches readPackageDeclarations' own bound -- a reconcile-all sweep never processes an unbounded package list. */
|
|
@@ -438,7 +447,7 @@ const SELF_REGISTRATION_REASON = "Packed cannot replace its own Armada service f
|
|
|
438
447
|
export async function reconcileAllDaemonServices(
|
|
439
448
|
piHome: string,
|
|
440
449
|
projectRoot: string | undefined,
|
|
441
|
-
installer: Pick<DaemonServiceInstaller, "install">,
|
|
450
|
+
installer: Pick<DaemonServiceInstaller, "install" | "listRegisteredVehicles" | "unregisterVehicleByName">,
|
|
442
451
|
): Promise<ReconcileAllResult> {
|
|
443
452
|
const packages = readInstalledPackagesAcrossScopes(piHome, projectRoot).slice(0, MAX_RECONCILE_PACKAGES);
|
|
444
453
|
const reconciled: ReconcileAllResult["reconciled"] = [];
|
|
@@ -470,7 +479,62 @@ export async function reconcileAllDaemonServices(
|
|
|
470
479
|
...(resolved.result.installed ? {} : { reason: resolved.result.reason }),
|
|
471
480
|
});
|
|
472
481
|
}
|
|
473
|
-
|
|
482
|
+
// listUnconfiguredDaemonDependencies deliberately walks the FULL top-level
|
|
483
|
+
// node_modules sweep, not just readInstalledPackagesAcrossScopes' own
|
|
484
|
+
// pi:-configured list above -- a root-pinned dependency (e.g. lector,
|
|
485
|
+
// pinned directly in piHome/npm/package.json ahead of whatever version its
|
|
486
|
+
// own pi-lector wrapper's tree would resolve) is a real, currently-running
|
|
487
|
+
// Vehicle that the configured-packages loop above never reaches at all.
|
|
488
|
+
// Folding it in here is what makes the discovered set below actually
|
|
489
|
+
// complete -- pruning against anything narrower would delete a Vehicle
|
|
490
|
+
// that's still genuinely there, just reachable by a different path.
|
|
491
|
+
for (const dep of listUnconfiguredDaemonDependencies(piHome)) seen.add(dep.vehicleName);
|
|
492
|
+
const { pruned, pruneFailed } = await pruneStaleVehicles(piHome, seen, installer);
|
|
493
|
+
return { reconciled, skipped, failed, pruned, pruneFailed };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Unregisters every Vehicle Armada currently declares that `discovered`
|
|
498
|
+
* (this same sweep's own complete union of configured-extension and
|
|
499
|
+
* root-pinned resolution) no longer produces at all -- the other half of
|
|
500
|
+
* what makes Armada authoritative for every Vehicle Packed knows about.
|
|
501
|
+
* Without this, a Vehicle whose packed.daemonService.name changed (or that
|
|
502
|
+
* a stale nested dependency copy once resolved under a different name --
|
|
503
|
+
* see this session's own live incident, a duplicate "web-spider-daemon"
|
|
504
|
+
* losing every restart's single-instance-lock race against the correctly-
|
|
505
|
+
* named "web-spider" Vehicle) just sits in the manifest forever: nothing
|
|
506
|
+
* ever re-discovers it to overwrite it, and nothing ever notices it's gone
|
|
507
|
+
* stale either.
|
|
508
|
+
*
|
|
509
|
+
* Scoped to only ever touch a Vehicle whose own `executable` lives under
|
|
510
|
+
* piHome's own npm/node_modules -- i.e. one only Packed itself could
|
|
511
|
+
* plausibly have registered in the first place. A Vehicle registered by an
|
|
512
|
+
* entirely different mechanism (a hand-rolled `armada upsert` pointing
|
|
513
|
+
* somewhere else on disk) is never a candidate, no matter how the
|
|
514
|
+
* discovered set above turns out -- this function has no way to know that
|
|
515
|
+
* caller's own intent, so it stays out of its way entirely. Packed's own
|
|
516
|
+
* Vehicle (PACKED_VEHICLE_NAME) is excluded for the same reason
|
|
517
|
+
* install()/remove()/restart() already refuse to touch it: reconcile runs
|
|
518
|
+
* from inside Packed's own process.
|
|
519
|
+
*/
|
|
520
|
+
async function pruneStaleVehicles(
|
|
521
|
+
piHome: string,
|
|
522
|
+
discovered: ReadonlySet<string>,
|
|
523
|
+
installer: Pick<DaemonServiceInstaller, "listRegisteredVehicles" | "unregisterVehicleByName">,
|
|
524
|
+
): Promise<Pick<ReconcileAllResult, "pruned" | "pruneFailed">> {
|
|
525
|
+
const managedRoot = join(piHome, "npm", "node_modules") + "/";
|
|
526
|
+
const pruned: ReconcileAllResult["pruned"] = [];
|
|
527
|
+
const pruneFailed: ReconcileAllResult["pruneFailed"] = [];
|
|
528
|
+
const registered = await installer.listRegisteredVehicles();
|
|
529
|
+
for (const vehicle of registered) {
|
|
530
|
+
if (vehicle.name === PACKED_VEHICLE_NAME) continue;
|
|
531
|
+
if (discovered.has(vehicle.name)) continue;
|
|
532
|
+
if (!vehicle.executable.startsWith(managedRoot)) continue;
|
|
533
|
+
const outcome = await installer.unregisterVehicleByName(vehicle.name);
|
|
534
|
+
if (outcome.ok) pruned.push({ vehicleName: vehicle.name, executable: vehicle.executable });
|
|
535
|
+
else pruneFailed.push({ vehicleName: vehicle.name, reason: outcome.diagnostics.map((d) => d.message).join("; ") || "unregister failed" });
|
|
536
|
+
}
|
|
537
|
+
return { pruned, pruneFailed };
|
|
474
538
|
}
|
|
475
539
|
|
|
476
540
|
export interface DaemonServiceInstaller {
|
|
@@ -486,6 +550,15 @@ export interface DaemonServiceInstaller {
|
|
|
486
550
|
piHome: string,
|
|
487
551
|
source: string,
|
|
488
552
|
): Promise<{ ok: true; restarted: boolean; reason?: string; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean }>;
|
|
553
|
+
/** Every Vehicle Armada currently declares -- see pruneStaleVehicles' own doc comment for why this exists. */
|
|
554
|
+
listRegisteredVehicles(): Promise<readonly VehicleSpec[]>;
|
|
555
|
+
/**
|
|
556
|
+
* Unregisters by Armada vehicle NAME directly, distinct from remove()'s
|
|
557
|
+
* own by-npm-source resolution -- pruning has no installed package left to
|
|
558
|
+
* resolve a source from; the whole point is the vehicle survived past
|
|
559
|
+
* whatever used to produce it.
|
|
560
|
+
*/
|
|
561
|
+
unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome>;
|
|
489
562
|
}
|
|
490
563
|
|
|
491
564
|
/**
|
|
@@ -498,6 +571,14 @@ export interface DaemonServiceInstaller {
|
|
|
498
571
|
export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
|
|
499
572
|
constructor(private readonly registrar: VehicleRegistrar = createVehicleRegistrar()) {}
|
|
500
573
|
|
|
574
|
+
async listRegisteredVehicles(): Promise<readonly VehicleSpec[]> {
|
|
575
|
+
return this.registrar.listRegistered();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome> {
|
|
579
|
+
return this.registrar.unregister(name);
|
|
580
|
+
}
|
|
581
|
+
|
|
501
582
|
async install(
|
|
502
583
|
piHome: string,
|
|
503
584
|
source: string,
|
package/service/test/cli.test.ts
CHANGED
|
@@ -123,6 +123,8 @@ class FakeDaemonServiceInstaller {
|
|
|
123
123
|
reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true }],
|
|
124
124
|
skipped: 0,
|
|
125
125
|
failed: [],
|
|
126
|
+
pruned: [],
|
|
127
|
+
pruneFailed: [],
|
|
126
128
|
};
|
|
127
129
|
}
|
|
128
130
|
}
|
|
@@ -605,6 +607,8 @@ describe("CLI", () => {
|
|
|
605
607
|
reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true }],
|
|
606
608
|
skipped: 0,
|
|
607
609
|
failed: [],
|
|
610
|
+
pruned: [],
|
|
611
|
+
pruneFailed: [],
|
|
608
612
|
});
|
|
609
613
|
});
|
|
610
614
|
|
|
@@ -1007,7 +1011,15 @@ describe("CLI", () => {
|
|
|
1007
1011
|
},
|
|
1008
1012
|
async reconcileServices(approved, projectRoot) {
|
|
1009
1013
|
calls.push(`reconcileServices:${approved}:${projectRoot}`);
|
|
1010
|
-
return {
|
|
1014
|
+
return {
|
|
1015
|
+
ok: true,
|
|
1016
|
+
output: "reconciled 0 Vehicle(s), skipped 0 non-daemon package(s)",
|
|
1017
|
+
reconciled: [],
|
|
1018
|
+
skipped: 0,
|
|
1019
|
+
failed: [],
|
|
1020
|
+
pruned: [],
|
|
1021
|
+
pruneFailed: [],
|
|
1022
|
+
};
|
|
1011
1023
|
},
|
|
1012
1024
|
async update(source) {
|
|
1013
1025
|
return { output: source, reloadRequired: false, alreadyUpToDate: true, pinned: false };
|
|
@@ -1303,6 +1315,12 @@ describe("daemon client", () => {
|
|
|
1303
1315
|
spec: { name: "pi-lsp", version: "1.0.0", binPath: "/opt/pi-lsp/cli.js", handlePath: "/tmp/pi-lsp.handle.json" },
|
|
1304
1316
|
};
|
|
1305
1317
|
},
|
|
1318
|
+
async listRegisteredVehicles() {
|
|
1319
|
+
return [];
|
|
1320
|
+
},
|
|
1321
|
+
async unregisterVehicleByName() {
|
|
1322
|
+
return { ok: true, manifestHash: "hash" as never, applied: [], diagnostics: [] };
|
|
1323
|
+
},
|
|
1306
1324
|
},
|
|
1307
1325
|
token: daemonToken,
|
|
1308
1326
|
stateDir: daemonDir,
|
|
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test";
|
|
|
2
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
|
+
import type { VehicleRegistrationOutcome, VehicleSpec } from "@danypops/armada";
|
|
5
6
|
import { createArmadaTestHarness } from "@danypops/armada/testing";
|
|
6
7
|
import type { MaintenanceTask } from "@danypops/vehicle-server/daemon";
|
|
7
8
|
import type { ServiceInstallResult, ServiceSpec } from "@danypops/vehicle-server/service";
|
|
@@ -121,6 +122,12 @@ class RecordingDaemonServiceInstaller {
|
|
|
121
122
|
> {
|
|
122
123
|
return { ok: false, reason: "not a daemon", notADaemon: true };
|
|
123
124
|
}
|
|
125
|
+
async listRegisteredVehicles(): Promise<readonly VehicleSpec[]> {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
async unregisterVehicleByName(): Promise<VehicleRegistrationOutcome> {
|
|
129
|
+
return { ok: true, manifestHash: "hash" as never, applied: [], diagnostics: [] };
|
|
130
|
+
}
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle drift, not just per-package on demand)", () => {
|
|
@@ -308,4 +315,171 @@ describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle dr
|
|
|
308
315
|
expect(snapshot?.updates.map((u) => u.name)).toContain("@danypops/probe-daemon");
|
|
309
316
|
});
|
|
310
317
|
});
|
|
318
|
+
|
|
319
|
+
describe("reconcileAllDaemonServices pruning -- unregisters a Vehicle no longer discoverable by any path (the-armada-registrar-jittor-web-spider-daemon-collision)", () => {
|
|
320
|
+
function stalePiHomeVehicle(piHome: string, name: string): VehicleSpec {
|
|
321
|
+
return {
|
|
322
|
+
name: name as VehicleSpec["name"],
|
|
323
|
+
version: "0.1.0",
|
|
324
|
+
executable: join(piHome, "npm", "node_modules", "@danypops", "old-package", "cli.ts"),
|
|
325
|
+
arguments: ["serve"],
|
|
326
|
+
handlePath: join(piHome, "stale-handle.json"),
|
|
327
|
+
restart: { policy: "never" },
|
|
328
|
+
readiness: { timeoutMs: 500, pollIntervalMs: 50 },
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
it("prunes a Vehicle whose executable lives under piHome's own node_modules once nothing discovers it anymore", async () => {
|
|
333
|
+
const piHome = fakePiHome([]); // deliberately empty -- old-package no longer configured or on disk
|
|
334
|
+
const harness = await createArmadaTestHarness();
|
|
335
|
+
try {
|
|
336
|
+
await harness.registrar.register(stalePiHomeVehicle(piHome, "old-package"));
|
|
337
|
+
expect(await harness.registrar.isRegistered("old-package")).toBe(true);
|
|
338
|
+
|
|
339
|
+
const installer = new RealDaemonServiceInstaller(harness.registrar);
|
|
340
|
+
const result = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
341
|
+
|
|
342
|
+
expect(result.pruned).toEqual([{ vehicleName: "old-package", executable: stalePiHomeVehicle(piHome, "old-package").executable }]);
|
|
343
|
+
expect(result.pruneFailed).toEqual([]);
|
|
344
|
+
expect(await harness.registrar.isRegistered("old-package")).toBe(false);
|
|
345
|
+
} finally {
|
|
346
|
+
await harness.dispose();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("never prunes a Vehicle registered outside piHome's own node_modules, regardless of discovery", async () => {
|
|
351
|
+
const piHome = fakePiHome([]);
|
|
352
|
+
const harness = await createArmadaTestHarness();
|
|
353
|
+
try {
|
|
354
|
+
await harness.registrar.register({
|
|
355
|
+
name: "externally-managed" as VehicleSpec["name"],
|
|
356
|
+
version: "1.0.0",
|
|
357
|
+
executable: "/opt/custom/cli.js",
|
|
358
|
+
arguments: ["serve"],
|
|
359
|
+
handlePath: join(piHome, "external-handle.json"),
|
|
360
|
+
restart: { policy: "never" },
|
|
361
|
+
readiness: { timeoutMs: 500, pollIntervalMs: 50 },
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const installer = new RealDaemonServiceInstaller(harness.registrar);
|
|
365
|
+
const result = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
366
|
+
|
|
367
|
+
expect(result.pruned).toEqual([]);
|
|
368
|
+
expect(await harness.registrar.isRegistered("externally-managed")).toBe(true);
|
|
369
|
+
} finally {
|
|
370
|
+
await harness.dispose();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("never prunes a Vehicle still discoverable through the configured-extension sweep -- only updates it", async () => {
|
|
375
|
+
const piHome = fakePiHome(["npm:@danypops/probe"]);
|
|
376
|
+
installMockDaemon(piHome, "@danypops/probe");
|
|
377
|
+
const harness = await createArmadaTestHarness();
|
|
378
|
+
try {
|
|
379
|
+
const installer = new RealDaemonServiceInstaller(harness.registrar);
|
|
380
|
+
const result = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
381
|
+
|
|
382
|
+
expect(result.pruned).toEqual([]);
|
|
383
|
+
expect(await harness.registrar.isRegistered("probe")).toBe(true);
|
|
384
|
+
} finally {
|
|
385
|
+
await harness.dispose();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("never prunes a Vehicle still discoverable only through the unconfigured-daemon-dependency sweep (e.g. a root-pinned package like lector)", async () => {
|
|
390
|
+
const piHome = fakePiHome([]); // deliberately NOT settings.json-configured
|
|
391
|
+
installUnconfiguredDaemonDependency(piHome, "@danypops/probe-daemon", "1.0.0");
|
|
392
|
+
const harness = await createArmadaTestHarness();
|
|
393
|
+
try {
|
|
394
|
+
await harness.registrar.register({
|
|
395
|
+
name: "probe-daemon" as VehicleSpec["name"],
|
|
396
|
+
version: "1.0.0",
|
|
397
|
+
executable: join(piHome, "npm", "node_modules", "@danypops", "probe-daemon", "cli.ts"),
|
|
398
|
+
arguments: ["serve"],
|
|
399
|
+
handlePath: join(piHome, "probe-daemon-handle.json"),
|
|
400
|
+
restart: { policy: "never" },
|
|
401
|
+
readiness: { timeoutMs: 500, pollIntervalMs: 50 },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const installer = new RealDaemonServiceInstaller(harness.registrar);
|
|
405
|
+
const result = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
406
|
+
|
|
407
|
+
expect(result.pruned).toEqual([]);
|
|
408
|
+
expect(await harness.registrar.isRegistered("probe-daemon")).toBe(true);
|
|
409
|
+
} finally {
|
|
410
|
+
await harness.dispose();
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("never prunes pi-packed's own Vehicle, even though reconcileAllDaemonServices always skips scanning its own package", async () => {
|
|
415
|
+
const piHome = fakePiHome([]);
|
|
416
|
+
const harness = await createArmadaTestHarness();
|
|
417
|
+
try {
|
|
418
|
+
await harness.registrar.register({
|
|
419
|
+
name: "pi-packed" as VehicleSpec["name"],
|
|
420
|
+
version: "1.0.0",
|
|
421
|
+
executable: join(piHome, "npm", "node_modules", "@danypops", "pi-packed", "service", "src", "cli", "cli.ts"),
|
|
422
|
+
arguments: ["serve"],
|
|
423
|
+
handlePath: join(piHome, "pi-packed-handle.json"),
|
|
424
|
+
restart: { policy: "never" },
|
|
425
|
+
readiness: { timeoutMs: 500, pollIntervalMs: 50 },
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const installer = new RealDaemonServiceInstaller(harness.registrar);
|
|
429
|
+
const result = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
430
|
+
|
|
431
|
+
expect(result.pruned).toEqual([]);
|
|
432
|
+
expect(await harness.registrar.isRegistered("pi-packed")).toBe(true);
|
|
433
|
+
} finally {
|
|
434
|
+
await harness.dispose();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("reproduces the real web-spider/web-spider-daemon collision: the orphaned duplicate is pruned, the correctly-discovered one survives", async () => {
|
|
439
|
+
const piHome = fakePiHome(["npm:@danypops/pi-web-spider"]);
|
|
440
|
+
// pi-web-spider's own dependency-walk discovers @danypops/web-spider-daemon,
|
|
441
|
+
// whose packed.daemonService.name overrides the vehicle name to "web-spider".
|
|
442
|
+
const piWebSpiderDir = join(piHome, "npm", "node_modules", "@danypops", "pi-web-spider");
|
|
443
|
+
mkdirSync(piWebSpiderDir, { recursive: true });
|
|
444
|
+
writeFileSync(
|
|
445
|
+
join(piWebSpiderDir, "package.json"),
|
|
446
|
+
JSON.stringify({ name: "@danypops/pi-web-spider", version: "1.0.0", dependencies: { "@danypops/web-spider-daemon": "^1.0.0" } }),
|
|
447
|
+
);
|
|
448
|
+
const daemonDir = join(piHome, "npm", "node_modules", "@danypops", "web-spider-daemon");
|
|
449
|
+
mkdirSync(daemonDir, { recursive: true });
|
|
450
|
+
writeFileSync(
|
|
451
|
+
join(daemonDir, "package.json"),
|
|
452
|
+
JSON.stringify({
|
|
453
|
+
name: "@danypops/web-spider-daemon",
|
|
454
|
+
version: "1.0.0",
|
|
455
|
+
packed: { daemonService: { name: "web-spider", binPath: "cli.ts", args: ["serve"] } },
|
|
456
|
+
}),
|
|
457
|
+
);
|
|
458
|
+
writeFileSync(join(daemonDir, "cli.ts"), "// Mock Vehicle entry point.\n");
|
|
459
|
+
|
|
460
|
+
const harness = await createArmadaTestHarness();
|
|
461
|
+
try {
|
|
462
|
+
// A leftover registration from before the packed.daemonService.name override existed --
|
|
463
|
+
// the exact orphan a prior Packed version would have produced and never cleaned up.
|
|
464
|
+
await harness.registrar.register({
|
|
465
|
+
name: "web-spider-daemon" as VehicleSpec["name"],
|
|
466
|
+
version: "0.9.0",
|
|
467
|
+
executable: join(daemonDir, "cli.ts"),
|
|
468
|
+
arguments: ["serve"],
|
|
469
|
+
handlePath: join(piHome, "web-spider-daemon-handle.json"),
|
|
470
|
+
restart: { policy: "never" },
|
|
471
|
+
readiness: { timeoutMs: 500, pollIntervalMs: 50 },
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const installer = new RealDaemonServiceInstaller(harness.registrar);
|
|
475
|
+
const result = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
476
|
+
|
|
477
|
+
expect(result.pruned).toEqual([{ vehicleName: "web-spider-daemon", executable: join(daemonDir, "cli.ts") }]);
|
|
478
|
+
expect(await harness.registrar.isRegistered("web-spider-daemon")).toBe(false);
|
|
479
|
+
expect(await harness.registrar.isRegistered("web-spider")).toBe(true);
|
|
480
|
+
} finally {
|
|
481
|
+
await harness.dispose();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
});
|
|
311
485
|
});
|
|
@@ -500,6 +500,7 @@ describe("RealDaemonServiceInstaller -- Armada registration through the in-proce
|
|
|
500
500
|
register: async () => ({ ok: false, diagnostics: [{ code: "X", severity: "error", path: "/", message: "native failure" }] }),
|
|
501
501
|
unregister: async () => ({ ok: false, diagnostics: [] }),
|
|
502
502
|
isRegistered: async () => false,
|
|
503
|
+
listRegistered: async () => [],
|
|
503
504
|
};
|
|
504
505
|
const installer = new RealDaemonServiceInstaller(registrar);
|
|
505
506
|
|
|
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test";
|
|
|
2
2
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import type { VehicleRegistrationOutcome, VehicleSpec } from "@danypops/armada";
|
|
5
6
|
import type { ServiceSpec } from "@danypops/vehicle-server/service";
|
|
6
7
|
import type { DaemonServiceInstaller } from "../src/daemon/daemon-service.ts";
|
|
7
8
|
import { createApp, type Deps } from "../src/daemon/service.ts";
|
|
@@ -123,6 +124,16 @@ class FakeDaemonServiceInstaller implements DaemonServiceInstaller {
|
|
|
123
124
|
return { ok: false, reason: this.restartResolveFailure, ...(this.restartNotADaemon ? { notADaemon: true } : {}) };
|
|
124
125
|
return { ok: true, restarted: this.restarted, reason: this.restartReason, spec: this.spec };
|
|
125
126
|
}
|
|
127
|
+
registeredVehicles: VehicleSpec[] = [];
|
|
128
|
+
unregisterByNameCalls: string[] = [];
|
|
129
|
+
async listRegisteredVehicles(): Promise<readonly VehicleSpec[]> {
|
|
130
|
+
return this.registeredVehicles;
|
|
131
|
+
}
|
|
132
|
+
async unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome> {
|
|
133
|
+
this.unregisterByNameCalls.push(name);
|
|
134
|
+
this.registeredVehicles = this.registeredVehicles.filter((vehicle) => vehicle.name !== name);
|
|
135
|
+
return { ok: true, manifestHash: "hash" as never, applied: [], diagnostics: [] };
|
|
136
|
+
}
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
const roots: string[] = [];
|