@blackbelt-technology/pi-agent-dashboard 0.2.9 → 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 +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -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/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- 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/prompt-expander.ts +25 -4
- 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 +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -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 +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -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__/cors.test.ts +34 -2
- 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-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -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__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- 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__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- 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-auth-routes.test.ts +13 -3
- 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__/recommended-routes.test.ts +389 -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__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- 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 +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- 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 +108 -9
- package/packages/server/src/browser-gateway.ts +16 -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 +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -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/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- 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/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- 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 +363 -26
- 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 +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- 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 +59 -3
- 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__/openspec-poller.test.ts +44 -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 +156 -0
- 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__/source-matching.test.ts +143 -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 +93 -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 +71 -49
- 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 +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -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
- package/packages/shared/src/types.ts +7 -0
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Smoke integration tests — validates end-to-end flows without SQLite.
|
|
3
3
|
*/
|
|
4
4
|
import { describe, it, expect, afterAll } from "vitest";
|
|
5
|
-
import {
|
|
5
|
+
import { createTestServer, type TestServerHandle } from "../test-support/test-server.js";
|
|
6
|
+
import type { DashboardServer } from "../server.js";
|
|
6
7
|
import { WebSocket } from "ws";
|
|
7
8
|
|
|
8
9
|
function waitForOpen(ws: WebSocket): Promise<void> {
|
|
@@ -24,22 +25,21 @@ function collectMsgs(ws: WebSocket, ms: number): Promise<any[]> {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
27
|
-
|
|
28
|
-
const piPort = 19071;
|
|
28
|
+
let handle: TestServerHandle;
|
|
29
29
|
let server: DashboardServer;
|
|
30
|
+
let httpPort: number;
|
|
31
|
+
let piPort: number;
|
|
30
32
|
|
|
31
33
|
describe("Smoke integration", () => {
|
|
32
34
|
afterAll(async () => {
|
|
33
|
-
if (
|
|
35
|
+
if (handle) await handle.stop();
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
it("9.2 — events flow and replay from memory on reconnect", async () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
await server.start();
|
|
39
|
+
handle = await createTestServer();
|
|
40
|
+
server = handle.server;
|
|
41
|
+
httpPort = handle.httpPort;
|
|
42
|
+
piPort = handle.piPort;
|
|
43
43
|
|
|
44
44
|
// Bridge connects and registers
|
|
45
45
|
const bridge = new WebSocket(`ws://localhost:${piPort}`);
|
|
@@ -25,6 +25,12 @@ vi.mock("node-pty", () => ({
|
|
|
25
25
|
})),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
+
// Mock platform/process.ts killProcess so the Windows path is observable in tests.
|
|
29
|
+
const mockKillProcess = vi.fn((..._args: unknown[]) => Promise.resolve({ ok: true, forced: false }));
|
|
30
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/process.js", () => ({
|
|
31
|
+
killProcess: (...args: unknown[]) => mockKillProcess(...args),
|
|
32
|
+
}));
|
|
33
|
+
|
|
28
34
|
describe("TerminalManager", () => {
|
|
29
35
|
let manager: TerminalManager;
|
|
30
36
|
let exitCallbacks: Array<(termId: string) => void>;
|
|
@@ -112,10 +118,44 @@ describe("TerminalManager", () => {
|
|
|
112
118
|
});
|
|
113
119
|
|
|
114
120
|
describe("kill", () => {
|
|
115
|
-
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
mockKillProcess.mockClear();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("POSIX: sends SIGHUP to PTY (bash on Linux ignores SIGTERM)", () => {
|
|
126
|
+
if (process.platform === "win32") return; // skipped on Windows; covered below
|
|
116
127
|
const session = manager.spawn("/tmp");
|
|
117
128
|
manager.kill(session.id);
|
|
118
129
|
expect(mockPtyKill).toHaveBeenCalledWith("SIGHUP");
|
|
130
|
+
expect(mockKillProcess).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("Windows: routes kill through platform killProcess (tree kill via taskkill /F /T)", () => {
|
|
134
|
+
if (process.platform !== "win32") return; // skipped off-Windows
|
|
135
|
+
const session = manager.spawn("C:\\tmp");
|
|
136
|
+
manager.kill(session.id);
|
|
137
|
+
// pty.kill MUST NOT be called on Windows — killProcess(pid) does the tree-kill.
|
|
138
|
+
expect(mockPtyKill).not.toHaveBeenCalled();
|
|
139
|
+
expect(mockKillProcess).toHaveBeenCalledWith(12345, expect.objectContaining({ timeoutMs: 2000 }));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("fallback cleanup fires if onExit does not within 3 s (simulates Windows ConPTY)", async () => {
|
|
143
|
+
vi.useFakeTimers();
|
|
144
|
+
try {
|
|
145
|
+
const session = manager.spawn("/tmp");
|
|
146
|
+
let exitCalled = false;
|
|
147
|
+
manager = createTerminalManager({
|
|
148
|
+
onExit: () => { exitCalled = true; },
|
|
149
|
+
});
|
|
150
|
+
const session2 = manager.spawn("/tmp");
|
|
151
|
+
manager.kill(session2.id);
|
|
152
|
+
// Simulate node-pty NOT firing onExit (the actual Windows failure mode).
|
|
153
|
+
await vi.advanceTimersByTimeAsync(3001);
|
|
154
|
+
expect(exitCalled).toBe(true);
|
|
155
|
+
expect(manager.get(session2.id)).toBeUndefined(); // removed from map
|
|
156
|
+
} finally {
|
|
157
|
+
vi.useRealTimers();
|
|
158
|
+
}
|
|
119
159
|
});
|
|
120
160
|
|
|
121
161
|
it("throws for unknown ID", () => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canary for createTestServer(): verifies that port:0 end-to-end resolution
|
|
3
|
+
* works and the helper returns non-zero, distinct ports.
|
|
4
|
+
*
|
|
5
|
+
* This test exists to de-risk the integration-test migration (tasks 4.x).
|
|
6
|
+
* If createServer / piGateway ever stop propagating resolved ports, this
|
|
7
|
+
* fails loudly before the other tests are touched.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
10
|
+
import { createTestServer, type TestServerHandle } from "../test-support/test-server.js";
|
|
11
|
+
|
|
12
|
+
let handle: TestServerHandle | undefined;
|
|
13
|
+
|
|
14
|
+
describe("createTestServer (port:0 canary)", () => {
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
if (handle) await handle.stop();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("resolves non-zero distinct ports and answers /api/health", async () => {
|
|
20
|
+
handle = await createTestServer();
|
|
21
|
+
|
|
22
|
+
expect(handle.httpPort).toBeGreaterThan(0);
|
|
23
|
+
expect(handle.piPort).toBeGreaterThan(0);
|
|
24
|
+
expect(handle.httpPort).not.toBe(handle.piPort);
|
|
25
|
+
|
|
26
|
+
const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
|
|
27
|
+
expect(res.status).toBe(200);
|
|
28
|
+
const body = await res.json();
|
|
29
|
+
expect(body.ok).toBe(true);
|
|
30
|
+
}, 15000);
|
|
31
|
+
});
|
|
@@ -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
|
+
});
|