@blackbelt-technology/pi-agent-dashboard 0.2.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 +342 -0
- package/README.md +619 -0
- package/docs/architecture.md +646 -0
- package/package.json +92 -0
- package/packages/extension/package.json +33 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
- package/packages/extension/src/__tests__/connection.test.ts +344 -0
- package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
- package/packages/extension/src/__tests__/git-info.test.ts +112 -0
- package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
- package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
- package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
- package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
- package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
- package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
- package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
- package/packages/extension/src/ask-user-tool.ts +63 -0
- package/packages/extension/src/bridge-context.ts +64 -0
- package/packages/extension/src/bridge.ts +926 -0
- package/packages/extension/src/command-handler.ts +538 -0
- package/packages/extension/src/connection.ts +204 -0
- package/packages/extension/src/dev-build.ts +39 -0
- package/packages/extension/src/event-forwarder.ts +40 -0
- package/packages/extension/src/flow-event-wiring.ts +102 -0
- package/packages/extension/src/git-info.ts +65 -0
- package/packages/extension/src/git-link-builder.ts +112 -0
- package/packages/extension/src/model-tracker.ts +56 -0
- package/packages/extension/src/pi-env.d.ts +23 -0
- package/packages/extension/src/process-metrics.ts +70 -0
- package/packages/extension/src/process-scanner.ts +396 -0
- package/packages/extension/src/prompt-expander.ts +87 -0
- package/packages/extension/src/provider-register.ts +276 -0
- package/packages/extension/src/server-auto-start.ts +87 -0
- package/packages/extension/src/server-launcher.ts +82 -0
- package/packages/extension/src/server-probe.ts +33 -0
- package/packages/extension/src/session-sync.ts +154 -0
- package/packages/extension/src/source-detector.ts +26 -0
- package/packages/extension/src/ui-proxy.ts +269 -0
- package/packages/extension/tsconfig.json +11 -0
- package/packages/server/package.json +37 -0
- package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
- package/packages/server/src/__tests__/auth.test.ts +224 -0
- package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
- package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
- package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
- package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
- package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
- package/packages/server/src/__tests__/config-api.test.ts +104 -0
- package/packages/server/src/__tests__/cors.test.ts +48 -0
- package/packages/server/src/__tests__/directory-service.test.ts +240 -0
- package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
- package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
- package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
- package/packages/server/src/__tests__/extension-register.test.ts +61 -0
- package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
- package/packages/server/src/__tests__/git-operations.test.ts +251 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
- package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
- package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
- package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
- package/packages/server/src/__tests__/json-store.test.ts +70 -0
- package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
- package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
- package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
- package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
- package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
- package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
- package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
- package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
- package/packages/server/src/__tests__/package-routes.test.ts +172 -0
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
- package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
- package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
- package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
- package/packages/server/src/__tests__/process-manager.test.ts +184 -0
- package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
- package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
- package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
- package/packages/server/src/__tests__/server-pid.test.ts +89 -0
- package/packages/server/src/__tests__/session-api.test.ts +244 -0
- package/packages/server/src/__tests__/session-diff.test.ts +138 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
- package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
- package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
- package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
- package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
- package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
- package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
- package/packages/server/src/__tests__/tunnel.test.ts +206 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
- package/packages/server/src/auth-plugin.ts +302 -0
- package/packages/server/src/auth.ts +323 -0
- package/packages/server/src/browse.ts +55 -0
- package/packages/server/src/browser-gateway.ts +495 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
- package/packages/server/src/browser-handlers/handler-context.ts +45 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
- package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
- package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
- package/packages/server/src/cli.ts +347 -0
- package/packages/server/src/config-api.ts +130 -0
- package/packages/server/src/directory-service.ts +162 -0
- package/packages/server/src/editor-detection.ts +60 -0
- package/packages/server/src/editor-manager.ts +352 -0
- package/packages/server/src/editor-proxy.ts +134 -0
- package/packages/server/src/editor-registry.ts +108 -0
- package/packages/server/src/event-status-extraction.ts +131 -0
- package/packages/server/src/event-wiring.ts +589 -0
- package/packages/server/src/extension-register.ts +92 -0
- package/packages/server/src/git-operations.ts +200 -0
- package/packages/server/src/headless-pid-registry.ts +207 -0
- package/packages/server/src/idle-timer.ts +61 -0
- package/packages/server/src/json-store.ts +32 -0
- package/packages/server/src/localhost-guard.ts +117 -0
- package/packages/server/src/memory-event-store.ts +193 -0
- package/packages/server/src/memory-session-manager.ts +123 -0
- package/packages/server/src/meta-persistence.ts +64 -0
- package/packages/server/src/migrate-persistence.ts +195 -0
- package/packages/server/src/npm-search-proxy.ts +143 -0
- package/packages/server/src/oauth-callback-server.ts +177 -0
- package/packages/server/src/openspec-archive.ts +60 -0
- package/packages/server/src/package-manager-wrapper.ts +200 -0
- package/packages/server/src/pending-fork-registry.ts +53 -0
- package/packages/server/src/pending-load-manager.ts +110 -0
- package/packages/server/src/pending-resume-registry.ts +69 -0
- package/packages/server/src/pi-gateway.ts +419 -0
- package/packages/server/src/pi-resource-scanner.ts +369 -0
- package/packages/server/src/preferences-store.ts +116 -0
- package/packages/server/src/process-manager.ts +311 -0
- package/packages/server/src/provider-auth-handlers.ts +438 -0
- package/packages/server/src/provider-auth-storage.ts +200 -0
- package/packages/server/src/resolve-path.ts +12 -0
- package/packages/server/src/routes/editor-routes.ts +86 -0
- package/packages/server/src/routes/file-routes.ts +116 -0
- package/packages/server/src/routes/git-routes.ts +89 -0
- package/packages/server/src/routes/openspec-routes.ts +99 -0
- package/packages/server/src/routes/package-routes.ts +172 -0
- package/packages/server/src/routes/provider-auth-routes.ts +244 -0
- package/packages/server/src/routes/provider-routes.ts +101 -0
- package/packages/server/src/routes/route-deps.ts +23 -0
- package/packages/server/src/routes/session-routes.ts +91 -0
- package/packages/server/src/routes/system-routes.ts +271 -0
- package/packages/server/src/server-pid.ts +84 -0
- package/packages/server/src/server.ts +554 -0
- package/packages/server/src/session-api.ts +330 -0
- package/packages/server/src/session-bootstrap.ts +80 -0
- package/packages/server/src/session-diff.ts +178 -0
- package/packages/server/src/session-discovery.ts +134 -0
- package/packages/server/src/session-file-reader.ts +135 -0
- package/packages/server/src/session-order-manager.ts +73 -0
- package/packages/server/src/session-scanner.ts +233 -0
- package/packages/server/src/session-stats-reader.ts +99 -0
- package/packages/server/src/terminal-gateway.ts +51 -0
- package/packages/server/src/terminal-manager.ts +241 -0
- package/packages/server/src/tunnel.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/__tests__/config.test.ts +358 -0
- package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
- package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
- package/packages/shared/src/__tests__/protocol.test.ts +243 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
- package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
- package/packages/shared/src/archive-types.ts +11 -0
- package/packages/shared/src/browser-protocol.ts +534 -0
- package/packages/shared/src/config.ts +245 -0
- package/packages/shared/src/diff-types.ts +41 -0
- package/packages/shared/src/editor-types.ts +18 -0
- package/packages/shared/src/mdns-discovery.ts +248 -0
- package/packages/shared/src/openspec-activity-detector.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +96 -0
- package/packages/shared/src/protocol.ts +369 -0
- package/packages/shared/src/resolve-jiti.ts +43 -0
- package/packages/shared/src/rest-api.ts +255 -0
- package/packages/shared/src/server-identity.ts +51 -0
- package/packages/shared/src/session-meta.ts +86 -0
- package/packages/shared/src/state-replay.ts +174 -0
- package/packages/shared/src/stats-extractor.ts +54 -0
- package/packages/shared/src/terminal-types.ts +18 -0
- package/packages/shared/src/types.ts +351 -0
- package/packages/shared/tsconfig.json +8 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseRemoteUrl, detectPlatform, buildGitLinks } from "../git-link-builder.js";
|
|
3
|
+
|
|
4
|
+
describe("parseRemoteUrl", () => {
|
|
5
|
+
it("parses SSH URL", () => {
|
|
6
|
+
expect(parseRemoteUrl("git@github.com:user/repo.git")).toEqual({
|
|
7
|
+
host: "github.com", user: "user", repo: "repo",
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("parses SSH URL without .git suffix", () => {
|
|
12
|
+
expect(parseRemoteUrl("git@github.com:user/repo")).toEqual({
|
|
13
|
+
host: "github.com", user: "user", repo: "repo",
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("parses HTTPS URL", () => {
|
|
18
|
+
expect(parseRemoteUrl("https://github.com/user/repo.git")).toEqual({
|
|
19
|
+
host: "github.com", user: "user", repo: "repo",
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("parses HTTPS URL without .git suffix", () => {
|
|
24
|
+
expect(parseRemoteUrl("https://gitlab.com/user/repo")).toEqual({
|
|
25
|
+
host: "gitlab.com", user: "user", repo: "repo",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("parses nested group paths", () => {
|
|
30
|
+
expect(parseRemoteUrl("git@gitlab.com:group/subgroup/repo.git")).toEqual({
|
|
31
|
+
host: "gitlab.com", user: "group/subgroup", repo: "repo",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns undefined for invalid URL", () => {
|
|
36
|
+
expect(parseRemoteUrl("not-a-url")).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("detectPlatform", () => {
|
|
41
|
+
it("detects GitHub", () => expect(detectPlatform("github.com")).toBe("github"));
|
|
42
|
+
it("detects GitLab", () => expect(detectPlatform("gitlab.com")).toBe("gitlab"));
|
|
43
|
+
it("detects Bitbucket", () => expect(detectPlatform("bitbucket.org")).toBe("bitbucket"));
|
|
44
|
+
it("detects Gitea", () => expect(detectPlatform("gitea.com")).toBe("gitea"));
|
|
45
|
+
it("detects Codeberg", () => expect(detectPlatform("codeberg.org")).toBe("codeberg"));
|
|
46
|
+
it("detects SourceHut", () => expect(detectPlatform("sr.ht")).toBe("sourcehut"));
|
|
47
|
+
it("returns undefined for unknown host", () => expect(detectPlatform("self-hosted.example.com")).toBeUndefined());
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("buildGitLinks", () => {
|
|
51
|
+
it("builds GitHub branch and PR links", () => {
|
|
52
|
+
const links = buildGitLinks("git@github.com:user/repo.git", "main", 42);
|
|
53
|
+
expect(links.branchUrl).toBe("https://github.com/user/repo/tree/main");
|
|
54
|
+
expect(links.prUrl).toBe("https://github.com/user/repo/pull/42");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("builds GitLab branch and MR links", () => {
|
|
58
|
+
const links = buildGitLinks("https://gitlab.com/user/repo.git", "main", 10);
|
|
59
|
+
expect(links.branchUrl).toBe("https://gitlab.com/user/repo/-/tree/main");
|
|
60
|
+
expect(links.prUrl).toBe("https://gitlab.com/user/repo/-/merge_requests/10");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("builds Bitbucket links", () => {
|
|
64
|
+
const links = buildGitLinks("git@bitbucket.org:user/repo.git", "develop", 5);
|
|
65
|
+
expect(links.branchUrl).toBe("https://bitbucket.org/user/repo/src/develop");
|
|
66
|
+
expect(links.prUrl).toBe("https://bitbucket.org/user/repo/pull-requests/5");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("builds Codeberg links", () => {
|
|
70
|
+
const links = buildGitLinks("https://codeberg.org/user/repo.git", "main", 3);
|
|
71
|
+
expect(links.branchUrl).toBe("https://codeberg.org/user/repo/src/branch/main");
|
|
72
|
+
expect(links.prUrl).toBe("https://codeberg.org/user/repo/pulls/3");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("builds SourceHut links", () => {
|
|
76
|
+
const links = buildGitLinks("git@sr.ht:user/repo.git", "main", 1);
|
|
77
|
+
expect(links.branchUrl).toBe("https://sr.ht/user/repo/tree/main");
|
|
78
|
+
expect(links.prUrl).toBe("https://sr.ht/user/repo/patches/1");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("URL-encodes branch names with slashes", () => {
|
|
82
|
+
const links = buildGitLinks("git@github.com:user/repo.git", "feat/my-feature");
|
|
83
|
+
expect(links.branchUrl).toBe("https://github.com/user/repo/tree/feat%2Fmy-feature");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns empty for unknown host", () => {
|
|
87
|
+
const links = buildGitLinks("git@self-hosted.example.com:user/repo.git", "main");
|
|
88
|
+
expect(links.branchUrl).toBeUndefined();
|
|
89
|
+
expect(links.prUrl).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("does not generate branch URL for detached HEAD", () => {
|
|
93
|
+
const links = buildGitLinks("git@github.com:user/repo.git", "HEAD");
|
|
94
|
+
expect(links.branchUrl).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("builds branch URL without PR", () => {
|
|
98
|
+
const links = buildGitLinks("git@github.com:user/repo.git", "main");
|
|
99
|
+
expect(links.branchUrl).toBe("https://github.com/user/repo/tree/main");
|
|
100
|
+
expect(links.prUrl).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { detectOpenSpecActivity } from "@blackbelt-technology/pi-dashboard-shared/openspec-activity-detector.js";
|
|
3
|
+
|
|
4
|
+
describe("detectOpenSpecActivity", () => {
|
|
5
|
+
describe("phase detection from skill file reads", () => {
|
|
6
|
+
it("detects apply phase from SKILL.md read", () => {
|
|
7
|
+
const result = detectOpenSpecActivity("read", {
|
|
8
|
+
path: ".pi/skills/openspec-apply-change/SKILL.md",
|
|
9
|
+
});
|
|
10
|
+
expect(result).toEqual({ phase: "apply" });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("detects explore phase", () => {
|
|
14
|
+
const result = detectOpenSpecActivity("read", {
|
|
15
|
+
path: ".pi/skills/openspec-explore/SKILL.md",
|
|
16
|
+
});
|
|
17
|
+
expect(result).toEqual({ phase: "explore" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("detects new phase", () => {
|
|
21
|
+
const result = detectOpenSpecActivity("read", {
|
|
22
|
+
path: ".pi/skills/openspec-new-change/SKILL.md",
|
|
23
|
+
});
|
|
24
|
+
expect(result).toEqual({ phase: "new" });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("detects continue phase", () => {
|
|
28
|
+
const result = detectOpenSpecActivity("read", {
|
|
29
|
+
path: ".pi/skills/openspec-continue-change/SKILL.md",
|
|
30
|
+
});
|
|
31
|
+
expect(result).toEqual({ phase: "continue" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("detects ff phase", () => {
|
|
35
|
+
const result = detectOpenSpecActivity("read", {
|
|
36
|
+
path: ".pi/skills/openspec-ff-change/SKILL.md",
|
|
37
|
+
});
|
|
38
|
+
expect(result).toEqual({ phase: "ff" });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("detects verify phase", () => {
|
|
42
|
+
const result = detectOpenSpecActivity("read", {
|
|
43
|
+
path: ".pi/skills/openspec-verify-change/SKILL.md",
|
|
44
|
+
});
|
|
45
|
+
expect(result).toEqual({ phase: "verify" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("detects archive phase", () => {
|
|
49
|
+
const result = detectOpenSpecActivity("read", {
|
|
50
|
+
path: ".pi/skills/openspec-archive-change/SKILL.md",
|
|
51
|
+
});
|
|
52
|
+
expect(result).toEqual({ phase: "archive" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("detects sync-specs phase", () => {
|
|
56
|
+
const result = detectOpenSpecActivity("read", {
|
|
57
|
+
path: ".pi/skills/openspec-sync-specs/SKILL.md",
|
|
58
|
+
});
|
|
59
|
+
expect(result).toEqual({ phase: "sync-specs" });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("detects onboard phase", () => {
|
|
63
|
+
const result = detectOpenSpecActivity("read", {
|
|
64
|
+
path: ".pi/skills/openspec-onboard/SKILL.md",
|
|
65
|
+
});
|
|
66
|
+
expect(result).toEqual({ phase: "onboard" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("handles absolute paths", () => {
|
|
70
|
+
const result = detectOpenSpecActivity("read", {
|
|
71
|
+
path: "/Users/dev/project/.pi/skills/openspec-apply-change/SKILL.md",
|
|
72
|
+
});
|
|
73
|
+
expect(result).toEqual({ phase: "apply" });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns null for non-openspec skill reads", () => {
|
|
77
|
+
const result = detectOpenSpecActivity("read", {
|
|
78
|
+
path: ".pi/skills/some-other-skill/SKILL.md",
|
|
79
|
+
});
|
|
80
|
+
expect(result).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns null for non-SKILL.md reads in openspec dirs", () => {
|
|
84
|
+
const result = detectOpenSpecActivity("read", {
|
|
85
|
+
path: ".pi/skills/openspec-apply-change/README.md",
|
|
86
|
+
});
|
|
87
|
+
expect(result).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("change name detection from CLI calls", () => {
|
|
92
|
+
it("detects change name from openspec status command", () => {
|
|
93
|
+
const result = detectOpenSpecActivity("bash", {
|
|
94
|
+
command: 'openspec status --change "session-sync" --json',
|
|
95
|
+
});
|
|
96
|
+
expect(result).toEqual({ changeName: "session-sync" });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("detects change name from openspec instructions command", () => {
|
|
100
|
+
const result = detectOpenSpecActivity("bash", {
|
|
101
|
+
command: 'openspec instructions apply --change "my-feature" --json',
|
|
102
|
+
});
|
|
103
|
+
expect(result).toEqual({ changeName: "my-feature" });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("detects change name without quotes", () => {
|
|
107
|
+
const result = detectOpenSpecActivity("bash", {
|
|
108
|
+
command: "openspec status --change session-sync --json",
|
|
109
|
+
});
|
|
110
|
+
expect(result).toEqual({ changeName: "session-sync" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("detects change name from openspec archive command", () => {
|
|
114
|
+
const result = detectOpenSpecActivity("bash", {
|
|
115
|
+
command: "openspec archive session-sync",
|
|
116
|
+
});
|
|
117
|
+
expect(result).toEqual({ changeName: "session-sync" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns null for non-openspec bash commands", () => {
|
|
121
|
+
const result = detectOpenSpecActivity("bash", {
|
|
122
|
+
command: "npm test",
|
|
123
|
+
});
|
|
124
|
+
expect(result).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("detects change name from openspec new change with quoted name", () => {
|
|
128
|
+
const result = detectOpenSpecActivity("bash", {
|
|
129
|
+
command: 'openspec new change "add-auth"',
|
|
130
|
+
});
|
|
131
|
+
expect(result).toEqual({ changeName: "add-auth" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("detects change name from openspec new change with unquoted name", () => {
|
|
135
|
+
const result = detectOpenSpecActivity("bash", {
|
|
136
|
+
command: "openspec new change add-auth",
|
|
137
|
+
});
|
|
138
|
+
expect(result).toEqual({ changeName: "add-auth" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("detects change name from openspec new change with cd prefix", () => {
|
|
142
|
+
const result = detectOpenSpecActivity("bash", {
|
|
143
|
+
command: 'cd /Users/dev/project && openspec new change "my-feature"',
|
|
144
|
+
});
|
|
145
|
+
expect(result).toEqual({ changeName: "my-feature" });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns null for openspec list (no change name)", () => {
|
|
149
|
+
const result = detectOpenSpecActivity("bash", {
|
|
150
|
+
command: "openspec list --json",
|
|
151
|
+
});
|
|
152
|
+
expect(result).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("change name detection from file reads", () => {
|
|
157
|
+
it("detects change name from openspec change file read", () => {
|
|
158
|
+
const result = detectOpenSpecActivity("read", {
|
|
159
|
+
path: "openspec/changes/session-sync/tasks.md",
|
|
160
|
+
});
|
|
161
|
+
expect(result).toEqual({ changeName: "session-sync" });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("detects change name from absolute path", () => {
|
|
165
|
+
const result = detectOpenSpecActivity("read", {
|
|
166
|
+
path: "/Users/dev/project/openspec/changes/my-feature/proposal.md",
|
|
167
|
+
});
|
|
168
|
+
expect(result).toEqual({ changeName: "my-feature" });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns null for non-openspec file reads", () => {
|
|
172
|
+
const result = detectOpenSpecActivity("read", {
|
|
173
|
+
path: "src/server/server.ts",
|
|
174
|
+
});
|
|
175
|
+
expect(result).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("change name detection from file writes", () => {
|
|
180
|
+
it("detects change name from openspec change file write", () => {
|
|
181
|
+
const result = detectOpenSpecActivity("write", {
|
|
182
|
+
path: "openspec/changes/session-sync/proposal.md",
|
|
183
|
+
});
|
|
184
|
+
expect(result).toEqual({ changeName: "session-sync" });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("detects change name from absolute path write", () => {
|
|
188
|
+
const result = detectOpenSpecActivity("write", {
|
|
189
|
+
path: "/Users/dev/project/openspec/changes/my-feature/spec.md",
|
|
190
|
+
});
|
|
191
|
+
expect(result).toEqual({ changeName: "my-feature" });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("returns null for non-openspec file writes", () => {
|
|
195
|
+
const result = detectOpenSpecActivity("write", {
|
|
196
|
+
path: "src/server/server.ts",
|
|
197
|
+
});
|
|
198
|
+
expect(result).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("edge cases", () => {
|
|
203
|
+
it("handles capitalized tool names (backward compatibility)", () => {
|
|
204
|
+
expect(detectOpenSpecActivity("Read", {
|
|
205
|
+
path: "openspec/changes/my-feature/proposal.md",
|
|
206
|
+
})).toEqual({ changeName: "my-feature" });
|
|
207
|
+
|
|
208
|
+
expect(detectOpenSpecActivity("Bash", {
|
|
209
|
+
command: 'openspec status --change "add-auth" --json',
|
|
210
|
+
})).toEqual({ changeName: "add-auth" });
|
|
211
|
+
|
|
212
|
+
expect(detectOpenSpecActivity("Write", {
|
|
213
|
+
path: "openspec/changes/my-feature/design.md",
|
|
214
|
+
})).toEqual({ changeName: "my-feature" });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("returns null for unknown tool names", () => {
|
|
218
|
+
const result = detectOpenSpecActivity("unknown", { path: "foo.ts" });
|
|
219
|
+
expect(result).toBeNull();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns null when args are missing", () => {
|
|
223
|
+
const result = detectOpenSpecActivity("read", undefined);
|
|
224
|
+
expect(result).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("returns null when args are empty", () => {
|
|
228
|
+
const result = detectOpenSpecActivity("read", {});
|
|
229
|
+
expect(result).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { SpawnSyncReturns } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
// We need to mock spawnSync before importing the module under test
|
|
5
|
+
const mockSpawnSync = vi.fn<(...args: any[]) => any>();
|
|
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 },
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Import after mock is set up
|
|
15
|
+
const { pollOpenSpec } = await import("@blackbelt-technology/pi-dashboard-shared/openspec-poller.js");
|
|
16
|
+
|
|
17
|
+
function ok(data: unknown): Partial<SpawnSyncReturns<string>> {
|
|
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
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockSpawnSync.mockReset();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("pollOpenSpec", () => {
|
|
47
|
+
it("returns initialized=false when CLI throws", () => {
|
|
48
|
+
mockSpawnSync.mockImplementation(() => { throw new Error("ENOENT"); });
|
|
49
|
+
const result = pollOpenSpec("/project");
|
|
50
|
+
expect(result).toEqual({ initialized: false, changes: [] });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns initialized=false when list returns non-zero exit", () => {
|
|
54
|
+
mockSpawnSync.mockReturnValue(fail());
|
|
55
|
+
const result = pollOpenSpec("/project");
|
|
56
|
+
expect(result).toEqual({ initialized: false, changes: [] });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns initialized=true with changes on success", () => {
|
|
60
|
+
mockCli(new Map([
|
|
61
|
+
["list", {
|
|
62
|
+
changes: [
|
|
63
|
+
{ name: "feat-a", status: "in-progress", completedTasks: 3, totalTasks: 5 },
|
|
64
|
+
{ name: "feat-b", status: "complete", completedTasks: 4, totalTasks: 4 },
|
|
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,
|
|
90
|
+
artifacts: [
|
|
91
|
+
{ id: "proposal", status: "done" },
|
|
92
|
+
{ id: "design", status: "ready" },
|
|
93
|
+
{ id: "tasks", status: "blocked" },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
expect(result.changes[1].status).toBe("complete");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("handles invalid JSON gracefully", () => {
|
|
100
|
+
mockSpawnSync.mockReturnValue({ status: 0, stdout: "not json {{{", stderr: "" });
|
|
101
|
+
const result = pollOpenSpec("/project");
|
|
102
|
+
expect(result).toEqual({ initialized: false, changes: [] });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("handles status call failure gracefully (empty artifacts)", () => {
|
|
106
|
+
mockCli(new Map([
|
|
107
|
+
["list", {
|
|
108
|
+
changes: [
|
|
109
|
+
{ name: "feat-a", status: "no-tasks", completedTasks: 0, totalTasks: 0 },
|
|
110
|
+
],
|
|
111
|
+
}],
|
|
112
|
+
// No status:feat-a — status call fails
|
|
113
|
+
]));
|
|
114
|
+
|
|
115
|
+
const result = pollOpenSpec("/project");
|
|
116
|
+
expect(result.initialized).toBe(true);
|
|
117
|
+
expect(result.changes[0].artifacts).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { startMetricsMonitor, stopMetricsMonitor, collectMetrics } from "../process-metrics.js";
|
|
3
|
+
|
|
4
|
+
describe("process-metrics", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
stopMetricsMonitor();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("collectMetrics returns valid shape without monitor", () => {
|
|
10
|
+
const m = collectMetrics();
|
|
11
|
+
expect(m.rss).toBeGreaterThan(0);
|
|
12
|
+
expect(m.heapUsed).toBeGreaterThan(0);
|
|
13
|
+
expect(m.heapTotal).toBeGreaterThan(0);
|
|
14
|
+
expect(typeof m.cpuPercent).toBe("number");
|
|
15
|
+
expect(typeof m.loadAvg1m).toBe("number");
|
|
16
|
+
// eventLoopMaxMs is undefined without monitor
|
|
17
|
+
expect(m.eventLoopMaxMs).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("collectMetrics includes eventLoopMaxMs with monitor", async () => {
|
|
21
|
+
startMetricsMonitor();
|
|
22
|
+
// Let the event loop tick to capture some delay
|
|
23
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
24
|
+
const m = collectMetrics();
|
|
25
|
+
expect(typeof m.eventLoopMaxMs).toBe("number");
|
|
26
|
+
expect(m.eventLoopMaxMs).toBeGreaterThanOrEqual(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("cpuPercent computes delta on second call", () => {
|
|
30
|
+
const first = collectMetrics();
|
|
31
|
+
// Do some CPU work
|
|
32
|
+
let x = 0;
|
|
33
|
+
for (let i = 0; i < 1_000_000; i++) x += Math.sqrt(i);
|
|
34
|
+
const second = collectMetrics();
|
|
35
|
+
// Both should be numbers, second should show some cpu
|
|
36
|
+
expect(typeof first.cpuPercent).toBe("number");
|
|
37
|
+
expect(typeof second.cpuPercent).toBe("number");
|
|
38
|
+
void x;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("startMetricsMonitor is idempotent", () => {
|
|
42
|
+
startMetricsMonitor();
|
|
43
|
+
startMetricsMonitor(); // should not throw
|
|
44
|
+
const m = collectMetrics();
|
|
45
|
+
expect(m.rss).toBeGreaterThan(0);
|
|
46
|
+
});
|
|
47
|
+
});
|