@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.
@@ -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 { ServiceInstallDeps } from "@danypops/vehicle-server/service";
6
- import { detectVehicleDaemonService, resolveDaemonServiceSpec, restartUserService } from "../src/daemon/daemon-service.ts";
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", { binPath: "dist/cli.js", args: ["serve"] });
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
- descriptorPath: expect.stringContaining("web-spider-daemon") as unknown as string,
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
- descriptorPath: expect.stringContaining("papyrus") as unknown as string,
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
- function fakeInstallDeps(over: Partial<ServiceInstallDeps> = {}): ServiceInstallDeps {
193
- return {
194
- platform: "linux",
195
- writeFile: () => {},
196
- readFile: () => null,
197
- removeFile: () => {},
198
- fileExists: () => false,
199
- mkdirp: () => {},
200
- runCommand: () => ({ ok: true, output: "" }),
201
- which: () => true,
202
- ...over,
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("restartUserService", () => {
207
- const spec = { name: "probe", binPath: "/opt/probe/cli.js", descriptorPath: "/home/user/.config/systemd/user/probe.service" };
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
- it("restarts via systemctl --user restart, using the descriptor's basename as the unit name", () => {
210
- const calls: Array<{ command: string; args: string[] }> = [];
211
- const deps = fakeInstallDeps({
212
- runCommand: (command, args) => {
213
- calls.push({ command, args });
214
- return { ok: true, output: "" };
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 result = restartUserService(spec, deps);
219
- expect(result).toEqual({ restarted: true });
220
- expect(calls).toEqual([{ command: "systemctl", args: ["--user", "restart", "probe.service"] }]);
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("reports a real systemctl failure without throwing", () => {
224
- const deps = fakeInstallDeps({ runCommand: () => ({ ok: false, output: "Unit probe.service not loaded" }) });
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
- const result = restartUserService(spec, deps);
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("refuses on a platform with no supported restart mechanism, rather than guessing at one", () => {
232
- const deps = fakeInstallDeps({ platform: "darwin" });
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
- const result = restartUserService(spec, deps);
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(/supervised service is installed/);
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 () => {
@@ -26,6 +26,7 @@ describe("package permission policy", () => {
26
26
  "install",
27
27
  "install_service",
28
28
  "restart_service",
29
+ "reconcile_services",
29
30
  "setup.apply",
30
31
  "update",
31
32
  "update.self",
@@ -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 { checkServiceUnitPaths, parseExecStartTokens, type ServiceDoctorDeps } from "../src/adoption/service-doctor.ts";
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 }, null, 2));
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({ name, version: "1.0.0", bin: { [name]: "cli.ts" }, dependencies: { "@danypops/vehicle-server": "^0.7.0" } }, null, 2),
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"), "// real entrypoint, present on disk\n");
31
+ writeFileSync(join(dir, "cli.ts"), "// entrypoint\n");
28
32
  return dir;
29
33
  }
30
34
 
31
- function fakeDeps(unitText: string | null): ServiceDoctorDeps {
35
+ function fakeDeps(status: unknown, fileExists = true): ServiceDoctorDeps {
32
36
  return {
33
- fileExists: () => unitText !== null,
34
- readFile: () => unitText,
35
- platform: "linux",
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("is silent on a non-linux platform without even reading installed packages", () => {
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" }, null, 2));
71
- const report = checkServiceUnitPaths(home, undefined, fakeDeps("ExecStart=\"/bin/true\"\n"));
72
- expect(report).toEqual({ ok: true, diagnostics: [], checked: 0 });
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("skips a daemon-shaped package with no systemd unit currently installed", () => {
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 unit = `ExecStart="/bin/sh" "${join(dir, "cli.ts")}" "serve"\n`;
86
- const report = checkServiceUnitPaths(home, undefined, fakeDeps(unit));
87
- expect(report.checked).toBe(1);
88
- expect(report.diagnostics).toEqual([]);
89
- expect(report.ok).toBe(true);
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("never flags a bare subcommand argument (no path separator) as a missing path", () => {
67
+ it("reports a missing Armada-managed executable", () => {
111
68
  const home = piHome(["npm:fakedaemon"]);
112
69
  const dir = installDaemonPackage(home, "fakedaemon");
113
- // "serve" alone would resolve relative to this process's own cwd and could
114
- // spuriously not exist there -- must never be treated as a path to check.
115
- const unit = `ExecStart="/bin/sh" "${join(dir, "cli.ts")}" "serve"\n`;
116
- const report = checkServiceUnitPaths(home, undefined, fakeDeps(unit));
117
- expect(report.diagnostics.some((d) => d.message.includes('"serve"'))).toBe(false);
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("degrades an unparseable ExecStart to a warning, never a false error", () => {
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 report = checkServiceUnitPaths(home, undefined, fakeDeps("[Service]\nType=simple\n"));
124
- expect(report.ok).toBe(true);
125
- expect(report.diagnostics).toEqual([
126
- {
127
- code: "SERVICE_EXEC_UNPARSEABLE",
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
  });