@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.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/AGENTS.md +67 -116
- package/README.md +93 -7
- package/docs/architecture.md +408 -9
- package/package.json +6 -4
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/bridge.ts +69 -2
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +16 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +13 -7
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +8 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +211 -10
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +56 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +71 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +63 -46
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for /api/tools REST routes.
|
|
3
|
+
*
|
|
4
|
+
* Covers: list, get single, rescan (all / one), set override, clear
|
|
5
|
+
* override, unknown-tool 404, bad-body 400, diagnostics text format.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
8
|
+
import Fastify, { type FastifyInstance } from "fastify";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
ToolRegistry,
|
|
14
|
+
OverridesStore,
|
|
15
|
+
registerDefaultTools,
|
|
16
|
+
type Strategy,
|
|
17
|
+
} from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
|
|
18
|
+
import { registerToolRoutes, formatDiagnostics } from "../routes/tool-routes.js";
|
|
19
|
+
|
|
20
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function noGuard() {
|
|
23
|
+
return async () => { /* allow all */ };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tmpOverridesPath(): string {
|
|
27
|
+
return path.join(
|
|
28
|
+
os.tmpdir(),
|
|
29
|
+
`tool-routes-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a registry with two fake tools: `pi` (resolves) and `ghost`
|
|
35
|
+
* (never resolves). The `override` strategy honors ctx.overrides so
|
|
36
|
+
* set/clear flows are observable.
|
|
37
|
+
*/
|
|
38
|
+
function buildRegistry(opts?: { existsPath?: string }): ToolRegistry {
|
|
39
|
+
const overrides = new OverridesStore({ filePath: tmpOverridesPath(), warn: () => {} });
|
|
40
|
+
const r = new ToolRegistry({ overrides, platform: "linux" });
|
|
41
|
+
|
|
42
|
+
const piStrategies: Strategy[] = [
|
|
43
|
+
{
|
|
44
|
+
name: "override",
|
|
45
|
+
run: (ctx) => ctx.overrides["pi"]
|
|
46
|
+
? { ok: true, path: ctx.overrides["pi"] }
|
|
47
|
+
: { ok: false, reason: "no override set" },
|
|
48
|
+
},
|
|
49
|
+
{ name: "where", run: () => ({ ok: true, path: opts?.existsPath ?? "/usr/bin/pi" }) },
|
|
50
|
+
];
|
|
51
|
+
const ghostStrategies: Strategy[] = [
|
|
52
|
+
{ name: "override", run: (ctx) => ctx.overrides["ghost"]
|
|
53
|
+
? { ok: true, path: ctx.overrides["ghost"] }
|
|
54
|
+
: { ok: false, reason: "no override set" } },
|
|
55
|
+
{ name: "where", run: () => ({ ok: false, reason: "not found on PATH" }) },
|
|
56
|
+
];
|
|
57
|
+
r.register({ name: "pi", kind: "binary", strategies: piStrategies });
|
|
58
|
+
r.register({ name: "ghost", kind: "binary", strategies: ghostStrategies });
|
|
59
|
+
return r;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildServer(registry: ToolRegistry): FastifyInstance {
|
|
63
|
+
const fastify = Fastify();
|
|
64
|
+
registerToolRoutes(fastify, { registry, networkGuard: noGuard() });
|
|
65
|
+
return fastify;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
describe("GET /api/tools", () => {
|
|
71
|
+
let fastify: FastifyInstance;
|
|
72
|
+
beforeEach(() => { fastify = buildServer(buildRegistry()); });
|
|
73
|
+
afterEach(async () => { await fastify.close(); });
|
|
74
|
+
|
|
75
|
+
it("returns all registered tools", async () => {
|
|
76
|
+
const res = await fastify.inject({ method: "GET", url: "/api/tools" });
|
|
77
|
+
expect(res.statusCode).toBe(200);
|
|
78
|
+
const body = res.json();
|
|
79
|
+
expect(body.success).toBe(true);
|
|
80
|
+
const names = body.data.tools.map((t: any) => t.name).sort();
|
|
81
|
+
expect(names).toEqual(["ghost", "pi"]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("GET /api/tools/:name", () => {
|
|
86
|
+
let fastify: FastifyInstance;
|
|
87
|
+
beforeEach(() => { fastify = buildServer(buildRegistry()); });
|
|
88
|
+
afterEach(async () => { await fastify.close(); });
|
|
89
|
+
|
|
90
|
+
it("returns the resolution for a known tool", async () => {
|
|
91
|
+
const res = await fastify.inject({ method: "GET", url: "/api/tools/pi" });
|
|
92
|
+
expect(res.statusCode).toBe(200);
|
|
93
|
+
const body = res.json();
|
|
94
|
+
expect(body.data.name).toBe("pi");
|
|
95
|
+
expect(body.data.ok).toBe(true);
|
|
96
|
+
expect(body.data.path).toBe("/usr/bin/pi");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("404s for an unregistered tool", async () => {
|
|
100
|
+
const res = await fastify.inject({ method: "GET", url: "/api/tools/bogus" });
|
|
101
|
+
expect(res.statusCode).toBe(404);
|
|
102
|
+
expect(res.json().error).toMatch(/Unknown tool/);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("POST /api/tools/rescan", () => {
|
|
107
|
+
it("rescan() without body clears all caches", async () => {
|
|
108
|
+
let calls = 0;
|
|
109
|
+
const overrides = new OverridesStore({ filePath: tmpOverridesPath(), warn: () => {} });
|
|
110
|
+
const r = new ToolRegistry({ overrides, platform: "linux" });
|
|
111
|
+
r.register({
|
|
112
|
+
name: "pi",
|
|
113
|
+
kind: "binary",
|
|
114
|
+
strategies: [{ name: "where", run: () => ({ ok: true, path: `/pi${++calls}` }) }],
|
|
115
|
+
});
|
|
116
|
+
const fastify = buildServer(r);
|
|
117
|
+
|
|
118
|
+
await fastify.inject({ method: "GET", url: "/api/tools/pi" });
|
|
119
|
+
expect(r.resolve("pi").path).toBe("/pi1");
|
|
120
|
+
|
|
121
|
+
const res = await fastify.inject({ method: "POST", url: "/api/tools/rescan", payload: {} });
|
|
122
|
+
expect(res.statusCode).toBe(200);
|
|
123
|
+
expect(res.json().data.tools[0].path).toBe("/pi2");
|
|
124
|
+
await fastify.close();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rescan({ name }) only clears that tool's cache", async () => {
|
|
128
|
+
let piCalls = 0, ghostCalls = 0;
|
|
129
|
+
const overrides = new OverridesStore({ filePath: tmpOverridesPath(), warn: () => {} });
|
|
130
|
+
const r = new ToolRegistry({ overrides, platform: "linux" });
|
|
131
|
+
r.register({
|
|
132
|
+
name: "pi",
|
|
133
|
+
kind: "binary",
|
|
134
|
+
strategies: [{ name: "where", run: () => ({ ok: true, path: `/pi${++piCalls}` }) }],
|
|
135
|
+
});
|
|
136
|
+
r.register({
|
|
137
|
+
name: "ghost",
|
|
138
|
+
kind: "binary",
|
|
139
|
+
strategies: [{ name: "where", run: () => ({ ok: true, path: `/ghost${++ghostCalls}` }) }],
|
|
140
|
+
});
|
|
141
|
+
const fastify = buildServer(r);
|
|
142
|
+
|
|
143
|
+
r.resolve("pi"); r.resolve("ghost");
|
|
144
|
+
await fastify.inject({
|
|
145
|
+
method: "POST", url: "/api/tools/rescan", payload: { name: "pi" },
|
|
146
|
+
});
|
|
147
|
+
expect(r.resolve("pi").path).toBe("/pi2");
|
|
148
|
+
expect(r.resolve("ghost").path).toBe("/ghost1"); // unchanged
|
|
149
|
+
await fastify.close();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("404s when rescanning an unknown name", async () => {
|
|
153
|
+
const fastify = buildServer(buildRegistry());
|
|
154
|
+
const res = await fastify.inject({
|
|
155
|
+
method: "POST", url: "/api/tools/rescan", payload: { name: "bogus" },
|
|
156
|
+
});
|
|
157
|
+
expect(res.statusCode).toBe(404);
|
|
158
|
+
await fastify.close();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("PUT /api/tools/:name (set override)", () => {
|
|
163
|
+
it("sets the override and returns refreshed Resolution", async () => {
|
|
164
|
+
const r = buildRegistry();
|
|
165
|
+
const fastify = buildServer(r);
|
|
166
|
+
|
|
167
|
+
const res = await fastify.inject({
|
|
168
|
+
method: "PUT", url: "/api/tools/pi",
|
|
169
|
+
payload: { path: "/custom/pi" },
|
|
170
|
+
});
|
|
171
|
+
expect(res.statusCode).toBe(200);
|
|
172
|
+
const body = res.json();
|
|
173
|
+
expect(body.data.source).toBe("override");
|
|
174
|
+
expect(body.data.path).toBe("/custom/pi");
|
|
175
|
+
await fastify.close();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("400s on missing path body", async () => {
|
|
179
|
+
const fastify = buildServer(buildRegistry());
|
|
180
|
+
const res = await fastify.inject({
|
|
181
|
+
method: "PUT", url: "/api/tools/pi", payload: {},
|
|
182
|
+
});
|
|
183
|
+
expect(res.statusCode).toBe(400);
|
|
184
|
+
await fastify.close();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("404s for unknown tool name", async () => {
|
|
188
|
+
const fastify = buildServer(buildRegistry());
|
|
189
|
+
const res = await fastify.inject({
|
|
190
|
+
method: "PUT", url: "/api/tools/bogus", payload: { path: "/x" },
|
|
191
|
+
});
|
|
192
|
+
expect(res.statusCode).toBe(404);
|
|
193
|
+
await fastify.close();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("DELETE /api/tools/:name (clear override)", () => {
|
|
198
|
+
it("clears the override and returns refreshed Resolution", async () => {
|
|
199
|
+
const r = buildRegistry();
|
|
200
|
+
r.setOverride("pi", "/custom/pi");
|
|
201
|
+
const fastify = buildServer(r);
|
|
202
|
+
|
|
203
|
+
const res = await fastify.inject({ method: "DELETE", url: "/api/tools/pi" });
|
|
204
|
+
expect(res.statusCode).toBe(200);
|
|
205
|
+
expect(res.json().data.path).toBe("/usr/bin/pi");
|
|
206
|
+
await fastify.close();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("404s for unknown tool", async () => {
|
|
210
|
+
const fastify = buildServer(buildRegistry());
|
|
211
|
+
const res = await fastify.inject({ method: "DELETE", url: "/api/tools/bogus" });
|
|
212
|
+
expect(res.statusCode).toBe(404);
|
|
213
|
+
await fastify.close();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("POST /api/tools/diagnostics", () => {
|
|
218
|
+
it("returns text/plain with per-tool headers and trail lines", async () => {
|
|
219
|
+
const fastify = buildServer(buildRegistry());
|
|
220
|
+
const res = await fastify.inject({ method: "POST", url: "/api/tools/diagnostics" });
|
|
221
|
+
expect(res.statusCode).toBe(200);
|
|
222
|
+
expect(res.headers["content-type"]).toMatch(/text\/plain/);
|
|
223
|
+
const body = res.body;
|
|
224
|
+
// Header line per tool
|
|
225
|
+
expect(body).toMatch(/^\[ok\] +pi /m);
|
|
226
|
+
expect(body).toMatch(/^\[miss\] +ghost /m);
|
|
227
|
+
// Each attempted strategy appears with a leading dash
|
|
228
|
+
expect(body).toMatch(/- where: ok/);
|
|
229
|
+
expect(body).toMatch(/- where: not found on PATH/);
|
|
230
|
+
await fastify.close();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("formatDiagnostics is stable for unit testing", () => {
|
|
234
|
+
const text = formatDiagnostics([
|
|
235
|
+
{
|
|
236
|
+
name: "pi", ok: true, path: "/usr/bin/pi", source: "system",
|
|
237
|
+
tried: [{ strategy: "where", result: "ok" }],
|
|
238
|
+
resolvedAt: 0,
|
|
239
|
+
},
|
|
240
|
+
]);
|
|
241
|
+
expect(text).toMatch(/\[ok\] +pi \(system\) → \/usr\/bin\/pi/);
|
|
242
|
+
expect(text).toMatch(/- where: ok/);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("integration with default tool definitions", () => {
|
|
247
|
+
it("the standard registry exposes pi, openspec, git, npm etc. via GET /api/tools", async () => {
|
|
248
|
+
const overrides = new OverridesStore({ filePath: tmpOverridesPath(), warn: () => {} });
|
|
249
|
+
const r = new ToolRegistry({ overrides, platform: "linux" });
|
|
250
|
+
registerDefaultTools(r, {
|
|
251
|
+
exists: () => false,
|
|
252
|
+
which: () => null,
|
|
253
|
+
npmRootGlobal: () => "",
|
|
254
|
+
});
|
|
255
|
+
const fastify = buildServer(r);
|
|
256
|
+
|
|
257
|
+
const res = await fastify.inject({ method: "GET", url: "/api/tools" });
|
|
258
|
+
expect(res.statusCode).toBe(200);
|
|
259
|
+
const names = res.json().data.tools.map((t: any) => t.name).sort();
|
|
260
|
+
expect(names).toEqual(expect.arrayContaining(["git", "node", "npm", "openspec", "pi", "pi-coding-agent", "zrok"]));
|
|
261
|
+
expect(names).not.toContain("tsx");
|
|
262
|
+
expect(names).not.toContain("pi-dashboard");
|
|
263
|
+
await fastify.close();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Clean up tmp overrides files
|
|
268
|
+
afterAll();
|
|
269
|
+
function afterAll() {
|
|
270
|
+
try {
|
|
271
|
+
for (const f of fs.readdirSync(os.tmpdir())) {
|
|
272
|
+
if (f.startsWith("tool-routes-test-")) {
|
|
273
|
+
try { fs.unlinkSync(path.join(os.tmpdir(), f)); } catch {}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch {}
|
|
277
|
+
}
|
|
@@ -52,6 +52,25 @@ describe("trustedNetworks config", () => {
|
|
|
52
52
|
expect(config.resolvedTrustedNetworks).toContain("10.0.0.0/8");
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
// Companion to the test above. The archived trusted-networks spec scenario
|
|
56
|
+
// "trustedNetworks merged with auth.bypassHosts" as written did NOT include
|
|
57
|
+
// a `providers` field; the test above silently adds one to make it pass.
|
|
58
|
+
// This second test exercises the literal spec scenario — it would have
|
|
59
|
+
// failed pre-fix (parseAuthConfig nuked the whole auth block when
|
|
60
|
+
// providers was absent) and demonstrates the scenario as written now holds.
|
|
61
|
+
// See openspec/changes/fix-trusted-networks-no-oauth.
|
|
62
|
+
it("should merge trustedNetworks with auth.bypassHosts (no providers configured)", () => {
|
|
63
|
+
fs.writeFileSync(configFile, JSON.stringify({
|
|
64
|
+
trustedNetworks: ["192.168.1.0/24"],
|
|
65
|
+
auth: {
|
|
66
|
+
bypassHosts: ["10.0.0.0/8"],
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
const config = loadConfig();
|
|
70
|
+
expect(config.resolvedTrustedNetworks).toContain("192.168.1.0/24");
|
|
71
|
+
expect(config.resolvedTrustedNetworks).toContain("10.0.0.0/8");
|
|
72
|
+
});
|
|
73
|
+
|
|
55
74
|
it("should deduplicate entries", () => {
|
|
56
75
|
fs.writeFileSync(configFile, JSON.stringify({
|
|
57
76
|
trustedNetworks: ["192.168.1.0/24"],
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end regression test for fix-trusted-networks-no-oauth.
|
|
3
|
+
*
|
|
4
|
+
* Round-trips a bypassHosts-only auth config through the full
|
|
5
|
+
* write → disk → loadConfig → resolvedTrustedNetworks path.
|
|
6
|
+
*
|
|
7
|
+
* This is the test that would have caught the bug activated by
|
|
8
|
+
* eb24780 (consolidate-trusted-networks). The archived tasks.md
|
|
9
|
+
* section 5.4 claimed this case was "covered by unit test" — but
|
|
10
|
+
* the cited unit test only checked the React onChange handler's
|
|
11
|
+
* return value in memory, never writing to disk or reloading.
|
|
12
|
+
*
|
|
13
|
+
* This test asserts the end state: after the UI's PUT /api/config
|
|
14
|
+
* equivalent fires, a subsequent loadConfig() sees the entry in
|
|
15
|
+
* resolvedTrustedNetworks.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
18
|
+
import { writeConfigPartial } from "../config-api.js";
|
|
19
|
+
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
|
|
24
|
+
describe("fix-trusted-networks-no-oauth: round-trip", () => {
|
|
25
|
+
let testDir: string;
|
|
26
|
+
let configFile: string;
|
|
27
|
+
let origHome: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
testDir = path.join(os.tmpdir(), `test-no-oauth-rt-${Date.now()}`);
|
|
31
|
+
fs.mkdirSync(path.join(testDir, ".pi", "dashboard"), { recursive: true });
|
|
32
|
+
configFile = path.join(testDir, ".pi", "dashboard", "config.json");
|
|
33
|
+
origHome = process.env.HOME!;
|
|
34
|
+
process.env.HOME = testDir;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
process.env.HOME = origHome;
|
|
39
|
+
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("UI add → save → reload → resolvedTrustedNetworks contains entry", () => {
|
|
43
|
+
// Simulate fresh config (no auth section).
|
|
44
|
+
fs.writeFileSync(configFile, JSON.stringify({ port: 8000 }));
|
|
45
|
+
|
|
46
|
+
// Simulate the UI PUT /api/config from Settings → Security → Add
|
|
47
|
+
// with no OAuth configured (the case that broke users).
|
|
48
|
+
const writeResult = writeConfigPartial({
|
|
49
|
+
auth: { providers: {}, bypassHosts: ["192.168.1.0/24"] },
|
|
50
|
+
});
|
|
51
|
+
expect(writeResult.success).toBe(true);
|
|
52
|
+
|
|
53
|
+
// Disk assertion — the bypassHosts must actually land on disk.
|
|
54
|
+
const written = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
55
|
+
expect(written.auth).toBeDefined();
|
|
56
|
+
expect(written.auth.bypassHosts).toEqual(["192.168.1.0/24"]);
|
|
57
|
+
|
|
58
|
+
// Reload assertion — loadConfig must surface the entry in
|
|
59
|
+
// resolvedTrustedNetworks, which is what the network guard
|
|
60
|
+
// and the WS upgrade handler consult.
|
|
61
|
+
const loaded = loadConfig();
|
|
62
|
+
expect(loaded.auth).toBeDefined();
|
|
63
|
+
expect(loaded.auth!.bypassHosts).toEqual(["192.168.1.0/24"]);
|
|
64
|
+
expect(loaded.resolvedTrustedNetworks).toContain("192.168.1.0/24");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("UI add with existing OAuth → round-trip preserves both", () => {
|
|
68
|
+
fs.writeFileSync(
|
|
69
|
+
configFile,
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
port: 8000,
|
|
72
|
+
auth: {
|
|
73
|
+
secret: "s",
|
|
74
|
+
providers: { github: { clientId: "abc", clientSecret: "xyz" } },
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const writeResult = writeConfigPartial({
|
|
80
|
+
auth: { bypassHosts: ["10.0.0.0/8"] },
|
|
81
|
+
});
|
|
82
|
+
expect(writeResult.success).toBe(true);
|
|
83
|
+
|
|
84
|
+
const loaded = loadConfig();
|
|
85
|
+
expect(loaded.auth).toBeDefined();
|
|
86
|
+
expect(loaded.auth!.providers.github).toBeDefined();
|
|
87
|
+
expect(loaded.auth!.bypassHosts).toEqual(["10.0.0.0/8"]);
|
|
88
|
+
expect(loaded.resolvedTrustedNetworks).toContain("10.0.0.0/8");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("UI clear → save → reload → entry gone from resolvedTrustedNetworks", () => {
|
|
92
|
+
fs.writeFileSync(
|
|
93
|
+
configFile,
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
auth: { providers: {}, bypassHosts: ["192.168.1.0/24"] },
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const writeResult = writeConfigPartial({
|
|
100
|
+
auth: { bypassHosts: [] },
|
|
101
|
+
});
|
|
102
|
+
expect(writeResult.success).toBe(true);
|
|
103
|
+
|
|
104
|
+
const loaded = loadConfig();
|
|
105
|
+
// Loader returns auth === undefined when nothing auth-relevant remains.
|
|
106
|
+
// Either way, the trusted entry must not survive.
|
|
107
|
+
expect(loaded.resolvedTrustedNetworks).not.toContain("192.168.1.0/24");
|
|
108
|
+
expect(loaded.resolvedTrustedNetworks).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("hand-edited config (no UI write) with bypassHosts only → loads correctly", () => {
|
|
112
|
+
// This path simulates a user who edited config.json by hand
|
|
113
|
+
// rather than through the UI. The fix must make loadConfig
|
|
114
|
+
// honor this shape.
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
configFile,
|
|
117
|
+
JSON.stringify({
|
|
118
|
+
auth: { providers: {}, bypassHosts: ["192.168.0.0/24"] },
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const loaded = loadConfig();
|
|
123
|
+
expect(loaded.auth).toBeDefined();
|
|
124
|
+
expect(loaded.resolvedTrustedNetworks).toContain("192.168.0.0/24");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests that `cleanupStaleZrok` routes liveness + termination through the
|
|
3
|
+
* shared platform module rather than raw `process.kill`.
|
|
4
|
+
*
|
|
5
|
+
* See change: route-kill-paths-through-platform.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
const killProcessSpy = vi.fn(async (_pid: number, _opts?: any) => ({ ok: true, forced: false }));
|
|
13
|
+
const isProcessAliveSpy = vi.fn((_pid: number) => true);
|
|
14
|
+
|
|
15
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/process.js", async () => {
|
|
16
|
+
const actual = await vi.importActual<typeof import("@blackbelt-technology/pi-dashboard-shared/platform/process.js")>(
|
|
17
|
+
"@blackbelt-technology/pi-dashboard-shared/platform/process.js",
|
|
18
|
+
);
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
killProcess: (pid: number, opts?: any) => killProcessSpy(pid, opts),
|
|
22
|
+
isProcessAlive: (pid: number) => isProcessAliveSpy(pid),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const { cleanupStaleZrok, writeZrokPid, readZrokPid } = await import("../tunnel.js");
|
|
27
|
+
|
|
28
|
+
function pidFile(): string {
|
|
29
|
+
return path.join(os.homedir(), ".pi", "dashboard", "zrok.pid");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("cleanupStaleZrok uses platform helpers", () => {
|
|
33
|
+
const original = readZrokPid();
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
killProcessSpy.mockClear();
|
|
37
|
+
killProcessSpy.mockImplementation(async () => ({ ok: true, forced: false }));
|
|
38
|
+
isProcessAliveSpy.mockClear();
|
|
39
|
+
isProcessAliveSpy.mockReturnValue(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
// Restore prior PID file content if any.
|
|
44
|
+
try {
|
|
45
|
+
if (original !== null) writeZrokPid(original);
|
|
46
|
+
else fs.unlinkSync(pidFile());
|
|
47
|
+
} catch { /* ignore */ }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("calls platform killProcess when a live stale PID exists", async () => {
|
|
51
|
+
writeZrokPid(654321);
|
|
52
|
+
isProcessAliveSpy.mockReturnValue(true);
|
|
53
|
+
|
|
54
|
+
await cleanupStaleZrok();
|
|
55
|
+
|
|
56
|
+
expect(isProcessAliveSpy).toHaveBeenCalledWith(654321);
|
|
57
|
+
expect(killProcessSpy).toHaveBeenCalledOnce();
|
|
58
|
+
expect(killProcessSpy).toHaveBeenCalledWith(654321, expect.any(Object));
|
|
59
|
+
// PID file removed after cleanup
|
|
60
|
+
expect(readZrokPid()).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("skips killProcess when PID is already dead but still removes PID file", async () => {
|
|
64
|
+
writeZrokPid(654322);
|
|
65
|
+
isProcessAliveSpy.mockReturnValue(false);
|
|
66
|
+
|
|
67
|
+
await cleanupStaleZrok();
|
|
68
|
+
|
|
69
|
+
expect(killProcessSpy).not.toHaveBeenCalled();
|
|
70
|
+
expect(readZrokPid()).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("no-ops when no PID file exists", async () => {
|
|
74
|
+
try { fs.unlinkSync(pidFile()); } catch { /* ignore */ }
|
|
75
|
+
await cleanupStaleZrok();
|
|
76
|
+
expect(killProcessSpy).not.toHaveBeenCalled();
|
|
77
|
+
expect(isProcessAliveSpy).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does not invoke process.kill directly", async () => {
|
|
81
|
+
writeZrokPid(654323);
|
|
82
|
+
isProcessAliveSpy.mockReturnValue(true);
|
|
83
|
+
const processKillSpy = vi.spyOn(process, "kill");
|
|
84
|
+
|
|
85
|
+
await cleanupStaleZrok();
|
|
86
|
+
|
|
87
|
+
expect(processKillSpy).not.toHaveBeenCalled();
|
|
88
|
+
processKillSpy.mockRestore();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -160,30 +160,32 @@ describe("PID file helpers", () => {
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
describe("cleanupStaleZrok", () => {
|
|
163
|
-
it("should do nothing when no PID file exists", () => {
|
|
163
|
+
it("should do nothing when no PID file exists", async () => {
|
|
164
164
|
vi.mocked(fs.readFileSync).mockImplementation(() => {
|
|
165
165
|
throw new Error("ENOENT");
|
|
166
166
|
});
|
|
167
167
|
const killSpy = vi.spyOn(process, "kill");
|
|
168
168
|
|
|
169
|
-
cleanupStaleZrok();
|
|
169
|
+
await cleanupStaleZrok();
|
|
170
170
|
|
|
171
171
|
expect(killSpy).not.toHaveBeenCalled();
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
it("should kill running stale process and remove PID file", () => {
|
|
174
|
+
it("should kill running stale process and remove PID file", async () => {
|
|
175
175
|
vi.mocked(fs.readFileSync).mockReturnValue("99999\n");
|
|
176
176
|
const killSpy = vi.spyOn(process, "kill").mockReturnValue(true);
|
|
177
177
|
vi.mocked(fs.unlinkSync).mockReturnValue(undefined);
|
|
178
178
|
|
|
179
|
-
cleanupStaleZrok
|
|
179
|
+
// cleanupStaleZrok became async when it moved to platform/process's
|
|
180
|
+
// killProcess (SIGTERM+grace+SIGKILL orchestration).
|
|
181
|
+
await cleanupStaleZrok();
|
|
180
182
|
|
|
181
183
|
expect(killSpy).toHaveBeenCalledWith(99999, 0);
|
|
182
184
|
expect(killSpy).toHaveBeenCalledWith(99999, "SIGTERM");
|
|
183
185
|
expect(fs.unlinkSync).toHaveBeenCalled();
|
|
184
186
|
});
|
|
185
187
|
|
|
186
|
-
it("should just remove PID file if process is not running", () => {
|
|
188
|
+
it("should just remove PID file if process is not running", async () => {
|
|
187
189
|
vi.mocked(fs.readFileSync).mockReturnValue("99999\n");
|
|
188
190
|
const killSpy = vi.spyOn(process, "kill").mockImplementation((_pid: number, signal?: string | number) => {
|
|
189
191
|
if (signal === 0) throw new Error("ESRCH");
|
|
@@ -191,7 +193,7 @@ describe("cleanupStaleZrok", () => {
|
|
|
191
193
|
});
|
|
192
194
|
vi.mocked(fs.unlinkSync).mockReturnValue(undefined);
|
|
193
195
|
|
|
194
|
-
cleanupStaleZrok();
|
|
196
|
+
await cleanupStaleZrok();
|
|
195
197
|
|
|
196
198
|
expect(killSpy).toHaveBeenCalledTimes(1);
|
|
197
199
|
expect(fs.unlinkSync).toHaveBeenCalled();
|
|
@@ -254,8 +256,12 @@ describe("scavengeOrphanZrokProcesses", () => {
|
|
|
254
256
|
const killed = scavengeOrphanZrokProcesses(8000);
|
|
255
257
|
|
|
256
258
|
expect(killed).toEqual([12345]);
|
|
257
|
-
|
|
259
|
+
// Negative PID targets the whole process group on Unix (killPidWithGroup's
|
|
260
|
+
// contract); positive PID on Windows. Match whichever platform we're on.
|
|
261
|
+
const expectedPid = process.platform === "win32" ? 12345 : -12345;
|
|
262
|
+
expect(killSpy).toHaveBeenCalledWith(expectedPid, "SIGTERM");
|
|
258
263
|
expect(killSpy).not.toHaveBeenCalledWith(12346, expect.anything());
|
|
264
|
+
expect(killSpy).not.toHaveBeenCalledWith(-12346, expect.anything());
|
|
259
265
|
});
|
|
260
266
|
|
|
261
267
|
it("should return empty array on ps failure", () => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { _resetWslTmuxCacheForTests } from "../process-manager.js";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Structural test: the WSL-tmux probe MUST have a module-scoped cache so it's
|
|
12
|
+
* invoked at most once per server lifetime. Without this cache, users on
|
|
13
|
+
* Windows without `wt.exe` pay the full WSL VM cold-start cost (1.5–30 s) on
|
|
14
|
+
* every + Session click.
|
|
15
|
+
*
|
|
16
|
+
* We assert the cache exists by source inspection (tight, deterministic) and
|
|
17
|
+
* that a reset helper is exported for tests.
|
|
18
|
+
*/
|
|
19
|
+
describe("WSL-tmux probe cache invariant", () => {
|
|
20
|
+
const src = readFileSync(
|
|
21
|
+
path.resolve(__dirname, "../process-manager.ts"),
|
|
22
|
+
"utf-8",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
it("process-manager.ts declares _wslTmuxAvailabilityCache", () => {
|
|
26
|
+
expect(src).toMatch(/let\s+_wslTmuxAvailabilityCache\s*:\s*boolean\s*\|\s*null\s*=\s*null/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("isWslTmuxAvailable() returns the cached value when non-null", () => {
|
|
30
|
+
expect(src).toMatch(/if\s*\(\s*_wslTmuxAvailabilityCache\s*!==\s*null\s*\)\s*return\s+_wslTmuxAvailabilityCache/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("fallback-log fires at most once per server run", () => {
|
|
34
|
+
expect(src).toMatch(/_wslFallbackLogged\s*=\s*true/);
|
|
35
|
+
expect(src).toMatch(/if\s*\(\s*!_wslTmuxAvailabilityCache\s*&&\s*!_wslFallbackLogged\s*\)/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("exports a cache-reset helper for tests", () => {
|
|
39
|
+
expect(typeof _resetWslTmuxCacheForTests).toBe("function");
|
|
40
|
+
// reset is idempotent — safe to call repeatedly
|
|
41
|
+
_resetWslTmuxCacheForTests();
|
|
42
|
+
_resetWslTmuxCacheForTests();
|
|
43
|
+
});
|
|
44
|
+
});
|