@danypops/pi-packed 0.19.12 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/protocol.d.ts +0 -1
- package/dist/protocol.d.ts.map +1 -1
- package/extension/src/menu-theme.ts +17 -12
- package/extension/src/model.ts +11 -4
- package/extension/src/tui.ts +2 -2
- package/package.json +19 -6
- package/service/src/adoption/doctor.ts +5 -1
- package/service/src/adoption/service-doctor.ts +67 -94
- package/service/src/cli/cli.ts +32 -60
- package/service/src/daemon/client.ts +20 -26
- package/service/src/daemon/daemon-service.ts +127 -37
- package/service/src/daemon/daemon.ts +24 -2
- package/service/src/daemon/service.ts +76 -11
- package/service/src/daemon/vehicle-registration.ts +173 -0
- package/service/src/public/client.ts +7 -19
- package/service/src/public/protocol.ts +0 -1
- package/service/src/security/security.ts +2 -0
- package/service/src/shared/constants.ts +2 -0
- package/service/test/cli.test.ts +121 -137
- package/service/test/daemon-kit-migration.test.ts +1 -0
- package/service/test/daemon-maintenance.test.ts +127 -0
- package/service/test/daemon-service.test.ts +93 -40
- package/service/test/public-client.test.ts +1 -1
- package/service/test/security.test.ts +1 -0
- package/service/test/service-doctor.test.ts +53 -95
- package/service/test/service.test.ts +89 -13
- package/service/test/vehicle-registration.test.ts +121 -0
|
@@ -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
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { ServiceSpec } from "@danypops/vehicle-server/service";
|
|
@@ -62,30 +62,45 @@ class FakeDaemonServiceInstaller implements DaemonServiceInstaller {
|
|
|
62
62
|
gotPiHome = "";
|
|
63
63
|
gotSource = "";
|
|
64
64
|
resolveFailure: string | undefined;
|
|
65
|
+
notADaemon = false;
|
|
65
66
|
installFailure: string | undefined;
|
|
67
|
+
removeGotPiHome = "";
|
|
68
|
+
removeGotSource = "";
|
|
66
69
|
restartGotPiHome = "";
|
|
67
70
|
restartGotSource = "";
|
|
68
71
|
restartResolveFailure: string | undefined;
|
|
72
|
+
restartNotADaemon = false;
|
|
69
73
|
restartReason: string | undefined;
|
|
70
74
|
restarted = true;
|
|
71
|
-
spec: ServiceSpec = {
|
|
72
|
-
|
|
75
|
+
spec: ServiceSpec = {
|
|
76
|
+
name: "probe",
|
|
77
|
+
version: "1.0.0",
|
|
78
|
+
binPath: "/opt/probe/cli.js",
|
|
79
|
+
handlePath: "/tmp/probe.handle.json",
|
|
80
|
+
};
|
|
81
|
+
async install(
|
|
73
82
|
piHome: string,
|
|
74
83
|
source: string,
|
|
75
|
-
): { ok: true; result: { installed: true } | { installed: false; reason: string }; spec: ServiceSpec } | { ok: false; reason: string } {
|
|
84
|
+
): Promise<{ ok: true; result: { installed: true } | { installed: false; reason: string }; spec: ServiceSpec } | { ok: false; reason: string }> {
|
|
76
85
|
this.gotPiHome = piHome;
|
|
77
86
|
this.gotSource = source;
|
|
78
|
-
if (this.resolveFailure) return { ok: false, reason: this.resolveFailure };
|
|
87
|
+
if (this.resolveFailure) return { ok: false, reason: this.resolveFailure, ...(this.notADaemon ? { notADaemon: true } : {}) };
|
|
79
88
|
if (this.installFailure) return { ok: true, result: { installed: false, reason: this.installFailure }, spec: this.spec };
|
|
80
89
|
return { ok: true, result: { installed: true }, spec: this.spec };
|
|
81
90
|
}
|
|
82
|
-
|
|
91
|
+
async remove(piHome: string, source: string): Promise<{ ok: true; result: { installed: true }; spec: ServiceSpec }> {
|
|
92
|
+
this.removeGotPiHome = piHome;
|
|
93
|
+
this.removeGotSource = source;
|
|
94
|
+
return { ok: true, result: { installed: true }, spec: this.spec };
|
|
95
|
+
}
|
|
96
|
+
async restart(
|
|
83
97
|
piHome: string,
|
|
84
98
|
source: string,
|
|
85
|
-
): { ok: true; restarted: boolean; reason?: string; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean } {
|
|
99
|
+
): Promise<{ ok: true; restarted: boolean; reason?: string; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean }> {
|
|
86
100
|
this.restartGotPiHome = piHome;
|
|
87
101
|
this.restartGotSource = source;
|
|
88
|
-
if (this.restartResolveFailure)
|
|
102
|
+
if (this.restartResolveFailure)
|
|
103
|
+
return { ok: false, reason: this.restartResolveFailure, ...(this.restartNotADaemon ? { notADaemon: true } : {}) };
|
|
89
104
|
return { ok: true, restarted: this.restarted, reason: this.restartReason, spec: this.spec };
|
|
90
105
|
}
|
|
91
106
|
}
|
|
@@ -224,7 +239,10 @@ describe("service app", () => {
|
|
|
224
239
|
|
|
225
240
|
it("POST /install accepts valid sources, reports failures in-band", async () => {
|
|
226
241
|
const inst = new FakeInstaller();
|
|
227
|
-
const
|
|
242
|
+
const service = new FakeDaemonServiceInstaller();
|
|
243
|
+
service.resolveFailure = "not a Vehicle";
|
|
244
|
+
service.notADaemon = true;
|
|
245
|
+
const app = createApp(deps({ inst, daemonServiceInstaller: service }));
|
|
228
246
|
for (const source of ["npm:foo", "npm:@scope/pkg@1.2.3", "git:github.com/u/r@v1", "https://github.com/u/r"]) {
|
|
229
247
|
const res = await app.fetch(
|
|
230
248
|
new Request("http://x/install", {
|
|
@@ -252,6 +270,23 @@ describe("service app", () => {
|
|
|
252
270
|
expect(body.output).toContain("npm ERR! 404");
|
|
253
271
|
});
|
|
254
272
|
|
|
273
|
+
it("POST /install configures an npm package's persistent Vehicle under the same approval", async () => {
|
|
274
|
+
const svc = new FakeDaemonServiceInstaller();
|
|
275
|
+
const piHome = mkdtempSync(join(tmpdir(), "packed-install-vehicle-"));
|
|
276
|
+
const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
|
|
277
|
+
const response = await app.fetch(
|
|
278
|
+
new Request("http://x/install", {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
281
|
+
body: JSON.stringify({ source: "npm:probe", approved: true }),
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
expect(response.status).toBe(200);
|
|
285
|
+
expect(await response.json()).toMatchObject({ ok: true, service: { name: "probe", binPath: "/opt/probe/cli.js" } });
|
|
286
|
+
expect(svc.gotPiHome).toBe(piHome);
|
|
287
|
+
expect(svc.gotSource).toBe("npm:probe");
|
|
288
|
+
});
|
|
289
|
+
|
|
255
290
|
it("POST /install-service rejects invalid sources and requires approval, matching /install's own guard", async () => {
|
|
256
291
|
const svc = new FakeDaemonServiceInstaller();
|
|
257
292
|
const app = createApp(deps({ daemonServiceInstaller: svc }));
|
|
@@ -307,7 +342,7 @@ describe("service app", () => {
|
|
|
307
342
|
const body = (await res.json()) as any;
|
|
308
343
|
expect(body.ok).toBe(true);
|
|
309
344
|
expect(body.output).toContain("probe");
|
|
310
|
-
expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js"
|
|
345
|
+
expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js" });
|
|
311
346
|
expect(svc.gotPiHome).toBe(piHome);
|
|
312
347
|
expect(svc.gotSource).toBe("npm:web-spider-daemon");
|
|
313
348
|
});
|
|
@@ -340,7 +375,7 @@ describe("service app", () => {
|
|
|
340
375
|
const body = (await installFailure.json()) as any;
|
|
341
376
|
expect(body.ok).toBe(false);
|
|
342
377
|
expect(body.output).toContain("no supported Linux init system");
|
|
343
|
-
expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js"
|
|
378
|
+
expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js" });
|
|
344
379
|
});
|
|
345
380
|
|
|
346
381
|
it("POST /restart-service rejects invalid sources and requires approval, matching /install-service's own guard", async () => {
|
|
@@ -385,7 +420,7 @@ describe("service app", () => {
|
|
|
385
420
|
const body = (await res.json()) as any;
|
|
386
421
|
expect(body.ok).toBe(true);
|
|
387
422
|
expect(body.restarted).toBe(true);
|
|
388
|
-
expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js"
|
|
423
|
+
expect(body.spec).toEqual({ name: "probe", binPath: "/opt/probe/cli.js" });
|
|
389
424
|
expect(svc.restartGotPiHome).toBe(piHome);
|
|
390
425
|
expect(svc.restartGotSource).toBe("npm:web-spider-daemon");
|
|
391
426
|
});
|
|
@@ -424,7 +459,10 @@ describe("service app", () => {
|
|
|
424
459
|
|
|
425
460
|
it("POST /update validates, authorizes, and delegates one Pi package source", async () => {
|
|
426
461
|
const inst = new FakeInstaller();
|
|
427
|
-
const
|
|
462
|
+
const service = new FakeDaemonServiceInstaller();
|
|
463
|
+
service.restartResolveFailure = "not a Vehicle";
|
|
464
|
+
service.restartNotADaemon = true;
|
|
465
|
+
const app = createApp(deps({ inst, daemonServiceInstaller: service }));
|
|
428
466
|
const denied = await app.fetch(
|
|
429
467
|
new Request("http://x/update", {
|
|
430
468
|
method: "POST",
|
|
@@ -452,6 +490,22 @@ describe("service app", () => {
|
|
|
452
490
|
expect(inst.updated).toBe("npm:pi-lsp");
|
|
453
491
|
});
|
|
454
492
|
|
|
493
|
+
it("POST /update reconciles an installed Vehicle after a real package change", async () => {
|
|
494
|
+
const svc = new FakeDaemonServiceInstaller();
|
|
495
|
+
const piHome = mkdtempSync(join(tmpdir(), "packed-update-vehicle-"));
|
|
496
|
+
const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
|
|
497
|
+
const response = await app.fetch(
|
|
498
|
+
new Request("http://x/update", {
|
|
499
|
+
method: "POST",
|
|
500
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
501
|
+
body: JSON.stringify({ source: "npm:probe", approved: true }),
|
|
502
|
+
}),
|
|
503
|
+
);
|
|
504
|
+
expect(await response.json()).toMatchObject({ ok: true, serviceReconciled: true });
|
|
505
|
+
expect(svc.restartGotPiHome).toBe(piHome);
|
|
506
|
+
expect(svc.restartGotSource).toBe("npm:probe");
|
|
507
|
+
});
|
|
508
|
+
|
|
455
509
|
it("POST /update reports an honest no-op instead of trusting pi's always-0-exit-code text", async () => {
|
|
456
510
|
const inst = new FakeInstaller();
|
|
457
511
|
inst.updateOutcome = { reloadRequired: false, alreadyUpToDate: true, pinned: true, previousVersion: "1.0.0", currentVersion: "1.0.0" };
|
|
@@ -527,6 +581,28 @@ describe("service app", () => {
|
|
|
527
581
|
expect(inst.removed).toBe("npm:pi-lsp");
|
|
528
582
|
});
|
|
529
583
|
|
|
584
|
+
it("POST /remove removes declared Vehicle state before deleting the package", async () => {
|
|
585
|
+
const piHome = mkdtempSync(join(tmpdir(), "packed-remove-vehicle-"));
|
|
586
|
+
const packageDir = join(piHome, "npm", "node_modules", "probe");
|
|
587
|
+
mkdirSync(packageDir, { recursive: true });
|
|
588
|
+
writeFileSync(
|
|
589
|
+
join(packageDir, "package.json"),
|
|
590
|
+
JSON.stringify({ name: "probe", version: "1.0.0", packed: { daemonService: { binPath: "cli.js" } } }),
|
|
591
|
+
);
|
|
592
|
+
const svc = new FakeDaemonServiceInstaller();
|
|
593
|
+
const app = createApp(deps({ piHome, daemonServiceInstaller: svc }));
|
|
594
|
+
const response = await app.fetch(
|
|
595
|
+
new Request("http://x/remove", {
|
|
596
|
+
method: "POST",
|
|
597
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
598
|
+
body: JSON.stringify({ name: "probe", approved: true }),
|
|
599
|
+
}),
|
|
600
|
+
);
|
|
601
|
+
expect((await response.json()) as unknown).toMatchObject({ ok: true, serviceRemoved: true });
|
|
602
|
+
expect(svc.removeGotPiHome).toBe(piHome);
|
|
603
|
+
expect(svc.removeGotSource).toBe("npm:probe");
|
|
604
|
+
});
|
|
605
|
+
|
|
530
606
|
it("GET /catalog serves the SQLite mirror", async () => {
|
|
531
607
|
const d = deps();
|
|
532
608
|
const db = openDb(dbPath(d.stateDir));
|