@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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for packages/shared/src/platform/process.ts.
|
|
3
|
+
*
|
|
4
|
+
* Every helper accepts injectable `platform`, `exec`, and `kill` parameters,
|
|
5
|
+
* so no `Object.defineProperty(process, "platform", ...)` mutation is needed.
|
|
6
|
+
* See change: consolidate-platform-handlers.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
findPortHolders,
|
|
11
|
+
parseNetstatListeners,
|
|
12
|
+
isProcessAlive,
|
|
13
|
+
killProcess,
|
|
14
|
+
killPidWithGroup,
|
|
15
|
+
} from "../platform/process.js";
|
|
16
|
+
|
|
17
|
+
describe("parseNetstatListeners", () => {
|
|
18
|
+
const selfPid = 99999;
|
|
19
|
+
|
|
20
|
+
it("parses a Windows netstat listener", () => {
|
|
21
|
+
const output = [
|
|
22
|
+
" Proto Local Address Foreign Address State PID",
|
|
23
|
+
" TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 12345",
|
|
24
|
+
].join("\r\n");
|
|
25
|
+
expect(parseNetstatListeners(output, 8000, selfPid)).toEqual([12345]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("excludes non-LISTENING rows", () => {
|
|
29
|
+
const output = " TCP 0.0.0.0:8000 0.0.0.0:0 ESTABLISHED 1111";
|
|
30
|
+
expect(parseNetstatListeners(output, 8000, selfPid)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("excludes current process PID", () => {
|
|
34
|
+
const output = ` TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING ${selfPid}`;
|
|
35
|
+
expect(parseNetstatListeners(output, 8000, selfPid)).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("only matches the requested port", () => {
|
|
39
|
+
const output = [
|
|
40
|
+
" TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 1111",
|
|
41
|
+
" TCP 0.0.0.0:18000 0.0.0.0:0 LISTENING 2222",
|
|
42
|
+
].join("\n");
|
|
43
|
+
expect(parseNetstatListeners(output, 8000, selfPid)).toEqual([1111]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles IPv6 addresses", () => {
|
|
47
|
+
const output = " TCP [::]:8000 [::]:0 LISTENING 7777";
|
|
48
|
+
expect(parseNetstatListeners(output, 8000, selfPid)).toEqual([7777]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("findPortHolders", () => {
|
|
53
|
+
it("uses netstat when platform=win32 is injected", () => {
|
|
54
|
+
const exec = vi.fn().mockReturnValue(
|
|
55
|
+
" TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 12345\n",
|
|
56
|
+
);
|
|
57
|
+
const result = findPortHolders(8000, { platform: "win32", exec });
|
|
58
|
+
expect(exec).toHaveBeenCalledOnce();
|
|
59
|
+
expect(exec.mock.calls[0][0]).toMatch(/netstat/i);
|
|
60
|
+
expect(result).toEqual([12345]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("uses lsof when platform=linux is injected", () => {
|
|
64
|
+
const exec = vi.fn().mockReturnValue("12345\n67890\n");
|
|
65
|
+
const result = findPortHolders(8000, { platform: "linux", exec });
|
|
66
|
+
expect(exec).toHaveBeenCalledOnce();
|
|
67
|
+
expect(exec.mock.calls[0][0]).toMatch(/lsof.*:8000/);
|
|
68
|
+
expect(result.sort()).toEqual([12345, 67890]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns [] on exec failure (best-effort)", () => {
|
|
72
|
+
const exec = vi.fn().mockImplementation(() => {
|
|
73
|
+
throw new Error("boom");
|
|
74
|
+
});
|
|
75
|
+
expect(findPortHolders(8000, { platform: "win32", exec })).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("isProcessAlive", () => {
|
|
80
|
+
it("returns true when kill(pid, 0) succeeds", () => {
|
|
81
|
+
const kill = vi.fn().mockReturnValue(undefined);
|
|
82
|
+
expect(isProcessAlive(12345, { kill })).toBe(true);
|
|
83
|
+
expect(kill).toHaveBeenCalledWith(12345, 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns false when kill(pid, 0) throws", () => {
|
|
87
|
+
const kill = vi.fn().mockImplementation(() => {
|
|
88
|
+
throw new Error("ESRCH");
|
|
89
|
+
});
|
|
90
|
+
expect(isProcessAlive(12345, { kill })).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("killProcess", () => {
|
|
95
|
+
it("uses taskkill on Windows", async () => {
|
|
96
|
+
const exec = vi.fn().mockReturnValue("");
|
|
97
|
+
const kill = vi.fn().mockReturnValue(undefined); // isProcessAlive → true
|
|
98
|
+
const result = await killProcess(12345, { platform: "win32", exec, kill });
|
|
99
|
+
expect(exec).toHaveBeenCalledWith(
|
|
100
|
+
expect.stringMatching(/taskkill\s+\/F\s+\/T\s+\/PID\s+12345/),
|
|
101
|
+
expect.any(Object),
|
|
102
|
+
);
|
|
103
|
+
expect(result).toEqual({ ok: true, forced: false });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns { ok: false } when pid already dead", async () => {
|
|
107
|
+
const kill = vi.fn().mockImplementation(() => {
|
|
108
|
+
throw new Error("ESRCH");
|
|
109
|
+
});
|
|
110
|
+
const result = await killProcess(12345, { platform: "linux", kill });
|
|
111
|
+
expect(result).toEqual({ ok: false, forced: false });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("sends SIGTERM on Unix and reports clean stop when process dies", async () => {
|
|
115
|
+
let aliveCount = 0;
|
|
116
|
+
const kill = vi.fn().mockImplementation((_pid, sig) => {
|
|
117
|
+
// isProcessAlive pre-check (signal 0) must succeed once to enter the branch
|
|
118
|
+
if (sig === 0) {
|
|
119
|
+
aliveCount++;
|
|
120
|
+
if (aliveCount === 1) return; // alive
|
|
121
|
+
throw new Error("ESRCH"); // dead after SIGTERM
|
|
122
|
+
}
|
|
123
|
+
if (sig === "SIGTERM") return;
|
|
124
|
+
throw new Error("unexpected signal");
|
|
125
|
+
});
|
|
126
|
+
const result = await killProcess(12345, { platform: "linux", kill, timeoutMs: 500 });
|
|
127
|
+
expect(kill).toHaveBeenCalledWith(12345, "SIGTERM");
|
|
128
|
+
expect(result).toEqual({ ok: true, forced: false });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("forces SIGKILL when process survives SIGTERM", async () => {
|
|
132
|
+
const kill = vi.fn().mockImplementation((_pid, sig) => {
|
|
133
|
+
if (sig === 0) return; // always alive during polling
|
|
134
|
+
if (sig === "SIGTERM" || sig === "SIGKILL") return;
|
|
135
|
+
});
|
|
136
|
+
const result = await killProcess(12345, { platform: "linux", kill, timeoutMs: 300 });
|
|
137
|
+
expect(kill).toHaveBeenCalledWith(12345, "SIGKILL");
|
|
138
|
+
expect(result).toEqual({ ok: true, forced: true });
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("killPidWithGroup", () => {
|
|
143
|
+
it("signals -pid on Unix (process group)", () => {
|
|
144
|
+
const kill = vi.fn();
|
|
145
|
+
killPidWithGroup(12345, "SIGTERM", { platform: "linux", kill });
|
|
146
|
+
expect(kill).toHaveBeenCalledWith(-12345, "SIGTERM");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("signals +pid on Windows (no process groups)", () => {
|
|
150
|
+
const kill = vi.fn();
|
|
151
|
+
killPidWithGroup(12345, "SIGTERM", { platform: "win32", kill });
|
|
152
|
+
expect(kill).toHaveBeenCalledWith(12345, "SIGTERM");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("signals -pid on macOS", () => {
|
|
156
|
+
const kill = vi.fn();
|
|
157
|
+
killPidWithGroup(99999, "SIGKILL", { platform: "darwin", kill });
|
|
158
|
+
expect(kill).toHaveBeenCalledWith(-99999, "SIGKILL");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for packages/shared/src/platform/runner.ts — the Recipe engine.
|
|
3
|
+
* Uses real subprocess execution against node itself (always available)
|
|
4
|
+
* so we test the full pipeline: resolve → spawn → parse → Result.
|
|
5
|
+
*
|
|
6
|
+
* See change: platform-command-executor.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
9
|
+
import { run, unwrap, resetResolverCache, type Recipe } from "../platform/runner.js";
|
|
10
|
+
|
|
11
|
+
// A trivial Recipe that runs `node --version` and returns the version string.
|
|
12
|
+
const NODE_VERSION: Recipe<Record<string, never>, string> = {
|
|
13
|
+
argv: () => [process.execPath, "--version"],
|
|
14
|
+
parse: (stdout) => stdout.trim(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// A Recipe that runs `node -e "process.exit(N)"` where N comes from input.
|
|
18
|
+
const NODE_EXIT: Recipe<{ code: number }, string> = {
|
|
19
|
+
argv: ({ code }) => [process.execPath, "-e", `process.exit(${code})`],
|
|
20
|
+
parse: (stdout) => stdout.trim(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// A Recipe that uses tolerate to accept exit 1.
|
|
24
|
+
const NODE_EXIT_1_TOLERATED: Recipe<Record<string, never>, string> = {
|
|
25
|
+
argv: () => [process.execPath, "-e", "process.exit(1)"],
|
|
26
|
+
parse: (stdout) => stdout.trim() || "exited-1-but-ok",
|
|
27
|
+
tolerate: [1],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// A Recipe that times out (sleeps 10s, we allow 100ms).
|
|
31
|
+
const NODE_SLEEP_LONG: Recipe<Record<string, never>, string> = {
|
|
32
|
+
argv: () => [process.execPath, "-e", "setTimeout(() => {}, 10000)"],
|
|
33
|
+
parse: (stdout) => stdout,
|
|
34
|
+
timeout: 100,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// A Recipe pointing at a binary that cannot be on PATH.
|
|
38
|
+
const NONEXISTENT_BINARY: Recipe<Record<string, never>, string> = {
|
|
39
|
+
argv: () => ["this-binary-does-not-exist-12345abcde", "--help"],
|
|
40
|
+
parse: (stdout) => stdout,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("run()", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
resetResolverCache();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("executes a successful recipe and returns parsed value", () => {
|
|
49
|
+
const result = run(NODE_VERSION, {});
|
|
50
|
+
expect(result.ok).toBe(true);
|
|
51
|
+
if (result.ok) {
|
|
52
|
+
expect(result.value).toMatch(/^v\d+\.\d+\.\d+/);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns { ok: false, error: not-found } when binary is missing", () => {
|
|
57
|
+
const result = run(NONEXISTENT_BINARY, {});
|
|
58
|
+
expect(result.ok).toBe(false);
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
expect(result.error.kind).toBe("not-found");
|
|
61
|
+
if (result.error.kind === "not-found") {
|
|
62
|
+
expect(result.error.binary).toBe("this-binary-does-not-exist-12345abcde");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns { ok: false, error: exit } when subprocess exits non-zero (not tolerated)", () => {
|
|
68
|
+
const result = run(NODE_EXIT, { code: 42 });
|
|
69
|
+
expect(result.ok).toBe(false);
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
expect(result.error.kind).toBe("exit");
|
|
72
|
+
if (result.error.kind === "exit") {
|
|
73
|
+
expect(result.error.code).toBe(42);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns { ok: true } when non-zero exit code is in recipe.tolerate", () => {
|
|
79
|
+
const result = run(NODE_EXIT_1_TOLERATED, {});
|
|
80
|
+
expect(result.ok).toBe(true);
|
|
81
|
+
if (result.ok) {
|
|
82
|
+
expect(result.value).toBe("exited-1-but-ok");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns { ok: false, error: timeout } when subprocess exceeds timeout", () => {
|
|
87
|
+
const result = run(NODE_SLEEP_LONG, {});
|
|
88
|
+
expect(result.ok).toBe(false);
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
expect(result.error.kind).toBe("timeout");
|
|
91
|
+
if (result.error.kind === "timeout") {
|
|
92
|
+
expect(result.error.timeoutMs).toBe(100);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, 2000);
|
|
96
|
+
|
|
97
|
+
it("ctx.timeout overrides recipe.timeout", () => {
|
|
98
|
+
// Recipe says 100ms, context says 10s — a 500ms subprocess should succeed.
|
|
99
|
+
const FAST: Recipe<Record<string, never>, string> = {
|
|
100
|
+
argv: () => [process.execPath, "-e", "setTimeout(() => process.exit(0), 200)"],
|
|
101
|
+
parse: () => "ok",
|
|
102
|
+
timeout: 50, // would cause timeout without override
|
|
103
|
+
};
|
|
104
|
+
const result = run(FAST, {}, { timeout: 5000 });
|
|
105
|
+
expect(result.ok).toBe(true);
|
|
106
|
+
}, 10000);
|
|
107
|
+
|
|
108
|
+
it("caches binary resolution across calls", () => {
|
|
109
|
+
// First call resolves + caches
|
|
110
|
+
const a = run(NODE_VERSION, {});
|
|
111
|
+
expect(a.ok).toBe(true);
|
|
112
|
+
// Second call reuses cache — behavior identical
|
|
113
|
+
const b = run(NODE_VERSION, {});
|
|
114
|
+
expect(b.ok).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("resetResolverCache forces re-resolution", () => {
|
|
118
|
+
const a = run(NODE_VERSION, {});
|
|
119
|
+
expect(a.ok).toBe(true);
|
|
120
|
+
resetResolverCache();
|
|
121
|
+
const b = run(NODE_VERSION, {});
|
|
122
|
+
expect(b.ok).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("passes cwd from ctx to the subprocess", () => {
|
|
126
|
+
const PWD: Recipe<Record<string, never>, string> = {
|
|
127
|
+
argv: () => [process.execPath, "-e", "process.stdout.write(process.cwd())"],
|
|
128
|
+
parse: (out) => out.trim(),
|
|
129
|
+
};
|
|
130
|
+
const result = run(PWD, {}, { cwd: process.cwd() });
|
|
131
|
+
expect(result.ok).toBe(true);
|
|
132
|
+
if (result.ok) {
|
|
133
|
+
// Normalize separators for cross-platform comparison
|
|
134
|
+
const normalizedOut = result.value.replace(/\\/g, "/").toLowerCase();
|
|
135
|
+
const normalizedCwd = process.cwd().replace(/\\/g, "/").toLowerCase();
|
|
136
|
+
expect(normalizedOut).toBe(normalizedCwd);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("passes env from ctx to the subprocess (merged over process.env)", () => {
|
|
141
|
+
const PRINT_ENV: Recipe<Record<string, never>, string> = {
|
|
142
|
+
argv: () => [process.execPath, "-e", "process.stdout.write(process.env.TEST_VAR_RUNNER || 'unset')"],
|
|
143
|
+
parse: (out) => out.trim(),
|
|
144
|
+
};
|
|
145
|
+
const result = run(PRINT_ENV, {}, { env: { TEST_VAR_RUNNER: "hello-from-ctx" } });
|
|
146
|
+
expect(result.ok).toBe(true);
|
|
147
|
+
if (result.ok) {
|
|
148
|
+
expect(result.value).toBe("hello-from-ctx");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("rejects recipes with empty argv", () => {
|
|
153
|
+
const EMPTY: Recipe<Record<string, never>, string> = {
|
|
154
|
+
argv: () => [],
|
|
155
|
+
parse: () => "",
|
|
156
|
+
};
|
|
157
|
+
const result = run(EMPTY, {});
|
|
158
|
+
expect(result.ok).toBe(false);
|
|
159
|
+
if (!result.ok) {
|
|
160
|
+
expect(result.error.kind).toBe("spawn-failure");
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("unwrap()", () => {
|
|
166
|
+
it("returns value on success", () => {
|
|
167
|
+
expect(unwrap({ ok: true, value: 42 }, 0)).toBe(42);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("returns fallback on error", () => {
|
|
171
|
+
expect(unwrap({ ok: false, error: { kind: "not-found", binary: "x" } }, 99)).toBe(99);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for packages/shared/src/platform/shell.ts.
|
|
3
|
+
* All four platform × env-present/absent branches are exercised via injected
|
|
4
|
+
* `platform` and `env`. No `process.env` or `process.platform` mutation.
|
|
5
|
+
* See change: consolidate-platform-handlers.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import { detectShell, getTerminalEnvHints } from "../platform/shell.js";
|
|
9
|
+
|
|
10
|
+
describe("detectShell", () => {
|
|
11
|
+
it("uses COMSPEC on Windows when present", () => {
|
|
12
|
+
expect(detectShell({
|
|
13
|
+
platform: "win32",
|
|
14
|
+
env: { COMSPEC: "C:\\Windows\\System32\\cmd.exe" },
|
|
15
|
+
})).toBe("C:\\Windows\\System32\\cmd.exe");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("falls back to powershell.exe on Windows when COMSPEC missing", () => {
|
|
19
|
+
expect(detectShell({ platform: "win32", env: {} })).toBe("powershell.exe");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("uses SHELL on Linux when present", () => {
|
|
23
|
+
expect(detectShell({
|
|
24
|
+
platform: "linux",
|
|
25
|
+
env: { SHELL: "/bin/zsh" },
|
|
26
|
+
})).toBe("/bin/zsh");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("falls back to /bin/bash on Linux when SHELL missing", () => {
|
|
30
|
+
expect(detectShell({ platform: "linux", env: {} })).toBe("/bin/bash");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("uses SHELL on macOS when present", () => {
|
|
34
|
+
expect(detectShell({
|
|
35
|
+
platform: "darwin",
|
|
36
|
+
env: { SHELL: "/bin/zsh" },
|
|
37
|
+
})).toBe("/bin/zsh");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to /bin/bash on macOS when SHELL missing", () => {
|
|
41
|
+
expect(detectShell({ platform: "darwin", env: {} })).toBe("/bin/bash");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("ignores SHELL on Windows (uses COMSPEC path)", () => {
|
|
45
|
+
// Even if some Windows environment sets SHELL (e.g. Git Bash), the PTY
|
|
46
|
+
// primitive must use COMSPEC / powershell because that's what node-pty
|
|
47
|
+
// can actually spawn on win32.
|
|
48
|
+
expect(detectShell({
|
|
49
|
+
platform: "win32",
|
|
50
|
+
env: { SHELL: "/usr/bin/bash" },
|
|
51
|
+
})).toBe("powershell.exe");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("getTerminalEnvHints", () => {
|
|
56
|
+
it("adds TERM=cygwin on Windows when TERM is not set", () => {
|
|
57
|
+
expect(getTerminalEnvHints({ platform: "win32", env: {} })).toEqual({ TERM: "cygwin" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("leaves TERM alone on Windows when already set", () => {
|
|
61
|
+
expect(getTerminalEnvHints({
|
|
62
|
+
platform: "win32",
|
|
63
|
+
env: { TERM: "xterm-256color" },
|
|
64
|
+
})).toEqual({});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("adds no hints on Linux", () => {
|
|
68
|
+
expect(getTerminalEnvHints({ platform: "linux", env: {} })).toEqual({});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("adds no hints on macOS", () => {
|
|
72
|
+
expect(getTerminalEnvHints({ platform: "darwin", env: {} })).toEqual({});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for platform/process-identify.ts.
|
|
3
|
+
*
|
|
4
|
+
* Uses an injected fake `exec` so we can simulate ps/tasklist output on
|
|
5
|
+
* any host OS. All tests pass `platform` explicitly.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
findPidByMarker,
|
|
10
|
+
isProcessLikePi,
|
|
11
|
+
isPiCommandLine,
|
|
12
|
+
} from "../platform/process-identify.js";
|
|
13
|
+
|
|
14
|
+
describe("isPiCommandLine", () => {
|
|
15
|
+
it("matches pi", () => {
|
|
16
|
+
expect(isPiCommandLine("/usr/bin/pi --mode rpc")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it("matches node", () => {
|
|
19
|
+
expect(isPiCommandLine("node cli.js")).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it("matches pi even with path prefixes", () => {
|
|
22
|
+
expect(isPiCommandLine("/opt/foo/pi --args")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it("does not match unrelated processes", () => {
|
|
25
|
+
expect(isPiCommandLine("/bin/bash")).toBe(false);
|
|
26
|
+
expect(isPiCommandLine("/usr/bin/zsh")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it("does not match substrings without word boundary", () => {
|
|
29
|
+
// "pip" and "typescript" must not match pi or node.
|
|
30
|
+
expect(isPiCommandLine("pip install something")).toBe(false);
|
|
31
|
+
expect(isPiCommandLine("/usr/bin/typescript-compiler")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("findPidByMarker", () => {
|
|
36
|
+
it("Windows returns empty array without execution", () => {
|
|
37
|
+
const exec = vi.fn(() => "should not be called");
|
|
38
|
+
const result = findPidByMarker("marker", { platform: "win32", exec: exec as any });
|
|
39
|
+
expect(result).toEqual([]);
|
|
40
|
+
expect(exec).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("Linux parses ps output and filters to sentinel lines", () => {
|
|
44
|
+
const fakeOutput = [
|
|
45
|
+
"12345 sh -c tail -f /dev/null | pi --mode rpc session-abc",
|
|
46
|
+
"67890 grep session-abc",
|
|
47
|
+
"11111 sleep 2147483647 | pi --mode rpc session-abc",
|
|
48
|
+
"22222 vim notes-about-session-abc.txt",
|
|
49
|
+
].join("\n");
|
|
50
|
+
const exec = vi.fn(() => fakeOutput) as any;
|
|
51
|
+
const result = findPidByMarker("session-abc", { platform: "linux", exec });
|
|
52
|
+
expect(result).toEqual([12345, 11111]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("macOS parses ps output similarly", () => {
|
|
56
|
+
const fakeOutput = "99999 tail -f /dev/null | pi --mode rpc s1";
|
|
57
|
+
const exec = vi.fn(() => fakeOutput) as any;
|
|
58
|
+
const result = findPidByMarker("s1", { platform: "darwin", exec });
|
|
59
|
+
expect(result).toEqual([99999]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns empty array when no match", () => {
|
|
63
|
+
const exec = vi.fn(() => "") as any;
|
|
64
|
+
const result = findPidByMarker("nothing", { platform: "linux", exec });
|
|
65
|
+
expect(result).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns empty array when exec throws (process dead / permission)", () => {
|
|
69
|
+
const exec = vi.fn(() => { throw new Error("no such command"); }) as any;
|
|
70
|
+
const result = findPidByMarker("x", { platform: "linux", exec });
|
|
71
|
+
expect(result).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("excludes lines without pi headless sentinels", () => {
|
|
75
|
+
const fakeOutput = "12345 some-random-process matching-marker-only";
|
|
76
|
+
const exec = vi.fn(() => fakeOutput) as any;
|
|
77
|
+
const result = findPidByMarker("matching-marker", { platform: "linux", exec });
|
|
78
|
+
expect(result).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("isProcessLikePi", () => {
|
|
83
|
+
it("Windows returns true unconditionally", () => {
|
|
84
|
+
const exec = vi.fn(() => "should not be called");
|
|
85
|
+
expect(isProcessLikePi(1234, { platform: "win32", exec: exec as any })).toBe(true);
|
|
86
|
+
expect(exec).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("Linux matches via /proc cmdline", () => {
|
|
90
|
+
const exec = vi.fn(() => "/usr/bin/node /opt/pi-coding-agent/dist/cli.js") as any;
|
|
91
|
+
expect(isProcessLikePi(1234, { platform: "linux", exec })).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("Linux does not match non-pi", () => {
|
|
95
|
+
const exec = vi.fn(() => "/bin/bash") as any;
|
|
96
|
+
expect(isProcessLikePi(1234, { platform: "linux", exec })).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("macOS uses ps -p -o command=", () => {
|
|
100
|
+
let capturedCmd = "";
|
|
101
|
+
const exec = ((cmd: string) => {
|
|
102
|
+
capturedCmd = cmd;
|
|
103
|
+
return "node cli.js --mode rpc";
|
|
104
|
+
}) as any;
|
|
105
|
+
expect(isProcessLikePi(555, { platform: "darwin", exec })).toBe(true);
|
|
106
|
+
expect(capturedCmd).toMatch(/ps -p 555 -o command=/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns false when process has exited (exec throws)", () => {
|
|
110
|
+
const exec = vi.fn(() => { throw new Error("no such process"); }) as any;
|
|
111
|
+
expect(isProcessLikePi(9999, { platform: "linux", exec })).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
BUNDLED_EXTENSION_IDS,
|
|
4
|
+
RECOMMENDED_EXTENSIONS,
|
|
5
|
+
getRecommendedExtension,
|
|
6
|
+
getRecommendedByStatus,
|
|
7
|
+
type RecommendedExtension,
|
|
8
|
+
} from "../recommended-extensions.js";
|
|
9
|
+
|
|
10
|
+
describe("RECOMMENDED_EXTENSIONS manifest", () => {
|
|
11
|
+
it("contains exactly the five expected entries", () => {
|
|
12
|
+
const ids = RECOMMENDED_EXTENSIONS.map((e) => e.id).sort();
|
|
13
|
+
expect(ids).toEqual(
|
|
14
|
+
[
|
|
15
|
+
"pi-anthropic-messages",
|
|
16
|
+
"pi-agent-browser",
|
|
17
|
+
"pi-flows",
|
|
18
|
+
"pi-web-access",
|
|
19
|
+
"tintinweb-pi-subagents",
|
|
20
|
+
].sort(),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("every entry has the required shape", () => {
|
|
25
|
+
for (const entry of RECOMMENDED_EXTENSIONS) {
|
|
26
|
+
expect(typeof entry.id).toBe("string");
|
|
27
|
+
expect(entry.id.length).toBeGreaterThan(0);
|
|
28
|
+
expect(typeof entry.source).toBe("string");
|
|
29
|
+
expect(entry.source.length).toBeGreaterThan(0);
|
|
30
|
+
expect(typeof entry.displayName).toBe("string");
|
|
31
|
+
expect(typeof entry.fallbackDescription).toBe("string");
|
|
32
|
+
expect(entry.fallbackDescription.length).toBeGreaterThan(10);
|
|
33
|
+
expect(["required", "strongly-suggested", "optional"]).toContain(entry.status);
|
|
34
|
+
expect(Array.isArray(entry.unlocks)).toBe(true);
|
|
35
|
+
expect(entry.unlocks.length).toBeGreaterThan(0);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("pi-anthropic-messages is marked required and uses HTTPS git URL", () => {
|
|
40
|
+
const entry = getRecommendedExtension("pi-anthropic-messages");
|
|
41
|
+
expect(entry).toBeDefined();
|
|
42
|
+
expect(entry?.status).toBe("required");
|
|
43
|
+
expect(entry?.source).toContain("https://github.com/BlackBeltTechnology/pi-anthropic-messages.git");
|
|
44
|
+
expect(entry?.autowired).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("pi-flows uses HTTPS git URL and registers flow-engine tools", () => {
|
|
48
|
+
const entry = getRecommendedExtension("pi-flows");
|
|
49
|
+
expect(entry).toBeDefined();
|
|
50
|
+
expect(entry?.source).toBe("https://github.com/BlackBeltTechnology/pi-flows.git");
|
|
51
|
+
expect(entry?.toolsRegistered).toContain("subagent");
|
|
52
|
+
expect(entry?.toolsRegistered).toContain("flow_write");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("tintinweb-pi-subagents registers Agent under its canonical capitalization", () => {
|
|
56
|
+
const entry = getRecommendedExtension("tintinweb-pi-subagents");
|
|
57
|
+
expect(entry).toBeDefined();
|
|
58
|
+
expect(entry?.source).toBe("npm:@tintinweb/pi-subagents");
|
|
59
|
+
expect(entry?.toolsRegistered).toContain("Agent");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("npm-sourced entries use the npm: prefix", () => {
|
|
63
|
+
const npmEntries = RECOMMENDED_EXTENSIONS.filter((e) => e.source.startsWith("npm:"));
|
|
64
|
+
expect(npmEntries.map((e) => e.id).sort()).toEqual(
|
|
65
|
+
["pi-agent-browser", "pi-web-access", "tintinweb-pi-subagents"].sort(),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("git-sourced entries use the https://github.com/.../.git HTTPS form", () => {
|
|
70
|
+
const gitEntries = RECOMMENDED_EXTENSIONS.filter((e) =>
|
|
71
|
+
e.source.startsWith("https://github.com/"),
|
|
72
|
+
);
|
|
73
|
+
for (const entry of gitEntries) {
|
|
74
|
+
expect(entry.source).toMatch(/^https:\/\/github\.com\/[^/]+\/[^/]+\.git$/);
|
|
75
|
+
}
|
|
76
|
+
expect(gitEntries.map((e) => e.id).sort()).toEqual(
|
|
77
|
+
["pi-anthropic-messages", "pi-flows"].sort(),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("getRecommendedExtension", () => {
|
|
83
|
+
it("returns the entry when id matches", () => {
|
|
84
|
+
const e = getRecommendedExtension("pi-web-access");
|
|
85
|
+
expect(e?.displayName).toBe("pi-web-access");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns undefined for unknown ids", () => {
|
|
89
|
+
expect(getRecommendedExtension("does-not-exist")).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("getRecommendedByStatus", () => {
|
|
94
|
+
it("filters by required", () => {
|
|
95
|
+
const required = getRecommendedByStatus("required");
|
|
96
|
+
expect(required.map((e) => e.id)).toEqual(["pi-anthropic-messages"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("filters by strongly-suggested", () => {
|
|
100
|
+
const suggested = getRecommendedByStatus("strongly-suggested");
|
|
101
|
+
expect(suggested.map((e) => e.id).sort()).toEqual(
|
|
102
|
+
["pi-flows", "pi-web-access", "tintinweb-pi-subagents"].sort(),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("filters by optional", () => {
|
|
107
|
+
const optional = getRecommendedByStatus("optional");
|
|
108
|
+
expect(optional.map((e) => e.id)).toEqual(["pi-agent-browser"]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("RecommendedExtension type", () => {
|
|
113
|
+
it("accepts a minimal entry", () => {
|
|
114
|
+
const entry: RecommendedExtension = {
|
|
115
|
+
id: "x",
|
|
116
|
+
source: "npm:x",
|
|
117
|
+
displayName: "X",
|
|
118
|
+
fallbackDescription: "A test extension description.",
|
|
119
|
+
status: "optional",
|
|
120
|
+
unlocks: ["something"],
|
|
121
|
+
};
|
|
122
|
+
expect(entry.id).toBe("x");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── BUNDLED_EXTENSION_IDS manifest (task 2 of bundle-first-party-extensions) ──
|
|
127
|
+
|
|
128
|
+
describe("BUNDLED_EXTENSION_IDS manifest", () => {
|
|
129
|
+
it("contains exactly the v0.x initial bundled set", () => {
|
|
130
|
+
expect([...BUNDLED_EXTENSION_IDS].sort()).toEqual(
|
|
131
|
+
["pi-anthropic-messages", "pi-flows"].sort(),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("every bundled id appears in RECOMMENDED_EXTENSIONS", () => {
|
|
136
|
+
const recommendedIds = new Set(RECOMMENDED_EXTENSIONS.map((e) => e.id));
|
|
137
|
+
for (const id of BUNDLED_EXTENSION_IDS) {
|
|
138
|
+
expect(recommendedIds.has(id)).toBe(true);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("every bundled id has a git-based source (no npm:, no local paths)", () => {
|
|
143
|
+
for (const id of BUNDLED_EXTENSION_IDS) {
|
|
144
|
+
const entry = RECOMMENDED_EXTENSIONS.find((e) => e.id === id);
|
|
145
|
+
expect(entry, `RECOMMENDED_EXTENSIONS missing entry for ${id}`).toBeDefined();
|
|
146
|
+
const source = entry!.source;
|
|
147
|
+
const isGit =
|
|
148
|
+
source.endsWith(".git") ||
|
|
149
|
+
source.startsWith("git@") ||
|
|
150
|
+
source.startsWith("git:") ||
|
|
151
|
+
/^https?:\/\/.+\/.+/.test(source);
|
|
152
|
+
expect(isGit, `${id} source is not git-based: ${source}`).toBe(true);
|
|
153
|
+
expect(source.startsWith("npm:"), `${id} must not be an npm source`).toBe(false);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|