@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +67 -116
- package/README.md +93 -7
- package/docs/architecture.md +408 -9
- package/package.json +6 -4
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/bridge.ts +69 -2
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +16 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +13 -7
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +8 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +211 -10
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +56 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +71 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +63 -46
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for enrichModelMetadata — the pure helper that resolves a discovered
|
|
3
|
+
* custom-provider model id against a catalog probe (typically backed by pi's
|
|
4
|
+
* `modelRegistry.find(provider, id)`) and falls back to api-appropriate
|
|
5
|
+
* defaults when the probe has no match.
|
|
6
|
+
*
|
|
7
|
+
* The helper takes an optional `probe` parameter so unit tests can supply a
|
|
8
|
+
* fake catalog without needing `@mariozechner/pi-ai` installed — in
|
|
9
|
+
* production, registerEntry() injects `modelRegistry.find` as the probe.
|
|
10
|
+
*
|
|
11
|
+
* Spec: openspec/changes/enrich-custom-provider-model-metadata/specs/provider-auth-bridge/spec.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from "vitest";
|
|
15
|
+
import { enrichModelMetadata, type CatalogProbe } from "../provider-register.js";
|
|
16
|
+
|
|
17
|
+
// Minimal fake catalog mirroring a subset of pi-ai's real MODELS export.
|
|
18
|
+
// Keys are `${provider}|${id}` so our probe is a single Map lookup.
|
|
19
|
+
const FAKE_CATALOG = new Map<string, any>([
|
|
20
|
+
[
|
|
21
|
+
"anthropic|claude-opus-4-7",
|
|
22
|
+
{
|
|
23
|
+
contextWindow: 1_000_000,
|
|
24
|
+
maxTokens: 128_000,
|
|
25
|
+
reasoning: true,
|
|
26
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
27
|
+
input: ["text", "image"],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
"anthropic|claude-sonnet-4-6",
|
|
32
|
+
{
|
|
33
|
+
contextWindow: 1_000_000,
|
|
34
|
+
maxTokens: 64_000,
|
|
35
|
+
reasoning: true,
|
|
36
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
37
|
+
input: ["text", "image"],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"anthropic|claude-haiku-4-5",
|
|
42
|
+
{
|
|
43
|
+
contextWindow: 200_000,
|
|
44
|
+
maxTokens: 64_000,
|
|
45
|
+
reasoning: true,
|
|
46
|
+
cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
47
|
+
input: ["text", "image"],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"openai|gpt-5",
|
|
52
|
+
{
|
|
53
|
+
contextWindow: 400_000,
|
|
54
|
+
maxTokens: 128_000,
|
|
55
|
+
reasoning: true,
|
|
56
|
+
cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
|
|
57
|
+
input: ["text", "image"],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"opencode|claude-opus-4-7",
|
|
62
|
+
{
|
|
63
|
+
// Deliberately different values so tests can prove the candidate-
|
|
64
|
+
// provider probe order is deterministic (anthropic checked first).
|
|
65
|
+
contextWindow: 999,
|
|
66
|
+
maxTokens: 1,
|
|
67
|
+
reasoning: true,
|
|
68
|
+
cost: { input: 9, output: 9, cacheRead: 9, cacheWrite: 9 },
|
|
69
|
+
input: ["text"],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const fakeProbe: CatalogProbe = (provider, id) =>
|
|
75
|
+
FAKE_CATALOG.get(`${provider}|${id}`) ?? null;
|
|
76
|
+
|
|
77
|
+
describe("enrichModelMetadata — catalog matches via probe", () => {
|
|
78
|
+
it("resolves `cc/claude-opus-4-7` + anthropic-messages to Opus 4.7 catalog entry (1M ctx)", () => {
|
|
79
|
+
const result = enrichModelMetadata("cc/claude-opus-4-7", "anthropic-messages", fakeProbe);
|
|
80
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
81
|
+
expect(result.maxTokens).toBe(128_000);
|
|
82
|
+
expect(result.reasoning).toBe(true);
|
|
83
|
+
expect(result.cost).toEqual({
|
|
84
|
+
input: 5,
|
|
85
|
+
output: 25,
|
|
86
|
+
cacheRead: 0.5,
|
|
87
|
+
cacheWrite: 6.25,
|
|
88
|
+
});
|
|
89
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("resolves bare `claude-sonnet-4-6` + anthropic-messages to Sonnet 4.6 (1M ctx)", () => {
|
|
93
|
+
const result = enrichModelMetadata("claude-sonnet-4-6", "anthropic-messages", fakeProbe);
|
|
94
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
95
|
+
expect(result.reasoning).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("resolves `anthropic/claude-opus-4-7` prefix to Opus 4.7 (1M ctx)", () => {
|
|
99
|
+
const result = enrichModelMetadata("anthropic/claude-opus-4-7", "anthropic-messages", fakeProbe);
|
|
100
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
101
|
+
expect(result.maxTokens).toBe(128_000);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("resolves `claude-haiku-4-5` + anthropic-messages to Haiku 4.5 (200k ctx — verifies we don't over-claim)", () => {
|
|
105
|
+
const result = enrichModelMetadata("claude-haiku-4-5", "anthropic-messages", fakeProbe);
|
|
106
|
+
expect(result.contextWindow).toBe(200_000);
|
|
107
|
+
expect(result.reasoning).toBe(true);
|
|
108
|
+
expect(result.cost.input).toBe(1);
|
|
109
|
+
expect(result.cost.output).toBe(5);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("resolves `openrouter/openai/gpt-5` prefix to gpt-5 via openai-completions candidates", () => {
|
|
113
|
+
const result = enrichModelMetadata("openrouter/openai/gpt-5", "openai-completions", fakeProbe);
|
|
114
|
+
expect(result.contextWindow).toBe(400_000);
|
|
115
|
+
expect(result.maxTokens).toBe(128_000);
|
|
116
|
+
expect(result.reasoning).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("enrichModelMetadata — fallback defaults (no probe or no match)", () => {
|
|
121
|
+
it("no probe supplied → falls back to api-appropriate default", () => {
|
|
122
|
+
const result = enrichModelMetadata("cc/claude-opus-4-7", "anthropic-messages");
|
|
123
|
+
expect(result.contextWindow).toBe(200_000);
|
|
124
|
+
expect(result.maxTokens).toBe(64_000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("unknown id + anthropic-messages returns 200k / 64k / no reasoning / zero cost / text+image", () => {
|
|
128
|
+
const result = enrichModelMetadata("some-unknown-anthropic-model", "anthropic-messages", fakeProbe);
|
|
129
|
+
expect(result.contextWindow).toBe(200_000);
|
|
130
|
+
expect(result.maxTokens).toBe(64_000);
|
|
131
|
+
expect(result.reasoning).toBe(false);
|
|
132
|
+
expect(result.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
133
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("unknown id + openai-completions returns 128k / 16k / zero cost / text+image", () => {
|
|
137
|
+
const result = enrichModelMetadata("some-unknown-openai-model", "openai-completions", fakeProbe);
|
|
138
|
+
expect(result.contextWindow).toBe(128_000);
|
|
139
|
+
expect(result.maxTokens).toBe(16_384);
|
|
140
|
+
expect(result.reasoning).toBe(false);
|
|
141
|
+
expect(result.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
142
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("unknown id + google-generative-ai returns 1M / 65k / zero cost / text+image", () => {
|
|
146
|
+
const result = enrichModelMetadata("some-unknown-gemini-model", "google-generative-ai", fakeProbe);
|
|
147
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
148
|
+
expect(result.maxTokens).toBe(65_536);
|
|
149
|
+
expect(result.reasoning).toBe(false);
|
|
150
|
+
expect(result.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
151
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("prefixed unknown + openai-completions falls back (both prefixed and bare miss)", () => {
|
|
155
|
+
const result = enrichModelMetadata("minimax/custom-private-model", "openai-completions", fakeProbe);
|
|
156
|
+
expect(result.contextWindow).toBe(128_000);
|
|
157
|
+
expect(result.maxTokens).toBe(16_384);
|
|
158
|
+
expect(result.cost.input).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("no api argument + no probe defaults to openai-completions fallback", () => {
|
|
162
|
+
const result = enrichModelMetadata("some-unknown-model");
|
|
163
|
+
expect(result.contextWindow).toBe(128_000);
|
|
164
|
+
expect(result.maxTokens).toBe(16_384);
|
|
165
|
+
expect(result.input).toEqual(["text", "image"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("unknown api string + probe still probes openai-completions candidates", () => {
|
|
169
|
+
const result = enrichModelMetadata("some-unknown-model", "some-weird-api", fakeProbe);
|
|
170
|
+
expect(result.contextWindow).toBe(128_000);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("probe that throws is tolerated (falls through to fallback)", () => {
|
|
174
|
+
const throwingProbe: CatalogProbe = () => {
|
|
175
|
+
throw new Error("registry not ready");
|
|
176
|
+
};
|
|
177
|
+
const result = enrichModelMetadata("claude-opus-4-7", "anthropic-messages", throwingProbe);
|
|
178
|
+
expect(result.contextWindow).toBe(200_000); // anthropic-messages fallback
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("enrichModelMetadata — probe order determinism", () => {
|
|
183
|
+
it("prefers `anthropic` over `opencode` for anthropic-messages (first in candidate list wins)", () => {
|
|
184
|
+
// The fake catalog has `claude-opus-4-7` under BOTH `anthropic` (1M) and
|
|
185
|
+
// `opencode` (999). With api=anthropic-messages, candidates = ["anthropic", "opencode"]
|
|
186
|
+
// so the anthropic entry must win.
|
|
187
|
+
const result = enrichModelMetadata("claude-opus-4-7", "anthropic-messages", fakeProbe);
|
|
188
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
189
|
+
expect(result.maxTokens).toBe(128_000);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("multi-segment prefix `openrouter/anthropic/claude-opus-4-7` resolves via bare-id lookup", () => {
|
|
193
|
+
// After stripping the last segment, `claude-opus-4-7` matches `anthropic` (1M).
|
|
194
|
+
const result = enrichModelMetadata(
|
|
195
|
+
"openrouter/anthropic/claude-opus-4-7",
|
|
196
|
+
"anthropic-messages",
|
|
197
|
+
fakeProbe,
|
|
198
|
+
);
|
|
199
|
+
expect(result.contextWindow).toBe(1_000_000);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -1,112 +1,124 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Tests for git-info.ts.
|
|
3
|
+
*
|
|
4
|
+
* The file now delegates to `@blackbelt-technology/pi-dashboard-shared/platform/git.js`
|
|
5
|
+
* (the Recipe-based tool module). We mock that module so the tests focus
|
|
6
|
+
* on the git-info orchestration logic (branch detection, detached HEAD
|
|
7
|
+
* fallback, PR detection) without spawning git.
|
|
8
|
+
*
|
|
9
|
+
* See change: platform-command-executor.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
12
|
+
|
|
13
|
+
const { currentBranchOr, headShaOr, remoteUrlOr, prNumberOr } = vi.hoisted(() => ({
|
|
14
|
+
currentBranchOr: vi.fn(),
|
|
15
|
+
headShaOr: vi.fn(),
|
|
16
|
+
remoteUrlOr: vi.fn(),
|
|
17
|
+
prNumberOr: vi.fn(),
|
|
6
18
|
}));
|
|
7
19
|
|
|
8
|
-
vi.mock("
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/git.js", () => ({
|
|
21
|
+
currentBranchOr,
|
|
22
|
+
headShaOr,
|
|
23
|
+
remoteUrlOr,
|
|
24
|
+
prNumberOr,
|
|
11
25
|
}));
|
|
12
26
|
|
|
27
|
+
import { gatherGitInfo, detectBranch, detectRemoteUrl, detectPrNumber } from "../git-info.js";
|
|
28
|
+
|
|
13
29
|
describe("git-info", () => {
|
|
14
30
|
beforeEach(() => {
|
|
15
|
-
|
|
31
|
+
currentBranchOr.mockReset();
|
|
32
|
+
headShaOr.mockReset();
|
|
33
|
+
remoteUrlOr.mockReset();
|
|
34
|
+
prNumberOr.mockReset();
|
|
16
35
|
});
|
|
17
36
|
|
|
18
37
|
describe("detectBranch", () => {
|
|
19
38
|
it("returns branch name", () => {
|
|
20
|
-
|
|
39
|
+
currentBranchOr.mockReturnValue("main");
|
|
21
40
|
expect(detectBranch("/test")).toBe("main");
|
|
22
41
|
});
|
|
23
42
|
|
|
24
43
|
it("returns undefined when not a git repo", () => {
|
|
25
|
-
|
|
44
|
+
currentBranchOr.mockReturnValue(undefined);
|
|
26
45
|
expect(detectBranch("/test")).toBeUndefined();
|
|
27
46
|
});
|
|
28
47
|
|
|
29
48
|
it("returns short SHA for detached HEAD", () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.mockReturnValueOnce("abc1234\n"); // rev-parse --short HEAD
|
|
49
|
+
currentBranchOr.mockReturnValue("HEAD");
|
|
50
|
+
headShaOr.mockReturnValue("abc1234");
|
|
33
51
|
expect(detectBranch("/test")).toBe("abc1234");
|
|
34
52
|
});
|
|
35
53
|
|
|
36
54
|
it("returns 'HEAD' as fallback if short SHA fails", () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.mockImplementationOnce(() => { throw new Error("fail"); });
|
|
55
|
+
currentBranchOr.mockReturnValue("HEAD");
|
|
56
|
+
headShaOr.mockReturnValue(undefined);
|
|
40
57
|
expect(detectBranch("/test")).toBe("HEAD");
|
|
41
58
|
});
|
|
42
59
|
});
|
|
43
60
|
|
|
44
61
|
describe("detectRemoteUrl", () => {
|
|
45
|
-
it("returns remote URL", () => {
|
|
46
|
-
|
|
47
|
-
expect(detectRemoteUrl("/test")).toBe("git@github.com:
|
|
62
|
+
it("returns origin remote URL", () => {
|
|
63
|
+
remoteUrlOr.mockReturnValue("git@github.com:org/repo.git");
|
|
64
|
+
expect(detectRemoteUrl("/test")).toBe("git@github.com:org/repo.git");
|
|
48
65
|
});
|
|
49
66
|
|
|
50
|
-
it("returns undefined when no
|
|
51
|
-
|
|
67
|
+
it("returns undefined when no remote is configured", () => {
|
|
68
|
+
remoteUrlOr.mockReturnValue(undefined);
|
|
52
69
|
expect(detectRemoteUrl("/test")).toBeUndefined();
|
|
53
70
|
});
|
|
54
71
|
});
|
|
55
72
|
|
|
56
73
|
describe("detectPrNumber", () => {
|
|
57
|
-
it("returns PR number
|
|
58
|
-
|
|
74
|
+
it("returns PR number when gh finds one", () => {
|
|
75
|
+
prNumberOr.mockReturnValue(42);
|
|
59
76
|
expect(detectPrNumber("/test")).toBe(42);
|
|
60
77
|
});
|
|
61
78
|
|
|
62
|
-
it("returns undefined when gh
|
|
63
|
-
|
|
79
|
+
it("returns undefined when gh is missing or no PR exists", () => {
|
|
80
|
+
prNumberOr.mockReturnValue(undefined);
|
|
64
81
|
expect(detectPrNumber("/test")).toBeUndefined();
|
|
65
82
|
});
|
|
66
83
|
});
|
|
67
84
|
|
|
68
85
|
describe("gatherGitInfo", () => {
|
|
69
|
-
it("returns full git info with links", () => {
|
|
70
|
-
execSyncMock
|
|
71
|
-
.mockReturnValueOnce("feat/foo\n") // branch
|
|
72
|
-
.mockReturnValueOnce("git@github.com:user/repo.git\n") // remote
|
|
73
|
-
.mockReturnValueOnce("7\n"); // PR
|
|
74
|
-
|
|
75
|
-
const info = gatherGitInfo("/test");
|
|
76
|
-
expect(info).toEqual({
|
|
77
|
-
gitBranch: "feat/foo",
|
|
78
|
-
gitBranchUrl: "https://github.com/user/repo/tree/feat%2Ffoo",
|
|
79
|
-
gitPrNumber: 7,
|
|
80
|
-
gitPrUrl: "https://github.com/user/repo/pull/7",
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
86
|
it("returns undefined when not a git repo", () => {
|
|
85
|
-
|
|
87
|
+
currentBranchOr.mockReturnValue(undefined);
|
|
86
88
|
expect(gatherGitInfo("/test")).toBeUndefined();
|
|
87
89
|
});
|
|
88
90
|
|
|
89
|
-
it("returns
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.mockImplementationOnce(() => { throw new Error("gh not found"); });
|
|
91
|
+
it("returns GitInfo for a repo with branch + remote + PR", () => {
|
|
92
|
+
currentBranchOr.mockReturnValue("feature/x");
|
|
93
|
+
remoteUrlOr.mockReturnValue("git@github.com:org/repo.git");
|
|
94
|
+
prNumberOr.mockReturnValue(123);
|
|
94
95
|
|
|
95
96
|
const info = gatherGitInfo("/test");
|
|
96
|
-
expect(info?.gitBranch).toBe("
|
|
97
|
-
expect(info?.gitPrNumber).
|
|
98
|
-
|
|
97
|
+
expect(info?.gitBranch).toBe("feature/x");
|
|
98
|
+
expect(info?.gitPrNumber).toBe(123);
|
|
99
|
+
// Branch URLs URL-encode slashes (feature/x → feature%2Fx) in some builders
|
|
100
|
+
expect(info?.gitBranchUrl).toMatch(/feature(\/|%2F)x/);
|
|
101
|
+
expect(info?.gitPrUrl).toContain("123");
|
|
99
102
|
});
|
|
100
103
|
|
|
101
|
-
it("returns
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.mockImplementationOnce(() => { throw new Error("gh not found"); });
|
|
104
|
+
it("returns GitInfo without links when there's no remote", () => {
|
|
105
|
+
currentBranchOr.mockReturnValue("main");
|
|
106
|
+
remoteUrlOr.mockReturnValue(undefined);
|
|
107
|
+
prNumberOr.mockReturnValue(undefined);
|
|
106
108
|
|
|
107
109
|
const info = gatherGitInfo("/test");
|
|
108
110
|
expect(info?.gitBranch).toBe("main");
|
|
109
111
|
expect(info?.gitBranchUrl).toBeUndefined();
|
|
110
112
|
});
|
|
113
|
+
|
|
114
|
+
it("handles detached HEAD with short SHA", () => {
|
|
115
|
+
currentBranchOr.mockReturnValue("HEAD");
|
|
116
|
+
headShaOr.mockReturnValue("abc1234");
|
|
117
|
+
remoteUrlOr.mockReturnValue(undefined);
|
|
118
|
+
prNumberOr.mockReturnValue(undefined);
|
|
119
|
+
|
|
120
|
+
const info = gatherGitInfo("/test");
|
|
121
|
+
expect(info?.gitBranch).toBe("abc1234");
|
|
122
|
+
});
|
|
111
123
|
});
|
|
112
124
|
});
|
|
@@ -1,119 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for openspec-poller.ts — the higher-level aggregator that combines
|
|
3
|
+
* `openspec list` + per-change `openspec status` into the dashboard's
|
|
4
|
+
* `OpenSpecData` shape.
|
|
5
|
+
*
|
|
6
|
+
* The file now delegates to `platform/openspec.ts` for the subprocess work.
|
|
7
|
+
* We mock that module so the tests focus on the aggregation logic
|
|
8
|
+
* (empty results, artifact mapping, per-change status failures) without
|
|
9
|
+
* spawning openspec.
|
|
10
|
+
*
|
|
11
|
+
* See change: platform-command-executor.
|
|
12
|
+
*/
|
|
1
13
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const mockExecFile = vi.fn<(...args: any[]) => any>();
|
|
7
|
-
vi.mock("node:child_process", () => ({
|
|
8
|
-
spawnSync: mockSpawnSync,
|
|
9
|
-
execFile: mockExecFile,
|
|
10
|
-
// re-export defaults that node:child_process has
|
|
11
|
-
default: { spawnSync: mockSpawnSync, execFile: mockExecFile },
|
|
14
|
+
|
|
15
|
+
const { listOr, statusOr } = vi.hoisted(() => ({
|
|
16
|
+
listOr: vi.fn(),
|
|
17
|
+
statusOr: vi.fn(),
|
|
12
18
|
}));
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return { status: 0, stdout: JSON.stringify(data), stderr: "" };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function fail(): Partial<SpawnSyncReturns<string>> {
|
|
22
|
-
return { status: 1, stdout: "", stderr: "error" };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function mockCli(responses: Map<string, unknown>) {
|
|
26
|
-
mockSpawnSync.mockImplementation((_cmd: any, args: any) => {
|
|
27
|
-
const a = args as string[];
|
|
28
|
-
if (a.includes("list")) {
|
|
29
|
-
const d = responses.get("list");
|
|
30
|
-
return d ? ok(d) : fail();
|
|
31
|
-
}
|
|
32
|
-
if (a.includes("status")) {
|
|
33
|
-
const idx = a.indexOf("--change");
|
|
34
|
-
const name = idx >= 0 ? a[idx + 1] : "";
|
|
35
|
-
const d = responses.get(`status:${name}`);
|
|
36
|
-
return d ? ok(d) : fail();
|
|
37
|
-
}
|
|
38
|
-
return fail();
|
|
39
|
-
});
|
|
40
|
-
}
|
|
20
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/openspec.js", () => ({
|
|
21
|
+
listOr,
|
|
22
|
+
statusOr,
|
|
23
|
+
}));
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
mockSpawnSync.mockReset();
|
|
44
|
-
});
|
|
25
|
+
import { pollOpenSpec } from "@blackbelt-technology/pi-dashboard-shared/openspec-poller.js";
|
|
45
26
|
|
|
46
27
|
describe("pollOpenSpec", () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
expect(result).toEqual({ initialized: false, changes: [] });
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
listOr.mockReset();
|
|
30
|
+
statusOr.mockReset();
|
|
51
31
|
});
|
|
52
32
|
|
|
53
|
-
it("returns initialized=false when list
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
33
|
+
it("returns initialized=false when list fails", () => {
|
|
34
|
+
listOr.mockReturnValue(null);
|
|
35
|
+
expect(pollOpenSpec("/test")).toEqual({ initialized: false, changes: [] });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns initialized=false when list returns non-array changes", () => {
|
|
39
|
+
listOr.mockReturnValue({ changes: "not an array" });
|
|
40
|
+
expect(pollOpenSpec("/test")).toEqual({ initialized: false, changes: [] });
|
|
57
41
|
});
|
|
58
42
|
|
|
59
43
|
it("returns initialized=true with changes on success", () => {
|
|
60
|
-
|
|
61
|
-
[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}],
|
|
67
|
-
["status:feat-a", {
|
|
68
|
-
artifacts: [
|
|
69
|
-
{ id: "proposal", status: "done" },
|
|
70
|
-
{ id: "design", status: "ready" },
|
|
71
|
-
{ id: "tasks", status: "blocked" },
|
|
72
|
-
],
|
|
73
|
-
}],
|
|
74
|
-
["status:feat-b", {
|
|
75
|
-
artifacts: [
|
|
76
|
-
{ id: "proposal", status: "done" },
|
|
77
|
-
{ id: "tasks", status: "done" },
|
|
78
|
-
],
|
|
79
|
-
}],
|
|
80
|
-
]));
|
|
81
|
-
|
|
82
|
-
const result = pollOpenSpec("/project");
|
|
83
|
-
expect(result.initialized).toBe(true);
|
|
84
|
-
expect(result.changes).toHaveLength(2);
|
|
85
|
-
expect(result.changes[0]).toEqual({
|
|
86
|
-
name: "feat-a",
|
|
87
|
-
status: "in-progress",
|
|
88
|
-
completedTasks: 3,
|
|
89
|
-
totalTasks: 5,
|
|
44
|
+
listOr.mockReturnValue({
|
|
45
|
+
changes: [
|
|
46
|
+
{ name: "add-auth", status: "in-progress", completedTasks: 3, totalTasks: 10 },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
statusOr.mockReturnValue({
|
|
90
50
|
artifacts: [
|
|
91
51
|
{ id: "proposal", status: "done" },
|
|
92
|
-
{ id: "
|
|
93
|
-
{ id: "tasks", status: "blocked" },
|
|
52
|
+
{ id: "tasks", status: "ready" },
|
|
94
53
|
],
|
|
95
54
|
});
|
|
96
|
-
expect(result.changes[1].status).toBe("complete");
|
|
97
|
-
});
|
|
98
55
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(result
|
|
56
|
+
const result = pollOpenSpec("/test");
|
|
57
|
+
expect(result.initialized).toBe(true);
|
|
58
|
+
expect(result.changes).toHaveLength(1);
|
|
59
|
+
expect(result.changes[0]).toMatchObject({
|
|
60
|
+
name: "add-auth",
|
|
61
|
+
status: "in-progress",
|
|
62
|
+
completedTasks: 3,
|
|
63
|
+
totalTasks: 10,
|
|
64
|
+
});
|
|
65
|
+
expect(result.changes[0].artifacts).toEqual([
|
|
66
|
+
{ id: "proposal", status: "done" },
|
|
67
|
+
{ id: "tasks", status: "ready" },
|
|
68
|
+
]);
|
|
103
69
|
});
|
|
104
70
|
|
|
105
71
|
it("handles status call failure gracefully (empty artifacts)", () => {
|
|
106
|
-
|
|
107
|
-
[
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const result = pollOpenSpec("/project");
|
|
72
|
+
listOr.mockReturnValue({
|
|
73
|
+
changes: [
|
|
74
|
+
{ name: "x", status: "complete", completedTasks: 5, totalTasks: 5 },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
statusOr.mockReturnValue(null); // status failed
|
|
78
|
+
|
|
79
|
+
const result = pollOpenSpec("/test");
|
|
116
80
|
expect(result.initialized).toBe(true);
|
|
117
81
|
expect(result.changes[0].artifacts).toEqual([]);
|
|
118
82
|
});
|
|
83
|
+
|
|
84
|
+
it("normalizes unknown status values to 'no-tasks'", () => {
|
|
85
|
+
listOr.mockReturnValue({
|
|
86
|
+
changes: [
|
|
87
|
+
{ name: "x", status: "weird-future-status", completedTasks: 0, totalTasks: 0 },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
statusOr.mockReturnValue(null);
|
|
91
|
+
|
|
92
|
+
const result = pollOpenSpec("/test");
|
|
93
|
+
expect(result.changes[0].status).toBe("no-tasks");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("normalizes unknown artifact statuses to 'blocked'", () => {
|
|
97
|
+
listOr.mockReturnValue({
|
|
98
|
+
changes: [
|
|
99
|
+
{ name: "x", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
statusOr.mockReturnValue({
|
|
103
|
+
artifacts: [{ id: "proposal", status: "some-new-state" }],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = pollOpenSpec("/test");
|
|
107
|
+
expect(result.changes[0].artifacts[0].status).toBe("blocked");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("calls statusOr once per change, with the change name", () => {
|
|
111
|
+
listOr.mockReturnValue({
|
|
112
|
+
changes: [
|
|
113
|
+
{ name: "a", status: "in-progress", completedTasks: 0, totalTasks: 1 },
|
|
114
|
+
{ name: "b", status: "complete", completedTasks: 2, totalTasks: 2 },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
statusOr.mockReturnValue({ artifacts: [] });
|
|
118
|
+
|
|
119
|
+
pollOpenSpec("/test");
|
|
120
|
+
expect(statusOr).toHaveBeenCalledTimes(2);
|
|
121
|
+
expect(statusOr).toHaveBeenNthCalledWith(1, { cwd: "/test", change: "a" });
|
|
122
|
+
expect(statusOr).toHaveBeenNthCalledWith(2, { cwd: "/test", change: "b" });
|
|
123
|
+
});
|
|
119
124
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests that `killProcessByPgid` routes through the platform's
|
|
3
|
+
* `killPidWithGroup` helper (not raw `process.kill`).
|
|
4
|
+
*
|
|
5
|
+
* See change: route-kill-paths-through-platform.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
8
|
+
|
|
9
|
+
const killPidWithGroupSpy = vi.fn((_pid: number, _sig: any, _opts?: any) => undefined);
|
|
10
|
+
|
|
11
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/process.js", async () => {
|
|
12
|
+
const actual = await vi.importActual<typeof import("@blackbelt-technology/pi-dashboard-shared/platform/process.js")>(
|
|
13
|
+
"@blackbelt-technology/pi-dashboard-shared/platform/process.js",
|
|
14
|
+
);
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
killPidWithGroup: (pid: number, sig: any, opts?: any) => killPidWithGroupSpy(pid, sig, opts),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const { killProcessByPgid } = await import("../process-scanner.js");
|
|
22
|
+
|
|
23
|
+
describe("killProcessByPgid platform routing", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
killPidWithGroupSpy.mockClear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("invokes killPidWithGroup with the resolved platform on Unix", () => {
|
|
29
|
+
const ok = killProcessByPgid(4242, { _platform: "linux" } as any);
|
|
30
|
+
expect(ok).toBe(true);
|
|
31
|
+
expect(killPidWithGroupSpy).toHaveBeenCalledTimes(1);
|
|
32
|
+
const [pid, sig, opts] = killPidWithGroupSpy.mock.calls[0];
|
|
33
|
+
expect(pid).toBe(4242);
|
|
34
|
+
expect(sig).toBe("SIGTERM");
|
|
35
|
+
expect(opts?.platform).toBe("linux");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("invokes killPidWithGroup with platform=darwin for macOS pgids", () => {
|
|
39
|
+
killProcessByPgid(9999, { _platform: "darwin" } as any);
|
|
40
|
+
const [pid, , opts] = killPidWithGroupSpy.mock.calls[0];
|
|
41
|
+
expect(pid).toBe(9999);
|
|
42
|
+
expect(opts?.platform).toBe("darwin");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does NOT call process.kill directly on Unix", () => {
|
|
46
|
+
const processKillSpy = vi.spyOn(process, "kill");
|
|
47
|
+
try {
|
|
48
|
+
killProcessByPgid(1234, { _platform: "linux" } as any);
|
|
49
|
+
} catch { /* ignore */ }
|
|
50
|
+
expect(processKillSpy).not.toHaveBeenCalled();
|
|
51
|
+
processKillSpy.mockRestore();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("reports failure if killPidWithGroup throws", () => {
|
|
55
|
+
killPidWithGroupSpy.mockImplementationOnce(() => {
|
|
56
|
+
throw new Error("ESRCH");
|
|
57
|
+
});
|
|
58
|
+
const ok = killProcessByPgid(4242, { _platform: "linux" } as any);
|
|
59
|
+
expect(ok).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|