@danypops/pi-packed 0.19.11 → 0.20.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.
@@ -79,29 +79,48 @@ class FakeDaemonServiceInstaller {
79
79
  restartGotSource = "";
80
80
  restartApproved = false;
81
81
  restartFail = false;
82
- async install(
83
- source: string,
84
- approved?: boolean,
85
- ): Promise<{ output: string; spec?: { name: string; binPath: string; descriptorPath: string } }> {
82
+ reconcileGotApproved: boolean | undefined;
83
+ reconcileGotProjectRoot: string | undefined;
84
+ reconcileFail = false;
85
+ async install(source: string, approved?: boolean) {
86
86
  this.gotSource = source;
87
87
  this.approved = approved === true;
88
88
  if (this.fail) throw new Error("install-service failed");
89
89
  return {
90
90
  output: `installed a persistent service for ${source}`,
91
- spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" },
91
+ spec: {
92
+ name: "probe",
93
+ version: "1.0.0",
94
+ binPath: "/opt/probe/cli.js",
95
+ handlePath: "/tmp/probe.handle.json",
96
+ },
92
97
  };
93
98
  }
94
- async restart(
95
- source: string,
96
- approved?: boolean,
97
- ): Promise<{ output: string; restarted?: boolean; spec?: { name: string; binPath: string; descriptorPath: string } }> {
99
+ async restart(source: string, approved?: boolean) {
98
100
  this.restartGotSource = source;
99
101
  this.restartApproved = approved === true;
100
102
  if (this.restartFail) throw new Error("restart-service failed");
101
103
  return {
102
104
  output: `restarted the persistent service for ${source}`,
103
105
  restarted: true,
104
- spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" },
106
+ spec: {
107
+ name: "probe",
108
+ version: "1.0.0",
109
+ binPath: "/opt/probe/cli.js",
110
+ handlePath: "/tmp/probe.handle.json",
111
+ },
112
+ };
113
+ }
114
+ async reconcileAll(approved?: boolean, projectRoot?: string) {
115
+ this.reconcileGotApproved = approved;
116
+ this.reconcileGotProjectRoot = projectRoot;
117
+ if (this.reconcileFail) throw new Error("reconcile-services failed");
118
+ return {
119
+ ok: true,
120
+ output: "reconciled 1 Vehicle(s), skipped 0 non-daemon package(s)",
121
+ reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true }],
122
+ skipped: 0,
123
+ failed: [],
105
124
  };
106
125
  }
107
126
  }
@@ -454,67 +473,15 @@ describe("CLI", () => {
454
473
  it("install runs with stable human and JSON output", async () => {
455
474
  const d = deps();
456
475
  expect((await cliRun(["install", "npm:foo"], d)).code).toBe(1);
457
- // --no-service scopes this test to the base install output; service
458
- // auto-registration composition has its own dedicated tests below.
459
- const human = await cliRun(["install", "npm:foo", "--approve", "--no-service"], d);
476
+ const human = await cliRun(["install", "npm:foo", "--approve"], d);
460
477
  expect(human.code).toBe(0);
461
478
  expect(human.out).toContain("Installed npm:foo");
462
479
  expect((d.inst as FakeInstaller).approved).toBe(true);
463
- const json = await cliRun(["install", "npm:foo", "--approve", "--no-service", "--json"], d);
480
+ const json = await cliRun(["install", "npm:foo", "--approve", "--json"], d);
464
481
  expect(json.code).toBe(0);
465
482
  expect(JSON.parse(json.out)).toEqual({ ok: true, source: "npm:foo", output: "Installed npm:foo" });
466
483
  });
467
484
 
468
- it("install auto-registers a detected daemon service under the same approval, silently for a non-daemon package", async () => {
469
- const d = deps();
470
- const human = await cliRun(["install", "npm:foo", "--approve"], d);
471
- expect(human.code).toBe(0);
472
- expect(human.out).toContain("Installed npm:foo");
473
- expect(human.out).toContain("installed a persistent service for npm:foo");
474
- expect((d.daemonService as FakeDaemonServiceInstaller).approved).toBe(true);
475
- const json = await cliRun(["install", "npm:foo", "--approve", "--json"], d);
476
- expect(json.code).toBe(0);
477
- expect(JSON.parse(json.out)).toEqual({
478
- ok: true,
479
- source: "npm:foo",
480
- output: "Installed npm:foo",
481
- serviceInstall: { detected: true, ok: true, output: "installed a persistent service for npm:foo" },
482
- });
483
-
484
- // notADaemon: the overwhelmingly common case (an ordinary, non-daemon package) stays silent.
485
- const notADaemon = deps();
486
- (notADaemon.daemonService as FakeDaemonServiceInstaller).install = async () => {
487
- throw Object.assign(
488
- new Error("foo does not declare a packed.daemonService manifest and no Vehicle-shaped daemon dependency was detected"),
489
- { notADaemon: true },
490
- );
491
- };
492
- const silent = await cliRun(["install", "npm:foo", "--approve", "--json"], notADaemon);
493
- expect(JSON.parse(silent.out)).toEqual({ ok: true, source: "npm:foo", output: "Installed npm:foo" });
494
-
495
- // A genuine failure (a daemon was detected but registration itself failed) is reported
496
- // without failing the install, which already succeeded.
497
- const realFailure = deps();
498
- (realFailure.daemonService as FakeDaemonServiceInstaller).fail = true;
499
- const failed = await cliRun(["install", "npm:foo", "--approve", "--json"], realFailure);
500
- expect(failed.code).toBe(0);
501
- expect(JSON.parse(failed.out)).toEqual({
502
- ok: true,
503
- source: "npm:foo",
504
- output: "Installed npm:foo",
505
- serviceInstall: { detected: true, ok: false, output: "install-service failed" },
506
- });
507
-
508
- // --no-service skips the attempt entirely -- the daemonService fake is never called.
509
- const skipped = deps();
510
- await cliRun(["install", "npm:foo", "--approve", "--no-service"], skipped);
511
- expect((skipped.daemonService as FakeDaemonServiceInstaller).gotSource).toBe("");
512
-
513
- // A non-npm source never attempts service detection -- daemon-service resolution only supports npm: today.
514
- const gitSource = deps();
515
- await cliRun(["install", "git:github.com/u/r@v1", "--approve"], gitSource);
516
- expect((gitSource.daemonService as FakeDaemonServiceInstaller).gotSource).toBe("");
517
- });
518
485
 
519
486
  it("install-service validates source", async () => {
520
487
  const d = deps();
@@ -536,7 +503,12 @@ describe("CLI", () => {
536
503
  ok: true,
537
504
  source: "npm:foo",
538
505
  output: "installed a persistent service for npm:foo",
539
- spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" },
506
+ spec: {
507
+ name: "probe",
508
+ version: "1.0.0",
509
+ binPath: "/opt/probe/cli.js",
510
+ handlePath: "/tmp/probe.handle.json",
511
+ },
540
512
  });
541
513
  });
542
514
 
@@ -555,33 +527,52 @@ describe("CLI", () => {
555
527
  expect(out).toContain("requires a running packed daemon");
556
528
  });
557
529
 
558
- it("update delegates one configured source with stable output and approval", async () => {
530
+ it("reconcile-services requires approval, then sweeps every installed package with stable human and JSON output", async () => {
559
531
  const d = deps();
560
- expect((await cliRun(["update", "npm:foo"], d)).code).toBe(1);
561
- // --no-service scopes this test to the base update output; service restart
562
- // composition has its own dedicated test below, mirroring install's own split.
563
- const human = await cliRun(["update", "npm:foo", "--approve", "--no-service"], d);
564
- expect(human.out).toContain("Updated npm:foo");
565
- expect((d.inst as FakeInstaller).updated).toBe("npm:foo");
566
- expect((d.inst as FakeInstaller).approved).toBe(true);
567
- const json = await cliRun(["update", "npm:foo", "--approve", "--no-service", "--json"], d);
532
+ expect((await cliRun(["reconcile-services"], d)).code).toBe(1);
533
+ const human = await cliRun(["reconcile-services", "--approve"], d);
534
+ expect(human.code).toBe(0);
535
+ expect(human.out).toContain("reconciled 1 Vehicle(s)");
536
+ expect((d.daemonService as FakeDaemonServiceInstaller).reconcileGotApproved).toBe(true);
537
+ const json = await cliRun(["reconcile-services", "--approve", "--json"], d);
538
+ expect(json.code).toBe(0);
568
539
  expect(JSON.parse(json.out)).toEqual({
569
540
  ok: true,
570
- source: "npm:foo",
571
- output: "Updated npm:foo",
572
- reloadRequired: true,
573
- alreadyUpToDate: false,
574
- pinned: false,
541
+ output: "reconciled 1 Vehicle(s), skipped 0 non-daemon package(s)",
542
+ reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true }],
543
+ skipped: 0,
544
+ failed: [],
575
545
  });
576
546
  });
577
547
 
578
- it("update restarts a registered daemon service after a real change, silently for a non-daemon package", async () => {
548
+ it("reconcile-services threads --project through to the sweep", async () => {
549
+ const d = deps();
550
+ await cliRun(["reconcile-services", "--approve", "--project", "/repo"], d);
551
+ expect((d.daemonService as FakeDaemonServiceInstaller).reconcileGotProjectRoot).toBe("/repo");
552
+ });
553
+
554
+ it("reconcile-services reports a sweep failure in-band with exit code 1", async () => {
555
+ const d = deps();
556
+ (d.daemonService as FakeDaemonServiceInstaller).reconcileFail = true;
557
+ const { code, out } = await cliRun(["reconcile-services", "--approve"], d);
558
+ expect(code).toBe(1);
559
+ expect(out).toContain("reconcile-services failed");
560
+ });
561
+
562
+ it("reconcile-services fails closed without a running daemon", async () => {
563
+ const d = deps({ daemonService: undefined });
564
+ const { code, out } = await cliRun(["reconcile-services", "--approve"], d);
565
+ expect(code).toBe(1);
566
+ expect(out).toContain("requires a running packed daemon");
567
+ });
568
+
569
+ it("update delegates one configured source with stable output and approval", async () => {
579
570
  const d = deps();
571
+ expect((await cliRun(["update", "npm:foo"], d)).code).toBe(1);
580
572
  const human = await cliRun(["update", "npm:foo", "--approve"], d);
581
- expect(human.code).toBe(0);
582
573
  expect(human.out).toContain("Updated npm:foo");
583
- expect(human.out).toContain("restarted the persistent service for npm:foo");
584
- expect((d.daemonService as FakeDaemonServiceInstaller).restartApproved).toBe(true);
574
+ expect((d.inst as FakeInstaller).updated).toBe("npm:foo");
575
+ expect((d.inst as FakeInstaller).approved).toBe(true);
585
576
  const json = await cliRun(["update", "npm:foo", "--approve", "--json"], d);
586
577
  expect(JSON.parse(json.out)).toEqual({
587
578
  ok: true,
@@ -590,47 +581,10 @@ describe("CLI", () => {
590
581
  reloadRequired: true,
591
582
  alreadyUpToDate: false,
592
583
  pinned: false,
593
- serviceRestart: { detected: true, ok: true, output: "restarted the persistent service for npm:foo" },
594
584
  });
595
-
596
- // notADaemon: the overwhelmingly common case (an ordinary, non-daemon package) stays silent.
597
- const notADaemon = deps();
598
- (notADaemon.daemonService as FakeDaemonServiceInstaller).restart = async () => {
599
- throw Object.assign(
600
- new Error("foo does not declare a packed.daemonService manifest and no Vehicle-shaped daemon dependency was detected"),
601
- { notADaemon: true },
602
- );
603
- };
604
- const silent = await cliRun(["update", "npm:foo", "--approve", "--json"], notADaemon);
605
- expect(JSON.parse(silent.out)).toEqual({
606
- ok: true,
607
- source: "npm:foo",
608
- output: "Updated npm:foo",
609
- reloadRequired: true,
610
- alreadyUpToDate: false,
611
- pinned: false,
612
- });
613
-
614
- // A genuine failure (a daemon was detected but restarting it failed) is reported
615
- // without failing the update, which already succeeded.
616
- const realFailure = deps();
617
- (realFailure.daemonService as FakeDaemonServiceInstaller).restartFail = true;
618
- const failed = await cliRun(["update", "npm:foo", "--approve", "--json"], realFailure);
619
- expect(failed.code).toBe(0);
620
- const failedBody = JSON.parse(failed.out);
621
- expect(failedBody.serviceRestart).toEqual({ detected: true, ok: false, output: "restart-service failed" });
622
-
623
- // --no-service skips the attempt entirely -- the daemonService fake is never called.
624
- const skipped = deps();
625
- await cliRun(["update", "npm:foo", "--approve", "--no-service"], skipped);
626
- expect((skipped.daemonService as FakeDaemonServiceInstaller).restartGotSource).toBe("");
627
-
628
- // A non-npm source never attempts service detection -- daemon-service resolution only supports npm: today.
629
- const gitSource = deps({ inst: new FakeInstaller() });
630
- await cliRun(["update", "git:github.com/u/r@v1", "--approve"], gitSource);
631
- expect((gitSource.daemonService as FakeDaemonServiceInstaller).restartGotSource).toBe("");
632
585
  });
633
586
 
587
+
634
588
  it("update --self requires approval under the guarded default, same as every other mutation", async () => {
635
589
  const d = deps({
636
590
  selfUpdater: {
@@ -851,19 +805,36 @@ describe("CLI", () => {
851
805
  },
852
806
  async installService(source) {
853
807
  calls.push(`installService:${source}`);
854
- return { output: source, spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" } };
808
+ return {
809
+ output: source,
810
+ spec: {
811
+ name: "probe",
812
+ version: "1.0.0",
813
+ binPath: "/opt/probe/cli.js",
814
+ handlePath: "/tmp/probe.handle.json",
815
+ },
816
+ };
855
817
  },
856
818
  async restartService(source) {
857
819
  calls.push(`restartService:${source}`);
858
820
  return {
859
821
  output: source,
860
822
  restarted: true,
861
- spec: { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/tmp/probe.service" },
823
+ spec: {
824
+ name: "probe",
825
+ version: "1.0.0",
826
+ binPath: "/opt/probe/cli.js",
827
+ handlePath: "/tmp/probe.handle.json",
828
+ },
862
829
  };
863
830
  },
864
831
  async remove(name) {
865
832
  return name;
866
833
  },
834
+ async reconcileServices(approved, projectRoot) {
835
+ calls.push(`reconcileServices:${approved}:${projectRoot}`);
836
+ return { ok: true, output: "reconciled 0 Vehicle(s), skipped 0 non-daemon package(s)", reconciled: [], skipped: 0, failed: [] };
837
+ },
867
838
  async update(source) {
868
839
  return { output: source, reloadRequired: false, alreadyUpToDate: true, pinned: false };
869
840
  },
@@ -898,7 +869,7 @@ describe("CLI", () => {
898
869
  },
899
870
  async doctor(projectRoot) {
900
871
  calls.push(`doctor:${projectRoot}`);
901
- return { ok: true, conflicts: [], extensions: [], scanned: 0, truncated: false };
872
+ return { ok: true, conflicts: [], extensions: [], scanned: 0, truncated: false, serviceUnits: [] };
902
873
  },
903
874
  async updatesForProject(projectRoot) {
904
875
  calls.push(`updatesForProject:${projectRoot}`);
@@ -939,6 +910,7 @@ describe("CLI", () => {
939
910
  extensions: [],
940
911
  scanned: 0,
941
912
  truncated: false,
913
+ serviceUnits: [],
942
914
  });
943
915
  expect(JSON.parse((await cliRun(["updates", "--project", "/tmp/project", "--json"], d)).out).updates).toEqual([]);
944
916
  expect(calls).toEqual([
@@ -1049,7 +1021,7 @@ describe("CLI", () => {
1049
1021
  const d = deps({ piHome });
1050
1022
 
1051
1023
  const clean = await cliRun(["doctor", "--json"], d);
1052
- expect(clean.code).toBe(0);
1024
+ expect(clean.code, clean.out).toBe(0);
1053
1025
  expect(JSON.parse(clean.out)).toMatchObject({ ok: true, conflicts: [] });
1054
1026
 
1055
1027
  const withProject = await cliRun(["doctor", "--project", projectRoot, "--json"], d);
@@ -1104,16 +1076,27 @@ describe("daemon client", () => {
1104
1076
  reg: new FakeRegistry([{ name: "pi-lsp", version: "0.3.0" }]),
1105
1077
  inst: daemonInstaller,
1106
1078
  daemonServiceInstaller: {
1107
- install: () => ({
1108
- ok: true,
1109
- result: { installed: true },
1110
- spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js", descriptorPath: "/tmp/pi-lsp.service" },
1111
- }),
1112
- restart: () => ({
1113
- ok: true,
1114
- restarted: true,
1115
- spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js", descriptorPath: "/tmp/pi-lsp.service" },
1116
- }),
1079
+ async install() {
1080
+ return {
1081
+ ok: true,
1082
+ result: { installed: true },
1083
+ spec: { name: "pi-lsp", version: "1.0.0", binPath: "/opt/pi-lsp/cli.js", handlePath: "/tmp/pi-lsp.handle.json" },
1084
+ };
1085
+ },
1086
+ async remove() {
1087
+ return {
1088
+ ok: true,
1089
+ result: { installed: true },
1090
+ spec: { name: "pi-lsp", version: "1.0.0", binPath: "/opt/pi-lsp/cli.js", handlePath: "/tmp/pi-lsp.handle.json" },
1091
+ };
1092
+ },
1093
+ async restart() {
1094
+ return {
1095
+ ok: true,
1096
+ restarted: true,
1097
+ spec: { name: "pi-lsp", version: "1.0.0", binPath: "/opt/pi-lsp/cli.js", handlePath: "/tmp/pi-lsp.handle.json" },
1098
+ };
1099
+ },
1117
1100
  },
1118
1101
  token: daemonToken,
1119
1102
  stateDir: daemonDir,
@@ -1243,11 +1226,11 @@ describe("daemon client", () => {
1243
1226
  expect((await client.setupApply(`${checkRoot}/pi-setup.json`, true)).ok).toBe(true);
1244
1227
  expect(await client.security()).toEqual({ mutationApproval: "always" });
1245
1228
  await expect(client.install("npm:pi-lsp")).rejects.toThrow("approval required");
1246
- expect(await client.install("npm:pi-lsp", true)).toBe("Installed npm:pi-lsp");
1229
+ expect(await client.install("npm:pi-lsp", true)).toBe("Installed npm:pi-lsp\ninstalled persistent Vehicle pi-lsp");
1247
1230
  await expect(client.installService("npm:pi-lsp")).rejects.toThrow("approval required");
1248
1231
  expect(await client.installService("npm:pi-lsp", true)).toEqual({
1249
1232
  output: "installed a persistent service for pi-lsp",
1250
- spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js", descriptorPath: "/tmp/pi-lsp.service" },
1233
+ spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js" },
1251
1234
  });
1252
1235
  expect(await client.remove("pi-lsp", true)).toBe("Removed npm:pi-lsp");
1253
1236
  expect(await client.piStatus()).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
@@ -1260,7 +1243,9 @@ describe("daemon client", () => {
1260
1243
  currentVersion: undefined,
1261
1244
  });
1262
1245
  const installer = new PackageDaemonInstaller(client);
1263
- expect(await installer.install("npm:pi-lsp@1.0.0", { approved: true })).toBe("Installed npm:pi-lsp@1.0.0");
1246
+ expect(await installer.install("npm:pi-lsp@1.0.0", { approved: true })).toBe(
1247
+ "Installed npm:pi-lsp@1.0.0\ninstalled persistent Vehicle pi-lsp",
1248
+ );
1264
1249
  expect(await installer.remove("npm:pi-lsp", { approved: true })).toBe("Removed npm:pi-lsp");
1265
1250
  expect((await installer.update("npm:pi-lsp", { approved: true })).output).toBe("Updated npm:pi-lsp");
1266
1251
  expect(await client.setMutationApproval("never", true)).toEqual({ mutationApproval: "never" });
@@ -142,6 +142,7 @@ describe("daemon-kit migration", () => {
142
142
  "package.install",
143
143
  "package.install_service",
144
144
  "package.restart_service",
145
+ "package.reconcile_services",
145
146
  "package.remove",
146
147
  "package.update",
147
148
  "resources.list",
@@ -0,0 +1,127 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { ServiceInstallResult, ServiceSpec } from "@danypops/vehicle-server/service";
6
+ import { daemonOptions } from "../src/daemon/daemon.ts";
7
+ import type { Installer, Registry } from "../src/packages/package.ts";
8
+ import { RECONCILE_INTERVAL_DEFAULT_MS } from "../src/shared/constants.ts";
9
+ import { resolvePackedPaths } from "../src/shared/paths.ts";
10
+
11
+ const roots: string[] = [];
12
+ afterEach(() => {
13
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
14
+ });
15
+
16
+ function fakePiHome(packages: string[]): string {
17
+ const piHome = mkdtempSync(join(tmpdir(), "packed-maintenance-"));
18
+ roots.push(piHome);
19
+ writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages }));
20
+ return piHome;
21
+ }
22
+
23
+ function fakePaths() {
24
+ const root = mkdtempSync(join(tmpdir(), "packed-maintenance-state-"));
25
+ roots.push(root);
26
+ return resolvePackedPaths({ env: { PI_PACKED_HOME: root } });
27
+ }
28
+
29
+ const registry: Registry = {
30
+ async search() {
31
+ return { total: 0, results: [] };
32
+ },
33
+ async searchPage() {
34
+ return { total: 0, results: [] };
35
+ },
36
+ async searchAll() {
37
+ return [];
38
+ },
39
+ async info(name) {
40
+ return { name, version: "1.0.0" };
41
+ },
42
+ };
43
+
44
+ const installer: Installer = {
45
+ async install(source) {
46
+ return `installed ${source}`;
47
+ },
48
+ async remove(source) {
49
+ return `removed ${source}`;
50
+ },
51
+ async update(source) {
52
+ return { output: `updated ${source}`, reloadRequired: false, alreadyUpToDate: true, pinned: false };
53
+ },
54
+ };
55
+
56
+ class RecordingDaemonServiceInstaller {
57
+ gotSources: string[] = [];
58
+ async install(
59
+ _piHome: string,
60
+ source: string,
61
+ ): Promise<{ ok: true; result: ServiceInstallResult; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean }> {
62
+ this.gotSources.push(source);
63
+ return { ok: false, reason: "not a daemon", notADaemon: true };
64
+ }
65
+ async remove(): Promise<{ ok: true; result: ServiceInstallResult; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean }> {
66
+ return { ok: false, reason: "not a daemon", notADaemon: true };
67
+ }
68
+ async restart(): Promise<
69
+ { ok: true; restarted: boolean; reason?: string; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean }
70
+ > {
71
+ return { ok: false, reason: "not a daemon", notADaemon: true };
72
+ }
73
+ }
74
+
75
+ describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle drift, not just per-package on demand)", () => {
76
+ it("includes a vehicle-reconcile task, defaulting to RECONCILE_INTERVAL_DEFAULT_MS", () => {
77
+ const piHome = fakePiHome([]);
78
+ const paths = fakePaths();
79
+ const options = daemonOptions({ paths, reg: registry, inst: installer, piHome, maintenanceTasks: [] });
80
+ // maintenanceTasks: [] is itself the override under test setup convention (see
81
+ // daemon-kit-migration.test.ts) for every OTHER test in this file that needs the
82
+ // real default list; assert its shape directly here via a second, non-overridden call.
83
+ expect(options.maintenanceTasks).toEqual([]);
84
+ });
85
+
86
+ it("the default (non-overridden) maintenance list names vehicle-reconcile at the configured interval", () => {
87
+ const piHome = fakePiHome([]);
88
+ const paths = fakePaths();
89
+ const installerSpy = new RecordingDaemonServiceInstaller();
90
+ const options = daemonOptions({ paths, reg: registry, inst: installer, piHome, daemonServiceInstaller: installerSpy });
91
+ const task = options.maintenanceTasks?.find((t) => t.name === "vehicle-reconcile");
92
+ expect(task).toBeDefined();
93
+ expect(task?.intervalMs).toBe(RECONCILE_INTERVAL_DEFAULT_MS);
94
+ });
95
+
96
+ it("vehicle-reconcile sweeps every package declared in piHome's own settings through the injected installer", async () => {
97
+ const piHome = fakePiHome(["npm:@danypops/probe", "npm:@danypops/other"]);
98
+ const paths = fakePaths();
99
+ const installerSpy = new RecordingDaemonServiceInstaller();
100
+ const options = daemonOptions({ paths, reg: registry, inst: installer, piHome, daemonServiceInstaller: installerSpy });
101
+ const task = options.maintenanceTasks?.find((t) => t.name === "vehicle-reconcile");
102
+ expect(task).toBeDefined();
103
+
104
+ // daemonOptions() also fires every default task once at startup (see the dedicated
105
+ // startup-self-heal test below) -- let that finish, then isolate this test's own
106
+ // explicit invocation from it rather than racing the two.
107
+ await new Promise((resolveTick) => setTimeout(resolveTick, 20));
108
+ installerSpy.gotSources = [];
109
+ await task?.run();
110
+
111
+ expect(installerSpy.gotSources).toEqual(["npm:@danypops/probe", "npm:@danypops/other"]);
112
+ });
113
+
114
+ it("runs vehicle-reconcile once automatically at startup, not just on its own interval -- the actual self-heal", async () => {
115
+ const piHome = fakePiHome(["npm:@danypops/probe"]);
116
+ const paths = fakePaths();
117
+ const installerSpy = new RecordingDaemonServiceInstaller();
118
+ mkdirSync(join(piHome, "npm", "node_modules"), { recursive: true });
119
+ daemonOptions({ paths, reg: registry, inst: installer, piHome, daemonServiceInstaller: installerSpy });
120
+
121
+ // The startup fire-and-forget is intentionally not awaited by daemonOptions itself
122
+ // (matching every other default maintenance task) -- give its microtasks a turn.
123
+ await new Promise((resolveTick) => setTimeout(resolveTick, 10));
124
+
125
+ expect(installerSpy.gotSources).toEqual(["npm:@danypops/probe"]);
126
+ });
127
+ });