@danypops/pi-packed 0.19.12 → 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/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
|
@@ -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
|
+
});
|
|
@@ -2,8 +2,8 @@ import { describe, expect, it } from "bun:test";
|
|
|
2
2
|
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type {
|
|
6
|
-
import { detectVehicleDaemonService,
|
|
5
|
+
import type { VehicleRegistrar, VehicleRegistrationOutcome } from "@danypops/armada";
|
|
6
|
+
import { detectVehicleDaemonService, RealDaemonServiceInstaller, resolveDaemonServiceSpec } from "../src/daemon/daemon-service.ts";
|
|
7
7
|
|
|
8
8
|
function fakePiHome(): string {
|
|
9
9
|
return mkdtempSync(join(tmpdir(), "packed-daemon-service-"));
|
|
@@ -26,7 +26,12 @@ function writeRawPackage(dir: string, pkg: Record<string, unknown>): void {
|
|
|
26
26
|
describe("resolveDaemonServiceSpec", () => {
|
|
27
27
|
it("builds a ServiceSpec from a package's packed.daemonService manifest", () => {
|
|
28
28
|
const piHome = fakePiHome();
|
|
29
|
-
writePackage(piHome, "@danypops/web-spider-daemon", {
|
|
29
|
+
writePackage(piHome, "@danypops/web-spider-daemon", {
|
|
30
|
+
binPath: "dist/cli.js",
|
|
31
|
+
args: ["serve"],
|
|
32
|
+
restartOnFailure: true,
|
|
33
|
+
restartSec: 2,
|
|
34
|
+
});
|
|
30
35
|
|
|
31
36
|
const result = resolveDaemonServiceSpec(piHome, "npm:@danypops/web-spider-daemon");
|
|
32
37
|
expect(result).toEqual({
|
|
@@ -34,9 +39,13 @@ describe("resolveDaemonServiceSpec", () => {
|
|
|
34
39
|
spec: {
|
|
35
40
|
name: "web-spider-daemon",
|
|
36
41
|
displayName: undefined,
|
|
42
|
+
version: "1.0.0",
|
|
37
43
|
binPath: join(piHome, "npm", "node_modules", "@danypops/web-spider-daemon", "dist/cli.js"),
|
|
38
44
|
args: ["serve"],
|
|
39
|
-
|
|
45
|
+
handlePath: expect.stringContaining("web-spider-daemon") as unknown as string,
|
|
46
|
+
workingDirectory: undefined,
|
|
47
|
+
restartOnFailure: true,
|
|
48
|
+
restartSec: 2,
|
|
40
49
|
},
|
|
41
50
|
});
|
|
42
51
|
});
|
|
@@ -92,9 +101,11 @@ describe("resolveDaemonServiceSpec", () => {
|
|
|
92
101
|
spec: {
|
|
93
102
|
name: "papyrus",
|
|
94
103
|
displayName: undefined,
|
|
104
|
+
version: "1.0.0",
|
|
95
105
|
binPath: join(dir, "src/cli.ts"),
|
|
96
106
|
args: ["serve"],
|
|
97
|
-
|
|
107
|
+
handlePath: expect.stringContaining("papyrus") as unknown as string,
|
|
108
|
+
workingDirectory: undefined,
|
|
98
109
|
},
|
|
99
110
|
});
|
|
100
111
|
});
|
|
@@ -189,50 +200,92 @@ describe("resolveDaemonServiceSpec", () => {
|
|
|
189
200
|
});
|
|
190
201
|
});
|
|
191
202
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
class FakeVehicleRegistrar implements VehicleRegistrar {
|
|
204
|
+
registered = new Map<string, unknown>();
|
|
205
|
+
outcome: VehicleRegistrationOutcome = { ok: true, manifestHash: "hash" as never, applied: [], diagnostics: [] };
|
|
206
|
+
async register(vehicle: unknown): Promise<VehicleRegistrationOutcome> {
|
|
207
|
+
if (this.outcome.ok) this.registered.set((vehicle as { name: string }).name, vehicle);
|
|
208
|
+
return this.outcome;
|
|
209
|
+
}
|
|
210
|
+
async unregister(name: string): Promise<VehicleRegistrationOutcome> {
|
|
211
|
+
if (this.outcome.ok) this.registered.delete(name);
|
|
212
|
+
return this.outcome;
|
|
213
|
+
}
|
|
214
|
+
async isRegistered(name: string): Promise<boolean> {
|
|
215
|
+
return this.registered.has(name);
|
|
216
|
+
}
|
|
204
217
|
}
|
|
205
218
|
|
|
206
|
-
describe("
|
|
207
|
-
|
|
219
|
+
describe("RealDaemonServiceInstaller -- Armada registration through the in-process registrar", () => {
|
|
220
|
+
it("install() resolves the package's Vehicle spec and registers it through Armada directly", async () => {
|
|
221
|
+
const piHome = fakePiHome();
|
|
222
|
+
writePackage(piHome, "@danypops/web-spider-daemon", { binPath: "dist/cli.js", args: ["serve"] });
|
|
223
|
+
const registrar = new FakeVehicleRegistrar();
|
|
224
|
+
const installer = new RealDaemonServiceInstaller(registrar);
|
|
225
|
+
|
|
226
|
+
const outcome = await installer.install(piHome, "npm:@danypops/web-spider-daemon");
|
|
208
227
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
expect(outcome).toMatchObject({ ok: true, result: { installed: true }, spec: { name: "web-spider-daemon" } });
|
|
229
|
+
expect(registrar.registered.has("web-spider-daemon")).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("install() reports a non-daemon package without ever touching the registrar", async () => {
|
|
233
|
+
const piHome = fakePiHome();
|
|
234
|
+
writePackage(piHome, "some-pkg", undefined);
|
|
235
|
+
const registrar = new FakeVehicleRegistrar();
|
|
236
|
+
const installer = new RealDaemonServiceInstaller(registrar);
|
|
237
|
+
|
|
238
|
+
const outcome = await installer.install(piHome, "npm:some-pkg");
|
|
239
|
+
|
|
240
|
+
expect(outcome).toMatchObject({ ok: false, notADaemon: true });
|
|
241
|
+
expect(registrar.registered.size).toBe(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("remove() unregisters the resolved Vehicle through Armada directly", async () => {
|
|
245
|
+
const piHome = fakePiHome();
|
|
246
|
+
writePackage(piHome, "@danypops/web-spider-daemon", { binPath: "dist/cli.js", args: ["serve"] });
|
|
247
|
+
const registrar = new FakeVehicleRegistrar();
|
|
248
|
+
const installer = new RealDaemonServiceInstaller(registrar);
|
|
249
|
+
await installer.install(piHome, "npm:@danypops/web-spider-daemon");
|
|
217
250
|
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
expect(
|
|
251
|
+
const outcome = await installer.remove(piHome, "npm:@danypops/web-spider-daemon");
|
|
252
|
+
|
|
253
|
+
expect(outcome).toMatchObject({ ok: true, result: { installed: true } });
|
|
254
|
+
expect(registrar.registered.has("web-spider-daemon")).toBe(false);
|
|
221
255
|
});
|
|
222
256
|
|
|
223
|
-
it("
|
|
224
|
-
const
|
|
257
|
+
it("restart() is a no-op, in-band, when Armada has no registration for this Vehicle yet", async () => {
|
|
258
|
+
const piHome = fakePiHome();
|
|
259
|
+
writePackage(piHome, "@danypops/web-spider-daemon", { binPath: "dist/cli.js", args: ["serve"] });
|
|
260
|
+
const registrar = new FakeVehicleRegistrar();
|
|
261
|
+
const installer = new RealDaemonServiceInstaller(registrar);
|
|
262
|
+
|
|
263
|
+
const outcome = await installer.restart(piHome, "npm:@danypops/web-spider-daemon");
|
|
225
264
|
|
|
226
|
-
|
|
227
|
-
expect(result.restarted).toBe(false);
|
|
228
|
-
expect(result.reason).toContain("Unit probe.service not loaded");
|
|
265
|
+
expect(outcome).toMatchObject({ ok: true, restarted: false });
|
|
229
266
|
});
|
|
230
267
|
|
|
231
|
-
it("
|
|
232
|
-
const
|
|
268
|
+
it("restart() re-registers (picking up a fresh on-disk version) once already registered", async () => {
|
|
269
|
+
const piHome = fakePiHome();
|
|
270
|
+
writePackage(piHome, "@danypops/web-spider-daemon", { binPath: "dist/cli.js", args: ["serve"] });
|
|
271
|
+
const registrar = new FakeVehicleRegistrar();
|
|
272
|
+
const installer = new RealDaemonServiceInstaller(registrar);
|
|
273
|
+
await installer.install(piHome, "npm:@danypops/web-spider-daemon");
|
|
274
|
+
|
|
275
|
+
const outcome = await installer.restart(piHome, "npm:@danypops/web-spider-daemon");
|
|
276
|
+
|
|
277
|
+
expect(outcome).toMatchObject({ ok: true, restarted: true });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("surfaces a failed Armada registration's diagnostics in-band, never as a thrown error", async () => {
|
|
281
|
+
const piHome = fakePiHome();
|
|
282
|
+
writePackage(piHome, "@danypops/web-spider-daemon", { binPath: "dist/cli.js", args: ["serve"] });
|
|
283
|
+
const registrar = new FakeVehicleRegistrar();
|
|
284
|
+
registrar.outcome = { ok: false, diagnostics: [{ code: "X", severity: "error", path: "/", message: "native failure" }] };
|
|
285
|
+
const installer = new RealDaemonServiceInstaller(registrar);
|
|
286
|
+
|
|
287
|
+
const outcome = await installer.install(piHome, "npm:@danypops/web-spider-daemon");
|
|
233
288
|
|
|
234
|
-
|
|
235
|
-
expect(result.restarted).toBe(false);
|
|
236
|
-
expect(result.reason).toContain('platform "darwin"');
|
|
289
|
+
expect(outcome).toMatchObject({ ok: true, result: { installed: false, reason: "native failure" } });
|
|
237
290
|
});
|
|
238
291
|
});
|
|
@@ -81,7 +81,7 @@ describe("ensureClient (packed daemon auto-spawn-or-wait decision)", () => {
|
|
|
81
81
|
retryAttempts: 2,
|
|
82
82
|
retryDelayMs: 0,
|
|
83
83
|
}),
|
|
84
|
-
).rejects.toThrow(/
|
|
84
|
+
).rejects.toThrow(/managed service is installed/);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
it("fails with a plain timeout message, and did spawn, when no service is installed and nothing ever becomes reachable", async () => {
|
|
@@ -2,7 +2,8 @@ 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 {
|
|
5
|
+
import type { ServiceDoctorDeps } from "../src/adoption/service-doctor.ts";
|
|
6
|
+
import { checkServiceUnitPaths } from "../src/adoption/service-doctor.ts";
|
|
6
7
|
|
|
7
8
|
const roots: string[] = [];
|
|
8
9
|
afterEach(() => {
|
|
@@ -12,131 +13,88 @@ afterEach(() => {
|
|
|
12
13
|
function piHome(settingsPackages: unknown[]): string {
|
|
13
14
|
const root = mkdtempSync(join(tmpdir(), "packed-service-doctor-"));
|
|
14
15
|
roots.push(root);
|
|
15
|
-
writeFileSync(join(root, "settings.json"), JSON.stringify({ packages: settingsPackages }
|
|
16
|
+
writeFileSync(join(root, "settings.json"), JSON.stringify({ packages: settingsPackages }));
|
|
16
17
|
return root;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/** A real installed package on disk, shaped so detectVehicleDaemonService recognizes it as a daemon (own bin + a real dependency on @danypops/vehicle-server). */
|
|
20
20
|
function installDaemonPackage(home: string, name: string): string {
|
|
21
21
|
const dir = join(home, "npm", "node_modules", name);
|
|
22
22
|
mkdirSync(dir, { recursive: true });
|
|
23
23
|
writeFileSync(
|
|
24
24
|
join(dir, "package.json"),
|
|
25
|
-
JSON.stringify({
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
name,
|
|
27
|
+
version: "1.0.0",
|
|
28
|
+
packed: { daemonService: { binPath: "cli.ts", handleFilename: "handle.json" } },
|
|
29
|
+
}),
|
|
26
30
|
);
|
|
27
|
-
writeFileSync(join(dir, "cli.ts"), "//
|
|
31
|
+
writeFileSync(join(dir, "cli.ts"), "// entrypoint\n");
|
|
28
32
|
return dir;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
function fakeDeps(
|
|
35
|
+
function fakeDeps(status: unknown, fileExists = true): ServiceDoctorDeps {
|
|
32
36
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
armadaCliPath: "/armada/cli.js",
|
|
38
|
+
fileExists: () => fileExists,
|
|
39
|
+
runCommand: () => ({ ok: true, output: JSON.stringify(status) }),
|
|
36
40
|
};
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
describe("parseExecStartTokens", () => {
|
|
40
|
-
it("parses shell-quoted tokens, unescaping the same characters shellQuote escapes", () => {
|
|
41
|
-
const unit = ['[Service]', 'ExecStart="/home/x/.bun/bin/bun" "/home/x/pkg/cli.ts" "serve"', ''].join("\n");
|
|
42
|
-
expect(parseExecStartTokens(unit)).toEqual(["/home/x/.bun/bin/bun", "/home/x/pkg/cli.ts", "serve"]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("unescapes a backslash-escaped quote/backslash/dollar/backtick inside a token", () => {
|
|
46
|
-
const unit = 'ExecStart="/bin/bun" "/weird \\"path\\" with \\\\ and \\$ and \\`"';
|
|
47
|
-
expect(parseExecStartTokens(unit)).toEqual(["/bin/bun", '/weird "path" with \\ and $ and `']);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("returns undefined when there is no ExecStart line at all", () => {
|
|
51
|
-
expect(parseExecStartTokens("[Service]\nType=simple\n")).toBeUndefined();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("returns undefined for an ExecStart line with no quoted tokens", () => {
|
|
55
|
-
expect(parseExecStartTokens("ExecStart=\n")).toBeUndefined();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
43
|
describe("checkServiceUnitPaths", () => {
|
|
60
|
-
it("
|
|
61
|
-
const home = piHome(["npm:whatever"]);
|
|
62
|
-
const report = checkServiceUnitPaths(home, undefined, { fileExists: () => true, readFile: () => "", platform: "darwin" });
|
|
63
|
-
expect(report).toEqual({ ok: true, diagnostics: [], checked: 0 });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("skips a package that never resolves to a Vehicle-shaped daemon", () => {
|
|
44
|
+
it("skips packages with no Armada-managed Vehicle", () => {
|
|
67
45
|
const home = piHome(["npm:not-a-daemon"]);
|
|
68
46
|
const dir = join(home, "npm", "node_modules", "not-a-daemon");
|
|
69
47
|
mkdirSync(dir, { recursive: true });
|
|
70
|
-
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "not-a-daemon", version: "1.0.0" }
|
|
71
|
-
|
|
72
|
-
|
|
48
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "not-a-daemon", version: "1.0.0" }));
|
|
49
|
+
expect(checkServiceUnitPaths(home, undefined, fakeDeps({ vehicles: [], diagnostics: [] }))).toEqual({
|
|
50
|
+
ok: true,
|
|
51
|
+
diagnostics: [],
|
|
52
|
+
checked: 0,
|
|
53
|
+
});
|
|
73
54
|
});
|
|
74
55
|
|
|
75
|
-
it("
|
|
76
|
-
const home = piHome(["npm:fakedaemon"]);
|
|
77
|
-
installDaemonPackage(home, "fakedaemon");
|
|
78
|
-
const report = checkServiceUnitPaths(home, undefined, { fileExists: () => false, readFile: () => null, platform: "linux" });
|
|
79
|
-
expect(report).toEqual({ ok: true, diagnostics: [], checked: 0 });
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("reports clean when the installed unit's ExecStart paths all still exist", () => {
|
|
56
|
+
it("reports a clean Armada-managed executable", () => {
|
|
83
57
|
const home = piHome(["npm:fakedaemon"]);
|
|
84
58
|
const dir = installDaemonPackage(home, "fakedaemon");
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
it("flags SERVICE_EXEC_PATH_MISSING with the exact missing path when ExecStart references a deleted file", () => {
|
|
93
|
-
const home = piHome(["npm:fakedaemon"]);
|
|
94
|
-
installDaemonPackage(home, "fakedaemon");
|
|
95
|
-
const missing = join(tmpdir(), "packed-service-doctor-definitely-missing", "cli.ts");
|
|
96
|
-
const unit = `ExecStart="/bin/sh" "${missing}" "serve"\n`;
|
|
97
|
-
const report = checkServiceUnitPaths(home, undefined, fakeDeps(unit));
|
|
98
|
-
expect(report.ok).toBe(false);
|
|
99
|
-
expect(report.diagnostics).toEqual([
|
|
100
|
-
{
|
|
101
|
-
code: "SERVICE_EXEC_PATH_MISSING",
|
|
102
|
-
severity: "error",
|
|
103
|
-
package: "fakedaemon",
|
|
104
|
-
unitName: "fakedaemon",
|
|
105
|
-
message: expect.stringContaining(missing),
|
|
106
|
-
},
|
|
107
|
-
]);
|
|
59
|
+
const report = checkServiceUnitPaths(
|
|
60
|
+
home,
|
|
61
|
+
undefined,
|
|
62
|
+
fakeDeps({ vehicles: [{ name: "fakedaemon", executable: join(dir, "cli.ts") }], diagnostics: [] }),
|
|
63
|
+
);
|
|
64
|
+
expect(report).toEqual({ ok: true, diagnostics: [], checked: 1 });
|
|
108
65
|
});
|
|
109
66
|
|
|
110
|
-
it("
|
|
67
|
+
it("reports a missing Armada-managed executable", () => {
|
|
111
68
|
const home = piHome(["npm:fakedaemon"]);
|
|
112
69
|
const dir = installDaemonPackage(home, "fakedaemon");
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
70
|
+
const executable = join(dir, "cli.ts");
|
|
71
|
+
const report = checkServiceUnitPaths(
|
|
72
|
+
home,
|
|
73
|
+
undefined,
|
|
74
|
+
fakeDeps({ vehicles: [{ name: "fakedaemon", executable }], diagnostics: [] }, false),
|
|
75
|
+
);
|
|
76
|
+
expect(report).toEqual({
|
|
77
|
+
ok: false,
|
|
78
|
+
checked: 1,
|
|
79
|
+
diagnostics: [
|
|
80
|
+
{
|
|
81
|
+
code: "SERVICE_EXEC_PATH_MISSING",
|
|
82
|
+
severity: "error",
|
|
83
|
+
package: "fakedaemon",
|
|
84
|
+
unitName: "fakedaemon",
|
|
85
|
+
message: expect.stringContaining(executable),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
118
89
|
});
|
|
119
90
|
|
|
120
|
-
it("
|
|
91
|
+
it("reports Armada status failure without guessing from native descriptors", () => {
|
|
121
92
|
const home = piHome(["npm:fakedaemon"]);
|
|
122
93
|
installDaemonPackage(home, "fakedaemon");
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
severity: "warning",
|
|
129
|
-
package: "fakedaemon",
|
|
130
|
-
unitName: "fakedaemon",
|
|
131
|
-
message: expect.any(String),
|
|
132
|
-
},
|
|
133
|
-
]);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("treats a raced fileExists=true/readFile=null as transient, not a finding", () => {
|
|
137
|
-
const home = piHome(["npm:fakedaemon"]);
|
|
138
|
-
installDaemonPackage(home, "fakedaemon");
|
|
139
|
-
const report = checkServiceUnitPaths(home, undefined, { fileExists: () => true, readFile: () => null, platform: "linux" });
|
|
140
|
-
expect(report).toEqual({ ok: true, diagnostics: [], checked: 1 });
|
|
94
|
+
const deps = fakeDeps({});
|
|
95
|
+
deps.runCommand = () => ({ ok: false, output: "armada unavailable" });
|
|
96
|
+
const report = checkServiceUnitPaths(home, undefined, deps);
|
|
97
|
+
expect(report.ok).toBe(false);
|
|
98
|
+
expect(report.diagnostics[0]?.code).toBe("SERVICE_STATUS_UNAVAILABLE");
|
|
141
99
|
});
|
|
142
100
|
});
|