@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.
- 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/model.ts +11 -4
- package/package.json +17 -5
- package/service/src/adoption/check.ts +39 -4
- package/service/src/adoption/doctor.ts +21 -1
- package/service/src/adoption/service-doctor.ts +101 -0
- 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/check.test.ts +76 -0
- package/service/test/cli.test.ts +123 -138
- 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 +100 -0
- 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
|
},
|
|
@@ -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
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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"
|
|
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(
|
|
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" });
|
|
@@ -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
|
+
});
|