@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
createEditorPidRegistry,
|
|
9
|
+
isDashboardOwnedCodeServer,
|
|
10
|
+
type PersistedEditorEntry,
|
|
11
|
+
} from "../editor-pid-registry.js";
|
|
12
|
+
|
|
13
|
+
function tempPidFile(): string {
|
|
14
|
+
const dir = mkdtempSync(join(tmpdir(), "editor-pid-reg-"));
|
|
15
|
+
return join(dir, "editor-pids.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readEntries(file: string): PersistedEditorEntry[] {
|
|
19
|
+
if (!existsSync(file)) return [];
|
|
20
|
+
return JSON.parse(readFileSync(file, "utf-8")).entries ?? [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const VALID_CMDLINE = `/usr/local/bin/code-server --auth none --bind-addr 127.0.0.1:63584 --user-data-dir ${path.join(os.homedir(), ".pi", "dashboard", "editors", "abc123def456")} /Users/me/project`;
|
|
24
|
+
|
|
25
|
+
const UNRELATED_CMDLINE = "/usr/local/bin/code-server --user-data-dir /Users/me/.config/Code";
|
|
26
|
+
|
|
27
|
+
describe("isDashboardOwnedCodeServer", () => {
|
|
28
|
+
it("returns true for a dashboard-owned code-server cmdline", () => {
|
|
29
|
+
expect(isDashboardOwnedCodeServer(VALID_CMDLINE)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns false for an unrelated code-server", () => {
|
|
33
|
+
expect(isDashboardOwnedCodeServer(UNRELATED_CMDLINE)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns false for null cmdline", () => {
|
|
37
|
+
expect(isDashboardOwnedCodeServer(null)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns false when --user-data-dir is missing", () => {
|
|
41
|
+
expect(isDashboardOwnedCodeServer("/usr/local/bin/code-server --bind-addr 127.0.0.1:1234")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("createEditorPidRegistry — register/remove/persist", () => {
|
|
46
|
+
it("register writes an entry to the JSON file", () => {
|
|
47
|
+
const file = tempPidFile();
|
|
48
|
+
const reg = createEditorPidRegistry({ pidFilePath: file });
|
|
49
|
+
reg.register({ id: "editor-aaa", pid: 5961, port: 63584, cwd: "/projects/app", dataDir: "/data" });
|
|
50
|
+
expect(reg.size()).toBe(1);
|
|
51
|
+
const entries = readEntries(file);
|
|
52
|
+
expect(entries).toHaveLength(1);
|
|
53
|
+
expect(entries[0]).toMatchObject({ id: "editor-aaa", pid: 5961, port: 63584, cwd: "/projects/app", dataDir: "/data" });
|
|
54
|
+
expect(entries[0].spawnedAt).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("remove deletes the entry from the JSON file", () => {
|
|
58
|
+
const file = tempPidFile();
|
|
59
|
+
const reg = createEditorPidRegistry({ pidFilePath: file });
|
|
60
|
+
reg.register({ id: "editor-aaa", pid: 1, port: 1, cwd: "/a", dataDir: "/d" });
|
|
61
|
+
reg.register({ id: "editor-bbb", pid: 2, port: 2, cwd: "/b", dataDir: "/d2" });
|
|
62
|
+
reg.remove("editor-aaa");
|
|
63
|
+
expect(reg.size()).toBe(1);
|
|
64
|
+
const entries = readEntries(file);
|
|
65
|
+
expect(entries).toHaveLength(1);
|
|
66
|
+
expect(entries[0].id).toBe("editor-bbb");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("persistence write failure does not throw from register", () => {
|
|
70
|
+
// Portable way to force writeJsonFile to fail: make the target path a
|
|
71
|
+
// directory so fs.writeFileSync (to path + ".tmp") succeeds but rename()
|
|
72
|
+
// onto a directory fails with EISDIR/EPERM on every platform.
|
|
73
|
+
const file = tempPidFile();
|
|
74
|
+
mkdirSync(file, { recursive: true }); // target exists as a directory
|
|
75
|
+
const reg = createEditorPidRegistry({ pidFilePath: file });
|
|
76
|
+
expect(() => reg.register({ id: "editor-aaa", pid: 1, port: 1, cwd: "/a", dataDir: "/d" })).not.toThrow();
|
|
77
|
+
// In-memory entry still tracked even when disk write failed.
|
|
78
|
+
expect(reg.size()).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("createEditorPidRegistry — cleanupOrphans", () => {
|
|
83
|
+
it("returns without throwing when file does not exist", async () => {
|
|
84
|
+
const file = join(mkdtempSync(join(tmpdir(), "missing-")), "nope.json");
|
|
85
|
+
const reg = createEditorPidRegistry({ pidFilePath: file });
|
|
86
|
+
await expect(reg.cleanupOrphans()).resolves.toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns without throwing when file is corrupt", async () => {
|
|
90
|
+
const file = tempPidFile();
|
|
91
|
+
writeFileSync(file, "{not json");
|
|
92
|
+
const reg = createEditorPidRegistry({ pidFilePath: file });
|
|
93
|
+
await expect(reg.cleanupOrphans()).resolves.toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("skips dead PIDs", async () => {
|
|
97
|
+
const file = tempPidFile();
|
|
98
|
+
writeFileSync(file, JSON.stringify({
|
|
99
|
+
entries: [{ id: "editor-x", pid: 999999, port: 1, cwd: "/a", dataDir: "/d", spawnedAt: new Date().toISOString() }],
|
|
100
|
+
}));
|
|
101
|
+
const killed: Array<{ pid: number; sig: string }> = [];
|
|
102
|
+
const reg = createEditorPidRegistry({
|
|
103
|
+
pidFilePath: file,
|
|
104
|
+
isProcessAlive: () => false,
|
|
105
|
+
getCmdline: () => VALID_CMDLINE,
|
|
106
|
+
kill: (pid, sig) => { killed.push({ pid, sig }); return true; },
|
|
107
|
+
graceMs: 1,
|
|
108
|
+
});
|
|
109
|
+
await reg.cleanupOrphans();
|
|
110
|
+
expect(killed).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does NOT signal a live PID whose cmdline doesn't match (PID reuse)", async () => {
|
|
114
|
+
const file = tempPidFile();
|
|
115
|
+
writeFileSync(file, JSON.stringify({
|
|
116
|
+
entries: [{ id: "editor-x", pid: 1234, port: 1, cwd: "/a", dataDir: "/d", spawnedAt: new Date().toISOString() }],
|
|
117
|
+
}));
|
|
118
|
+
const killed: Array<{ pid: number; sig: string }> = [];
|
|
119
|
+
const reg = createEditorPidRegistry({
|
|
120
|
+
pidFilePath: file,
|
|
121
|
+
isProcessAlive: () => true,
|
|
122
|
+
getCmdline: () => UNRELATED_CMDLINE,
|
|
123
|
+
kill: (pid, sig) => { killed.push({ pid, sig }); return true; },
|
|
124
|
+
graceMs: 1,
|
|
125
|
+
});
|
|
126
|
+
await reg.cleanupOrphans();
|
|
127
|
+
expect(killed).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("does NOT signal when cmdline lookup fails (cannot verify)", async () => {
|
|
131
|
+
const file = tempPidFile();
|
|
132
|
+
writeFileSync(file, JSON.stringify({
|
|
133
|
+
entries: [{ id: "editor-x", pid: 1234, port: 1, cwd: "/a", dataDir: "/d", spawnedAt: new Date().toISOString() }],
|
|
134
|
+
}));
|
|
135
|
+
const killed: Array<{ pid: number; sig: string }> = [];
|
|
136
|
+
const reg = createEditorPidRegistry({
|
|
137
|
+
pidFilePath: file,
|
|
138
|
+
isProcessAlive: () => true,
|
|
139
|
+
getCmdline: () => null,
|
|
140
|
+
kill: (pid, sig) => { killed.push({ pid, sig }); return true; },
|
|
141
|
+
graceMs: 1,
|
|
142
|
+
});
|
|
143
|
+
await reg.cleanupOrphans();
|
|
144
|
+
expect(killed).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("SIGTERMs verified live orphans then SIGKILLs survivors", async () => {
|
|
148
|
+
const file = tempPidFile();
|
|
149
|
+
writeFileSync(file, JSON.stringify({
|
|
150
|
+
entries: [
|
|
151
|
+
{ id: "editor-x", pid: 5961, port: 63584, cwd: "/a", dataDir: "/d1", spawnedAt: new Date().toISOString() },
|
|
152
|
+
{ id: "editor-y", pid: 5962, port: 63585, cwd: "/b", dataDir: "/d2", spawnedAt: new Date().toISOString() },
|
|
153
|
+
],
|
|
154
|
+
}));
|
|
155
|
+
const killed: Array<{ pid: number; sig: string }> = [];
|
|
156
|
+
// First call: alive. After SIGTERM grace, simulate that 5961 died, 5962 survived.
|
|
157
|
+
let phase: "before" | "after" = "before";
|
|
158
|
+
const reg = createEditorPidRegistry({
|
|
159
|
+
pidFilePath: file,
|
|
160
|
+
isProcessAlive: (pid) => phase === "before" ? true : pid === 5962,
|
|
161
|
+
getCmdline: () => VALID_CMDLINE,
|
|
162
|
+
kill: (pid, sig) => { killed.push({ pid, sig }); return true; },
|
|
163
|
+
graceMs: 1,
|
|
164
|
+
});
|
|
165
|
+
// Toggle phase right after SIGTERMs are sent.
|
|
166
|
+
const origSetTimeout = setTimeout;
|
|
167
|
+
const promise = reg.cleanupOrphans();
|
|
168
|
+
// microtask flip
|
|
169
|
+
queueMicrotask(() => { phase = "after"; });
|
|
170
|
+
await promise;
|
|
171
|
+
|
|
172
|
+
expect(killed.filter((k) => k.sig === "SIGTERM").map((k) => k.pid).sort()).toEqual([5961, 5962]);
|
|
173
|
+
expect(killed.filter((k) => k.sig === "SIGKILL").map((k) => k.pid)).toEqual([5962]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("rewrites the registry file empty after sweep", async () => {
|
|
177
|
+
const file = tempPidFile();
|
|
178
|
+
writeFileSync(file, JSON.stringify({
|
|
179
|
+
entries: [{ id: "editor-x", pid: 5961, port: 1, cwd: "/a", dataDir: "/d", spawnedAt: new Date().toISOString() }],
|
|
180
|
+
}));
|
|
181
|
+
const reg = createEditorPidRegistry({
|
|
182
|
+
pidFilePath: file,
|
|
183
|
+
isProcessAlive: () => true,
|
|
184
|
+
getCmdline: () => VALID_CMDLINE,
|
|
185
|
+
kill: () => true,
|
|
186
|
+
graceMs: 1,
|
|
187
|
+
});
|
|
188
|
+
await reg.cleanupOrphans();
|
|
189
|
+
expect(readEntries(file)).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
|
|
3
|
-
const { mockedExecSync } = vi.hoisted(() => ({
|
|
3
|
+
const { mockedExecSync, mockedSpawnSync } = vi.hoisted(() => ({
|
|
4
4
|
mockedExecSync: vi.fn(),
|
|
5
|
+
mockedSpawnSync: vi.fn(),
|
|
5
6
|
}));
|
|
6
7
|
|
|
8
|
+
// platform/process.ts and platform/tools.ts now use spawnSync via whereAllLines
|
|
9
|
+
// and isProcessRunning; both must be mocked to exercise the detection path in
|
|
10
|
+
// isolation. Default return is status:1 (not found) so each test explicitly
|
|
11
|
+
// overrides what it needs.
|
|
7
12
|
vi.mock("node:child_process", () => ({
|
|
8
|
-
default: { execSync: mockedExecSync },
|
|
13
|
+
default: { execSync: mockedExecSync, spawnSync: mockedSpawnSync },
|
|
9
14
|
execSync: mockedExecSync,
|
|
15
|
+
spawnSync: mockedSpawnSync,
|
|
10
16
|
}));
|
|
11
17
|
|
|
12
18
|
import { detectEditors, isProcessRunning, isProcessRunningWin32, EDITORS, type DetectedEditor } from "../editor-registry.js";
|
|
@@ -14,6 +20,9 @@ import { detectEditors, isProcessRunning, isProcessRunningWin32, EDITORS, type D
|
|
|
14
20
|
describe("editor-registry", () => {
|
|
15
21
|
beforeEach(() => {
|
|
16
22
|
vi.resetAllMocks();
|
|
23
|
+
// Default: spawnSync reports not-found so any un-overridden test still
|
|
24
|
+
// sees clean state rather than stale returns from previous tests.
|
|
25
|
+
mockedSpawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
|
|
17
26
|
});
|
|
18
27
|
|
|
19
28
|
describe("EDITORS", () => {
|
|
@@ -74,19 +83,22 @@ describe("editor-registry", () => {
|
|
|
74
83
|
|
|
75
84
|
describe("detectEditors", () => {
|
|
76
85
|
it("should return editor when process is running AND CLI is available", () => {
|
|
86
|
+
// platform/process.ts isProcessRunning uses execSync (pgrep) internally.
|
|
77
87
|
mockedExecSync.mockImplementation((cmd) => {
|
|
78
88
|
const s = String(cmd);
|
|
79
89
|
if (s.includes("pgrep")) {
|
|
80
|
-
|
|
81
|
-
if (s.includes("Zed")) return Buffer.from("12345\n");
|
|
82
|
-
throw new Error("not found");
|
|
83
|
-
}
|
|
84
|
-
if (s.includes("which")) {
|
|
85
|
-
if (s.includes("zed")) return Buffer.from("/usr/local/bin/zed\n");
|
|
90
|
+
if (s.includes("Zed") || s.includes("zed")) return Buffer.from("12345\n");
|
|
86
91
|
throw new Error("not found");
|
|
87
92
|
}
|
|
88
93
|
throw new Error("unexpected command");
|
|
89
94
|
});
|
|
95
|
+
// ToolResolver.which uses spawnSync for `which`/`where` lookup.
|
|
96
|
+
mockedSpawnSync.mockImplementation((cmd, args) => {
|
|
97
|
+
if ((cmd === "which" || cmd === "where") && args?.[0] === "zed") {
|
|
98
|
+
return { status: 0, stdout: "/usr/local/bin/zed\n", stderr: "" };
|
|
99
|
+
}
|
|
100
|
+
return { status: 1, stdout: "", stderr: "" };
|
|
101
|
+
});
|
|
90
102
|
|
|
91
103
|
const result = detectEditors("/some/project");
|
|
92
104
|
expect(result).toEqual([{ id: "zed", name: "Zed" }]);
|
|
@@ -96,9 +108,9 @@ describe("editor-registry", () => {
|
|
|
96
108
|
mockedExecSync.mockImplementation((cmd) => {
|
|
97
109
|
const s = String(cmd);
|
|
98
110
|
if (s.includes("pgrep")) throw new Error("not found");
|
|
99
|
-
if (s.includes("which")) return Buffer.from("/usr/local/bin/zed\n");
|
|
100
111
|
throw new Error("unexpected command");
|
|
101
112
|
});
|
|
113
|
+
mockedSpawnSync.mockImplementation(() => ({ status: 0, stdout: "/usr/local/bin/zed\n", stderr: "" }));
|
|
102
114
|
|
|
103
115
|
const result = detectEditors("/some/project");
|
|
104
116
|
expect(result).toEqual([]);
|
|
@@ -108,9 +120,9 @@ describe("editor-registry", () => {
|
|
|
108
120
|
mockedExecSync.mockImplementation((cmd) => {
|
|
109
121
|
const s = String(cmd);
|
|
110
122
|
if (s.includes("pgrep")) return Buffer.from("12345\n");
|
|
111
|
-
if (s.includes("which")) throw new Error("not found");
|
|
112
123
|
throw new Error("unexpected command");
|
|
113
124
|
});
|
|
125
|
+
mockedSpawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
|
|
114
126
|
|
|
115
127
|
const result = detectEditors("/some/project");
|
|
116
128
|
expect(result).toEqual([]);
|
|
@@ -124,13 +136,15 @@ describe("editor-registry", () => {
|
|
|
124
136
|
if (s.includes("Visual Studio Code") || s.includes("code")) return Buffer.from("67890\n");
|
|
125
137
|
throw new Error("not found");
|
|
126
138
|
}
|
|
127
|
-
if (s.includes("which")) {
|
|
128
|
-
if (s.includes("zed")) return Buffer.from("/usr/local/bin/zed\n");
|
|
129
|
-
if (s.includes("code")) return Buffer.from("/usr/local/bin/code\n");
|
|
130
|
-
throw new Error("not found");
|
|
131
|
-
}
|
|
132
139
|
throw new Error("unexpected command");
|
|
133
140
|
});
|
|
141
|
+
mockedSpawnSync.mockImplementation((cmd, args) => {
|
|
142
|
+
if (cmd === "which" || cmd === "where") {
|
|
143
|
+
if (args?.[0] === "zed") return { status: 0, stdout: "/usr/local/bin/zed\n", stderr: "" };
|
|
144
|
+
if (args?.[0] === "code") return { status: 0, stdout: "/usr/local/bin/code\n", stderr: "" };
|
|
145
|
+
}
|
|
146
|
+
return { status: 1, stdout: "", stderr: "" };
|
|
147
|
+
});
|
|
134
148
|
|
|
135
149
|
const result = detectEditors("/some/project");
|
|
136
150
|
expect(result).toEqual([
|
|
@@ -30,7 +30,11 @@ describe("findBundledExtension - AppImage guard", () => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it("returns null when extension does not exist", () => {
|
|
33
|
-
|
|
33
|
+
// Disable Strategy 2 (node-resolver fallback) so this test exercises
|
|
34
|
+
// the AppImage guard path in isolation.
|
|
35
|
+
expect(
|
|
36
|
+
findBundledExtension(tmpDir, { resolvePackage: () => null }),
|
|
37
|
+
).toBeNull();
|
|
34
38
|
});
|
|
35
39
|
|
|
36
40
|
// Note: We can't easily test the /tmp/.mount_ guard with real paths
|
|
@@ -26,7 +26,9 @@ describe("bridge extension registration (server context)", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it("findBundledExtension returns null when extension dir does not exist", () => {
|
|
29
|
-
|
|
29
|
+
// Strategy 2 (require.resolve) would find the monorepo extension;
|
|
30
|
+
// disable it for this test so we exercise Strategy 1 in isolation.
|
|
31
|
+
const result = findBundledExtension(tmpDir, { resolvePackage: () => null });
|
|
30
32
|
expect(result).toBeNull();
|
|
31
33
|
});
|
|
32
34
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cross-platform port-holder detection.
|
|
3
|
+
* See change: fix-windows-server-parity.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi } from "vitest";
|
|
6
|
+
import { findPortHolders, parseNetstatListeners } from "../cli.js";
|
|
7
|
+
|
|
8
|
+
describe("parseNetstatListeners", () => {
|
|
9
|
+
const selfPid = 99999;
|
|
10
|
+
|
|
11
|
+
it("parses Windows netstat -ano output for a listening PID", () => {
|
|
12
|
+
const output = [
|
|
13
|
+
"Active Connections",
|
|
14
|
+
"",
|
|
15
|
+
" Proto Local Address Foreign Address State PID",
|
|
16
|
+
" TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 12345",
|
|
17
|
+
" TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4",
|
|
18
|
+
" TCP 127.0.0.1:8000 127.0.0.1:54321 ESTABLISHED 23456",
|
|
19
|
+
].join("\r\n");
|
|
20
|
+
|
|
21
|
+
expect(parseNetstatListeners(output, 8000, selfPid)).toEqual([12345]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("excludes non-LISTENING rows (ESTABLISHED, TIME_WAIT)", () => {
|
|
25
|
+
const output = [
|
|
26
|
+
" TCP 0.0.0.0:9999 0.0.0.0:0 ESTABLISHED 11111",
|
|
27
|
+
" TCP 0.0.0.0:9999 0.0.0.0:0 TIME_WAIT 22222",
|
|
28
|
+
].join("\n");
|
|
29
|
+
expect(parseNetstatListeners(output, 9999, selfPid)).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("excludes the current process PID", () => {
|
|
33
|
+
const output = ` TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING ${selfPid}`;
|
|
34
|
+
expect(parseNetstatListeners(output, 8000, selfPid)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("only matches the requested port (suffix-based)", () => {
|
|
38
|
+
const output = [
|
|
39
|
+
" TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 1111",
|
|
40
|
+
" TCP 0.0.0.0:18000 0.0.0.0:0 LISTENING 2222",
|
|
41
|
+
" TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 3333",
|
|
42
|
+
].join("\n");
|
|
43
|
+
expect(parseNetstatListeners(output, 8000, selfPid).sort()).toEqual([1111]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns empty array for empty or garbage input", () => {
|
|
47
|
+
expect(parseNetstatListeners("", 8000, selfPid)).toEqual([]);
|
|
48
|
+
expect(parseNetstatListeners("not a netstat output\nblah", 8000, selfPid)).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("handles IPv6 listening addresses", () => {
|
|
52
|
+
const output = " TCP [::]:8000 [::]:0 LISTENING 7777";
|
|
53
|
+
expect(parseNetstatListeners(output, 8000, selfPid)).toEqual([7777]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("findPortHolders", () => {
|
|
58
|
+
it("uses netstat on Windows (via injected exec)", () => {
|
|
59
|
+
const originalPlatform = process.platform;
|
|
60
|
+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
61
|
+
try {
|
|
62
|
+
const exec = vi.fn().mockReturnValue(
|
|
63
|
+
" TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 12345\n",
|
|
64
|
+
);
|
|
65
|
+
const result = findPortHolders(8000, exec as any);
|
|
66
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(exec.mock.calls[0][0]).toMatch(/netstat/i);
|
|
68
|
+
expect(exec.mock.calls[0][0]).not.toMatch(/lsof/i);
|
|
69
|
+
expect(result).toEqual([12345]);
|
|
70
|
+
} finally {
|
|
71
|
+
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("uses lsof on Unix (via injected exec)", () => {
|
|
76
|
+
const originalPlatform = process.platform;
|
|
77
|
+
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
|
|
78
|
+
try {
|
|
79
|
+
const exec = vi.fn().mockReturnValue("12345\n67890\n");
|
|
80
|
+
const result = findPortHolders(8000, exec as any);
|
|
81
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(exec.mock.calls[0][0]).toMatch(/lsof/);
|
|
83
|
+
expect(exec.mock.calls[0][0]).toContain(":8000");
|
|
84
|
+
expect(result.sort()).toEqual([12345, 67890].sort());
|
|
85
|
+
} finally {
|
|
86
|
+
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns empty array on exec failure (best-effort)", () => {
|
|
91
|
+
const exec = vi.fn().mockImplementation(() => { throw new Error("boom"); });
|
|
92
|
+
expect(findPortHolders(8000, exec as any)).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for the postinstall `fix-pty-permissions.cjs` script.
|
|
3
|
+
*
|
|
4
|
+
* Ensures that after `npm install` the native `spawn-helper` binary in the
|
|
5
|
+
* current platform's `node-pty` prebuild directory has at least one execute
|
|
6
|
+
* bit set. Without this, `pty.spawn(...)` fails with "posix_spawnp failed."
|
|
7
|
+
* and the dashboard's "New Terminal" button appears dead.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { existsSync, statSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
|
|
14
|
+
describe("fix-pty-permissions", () => {
|
|
15
|
+
it.skipIf(process.platform === "win32")(
|
|
16
|
+
"spawn-helper for current platform is executable",
|
|
17
|
+
() => {
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const ptyPkg = require.resolve("node-pty/package.json");
|
|
20
|
+
const ptyRoot = dirname(ptyPkg);
|
|
21
|
+
const platformDir =
|
|
22
|
+
process.platform === "darwin"
|
|
23
|
+
? process.arch === "arm64"
|
|
24
|
+
? "darwin-arm64"
|
|
25
|
+
: "darwin-x64"
|
|
26
|
+
: process.arch === "arm64"
|
|
27
|
+
? "linux-arm64"
|
|
28
|
+
: "linux-x64";
|
|
29
|
+
|
|
30
|
+
// node-pty ships pre-packed binaries under `prebuilds/<platform>-<arch>/`
|
|
31
|
+
// when the tarball includes a prebuild for the host platform (macOS,
|
|
32
|
+
// Windows, some Linux builds). Otherwise node-pty's install script
|
|
33
|
+
// falls back to `node-gyp rebuild`, producing artifacts under
|
|
34
|
+
// `build/Release/` instead. Accept either location so this test is
|
|
35
|
+
// stable across local dev (prebuilt) and Linux CI (built from source).
|
|
36
|
+
const candidates = [
|
|
37
|
+
join(ptyRoot, "prebuilds", platformDir, "spawn-helper"),
|
|
38
|
+
join(ptyRoot, "build", "Release", "spawn-helper"),
|
|
39
|
+
];
|
|
40
|
+
const helper = candidates.find((p) => existsSync(p));
|
|
41
|
+
|
|
42
|
+
if (!helper) {
|
|
43
|
+
// No spawn-helper anywhere means node-pty's install step did not
|
|
44
|
+
// produce the binary on this host (e.g. missing build toolchain).
|
|
45
|
+
// The fix-permissions script is defensive — it silently skips when
|
|
46
|
+
// the prebuild dir is absent — so skipping here matches runtime
|
|
47
|
+
// behavior rather than masking a real regression.
|
|
48
|
+
console.warn(
|
|
49
|
+
`[fix-pty-permissions.test] no spawn-helper found at any of: ${candidates.join(", ")} — skipping`,
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mode = statSync(helper).mode;
|
|
55
|
+
// At least one execute bit (owner/group/other) must be set.
|
|
56
|
+
expect(mode & 0o111).not.toBe(0);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
});
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for handleForceKill in session-action-handler.
|
|
3
|
+
*
|
|
4
|
+
* Kill-path routing (see change: route-kill-paths-through-platform):
|
|
5
|
+
* we verify that the handler delegates to the platform `killProcess`
|
|
6
|
+
* helper rather than calling `process.kill(...)` directly. Cross-OS
|
|
7
|
+
* behavior of `killProcess` itself is covered in
|
|
8
|
+
* `packages/shared/src/__tests__/platform-process.test.ts`.
|
|
3
9
|
*/
|
|
4
10
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
|
|
12
|
+
// Spy on the platform module so we can assert the handler routes through it.
|
|
13
|
+
const killProcessSpy = vi.fn(async (_pid: number, _opts?: any) => ({ ok: true, forced: false }));
|
|
14
|
+
const isProcessAliveSpy = vi.fn((_pid: number) => false);
|
|
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 { handleForceKill } = await import("../browser-handlers/session-action-handler.js");
|
|
27
|
+
type BrowserHandlerContext = import("../browser-handlers/handler-context.js").BrowserHandlerContext;
|
|
7
28
|
|
|
8
29
|
function createMockContext(sessionOverrides?: Record<string, any>): BrowserHandlerContext & { sent: any[]; broadcasts: any[] } {
|
|
9
30
|
const sent: any[] = [];
|
|
@@ -44,7 +65,10 @@ function createMockContext(sessionOverrides?: Record<string, any>): BrowserHandl
|
|
|
44
65
|
|
|
45
66
|
describe("handleForceKill", () => {
|
|
46
67
|
beforeEach(() => {
|
|
47
|
-
|
|
68
|
+
killProcessSpy.mockClear();
|
|
69
|
+
killProcessSpy.mockImplementation(async () => ({ ok: true, forced: false }));
|
|
70
|
+
isProcessAliveSpy.mockClear();
|
|
71
|
+
isProcessAliveSpy.mockReturnValue(false);
|
|
48
72
|
});
|
|
49
73
|
|
|
50
74
|
it("should close bridge WebSocket and mark session ended when no PID", async () => {
|
|
@@ -61,19 +85,44 @@ describe("handleForceKill", () => {
|
|
|
61
85
|
expect(result.message).toContain("no PID");
|
|
62
86
|
});
|
|
63
87
|
|
|
64
|
-
it("should
|
|
65
|
-
|
|
66
|
-
const ctx = createMockContext({ pid: 2147483647 });
|
|
88
|
+
it("should delegate termination to platform killProcess with 2s timeout", async () => {
|
|
89
|
+
const ctx = createMockContext({ pid: 12345 });
|
|
67
90
|
|
|
68
91
|
await handleForceKill({ type: "force_kill", sessionId: "sess-1" }, ctx);
|
|
69
92
|
|
|
93
|
+
expect(killProcessSpy).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(killProcessSpy).toHaveBeenCalledWith(12345, expect.objectContaining({ timeoutMs: 2000 }));
|
|
95
|
+
|
|
70
96
|
expect(ctx.piGateway.closeSession).toHaveBeenCalledWith("sess-1");
|
|
71
97
|
expect(ctx.sessionManager.update).toHaveBeenCalledWith("sess-1", expect.objectContaining({ status: "ended" }));
|
|
72
|
-
|
|
98
|
+
|
|
99
|
+
const result = ctx.sent.find((m: any) => m.type === "force_kill_result");
|
|
100
|
+
expect(result).toBeDefined();
|
|
101
|
+
expect(result.success).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should report already-exited when killProcess reports pid not alive", async () => {
|
|
105
|
+
killProcessSpy.mockResolvedValueOnce({ ok: false, forced: false });
|
|
106
|
+
const ctx = createMockContext({ pid: 2147483647 });
|
|
107
|
+
|
|
108
|
+
await handleForceKill({ type: "force_kill", sessionId: "sess-1" }, ctx);
|
|
109
|
+
|
|
73
110
|
const result = ctx.sent.find((m: any) => m.type === "force_kill_result");
|
|
74
111
|
expect(result).toBeDefined();
|
|
75
112
|
expect(result.success).toBe(true);
|
|
76
|
-
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should not call process.kill directly (must route through platform)", async () => {
|
|
116
|
+
const processKillSpy = vi.spyOn(process, "kill");
|
|
117
|
+
const ctx = createMockContext({ pid: 12345 });
|
|
118
|
+
|
|
119
|
+
await handleForceKill({ type: "force_kill", sessionId: "sess-1" }, ctx);
|
|
120
|
+
|
|
121
|
+
// handleForceKill must NOT invoke process.kill; all termination goes
|
|
122
|
+
// through killProcess from the platform module.
|
|
123
|
+
expect(processKillSpy).not.toHaveBeenCalled();
|
|
124
|
+
expect(killProcessSpy).toHaveBeenCalledOnce();
|
|
125
|
+
processKillSpy.mockRestore();
|
|
77
126
|
});
|
|
78
127
|
|
|
79
128
|
it("should broadcast session_updated with ended status", async () => {
|
|
@@ -18,7 +18,9 @@ function git(cmd: string, cwd: string) {
|
|
|
18
18
|
|
|
19
19
|
function makeRepo(): string {
|
|
20
20
|
const dir = mkdtempSync(join(tmpdir(), "git-ops-test-"));
|
|
21
|
-
|
|
21
|
+
// Force `main` as the default branch so tests are deterministic regardless
|
|
22
|
+
// of the host user's `init.defaultBranch` config.
|
|
23
|
+
git("-c init.defaultBranch=main init", dir);
|
|
22
24
|
git("config user.email test@test.com", dir);
|
|
23
25
|
git("config user.name Test", dir);
|
|
24
26
|
// Initial commit so we have a branch
|
|
@@ -122,7 +124,7 @@ describe("git-operations", () => {
|
|
|
122
124
|
writeFileSync(join(repo, "remote.txt"), "data");
|
|
123
125
|
git("add .", repo);
|
|
124
126
|
git("commit -m remote-only", repo);
|
|
125
|
-
git("checkout
|
|
127
|
+
git("checkout main", repo);
|
|
126
128
|
|
|
127
129
|
// Fetch in clone
|
|
128
130
|
git("fetch origin", clone);
|
|
@@ -151,7 +153,7 @@ describe("git-operations", () => {
|
|
|
151
153
|
describe("checkoutBranch", () => {
|
|
152
154
|
it("checks out a local branch on clean repo", () => {
|
|
153
155
|
git("checkout -b feature-x", repo);
|
|
154
|
-
git("checkout
|
|
156
|
+
git("checkout main", repo);
|
|
155
157
|
const result = checkoutBranch(repo, "feature-x", false);
|
|
156
158
|
expect(result.success).toBe(true);
|
|
157
159
|
const head = execSync("git rev-parse --abbrev-ref HEAD", { cwd: repo, encoding: "utf-8" }).trim();
|
|
@@ -160,7 +162,7 @@ describe("git-operations", () => {
|
|
|
160
162
|
|
|
161
163
|
it("returns dirty when working tree is dirty and stash=false", () => {
|
|
162
164
|
git("checkout -b feature-y", repo);
|
|
163
|
-
git("checkout
|
|
165
|
+
git("checkout main", repo);
|
|
164
166
|
writeFileSync(join(repo, "README.md"), "dirty");
|
|
165
167
|
const result = checkoutBranch(repo, "feature-y", false);
|
|
166
168
|
expect(result.success).toBe(false);
|
|
@@ -172,7 +174,7 @@ describe("git-operations", () => {
|
|
|
172
174
|
|
|
173
175
|
it("stashes and checks out when stash=true", () => {
|
|
174
176
|
git("checkout -b feature-z", repo);
|
|
175
|
-
git("checkout
|
|
177
|
+
git("checkout main", repo);
|
|
176
178
|
writeFileSync(join(repo, "README.md"), "dirty");
|
|
177
179
|
const result = checkoutBranch(repo, "feature-z", true);
|
|
178
180
|
expect(result.success).toBe(true);
|
|
@@ -184,7 +186,7 @@ describe("git-operations", () => {
|
|
|
184
186
|
});
|
|
185
187
|
|
|
186
188
|
it("returns success when already on target branch", () => {
|
|
187
|
-
const result = checkoutBranch(repo, "
|
|
189
|
+
const result = checkoutBranch(repo, "main", false);
|
|
188
190
|
expect(result.success).toBe(true);
|
|
189
191
|
});
|
|
190
192
|
|
|
@@ -194,7 +196,7 @@ describe("git-operations", () => {
|
|
|
194
196
|
writeFileSync(join(repo, "r.txt"), "data");
|
|
195
197
|
git("add .", repo);
|
|
196
198
|
git("commit -m r", repo);
|
|
197
|
-
git("checkout
|
|
199
|
+
git("checkout main", repo);
|
|
198
200
|
|
|
199
201
|
const clone = mkdtempSync(join(tmpdir(), "git-ops-clone-"));
|
|
200
202
|
try {
|