@danypops/pi-packed 0.19.12 → 0.20.1
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/README.md +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/protocol.d.ts +0 -1
- package/dist/protocol.d.ts.map +1 -1
- package/extension/src/menu-theme.ts +17 -12
- package/extension/src/model.ts +11 -4
- package/extension/src/tui.ts +2 -2
- package/package.json +19 -6
- package/service/src/adoption/doctor.ts +5 -1
- package/service/src/adoption/service-doctor.ts +67 -94
- package/service/src/cli/cli.ts +32 -60
- package/service/src/daemon/client.ts +20 -26
- package/service/src/daemon/daemon-service.ts +127 -37
- package/service/src/daemon/daemon.ts +24 -2
- package/service/src/daemon/service.ts +76 -11
- package/service/src/daemon/vehicle-registration.ts +173 -0
- package/service/src/public/client.ts +7 -19
- package/service/src/public/protocol.ts +0 -1
- package/service/src/security/security.ts +2 -0
- package/service/src/shared/constants.ts +2 -0
- package/service/test/cli.test.ts +121 -137
- package/service/test/daemon-kit-migration.test.ts +1 -0
- package/service/test/daemon-maintenance.test.ts +127 -0
- package/service/test/daemon-service.test.ts +93 -40
- package/service/test/public-client.test.ts +1 -1
- package/service/test/security.test.ts +1 -0
- package/service/test/service-doctor.test.ts +53 -95
- package/service/test/service.test.ts +89 -13
- package/service/test/vehicle-registration.test.ts +121 -0
package/service/test/cli.test.ts
CHANGED
|
@@ -79,29 +79,48 @@ class FakeDaemonServiceInstaller {
|
|
|
79
79
|
restartGotSource = "";
|
|
80
80
|
restartApproved = false;
|
|
81
81
|
restartFail = false;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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", "--
|
|
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: {
|
|
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("
|
|
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(["
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
expect(
|
|
565
|
-
|
|
566
|
-
expect(
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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("
|
|
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(
|
|
584
|
-
expect((d.
|
|
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 {
|
|
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: {
|
|
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
|
},
|
|
@@ -1050,7 +1021,7 @@ describe("CLI", () => {
|
|
|
1050
1021
|
const d = deps({ piHome });
|
|
1051
1022
|
|
|
1052
1023
|
const clean = await cliRun(["doctor", "--json"], d);
|
|
1053
|
-
expect(clean.code).toBe(0);
|
|
1024
|
+
expect(clean.code, clean.out).toBe(0);
|
|
1054
1025
|
expect(JSON.parse(clean.out)).toMatchObject({ ok: true, conflicts: [] });
|
|
1055
1026
|
|
|
1056
1027
|
const withProject = await cliRun(["doctor", "--project", projectRoot, "--json"], d);
|
|
@@ -1105,16 +1076,27 @@ describe("daemon client", () => {
|
|
|
1105
1076
|
reg: new FakeRegistry([{ name: "pi-lsp", version: "0.3.0" }]),
|
|
1106
1077
|
inst: daemonInstaller,
|
|
1107
1078
|
daemonServiceInstaller: {
|
|
1108
|
-
install
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
+
},
|
|
1118
1100
|
},
|
|
1119
1101
|
token: daemonToken,
|
|
1120
1102
|
stateDir: daemonDir,
|
|
@@ -1244,11 +1226,11 @@ describe("daemon client", () => {
|
|
|
1244
1226
|
expect((await client.setupApply(`${checkRoot}/pi-setup.json`, true)).ok).toBe(true);
|
|
1245
1227
|
expect(await client.security()).toEqual({ mutationApproval: "always" });
|
|
1246
1228
|
await expect(client.install("npm:pi-lsp")).rejects.toThrow("approval required");
|
|
1247
|
-
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");
|
|
1248
1230
|
await expect(client.installService("npm:pi-lsp")).rejects.toThrow("approval required");
|
|
1249
1231
|
expect(await client.installService("npm:pi-lsp", true)).toEqual({
|
|
1250
1232
|
output: "installed a persistent service for pi-lsp",
|
|
1251
|
-
spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js"
|
|
1233
|
+
spec: { name: "pi-lsp", binPath: "/opt/pi-lsp/cli.js" },
|
|
1252
1234
|
});
|
|
1253
1235
|
expect(await client.remove("pi-lsp", true)).toBe("Removed npm:pi-lsp");
|
|
1254
1236
|
expect(await client.piStatus()).toEqual({ current: "0.82.1", latest: "0.83.0", upToDate: false });
|
|
@@ -1261,7 +1243,9 @@ describe("daemon client", () => {
|
|
|
1261
1243
|
currentVersion: undefined,
|
|
1262
1244
|
});
|
|
1263
1245
|
const installer = new PackageDaemonInstaller(client);
|
|
1264
|
-
expect(await installer.install("npm:pi-lsp@1.0.0", { approved: true })).toBe(
|
|
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
|
+
);
|
|
1265
1249
|
expect(await installer.remove("npm:pi-lsp", { approved: true })).toBe("Removed npm:pi-lsp");
|
|
1266
1250
|
expect((await installer.update("npm:pi-lsp", { approved: true })).output).toBe("Updated npm:pi-lsp");
|
|
1267
1251
|
expect(await client.setMutationApproval("never", true)).toEqual({ mutationApproval: "never" });
|
|
@@ -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
|
+
});
|