@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,28 +2,26 @@
|
|
|
2
2
|
* Tests for GET /api/health endpoint.
|
|
3
3
|
*/
|
|
4
4
|
import { describe, it, expect, afterEach } from "vitest";
|
|
5
|
-
import {
|
|
5
|
+
import { createTestServer, type TestServerHandle } from "../test-support/test-server.js";
|
|
6
|
+
import type { DashboardServer } from "../server.js";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let server: DashboardServer;
|
|
8
|
+
let handle: TestServerHandle | undefined;
|
|
9
|
+
let server: DashboardServer | undefined;
|
|
10
10
|
|
|
11
11
|
describe("GET /api/health", () => {
|
|
12
12
|
afterEach(async () => {
|
|
13
|
-
if (
|
|
14
|
-
try { await
|
|
13
|
+
if (handle) {
|
|
14
|
+
try { await handle.stop(); } catch { /* already stopped */ }
|
|
15
|
+
handle = undefined;
|
|
16
|
+
server = undefined;
|
|
15
17
|
}
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
it("should return ok, pid, and uptime", async () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
autoShutdown: false, shutdownIdleSeconds: 999, tunnel: false,
|
|
22
|
-
editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
|
|
23
|
-
});
|
|
24
|
-
await server.start();
|
|
21
|
+
handle = await createTestServer();
|
|
22
|
+
server = handle.server;
|
|
25
23
|
|
|
26
|
-
const res = await fetch(`http://localhost:${httpPort}/api/health`);
|
|
24
|
+
const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
|
|
27
25
|
expect(res.status).toBe(200);
|
|
28
26
|
|
|
29
27
|
const body = await res.json();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the PI_DASHBOARD_ALLOW_MULTIPLE escape hatch.
|
|
3
|
+
*
|
|
4
|
+
* The escape hatch is evaluated in `cli.ts::runForeground`; here we test
|
|
5
|
+
* the pure predicate `isLockDisabled` plus a behavioral test that confirms
|
|
6
|
+
* NO metadata is written when the lock is skipped.
|
|
7
|
+
*
|
|
8
|
+
* See change: single-dashboard-per-home, task 14.3.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { isLockDisabled, acquireOrAttach } from "../home-lock.js";
|
|
15
|
+
|
|
16
|
+
let tmpHome: string;
|
|
17
|
+
let lockPath: string;
|
|
18
|
+
let metaPath: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-escape-hatch-"));
|
|
22
|
+
lockPath = path.join(tmpHome, ".pi", "dashboard", "server.lock");
|
|
23
|
+
metaPath = `${lockPath}.meta.json`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("escape hatch", () => {
|
|
31
|
+
it("isLockDisabled true for =1 and =true", () => {
|
|
32
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "1" })).toBe(true);
|
|
33
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "true" })).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("isLockDisabled false when unset or other values", () => {
|
|
37
|
+
expect(isLockDisabled({})).toBe(false);
|
|
38
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "" })).toBe(false);
|
|
39
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "yes" })).toBe(false);
|
|
40
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "0" })).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("when caller skips acquireOrAttach (escape hatch on), no metadata sidecar exists", () => {
|
|
44
|
+
// The CLI-level behavior when PI_DASHBOARD_ALLOW_MULTIPLE is set is to
|
|
45
|
+
// NOT call acquireOrAttach at all. We simulate that: the fact that we
|
|
46
|
+
// never called acquireOrAttach means the sidecar was never written.
|
|
47
|
+
expect(fs.existsSync(metaPath)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("when lock IS acquired, metadata is written (control)", async () => {
|
|
51
|
+
const r = await acquireOrAttach({
|
|
52
|
+
httpPort: 8000, piPort: 9999, version: "t",
|
|
53
|
+
hooks: { lockPath, metaPath, staleMs: 500 },
|
|
54
|
+
});
|
|
55
|
+
expect(r.mode).toBe("acquired");
|
|
56
|
+
expect(fs.existsSync(metaPath)).toBe(true);
|
|
57
|
+
if (r.mode === "acquired") await r.release();
|
|
58
|
+
expect(fs.existsSync(metaPath)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for signal-handler installation. See change: single-dashboard-per-home.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from "vitest";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
import { installReleaseHandlers } from "../home-lock-release.js";
|
|
7
|
+
|
|
8
|
+
function fakeProcess() {
|
|
9
|
+
const ee = new EventEmitter() as unknown as NodeJS.Process;
|
|
10
|
+
(ee as unknown as { exit: (code: number) => void }).exit = vi.fn();
|
|
11
|
+
return ee;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("installReleaseHandlers", () => {
|
|
15
|
+
it("registers SIGINT, SIGTERM, SIGHUP, SIGBREAK, and exit handlers", () => {
|
|
16
|
+
const proc = fakeProcess();
|
|
17
|
+
const onSpy = vi.spyOn(proc, "on");
|
|
18
|
+
installReleaseHandlers(async () => {}, { proc });
|
|
19
|
+
const registered = onSpy.mock.calls.map(c => c[0]);
|
|
20
|
+
expect(registered).toEqual(expect.arrayContaining(["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK", "exit"]));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("calls release() on SIGTERM", async () => {
|
|
24
|
+
const proc = fakeProcess();
|
|
25
|
+
const release = vi.fn(async () => {});
|
|
26
|
+
installReleaseHandlers(release, { proc });
|
|
27
|
+
proc.emit("SIGTERM");
|
|
28
|
+
// Handler is async — let microtasks flush.
|
|
29
|
+
await new Promise(r => setImmediate(r));
|
|
30
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("calls release() on SIGBREAK (Windows Ctrl+Break)", async () => {
|
|
34
|
+
// On POSIX Node never emits SIGBREAK, but the handler must still be
|
|
35
|
+
// wired so Windows Ctrl+Break triggers lock release. Exercising via a
|
|
36
|
+
// fake process guarantees the registration + dispatch path works.
|
|
37
|
+
const proc = fakeProcess();
|
|
38
|
+
const release = vi.fn(async () => {});
|
|
39
|
+
installReleaseHandlers(release, { proc });
|
|
40
|
+
proc.emit("SIGBREAK" as NodeJS.Signals);
|
|
41
|
+
await new Promise(r => setImmediate(r));
|
|
42
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("calls release() on SIGHUP", async () => {
|
|
46
|
+
const proc = fakeProcess();
|
|
47
|
+
const release = vi.fn(async () => {});
|
|
48
|
+
installReleaseHandlers(release, { proc });
|
|
49
|
+
proc.emit("SIGHUP");
|
|
50
|
+
await new Promise(r => setImmediate(r));
|
|
51
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not double-release on repeated signals", async () => {
|
|
55
|
+
const proc = fakeProcess();
|
|
56
|
+
const release = vi.fn(async () => {});
|
|
57
|
+
installReleaseHandlers(release, { proc });
|
|
58
|
+
proc.emit("SIGTERM");
|
|
59
|
+
proc.emit("SIGTERM");
|
|
60
|
+
proc.emit("SIGINT");
|
|
61
|
+
await new Promise(r => setImmediate(r));
|
|
62
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns a dispose function that removes handlers", () => {
|
|
66
|
+
const proc = fakeProcess();
|
|
67
|
+
const release = vi.fn(async () => {});
|
|
68
|
+
const dispose = installReleaseHandlers(release, { proc });
|
|
69
|
+
dispose();
|
|
70
|
+
proc.emit("SIGTERM");
|
|
71
|
+
// After dispose, the release must not fire.
|
|
72
|
+
expect(release).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("swallows release errors but logs them", async () => {
|
|
76
|
+
const proc = fakeProcess();
|
|
77
|
+
const logs: string[] = [];
|
|
78
|
+
const release = vi.fn(async () => { throw new Error("boom"); });
|
|
79
|
+
installReleaseHandlers(release, { proc, log: (m) => logs.push(m) });
|
|
80
|
+
proc.emit("SIGTERM");
|
|
81
|
+
await new Promise(r => setImmediate(r));
|
|
82
|
+
await new Promise(r => setImmediate(r));
|
|
83
|
+
expect(logs.join("\n")).toContain("boom");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the per-HOME advisory lock.
|
|
3
|
+
* See change: single-dashboard-per-home.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
canonicalHomedir,
|
|
11
|
+
getLockPath,
|
|
12
|
+
getMetaPath,
|
|
13
|
+
readMetadata,
|
|
14
|
+
writeMetadataAtomic,
|
|
15
|
+
removeMetadata,
|
|
16
|
+
acquireOrAttach,
|
|
17
|
+
isLockHolderResponsive,
|
|
18
|
+
isLockDisabled,
|
|
19
|
+
InstanceLockMismatchError,
|
|
20
|
+
type LockMetadata,
|
|
21
|
+
} from "../home-lock.js";
|
|
22
|
+
|
|
23
|
+
// Fresh tmp dir per test → real FS (proper-lockfile needs real FS semantics).
|
|
24
|
+
let tmpHome: string;
|
|
25
|
+
let lockPath: string;
|
|
26
|
+
let metaPath: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-home-lock-test-"));
|
|
30
|
+
lockPath = path.join(tmpHome, ".pi", "dashboard", "server.lock");
|
|
31
|
+
metaPath = `${lockPath}.meta.json`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
try {
|
|
36
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
37
|
+
} catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function baseConfig(overrides: Partial<Parameters<typeof acquireOrAttach>[0]> = {}) {
|
|
43
|
+
return {
|
|
44
|
+
httpPort: 8000,
|
|
45
|
+
piPort: 9999,
|
|
46
|
+
version: "0.0.0-test",
|
|
47
|
+
hooks: {
|
|
48
|
+
lockPath,
|
|
49
|
+
metaPath,
|
|
50
|
+
staleMs: 500,
|
|
51
|
+
probeHealth: async () => ({ running: false }),
|
|
52
|
+
isProcessAlive: () => false,
|
|
53
|
+
...(overrides.hooks ?? {}),
|
|
54
|
+
},
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("canonicalHomedir + paths", () => {
|
|
60
|
+
it("returns a path containing .pi/dashboard/server.lock", () => {
|
|
61
|
+
const p = getLockPath();
|
|
62
|
+
expect(p.endsWith(path.join(".pi", "dashboard", "server.lock"))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("getMetaPath appends .meta.json", () => {
|
|
66
|
+
expect(getMetaPath("/x/y/server.lock")).toBe("/x/y/server.lock.meta.json");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("canonicalHomedir survives even when homedir is unreadable (tolerant)", () => {
|
|
70
|
+
expect(typeof canonicalHomedir()).toBe("string");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("ignores $HOME env override — lock path always derives from os.homedir()", () => {
|
|
74
|
+
// The design (§4) explicitly states $HOME must NOT influence the lock
|
|
75
|
+
// path: Git Bash sets $HOME=/c/Users/R while os.homedir()=C:\Users\R,
|
|
76
|
+
// which would otherwise produce two divergent canonical locks. Here we
|
|
77
|
+
// prove the invariant by construction: mutate process.env.HOME and
|
|
78
|
+
// verify getLockPath() doesn't change.
|
|
79
|
+
const original = process.env.HOME;
|
|
80
|
+
const before = getLockPath();
|
|
81
|
+
try {
|
|
82
|
+
process.env.HOME = "/garbage/not/a/real/path/" + Math.random();
|
|
83
|
+
const after = getLockPath();
|
|
84
|
+
expect(after).toBe(before);
|
|
85
|
+
} finally {
|
|
86
|
+
if (original === undefined) delete process.env.HOME;
|
|
87
|
+
else process.env.HOME = original;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("symlinked homedir canonicalizes to the same lock path on repeated calls", () => {
|
|
92
|
+
const real = fs.mkdtempSync(path.join(os.tmpdir(), "pi-real-"));
|
|
93
|
+
const link = path.join(os.tmpdir(), `pi-link-${Date.now()}-${Math.random()}`);
|
|
94
|
+
fs.symlinkSync(real, link);
|
|
95
|
+
try {
|
|
96
|
+
const a = fs.realpathSync(link);
|
|
97
|
+
const b = fs.realpathSync(link);
|
|
98
|
+
expect(a).toBe(b);
|
|
99
|
+
expect(a).toBe(fs.realpathSync(real));
|
|
100
|
+
} finally {
|
|
101
|
+
try { fs.unlinkSync(link); } catch { /* ignore */ }
|
|
102
|
+
fs.rmSync(real, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("writeMetadataAtomic + readMetadata", () => {
|
|
108
|
+
it("round-trips a metadata object", () => {
|
|
109
|
+
const meta: LockMetadata = {
|
|
110
|
+
pid: 1, ppid: 0, httpPort: 8000, piPort: 9999,
|
|
111
|
+
startedAt: 1, identity: "i", version: "v", url: "http://localhost:8000", hostname: "h",
|
|
112
|
+
};
|
|
113
|
+
writeMetadataAtomic(meta, metaPath);
|
|
114
|
+
expect(readMetadata(metaPath)).toEqual(meta);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("readMetadata returns null when file is missing", () => {
|
|
118
|
+
expect(readMetadata(metaPath)).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("readMetadata returns null when JSON is corrupt", () => {
|
|
122
|
+
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
|
|
123
|
+
fs.writeFileSync(metaPath, "{not json");
|
|
124
|
+
expect(readMetadata(metaPath)).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("readMetadata returns null for shape-mismatched JSON", () => {
|
|
128
|
+
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
|
|
129
|
+
fs.writeFileSync(metaPath, JSON.stringify({ foo: "bar" }));
|
|
130
|
+
expect(readMetadata(metaPath)).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("removeMetadata is silent on missing file", () => {
|
|
134
|
+
expect(() => removeMetadata(metaPath)).not.toThrow();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("isLockHolderResponsive", () => {
|
|
139
|
+
const meta: LockMetadata = {
|
|
140
|
+
pid: 12345, ppid: 0, httpPort: 8000, piPort: 9999,
|
|
141
|
+
startedAt: 0, identity: "id-A", version: "v", url: "http://localhost:8000", hostname: "h",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
it("returns 'dead' when PID is gone", async () => {
|
|
145
|
+
const result = await isLockHolderResponsive(meta, { isProcessAlive: () => false });
|
|
146
|
+
expect(result).toBe("dead");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("returns 'dead' when port is not responding", async () => {
|
|
150
|
+
const result = await isLockHolderResponsive(meta, {
|
|
151
|
+
isProcessAlive: () => true,
|
|
152
|
+
probeHealth: async () => ({ running: false }),
|
|
153
|
+
});
|
|
154
|
+
expect(result).toBe("dead");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("returns 'alive-match' when identity matches", async () => {
|
|
158
|
+
const result = await isLockHolderResponsive(meta, {
|
|
159
|
+
isProcessAlive: () => true,
|
|
160
|
+
probeHealth: async () => ({ running: true, identity: "id-A", pid: 12345 }),
|
|
161
|
+
});
|
|
162
|
+
expect(result).toBe("alive-match");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns 'alive-mismatch' when identity differs", async () => {
|
|
166
|
+
const result = await isLockHolderResponsive(meta, {
|
|
167
|
+
isProcessAlive: () => true,
|
|
168
|
+
probeHealth: async () => ({ running: true, identity: "id-B", pid: 99999 }),
|
|
169
|
+
});
|
|
170
|
+
expect(result).toBe("alive-mismatch");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("falls back to PID match when identity missing", async () => {
|
|
174
|
+
const matchByPid = await isLockHolderResponsive(meta, {
|
|
175
|
+
isProcessAlive: () => true,
|
|
176
|
+
probeHealth: async () => ({ running: true, pid: 12345 }),
|
|
177
|
+
});
|
|
178
|
+
expect(matchByPid).toBe("alive-match");
|
|
179
|
+
|
|
180
|
+
const misMatchByPid = await isLockHolderResponsive(meta, {
|
|
181
|
+
isProcessAlive: () => true,
|
|
182
|
+
probeHealth: async () => ({ running: true, pid: 99999 }),
|
|
183
|
+
});
|
|
184
|
+
expect(misMatchByPid).toBe("alive-mismatch");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("acquireOrAttach", () => {
|
|
189
|
+
it("acquires a fresh lock and writes metadata", async () => {
|
|
190
|
+
const result = await acquireOrAttach(baseConfig());
|
|
191
|
+
expect(result.mode).toBe("acquired");
|
|
192
|
+
const meta = readMetadata(metaPath);
|
|
193
|
+
expect(meta).not.toBeNull();
|
|
194
|
+
expect(meta?.pid).toBe(process.pid);
|
|
195
|
+
expect(meta?.httpPort).toBe(8000);
|
|
196
|
+
if (result.mode === "acquired") await result.release();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("release() removes the metadata sidecar", async () => {
|
|
200
|
+
const result = await acquireOrAttach(baseConfig());
|
|
201
|
+
expect(result.mode).toBe("acquired");
|
|
202
|
+
if (result.mode === "acquired") {
|
|
203
|
+
await result.release();
|
|
204
|
+
expect(readMetadata(metaPath)).toBeNull();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("release() is idempotent", async () => {
|
|
209
|
+
const result = await acquireOrAttach(baseConfig());
|
|
210
|
+
if (result.mode === "acquired") {
|
|
211
|
+
await result.release();
|
|
212
|
+
await expect(result.release()).resolves.toBeUndefined();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("attaches when a live dashboard already holds the lock", async () => {
|
|
217
|
+
// Acquire as "another process" first.
|
|
218
|
+
const first = await acquireOrAttach(baseConfig({
|
|
219
|
+
identity: "first-instance",
|
|
220
|
+
}));
|
|
221
|
+
expect(first.mode).toBe("acquired");
|
|
222
|
+
|
|
223
|
+
// Now mount a probe that says the first is alive + matches.
|
|
224
|
+
const second = await acquireOrAttach(baseConfig({
|
|
225
|
+
hooks: {
|
|
226
|
+
lockPath, metaPath, staleMs: 500,
|
|
227
|
+
isProcessAlive: () => true,
|
|
228
|
+
probeHealth: async () => ({ running: true, identity: "first-instance", pid: process.pid }),
|
|
229
|
+
},
|
|
230
|
+
}));
|
|
231
|
+
expect(second.mode).toBe("attach");
|
|
232
|
+
if (second.mode === "attach") {
|
|
233
|
+
expect(second.meta.identity).toBe("first-instance");
|
|
234
|
+
}
|
|
235
|
+
if (first.mode === "acquired") await first.release();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("throws InstanceLockMismatchError on identity mismatch", async () => {
|
|
239
|
+
const first = await acquireOrAttach(baseConfig({ identity: "mine" }));
|
|
240
|
+
expect(first.mode).toBe("acquired");
|
|
241
|
+
|
|
242
|
+
await expect(
|
|
243
|
+
acquireOrAttach(baseConfig({
|
|
244
|
+
hooks: {
|
|
245
|
+
lockPath, metaPath, staleMs: 500,
|
|
246
|
+
isProcessAlive: () => true,
|
|
247
|
+
probeHealth: async () => ({ running: true, identity: "someone-else", pid: 99999 }),
|
|
248
|
+
},
|
|
249
|
+
})),
|
|
250
|
+
).rejects.toBeInstanceOf(InstanceLockMismatchError);
|
|
251
|
+
|
|
252
|
+
if (first.mode === "acquired") await first.release();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("steals a stale lock (process dead)", async () => {
|
|
256
|
+
const first = await acquireOrAttach(baseConfig({ identity: "stale-holder" }));
|
|
257
|
+
expect(first.mode).toBe("acquired");
|
|
258
|
+
// Don't release — simulate a crash. Then attempt to reacquire with
|
|
259
|
+
// isProcessAlive=false → steal path.
|
|
260
|
+
|
|
261
|
+
// proper-lockfile's `stale` option needs the staleMs to have elapsed.
|
|
262
|
+
// We pass a 1ms stale threshold in baseConfig via the hooks override.
|
|
263
|
+
await new Promise(r => setTimeout(r, 50));
|
|
264
|
+
const second = await acquireOrAttach(baseConfig({
|
|
265
|
+
hooks: {
|
|
266
|
+
lockPath, metaPath, staleMs: 1,
|
|
267
|
+
isProcessAlive: () => false,
|
|
268
|
+
probeHealth: async () => ({ running: false }),
|
|
269
|
+
},
|
|
270
|
+
}));
|
|
271
|
+
expect(second.mode).toBe("acquired");
|
|
272
|
+
if (second.mode === "acquired") await second.release();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("steals lock when metadata is corrupt", async () => {
|
|
276
|
+
const first = await acquireOrAttach(baseConfig());
|
|
277
|
+
expect(first.mode).toBe("acquired");
|
|
278
|
+
// Corrupt metadata but leave proper-lockfile in place.
|
|
279
|
+
fs.writeFileSync(metaPath, "{not json");
|
|
280
|
+
await new Promise(r => setTimeout(r, 50));
|
|
281
|
+
|
|
282
|
+
const second = await acquireOrAttach(baseConfig({
|
|
283
|
+
hooks: {
|
|
284
|
+
lockPath, metaPath, staleMs: 1,
|
|
285
|
+
isProcessAlive: () => false,
|
|
286
|
+
probeHealth: async () => ({ running: false }),
|
|
287
|
+
},
|
|
288
|
+
}));
|
|
289
|
+
expect(second.mode).toBe("acquired");
|
|
290
|
+
if (second.mode === "acquired") await second.release();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("isLockDisabled", () => {
|
|
295
|
+
it("returns true for PI_DASHBOARD_ALLOW_MULTIPLE=1", () => {
|
|
296
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "1" })).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
it("returns true for PI_DASHBOARD_ALLOW_MULTIPLE=true", () => {
|
|
299
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "true" })).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
it("returns false when unset", () => {
|
|
302
|
+
expect(isLockDisabled({})).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
it("returns false for other values", () => {
|
|
305
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "0" })).toBe(false);
|
|
306
|
+
expect(isLockDisabled({ PI_DASHBOARD_ALLOW_MULTIPLE: "yes" })).toBe(false);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for isPiCommandLine (pure predicate used by isPiProcess).
|
|
3
|
+
* See change: fix-windows-server-parity.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { isPiCommandLine } from "../browser-handlers/session-action-handler.js";
|
|
7
|
+
|
|
8
|
+
describe("isPiCommandLine", () => {
|
|
9
|
+
it("matches a typical pi cli invocation", () => {
|
|
10
|
+
expect(isPiCommandLine("/usr/bin/node /usr/local/lib/node_modules/@mariozechner/pi-coding-agent/dist/cli.js")).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("matches when only 'pi' appears as a word", () => {
|
|
14
|
+
expect(isPiCommandLine("pi --mode rpc")).toBe(true);
|
|
15
|
+
expect(isPiCommandLine("/opt/pi/bin/pi")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("matches when only 'node' appears as a word", () => {
|
|
19
|
+
expect(isPiCommandLine("node server.js")).toBe(true);
|
|
20
|
+
expect(isPiCommandLine("/usr/bin/node --import tsx /app.ts")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("does not match unrelated commands", () => {
|
|
24
|
+
expect(isPiCommandLine("/bin/bash -c sleep 10")).toBe(false);
|
|
25
|
+
expect(isPiCommandLine("python3 script.py")).toBe(false);
|
|
26
|
+
expect(isPiCommandLine("")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does not match substrings of other words", () => {
|
|
30
|
+
// \b word-boundary: 'api', 'epic', 'snode' must NOT match 'pi'/'node'
|
|
31
|
+
expect(isPiCommandLine("api-server --port 8000")).toBe(false);
|
|
32
|
+
expect(isPiCommandLine("epic-game.exe")).toBe(false);
|
|
33
|
+
// 'snode' is actually a whole word containing "node" at the end; \bnode\b requires word boundary
|
|
34
|
+
expect(isPiCommandLine("running snode-worker")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildNodeUpgradeMessage, isAffectedNode } from "../node-guard.js";
|
|
3
|
+
|
|
4
|
+
describe("isAffectedNode", () => {
|
|
5
|
+
it("returns true for v22.0.0 (lower bound of 22.x affected)", () => {
|
|
6
|
+
expect(isAffectedNode("v22.0.0")).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns true for v22.17.999 (upper bound of 22.x affected)", () => {
|
|
10
|
+
expect(isAffectedNode("v22.17.999")).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns false for v22.18.0 (first 22.x fixed)", () => {
|
|
14
|
+
expect(isAffectedNode("v22.18.0")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns false for v22.22.2 (current LTS)", () => {
|
|
18
|
+
expect(isAffectedNode("v22.22.2")).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns true for v24.1.0 (lower bound of 24.x affected)", () => {
|
|
22
|
+
expect(isAffectedNode("v24.1.0")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns true for v24.2.999 (upper bound of 24.x affected)", () => {
|
|
26
|
+
expect(isAffectedNode("v24.2.999")).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns false for v24.3.0 (first 24.x fixed)", () => {
|
|
30
|
+
expect(isAffectedNode("v24.3.0")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns false for v24.0.0 (below affected range)", () => {
|
|
34
|
+
expect(isAffectedNode("v24.0.0")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns false for v25.0.0 (entire 25.x unaffected)", () => {
|
|
38
|
+
expect(isAffectedNode("v25.0.0")).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns false for v20.x (pre-bug range)", () => {
|
|
42
|
+
expect(isAffectedNode("v20.15.0")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns false for v23.x (odd releases all unaffected)", () => {
|
|
46
|
+
expect(isAffectedNode("v23.5.0")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("accepts versions without the v prefix", () => {
|
|
50
|
+
expect(isAffectedNode("22.17.0")).toBe(true);
|
|
51
|
+
expect(isAffectedNode("22.18.0")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns false for malformed input rather than throwing", () => {
|
|
55
|
+
expect(isAffectedNode("")).toBe(false);
|
|
56
|
+
expect(isAffectedNode("not-a-version")).toBe(false);
|
|
57
|
+
expect(isAffectedNode("v22")).toBe(false);
|
|
58
|
+
expect(isAffectedNode("22.17")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("buildNodeUpgradeMessage", () => {
|
|
63
|
+
it("interpolates the running version into the message", () => {
|
|
64
|
+
const msg = buildNodeUpgradeMessage("v22.17.1");
|
|
65
|
+
expect(msg).toContain("v22.17.1");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("includes the upstream Node issue link", () => {
|
|
69
|
+
const msg = buildNodeUpgradeMessage("v22.17.1");
|
|
70
|
+
expect(msg).toContain("https://github.com/nodejs/node/issues/58515");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("names the minimum acceptable versions", () => {
|
|
74
|
+
const msg = buildNodeUpgradeMessage("v22.17.1");
|
|
75
|
+
expect(msg).toMatch(/22\.18/);
|
|
76
|
+
expect(msg).toMatch(/24\.3/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("suggests nvm, brew, and Windows installer paths", () => {
|
|
80
|
+
const msg = buildNodeUpgradeMessage("v22.17.1");
|
|
81
|
+
expect(msg).toMatch(/nvm/);
|
|
82
|
+
expect(msg).toMatch(/brew/);
|
|
83
|
+
expect(msg).toMatch(/nodejs\.org/);
|
|
84
|
+
});
|
|
85
|
+
});
|