@danypops/pi-packed 0.19.11 → 0.19.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.19.11",
3
+ "version": "0.19.12",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -487,11 +487,40 @@ function cleanupManifestCheck(context: Context): void {
487
487
  }
488
488
  }
489
489
 
490
+ /** A conventional test file (path under a test/tests/__tests__ directory, or
491
+ * a .test./.spec. basename) never runs on a consumer's machine at
492
+ * install/import time -- it has no bearing on what a package's *shipped
493
+ * runtime* code actually imports, even though npm's own `files` field
494
+ * (a plain directory-prefix allowlist, e.g. "service") has no way to exclude
495
+ * it short of a package author remembering to. Real bug this fixes: a test
496
+ * file's own fixture data can contain string literals shaped exactly like
497
+ * real import statements (deliberately, to test this very checker's
498
+ * detection logic) without being real imports of the file that contains them. */
499
+ function isTestFile(path: string): boolean {
500
+ if (/(?:^|\/)(?:test|tests|__tests__)\//.test(path)) return true;
501
+ return /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(path);
502
+ }
503
+
490
504
  /** Shipped .js/.ts/.cjs/.mjs files that are actually contained within the
491
- * package root (never a symlink escape) -- the same file universe every
492
- * source-scanning check works over. */
505
+ * package root (never a symlink escape), excluding test files -- the same
506
+ * file universe every source-scanning check works over. */
493
507
  function shippedSourceFiles(context: Context): string[] {
494
- return shippedFiles(context).filter((path) => /\.[cm]?[jt]s$/.test(path) && isContainedFile(context.root, path));
508
+ return shippedFiles(context).filter(
509
+ (path) => /\.[cm]?[jt]s$/.test(path) && !isTestFile(path) && isContainedFile(context.root, path),
510
+ );
511
+ }
512
+
513
+ /**
514
+ * Strips line and block comments before the import-specifier regex below runs
515
+ * -- a comment can contain an arbitrary quoted "from ..." substring the regex
516
+ * would otherwise match as if it were real code (confirmed live against a
517
+ * JSDoc comment describing a bug fix in prose that happened to contain
518
+ * exactly that shape). Not a full lexer: a string literal that itself
519
+ * contains a comment delimiter could still confuse this, a pre-existing
520
+ * limit of a regex-based scan, not a regression this introduces.
521
+ */
522
+ function stripComments(source: string): string {
523
+ return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, "");
495
524
  }
496
525
 
497
526
  /** Every import/require specifier literal in one file, bounded by
@@ -500,7 +529,7 @@ function shippedSourceFiles(context: Context): string[] {
500
529
  function importSpecifiersIn(context: Context, file: string): string[] | undefined {
501
530
  const absolute = join(context.root, file);
502
531
  if (lstatSync(absolute).size > MAX_SOURCE_BYTES) return undefined;
503
- const source = readFileSync(absolute, "utf8");
532
+ const source = stripComments(readFileSync(absolute, "utf8"));
504
533
  return [...source.matchAll(/(?:from\s*|import\s*\(|require\s*\()\s*["']([^"']+)["']/g)].map((match) => match[1]!);
505
534
  }
506
535
 
@@ -523,6 +552,12 @@ function dependencyCheck(context: Context): void {
523
552
  for (const specifier of importSpecifiersIn(context, file) ?? []) {
524
553
  if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("node:") || specifier.startsWith("bun:")) continue;
525
554
  const name = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0]!;
555
+ // A package importing its own subpath exports (e.g. "@scope/pkg/client")
556
+ // is not a runtime dependency on itself -- there is no real range to
557
+ // declare, and node/bun resolve a self-reference via the package's own
558
+ // name field, not a node_modules entry that a `dependencies` line would
559
+ // describe.
560
+ if (name === context.pkg.name) continue;
526
561
  if (CORE_PACKAGES.has(name)) {
527
562
  if (peers[name] !== "*")
528
563
  context.add({
@@ -7,7 +7,9 @@
7
7
  * answers the same question proactively, before pi ever runs.
8
8
  */
9
9
  import { join } from "node:path";
10
+ import { createNodeServiceInstallDeps } from "@danypops/vehicle-server/service";
10
11
  import { listPackageResources, type PackageResources, resolveInstalledDir } from "../packages/resources.ts";
12
+ import { checkServiceUnitPaths, type ServiceUnitDiagnostic } from "./service-doctor.ts";
11
13
  import { runExtensionSmoke, type SmokeOptions, type SmokeRegistrations } from "./smoke.ts";
12
14
 
13
15
  const REGISTRATION_KINDS = ["tools", "commands", "shortcuts", "flags"] as const;
@@ -50,6 +52,8 @@ export interface DoctorReport {
50
52
  extensions: DoctorExtensionResult[];
51
53
  scanned: number;
52
54
  truncated: boolean;
55
+ /** A daemon-backed package's systemd --user unit referencing a path that no longer exists on disk -- see service-doctor.ts. Empty (not omitted) on a platform without systemd --user coverage. */
56
+ serviceUnits: ServiceUnitDiagnostic[];
53
57
  }
54
58
 
55
59
  interface ScanJob {
@@ -112,7 +116,16 @@ export async function runDoctor(piHome: string, projectRoot?: string, options: S
112
116
  }
113
117
  conflicts.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
114
118
 
115
- return { ok: conflicts.length === 0 && !anyNotOk, conflicts, extensions, scanned: bounded.length, truncated };
119
+ const serviceReport = checkServiceUnitPaths(piHome, projectRoot, createNodeServiceInstallDeps());
120
+
121
+ return {
122
+ ok: conflicts.length === 0 && !anyNotOk && serviceReport.ok,
123
+ conflicts,
124
+ extensions,
125
+ scanned: bounded.length,
126
+ truncated,
127
+ serviceUnits: serviceReport.diagnostics,
128
+ };
116
129
  }
117
130
 
118
131
  function formatClaimant(claim: DoctorClaim): string {
@@ -130,6 +143,9 @@ export function formatDoctorReport(report: DoctorReport, json: boolean): string
130
143
  if (extension.status === "ok") continue;
131
144
  out += `\n${extension.status.toUpperCase()} ${formatClaimant({ name: extension.name, source: extension.source, scope: extension.scope, extension: extension.extension })}${extension.message ? `: ${extension.message}` : ""}\n`;
132
145
  }
146
+ for (const diagnostic of report.serviceUnits) {
147
+ out += `\n${diagnostic.severity.toUpperCase()} ${diagnostic.code} ${diagnostic.package} (${diagnostic.unitName}.service): ${diagnostic.message}\n`;
148
+ }
133
149
  if (report.truncated) out += "\nOutput truncated: more enabled extensions exist than this run's bound.\n";
134
150
  return out;
135
151
  }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * service-doctor.ts — checks every installed package's own daemon systemd
3
+ * --user unit (if one is registered) for an ExecStart path that no longer
4
+ * exists on disk. A unit is written once, at install/service-install time,
5
+ * and never re-validated against the package's current on-disk layout --
6
+ * confirmed live: a refactor that moved a package's own CLI entrypoint left
7
+ * a real, already-registered unit pointing at a deleted file. The process
8
+ * kept running only because it had been loaded into memory before the file
9
+ * vanished; the next crash or reboot would have failed to restart at all,
10
+ * silently, until someone happened to notice the daemon was down.
11
+ *
12
+ * Deliberately separate from doctor.ts's own DoctorReport (extension
13
+ * registration-conflict scanning) -- a different subject (installed
14
+ * services vs. loaded extensions), composed alongside it in runDoctor()
15
+ * rather than folded into its shape.
16
+ */
17
+ import { existsSync } from "node:fs";
18
+ import type { ServiceInstallDeps } from "@danypops/vehicle-server/service";
19
+ import { isServiceInstalled } from "@danypops/vehicle-server/service";
20
+ import { readInstalledPackagesAcrossScopes } from "../packages/installed.ts";
21
+ import { resolveDaemonServiceSpec } from "../daemon/daemon-service.ts";
22
+
23
+ export interface ServiceUnitDiagnostic {
24
+ code: "SERVICE_EXEC_PATH_MISSING" | "SERVICE_EXEC_UNPARSEABLE";
25
+ severity: "error" | "warning";
26
+ package: string;
27
+ unitName: string;
28
+ message: string;
29
+ }
30
+
31
+ export interface ServiceDoctorReport {
32
+ ok: boolean;
33
+ diagnostics: ServiceUnitDiagnostic[];
34
+ checked: number;
35
+ }
36
+
37
+ /**
38
+ * Tokenizes ExecStart's own shell-quoted argument list -- matching
39
+ * generateSystemdUnit's shellQuote, which wraps every argument in double
40
+ * quotes and backslash-escapes only ", \, $, and ` inside them. Returns
41
+ * undefined for a missing or unrecognizable ExecStart line rather than
42
+ * guessing; the caller degrades that to a warning, never a false error.
43
+ */
44
+ export function parseExecStartTokens(unitText: string): string[] | undefined {
45
+ const line = unitText.split("\n").find((entry) => entry.trimStart().startsWith("ExecStart="));
46
+ if (!line) return undefined;
47
+ const raw = line.slice(line.indexOf("=") + 1);
48
+ const tokens: string[] = [];
49
+ const pattern = /"((?:[^"\\]|\\.)*)"/g;
50
+ let match: RegExpExecArray | null = pattern.exec(raw);
51
+ while (match !== null) {
52
+ tokens.push(match[1]!.replace(/\\(["\\$`])/g, "$1"));
53
+ match = pattern.exec(raw);
54
+ }
55
+ return tokens.length > 0 ? tokens : undefined;
56
+ }
57
+
58
+ /** A token worth existence-checking is one that's actually path-shaped -- a bare word like "serve" (a real subcommand argument, not a path) would otherwise resolve relative to this check's own cwd and produce a false SERVICE_EXEC_PATH_MISSING. */
59
+ function looksLikePath(token: string): boolean {
60
+ return token.includes("/");
61
+ }
62
+
63
+ export interface ServiceDoctorDeps extends Pick<ServiceInstallDeps, "fileExists" | "readFile"> {
64
+ platform?: string;
65
+ }
66
+
67
+ /**
68
+ * For every installed package that resolves to a real Vehicle daemon spec
69
+ * (same resolution daemon-service.ts's own install/restart commands use)
70
+ * and currently has a systemd --user unit registered, reads that unit's
71
+ * actual on-disk ExecStart (not a freshly recomputed "correct" one -- the
72
+ * whole point is catching a real unit that has drifted from what's true
73
+ * today) and flags any referenced path that no longer exists.
74
+ *
75
+ * Linux/systemd only, matching installUserService's own platform coverage
76
+ * -- macOS/Windows unit shapes differ and aren't checked here yet.
77
+ */
78
+ export function checkServiceUnitPaths(piHome: string, projectRoot: string | undefined, deps: ServiceDoctorDeps): ServiceDoctorReport {
79
+ const diagnostics: ServiceUnitDiagnostic[] = [];
80
+ const platform = deps.platform ?? process.platform;
81
+ let checked = 0;
82
+ if (platform !== "linux") return { ok: true, diagnostics, checked };
83
+
84
+ for (const pkg of readInstalledPackagesAcrossScopes(piHome, projectRoot)) {
85
+ const resolved = resolveDaemonServiceSpec(piHome, `npm:${pkg.name}`);
86
+ if (!resolved.ok) continue;
87
+ const spec = resolved.spec;
88
+ if (!isServiceInstalled(spec, deps as ServiceInstallDeps)) continue;
89
+ checked++;
90
+
91
+ const unitText = deps.readFile(spec.descriptorPath);
92
+ if (unitText === null) continue; // isServiceInstalled said yes but the read raced/failed -- transient, not a real finding.
93
+
94
+ const tokens = parseExecStartTokens(unitText);
95
+ if (!tokens) {
96
+ diagnostics.push({
97
+ code: "SERVICE_EXEC_UNPARSEABLE",
98
+ severity: "warning",
99
+ package: pkg.name,
100
+ unitName: spec.name,
101
+ message: `could not parse ExecStart in ${spec.descriptorPath}`,
102
+ });
103
+ continue;
104
+ }
105
+
106
+ for (const token of tokens) {
107
+ if (!looksLikePath(token) || existsSync(token)) continue;
108
+ diagnostics.push({
109
+ code: "SERVICE_EXEC_PATH_MISSING",
110
+ severity: "error",
111
+ package: pkg.name,
112
+ unitName: spec.name,
113
+ message: `${spec.name}.service's ExecStart references a path that no longer exists: ${token} -- the service will fail to (re)start the next time it stops. Reinstall it (packed install-service npm:${pkg.name}) to regenerate.`,
114
+ });
115
+ }
116
+ }
117
+
118
+ return { ok: diagnostics.every((diagnostic) => diagnostic.severity !== "error"), diagnostics, checked };
119
+ }
120
+
121
+ export function formatServiceDoctorReport(report: ServiceDoctorReport, json: boolean): string {
122
+ if (json) return `${JSON.stringify(report)}\n`;
123
+ let out = `${report.ok ? "PASS" : "FAIL"} — ${report.checked} service unit(s) checked, ${report.diagnostics.length} issue(s)\n`;
124
+ for (const diagnostic of report.diagnostics) {
125
+ out += `${diagnostic.severity.toUpperCase()} ${diagnostic.code} ${diagnostic.package} (${diagnostic.unitName}.service): ${diagnostic.message}\n`;
126
+ }
127
+ return out;
128
+ }
@@ -366,3 +366,79 @@ describe("packed check", () => {
366
366
  expect(codes(report.diagnostics)).toEqual(["PKG_JSON_INVALID"]);
367
367
  });
368
368
  });
369
+
370
+ describe("dependencyCheck false-positive regressions (packed check)", () => {
371
+ it("never scans a test file's own fixture strings as if they were real imports of that file", async () => {
372
+ // A checker's own test suite legitimately contains string literals shaped exactly
373
+ // like real import statements (fixture data for testing detection itself) -- these
374
+ // must never be attributed to the test file that merely contains them as text.
375
+ const root = fixture(
376
+ { ...base, files: ["extensions", "test", "README.md", "LICENSE"] },
377
+ {
378
+ "extensions/index.ts": "export default function () {}",
379
+ "test/check.test.ts": 'const fixtureSource = \'import lodash from "lodash";\';\n',
380
+ "README.md": "# Example",
381
+ LICENSE: "MIT",
382
+ },
383
+ );
384
+ const report = await checkPackage(root, { generic: false });
385
+ expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
386
+ });
387
+
388
+ it("never scans a test-directory file outside a *.test.ts basename either (e.g. a shared test helper)", async () => {
389
+ const root = fixture(
390
+ { ...base, files: ["extensions", "test", "README.md", "LICENSE"] },
391
+ {
392
+ "extensions/index.ts": "export default function () {}",
393
+ "test/helper.ts": 'import leftpad from "leftpad";\n',
394
+ "README.md": "# Example",
395
+ LICENSE: "MIT",
396
+ },
397
+ );
398
+ const report = await checkPackage(root, { generic: false });
399
+ expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
400
+ });
401
+
402
+ it("never treats a quoted 'from ...' phrase inside a comment as a real import specifier", async () => {
403
+ const root = fixture(
404
+ { ...base, files: ["extensions", "README.md", "LICENSE"] },
405
+ {
406
+ "extensions/index.ts":
407
+ '/**\n * migrated "latest" from "some-package that looks like an import"\n */\nexport default function () {}\n',
408
+ "README.md": "# Example",
409
+ LICENSE: "MIT",
410
+ },
411
+ );
412
+ const report = await checkPackage(root, { generic: false });
413
+ expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
414
+ });
415
+
416
+ it("still catches a real RUNTIME_DEPENDENCY_MISSING import sitting right next to a misleading comment", async () => {
417
+ const root = fixture(
418
+ { ...base, files: ["extensions", "README.md", "LICENSE"] },
419
+ {
420
+ "extensions/index.ts":
421
+ '// migrated "latest" from "totally not a real package"\nimport real from "real-missing-dep";\nexport default function () { return real; }\n',
422
+ "README.md": "# Example",
423
+ LICENSE: "MIT",
424
+ },
425
+ );
426
+ const report = await checkPackage(root, { generic: false });
427
+ expect(codes(report.diagnostics)).toContain("RUNTIME_DEPENDENCY_MISSING");
428
+ const diagnostic = report.diagnostics.find((d) => d.code === "RUNTIME_DEPENDENCY_MISSING")!;
429
+ expect(diagnostic.message).toContain("real-missing-dep");
430
+ });
431
+
432
+ it("never flags a package's own self-import of its own subpath export as a missing runtime dependency", async () => {
433
+ const root = fixture(
434
+ { ...base, name: "@scope/self-ref", files: ["extensions", "README.md", "LICENSE"] },
435
+ {
436
+ "extensions/index.ts": 'import { thing } from "@scope/self-ref/client";\nexport default function () { return thing; }\n',
437
+ "README.md": "# Example",
438
+ LICENSE: "MIT",
439
+ },
440
+ );
441
+ const report = await checkPackage(root, { generic: false });
442
+ expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
443
+ });
444
+ });
@@ -898,7 +898,7 @@ describe("CLI", () => {
898
898
  },
899
899
  async doctor(projectRoot) {
900
900
  calls.push(`doctor:${projectRoot}`);
901
- return { ok: true, conflicts: [], extensions: [], scanned: 0, truncated: false };
901
+ return { ok: true, conflicts: [], extensions: [], scanned: 0, truncated: false, serviceUnits: [] };
902
902
  },
903
903
  async updatesForProject(projectRoot) {
904
904
  calls.push(`updatesForProject:${projectRoot}`);
@@ -939,6 +939,7 @@ describe("CLI", () => {
939
939
  extensions: [],
940
940
  scanned: 0,
941
941
  truncated: false,
942
+ serviceUnits: [],
942
943
  });
943
944
  expect(JSON.parse((await cliRun(["updates", "--project", "/tmp/project", "--json"], d)).out).updates).toEqual([]);
944
945
  expect(calls).toEqual([
@@ -0,0 +1,142 @@
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 { checkServiceUnitPaths, parseExecStartTokens, type ServiceDoctorDeps } from "../src/adoption/service-doctor.ts";
6
+
7
+ const roots: string[] = [];
8
+ afterEach(() => {
9
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
10
+ });
11
+
12
+ function piHome(settingsPackages: unknown[]): string {
13
+ const root = mkdtempSync(join(tmpdir(), "packed-service-doctor-"));
14
+ roots.push(root);
15
+ writeFileSync(join(root, "settings.json"), JSON.stringify({ packages: settingsPackages }, null, 2));
16
+ return root;
17
+ }
18
+
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
+ function installDaemonPackage(home: string, name: string): string {
21
+ const dir = join(home, "npm", "node_modules", name);
22
+ mkdirSync(dir, { recursive: true });
23
+ writeFileSync(
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),
26
+ );
27
+ writeFileSync(join(dir, "cli.ts"), "// real entrypoint, present on disk\n");
28
+ return dir;
29
+ }
30
+
31
+ function fakeDeps(unitText: string | null): ServiceDoctorDeps {
32
+ return {
33
+ fileExists: () => unitText !== null,
34
+ readFile: () => unitText,
35
+ platform: "linux",
36
+ };
37
+ }
38
+
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
+ 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", () => {
67
+ const home = piHome(["npm:not-a-daemon"]);
68
+ const dir = join(home, "npm", "node_modules", "not-a-daemon");
69
+ 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 });
73
+ });
74
+
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", () => {
83
+ const home = piHome(["npm:fakedaemon"]);
84
+ 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
+ ]);
108
+ });
109
+
110
+ it("never flags a bare subcommand argument (no path separator) as a missing path", () => {
111
+ const home = piHome(["npm:fakedaemon"]);
112
+ 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);
118
+ });
119
+
120
+ it("degrades an unparseable ExecStart to a warning, never a false error", () => {
121
+ const home = piHome(["npm:fakedaemon"]);
122
+ 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 });
141
+ });
142
+ });