@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4
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 +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +11 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- package/packages/shared/src/launch-source-flag.ts +0 -14
|
@@ -35,11 +35,11 @@ const FAKE_MANIFEST: readonly RecommendedExtension[] = [
|
|
|
35
35
|
toolsRegistered: [],
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
|
-
id: "
|
|
39
|
-
source: "npm
|
|
40
|
-
displayName: "
|
|
38
|
+
id: "pi-dashboard-subagents",
|
|
39
|
+
source: "npm:pi-dashboard-subagents",
|
|
40
|
+
displayName: "pi-dashboard-subagents",
|
|
41
41
|
fallbackDescription: "Sub-agents for pi.",
|
|
42
|
-
status: "
|
|
42
|
+
status: "optional",
|
|
43
43
|
unlocks: [],
|
|
44
44
|
toolsRegistered: [],
|
|
45
45
|
},
|
|
@@ -49,7 +49,7 @@ describe("extractBasenameFromSource", () => {
|
|
|
49
49
|
it("strips npm: prefix and version pin", () => {
|
|
50
50
|
expect(extractBasenameFromSource("npm:pi-agent-browser")).toBe("pi-agent-browser");
|
|
51
51
|
expect(extractBasenameFromSource("npm:pi-agent-browser@1.2.3")).toBe("pi-agent-browser");
|
|
52
|
-
expect(extractBasenameFromSource("npm:@
|
|
52
|
+
expect(extractBasenameFromSource("npm:@scope/example-pkg")).toBe("@scope/example-pkg"); // generic scoped-name parsing
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it("strips .git suffix from git URLs", () => {
|
|
@@ -70,8 +70,8 @@ describe("extractBasenameFromSource", () => {
|
|
|
70
70
|
describe("matchRecommendedEntry", () => {
|
|
71
71
|
it("matches by exact source", () => {
|
|
72
72
|
expect(
|
|
73
|
-
matchRecommendedEntry("npm
|
|
74
|
-
).toBe("
|
|
73
|
+
matchRecommendedEntry("npm:pi-dashboard-subagents", FAKE_MANIFEST)?.id,
|
|
74
|
+
).toBe("pi-dashboard-subagents");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
it("matches git source regardless of trailing slash / case", () => {
|
|
@@ -115,21 +115,21 @@ describe("enrichInstalledRow", () => {
|
|
|
115
115
|
|
|
116
116
|
it("enriches a recommended npm row with displayName and description from manifest", () => {
|
|
117
117
|
const row: RawInstalledRow = {
|
|
118
|
-
source: "npm
|
|
118
|
+
source: "npm:pi-dashboard-subagents",
|
|
119
119
|
scope: "user",
|
|
120
120
|
filtered: false,
|
|
121
121
|
installedPath: "/fake/path",
|
|
122
122
|
};
|
|
123
123
|
const out = enrichInstalledRow(row, {
|
|
124
124
|
...baseDeps,
|
|
125
|
-
readMeta: () => ({ version: "0.
|
|
125
|
+
readMeta: () => ({ version: "0.1.1", description: "Live npm desc" }),
|
|
126
126
|
existsFn: () => false,
|
|
127
127
|
resourcesPath: "/res",
|
|
128
128
|
});
|
|
129
|
-
expect(out.displayName).toBe("
|
|
129
|
+
expect(out.displayName).toBe("pi-dashboard-subagents");
|
|
130
130
|
// Recommended manifest description wins over package.json description.
|
|
131
131
|
expect(out.description).toBe("Sub-agents for pi.");
|
|
132
|
-
expect(out.version).toBe("0.
|
|
132
|
+
expect(out.version).toBe("0.1.1");
|
|
133
133
|
expect(out.isRecommended).toBe(true);
|
|
134
134
|
expect(out.isBundled).toBe(false);
|
|
135
135
|
});
|
|
@@ -173,7 +173,7 @@ describe("enrichInstalledRow", () => {
|
|
|
173
173
|
|
|
174
174
|
it("handles missing installedPath silently", () => {
|
|
175
175
|
const row: RawInstalledRow = {
|
|
176
|
-
source: "npm
|
|
176
|
+
source: "npm:pi-dashboard-subagents",
|
|
177
177
|
scope: "user",
|
|
178
178
|
filtered: false,
|
|
179
179
|
};
|
|
@@ -17,13 +17,10 @@ describe("isActivityEvent", () => {
|
|
|
17
17
|
"agent_start",
|
|
18
18
|
"agent_end",
|
|
19
19
|
"bash_output",
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"architect_started",
|
|
25
|
-
"architect_complete",
|
|
26
|
-
"architect_cancelled",
|
|
20
|
+
// Flow / architect events removed: per change
|
|
21
|
+
// pluginize-flows-via-registry the shell carries no flow
|
|
22
|
+
// knowledge. lastActivityAt for flow-only sessions is bumped
|
|
23
|
+
// by the constituent tool/agent events the flow generates.
|
|
27
24
|
];
|
|
28
25
|
|
|
29
26
|
for (const t of included) {
|
|
@@ -112,14 +112,18 @@ describe("package-routes", () => {
|
|
|
112
112
|
|
|
113
113
|
it("matches a row to RECOMMENDED_EXTENSIONS by source", async () => {
|
|
114
114
|
wrapper.listInstalled.mockReturnValueOnce([
|
|
115
|
-
{
|
|
115
|
+
{
|
|
116
|
+
source: "https://github.com/BlackBeltTechnology/pi-dashboard-subagents.git",
|
|
117
|
+
scope: "user",
|
|
118
|
+
filtered: false,
|
|
119
|
+
},
|
|
116
120
|
]);
|
|
117
121
|
const res = await app.inject({ method: "GET", url: "/api/packages/installed?scope=global" });
|
|
118
122
|
const body = JSON.parse(res.body);
|
|
119
123
|
const row = body.data[0];
|
|
120
124
|
expect(row.isRecommended).toBe(true);
|
|
121
125
|
// displayName comes from the recommended manifest.
|
|
122
|
-
expect(row.displayName).toBe("
|
|
126
|
+
expect(row.displayName).toBe("pi-dashboard-subagents");
|
|
123
127
|
});
|
|
124
128
|
|
|
125
129
|
it("missing installedPath does not break enrichment", async () => {
|
|
@@ -168,19 +168,16 @@ describe("pi-changelog-routes", () => {
|
|
|
168
168
|
expect(res.statusCode).toBe(400);
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
expect(res.statusCode).toBe(503);
|
|
182
|
-
expect(res.json().bootstrap).toBe("installing");
|
|
183
|
-
});
|
|
171
|
+
// NOTE: "returns 503 when bootstrap is not ready" test removed.
|
|
172
|
+
// The bootstrap gate on this route was deliberately removed in change
|
|
173
|
+
// `eliminate-electron-runtime-install` (task 3.5, 2026-05-23). The
|
|
174
|
+
// route file's own docstring confirms it: "Bootstrap gate removed
|
|
175
|
+
// under change: eliminate-electron-runtime-install (task 3.5)". The
|
|
176
|
+
// `PiChangelogRouteDeps` interface comment also says the field was
|
|
177
|
+
// removed; the route is unconditionally available. This test was
|
|
178
|
+
// documented as deferred to a "Phase 3.9 sweep" in
|
|
179
|
+
// eliminate-electron-runtime-install/tasks.md task 5.9; this is that
|
|
180
|
+
// sweep.
|
|
184
181
|
|
|
185
182
|
it("returns no releases when from === to", async () => {
|
|
186
183
|
makeManagedPkg("@mariozechner/pi-coding-agent", {
|
|
@@ -20,7 +20,7 @@ describe("PiCoreChecker._internal.looksLikePiEcosystem", () => {
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("rejects scoped pi-* packages that are NOT in the whitelist", () => {
|
|
23
|
-
expect(_internal.looksLikePiEcosystem("@
|
|
23
|
+
expect(_internal.looksLikePiEcosystem("@scope/pi-fake")).toBe(false);
|
|
24
24
|
expect(_internal.looksLikePiEcosystem("@benvargas/pi-claude-code-use")).toBe(false);
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -105,7 +105,7 @@ describe("PiCoreChecker.getStatus", () => {
|
|
|
105
105
|
dependencies: {
|
|
106
106
|
"pi-agent-browser": { version: "0.1.0" },
|
|
107
107
|
"pi-web-access": { version: "0.10.6" },
|
|
108
|
-
"
|
|
108
|
+
"pi-dashboard-subagents": { version: "0.1.1" },
|
|
109
109
|
},
|
|
110
110
|
}),
|
|
111
111
|
fetchLatest: async () => null,
|
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
readPiCompatibility,
|
|
16
16
|
readCurrentPiVersion,
|
|
17
17
|
computeCompatibility,
|
|
18
|
-
_resetVersionSkewCache,
|
|
19
18
|
} from "../pi-version-skew.js";
|
|
20
19
|
import type { ToolRegistry, Resolution } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
|
|
21
20
|
|
|
22
21
|
describe("pi-version-skew", () => {
|
|
23
22
|
beforeEach(() => {
|
|
24
|
-
_resetVersionSkewCache
|
|
23
|
+
// Cache (formerly `_resetVersionSkewCache`) removed under change:
|
|
24
|
+
// eliminate-electron-runtime-install (task 3.6) along with
|
|
25
|
+
// updateBootstrapCompatibility. Tests now exercise pure helpers only.
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
describe("parseVersion", () => {
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route tests for `GET /api/plugins` and `POST /api/plugins/:id/toggle`.
|
|
3
|
+
*
|
|
4
|
+
* See change: add-plugin-activation-ui.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import Fastify, { type FastifyInstance } from "fastify";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { registerPluginActivationRoutes } from "../routes/plugin-activation-routes.js";
|
|
12
|
+
import {
|
|
13
|
+
clearDiscoveryCache,
|
|
14
|
+
clearStatusStore,
|
|
15
|
+
discoverPlugins,
|
|
16
|
+
getPluginStatusStore,
|
|
17
|
+
} from "@blackbelt-technology/dashboard-plugin-runtime/server";
|
|
18
|
+
|
|
19
|
+
const HOME_OVERRIDE = path.join(os.tmpdir(), "pi-dashboard-activation-test-" + process.pid);
|
|
20
|
+
|
|
21
|
+
function makeRepoRootWithPlugin(id: string, displayName: string, opts: { enabledInConfig?: boolean } = {}) {
|
|
22
|
+
const repoRoot = path.join(os.tmpdir(), `activation-routes-test-${id}-${Date.now()}`);
|
|
23
|
+
const pkgDir = path.join(repoRoot, "packages", id);
|
|
24
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
25
|
+
fs.writeFileSync(
|
|
26
|
+
path.join(pkgDir, "package.json"),
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
name: `@test/${id}`,
|
|
29
|
+
version: "0.0.0",
|
|
30
|
+
"pi-dashboard-plugin": { id, displayName, claims: [] },
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (opts.enabledInConfig !== undefined) {
|
|
35
|
+
fs.mkdirSync(path.join(HOME_OVERRIDE, ".pi", "dashboard"), { recursive: true });
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json"),
|
|
38
|
+
JSON.stringify({ plugins: { [id]: { enabled: opts.enabledInConfig } } }, null, 2),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return repoRoot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeRepoRootWithPluginPair(
|
|
45
|
+
rootSuffix: string,
|
|
46
|
+
a: { id: string; displayName: string; enabledInConfig?: boolean },
|
|
47
|
+
b: { id: string; displayName: string; dependsOn: string[]; enabledInConfig?: boolean },
|
|
48
|
+
) {
|
|
49
|
+
const repoRoot = path.join(os.tmpdir(), `activation-routes-test-${rootSuffix}-${Date.now()}`);
|
|
50
|
+
for (const p of [a, b]) {
|
|
51
|
+
const pkgDir = path.join(repoRoot, "packages", p.id);
|
|
52
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
53
|
+
const manifest: Record<string, unknown> = { id: p.id, displayName: p.displayName, claims: [] };
|
|
54
|
+
if ("dependsOn" in p && p.dependsOn) manifest.dependsOn = p.dependsOn;
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(pkgDir, "package.json"),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
name: `@test/${p.id}`,
|
|
59
|
+
version: "0.0.0",
|
|
60
|
+
"pi-dashboard-plugin": manifest,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const cfgPlugins: Record<string, { enabled: boolean }> = {};
|
|
65
|
+
if (a.enabledInConfig !== undefined) cfgPlugins[a.id] = { enabled: a.enabledInConfig };
|
|
66
|
+
if (b.enabledInConfig !== undefined) cfgPlugins[b.id] = { enabled: b.enabledInConfig };
|
|
67
|
+
if (Object.keys(cfgPlugins).length > 0) {
|
|
68
|
+
fs.mkdirSync(path.join(HOME_OVERRIDE, ".pi", "dashboard"), { recursive: true });
|
|
69
|
+
fs.writeFileSync(
|
|
70
|
+
path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json"),
|
|
71
|
+
JSON.stringify({ plugins: cfgPlugins }, null, 2),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return repoRoot;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let originalHome: string | undefined;
|
|
78
|
+
async function makeApp(repoRoot: string, broadcasts: unknown[] = []): Promise<FastifyInstance> {
|
|
79
|
+
const app = Fastify({ logger: false });
|
|
80
|
+
registerPluginActivationRoutes(app, {
|
|
81
|
+
networkGuard: async () => undefined,
|
|
82
|
+
broadcast: (m) => broadcasts.push(m),
|
|
83
|
+
repoRoot,
|
|
84
|
+
});
|
|
85
|
+
await app.ready();
|
|
86
|
+
return app;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("/api/plugins", () => {
|
|
90
|
+
let app: FastifyInstance;
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
clearDiscoveryCache();
|
|
93
|
+
clearStatusStore();
|
|
94
|
+
fs.rmSync(HOME_OVERRIDE, { recursive: true, force: true });
|
|
95
|
+
originalHome = process.env.HOME;
|
|
96
|
+
process.env.HOME = HOME_OVERRIDE;
|
|
97
|
+
});
|
|
98
|
+
afterEach(async () => {
|
|
99
|
+
await app?.close();
|
|
100
|
+
if (originalHome !== undefined) process.env.HOME = originalHome;
|
|
101
|
+
else delete process.env.HOME;
|
|
102
|
+
fs.rmSync(HOME_OVERRIDE, { recursive: true, force: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns every discovered plugin with manifest summary + status", async () => {
|
|
106
|
+
const repoRoot = makeRepoRootWithPlugin("act-a", "Act A");
|
|
107
|
+
// Seed status so the row carries the runtime view too.
|
|
108
|
+
getPluginStatusStore().setStatus({
|
|
109
|
+
id: "act-a",
|
|
110
|
+
displayName: "Act A",
|
|
111
|
+
enabled: true,
|
|
112
|
+
loaded: true,
|
|
113
|
+
claims: 0,
|
|
114
|
+
});
|
|
115
|
+
app = await makeApp(repoRoot);
|
|
116
|
+
|
|
117
|
+
const res = await app.inject({ method: "GET", url: "/api/plugins" });
|
|
118
|
+
expect(res.statusCode).toBe(200);
|
|
119
|
+
const body = res.json() as { success: boolean; plugins: any[] };
|
|
120
|
+
expect(body.success).toBe(true);
|
|
121
|
+
expect(body.plugins).toHaveLength(1);
|
|
122
|
+
expect(body.plugins[0].id).toBe("act-a");
|
|
123
|
+
expect(body.plugins[0].displayName).toBe("Act A");
|
|
124
|
+
expect(body.plugins[0].status.enabled).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("POST /api/plugins/:id/toggle", () => {
|
|
129
|
+
let app: FastifyInstance;
|
|
130
|
+
let broadcasts: unknown[];
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
clearDiscoveryCache();
|
|
133
|
+
clearStatusStore();
|
|
134
|
+
fs.rmSync(HOME_OVERRIDE, { recursive: true, force: true });
|
|
135
|
+
originalHome = process.env.HOME;
|
|
136
|
+
process.env.HOME = HOME_OVERRIDE;
|
|
137
|
+
broadcasts = [];
|
|
138
|
+
});
|
|
139
|
+
afterEach(async () => {
|
|
140
|
+
await app?.close();
|
|
141
|
+
if (originalHome !== undefined) process.env.HOME = originalHome;
|
|
142
|
+
else delete process.env.HOME;
|
|
143
|
+
fs.rmSync(HOME_OVERRIDE, { recursive: true, force: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("persists enabled=false to config.json and broadcasts plugin_config_update", async () => {
|
|
147
|
+
const repoRoot = makeRepoRootWithPlugin("act-b", "Act B");
|
|
148
|
+
app = await makeApp(repoRoot, broadcasts);
|
|
149
|
+
|
|
150
|
+
const res = await app.inject({
|
|
151
|
+
method: "POST",
|
|
152
|
+
url: "/api/plugins/act-b/toggle",
|
|
153
|
+
payload: { enabled: false },
|
|
154
|
+
});
|
|
155
|
+
expect(res.statusCode).toBe(200);
|
|
156
|
+
const body = res.json() as { success: boolean; restartRequired: boolean };
|
|
157
|
+
expect(body.restartRequired).toBe(true);
|
|
158
|
+
|
|
159
|
+
const cfgPath = path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json");
|
|
160
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
161
|
+
expect(cfg.plugins["act-b"].enabled).toBe(false);
|
|
162
|
+
|
|
163
|
+
expect(broadcasts).toHaveLength(1);
|
|
164
|
+
expect((broadcasts[0] as any).type).toBe("plugin_config_update");
|
|
165
|
+
expect((broadcasts[0] as any).id).toBe("act-b");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("returns 404 for unknown plugin id", async () => {
|
|
169
|
+
const repoRoot = makeRepoRootWithPlugin("act-c", "Act C");
|
|
170
|
+
app = await makeApp(repoRoot, broadcasts);
|
|
171
|
+
|
|
172
|
+
const res = await app.inject({
|
|
173
|
+
method: "POST",
|
|
174
|
+
url: "/api/plugins/no-such/toggle",
|
|
175
|
+
payload: { enabled: false },
|
|
176
|
+
});
|
|
177
|
+
expect(res.statusCode).toBe(404);
|
|
178
|
+
// config.json should not have been created
|
|
179
|
+
const cfgPath = path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json");
|
|
180
|
+
expect(fs.existsSync(cfgPath)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns 400 when body.enabled is not a boolean", async () => {
|
|
184
|
+
const repoRoot = makeRepoRootWithPlugin("act-d", "Act D");
|
|
185
|
+
app = await makeApp(repoRoot, broadcasts);
|
|
186
|
+
|
|
187
|
+
const res = await app.inject({
|
|
188
|
+
method: "POST",
|
|
189
|
+
url: "/api/plugins/act-d/toggle",
|
|
190
|
+
payload: { enabled: "yes" },
|
|
191
|
+
});
|
|
192
|
+
expect(res.statusCode).toBe(400);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ---- Dependency-graph cascade tests (Robert's Layer 2 spec) ------------
|
|
196
|
+
|
|
197
|
+
it("enabling a plugin with a missing dep returns 409 + blockers", async () => {
|
|
198
|
+
const repoRoot = makeRepoRootWithPluginPair(
|
|
199
|
+
"cascade-blocker",
|
|
200
|
+
{ id: "orphan-a", displayName: "Orphan A" },
|
|
201
|
+
{ id: "orphan-b", displayName: "Orphan B", dependsOn: ["missing-x"], enabledInConfig: false },
|
|
202
|
+
);
|
|
203
|
+
app = await makeApp(repoRoot, broadcasts);
|
|
204
|
+
|
|
205
|
+
const res = await app.inject({
|
|
206
|
+
method: "POST",
|
|
207
|
+
url: "/api/plugins/orphan-b/toggle",
|
|
208
|
+
payload: { enabled: true },
|
|
209
|
+
});
|
|
210
|
+
expect(res.statusCode).toBe(409);
|
|
211
|
+
const body = res.json() as { success: boolean; reason: string; blockers: string[] };
|
|
212
|
+
expect(body.reason).toBe("blockers");
|
|
213
|
+
expect(body.blockers).toEqual(["missing-x"]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("enabling cascades disabled deps and writes atomically", async () => {
|
|
217
|
+
const repoRoot = makeRepoRootWithPluginPair(
|
|
218
|
+
"cascade-enable",
|
|
219
|
+
{ id: "cas-a", displayName: "Cas A", enabledInConfig: false },
|
|
220
|
+
{ id: "cas-b", displayName: "Cas B", dependsOn: ["cas-a"], enabledInConfig: false },
|
|
221
|
+
);
|
|
222
|
+
app = await makeApp(repoRoot, broadcasts);
|
|
223
|
+
|
|
224
|
+
const res = await app.inject({
|
|
225
|
+
method: "POST",
|
|
226
|
+
url: "/api/plugins/cas-b/toggle",
|
|
227
|
+
payload: { enabled: true },
|
|
228
|
+
});
|
|
229
|
+
expect(res.statusCode).toBe(200);
|
|
230
|
+
const body = res.json() as { success: boolean; cascade: { enable: string[] } };
|
|
231
|
+
expect(body.cascade.enable).toEqual(["cas-a"]);
|
|
232
|
+
|
|
233
|
+
const cfg = JSON.parse(
|
|
234
|
+
fs.readFileSync(path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json"), "utf-8"),
|
|
235
|
+
);
|
|
236
|
+
expect(cfg.plugins["cas-a"].enabled).toBe(true);
|
|
237
|
+
expect(cfg.plugins["cas-b"].enabled).toBe(true);
|
|
238
|
+
|
|
239
|
+
// Two broadcasts, one per affected id.
|
|
240
|
+
const ids = broadcasts.map((m) => (m as { id: string }).id).sort();
|
|
241
|
+
expect(ids).toEqual(["cas-a", "cas-b"]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("disabling cascades enabled dependents and writes atomically", async () => {
|
|
245
|
+
const repoRoot = makeRepoRootWithPluginPair(
|
|
246
|
+
"cascade-disable",
|
|
247
|
+
{ id: "dis-a", displayName: "Dis A", enabledInConfig: true },
|
|
248
|
+
{ id: "dis-b", displayName: "Dis B", dependsOn: ["dis-a"], enabledInConfig: true },
|
|
249
|
+
);
|
|
250
|
+
app = await makeApp(repoRoot, broadcasts);
|
|
251
|
+
|
|
252
|
+
const res = await app.inject({
|
|
253
|
+
method: "POST",
|
|
254
|
+
url: "/api/plugins/dis-a/toggle",
|
|
255
|
+
payload: { enabled: false },
|
|
256
|
+
});
|
|
257
|
+
expect(res.statusCode).toBe(200);
|
|
258
|
+
const body = res.json() as { success: boolean; cascade: { disable: string[] } };
|
|
259
|
+
expect(body.cascade.disable).toEqual(["dis-b"]);
|
|
260
|
+
|
|
261
|
+
const cfg = JSON.parse(
|
|
262
|
+
fs.readFileSync(path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json"), "utf-8"),
|
|
263
|
+
);
|
|
264
|
+
expect(cfg.plugins["dis-a"].enabled).toBe(false);
|
|
265
|
+
expect(cfg.plugins["dis-b"].enabled).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for PluginIntentCache — server-side replay store for plugin intents.
|
|
3
|
+
*
|
|
4
|
+
* See change: adopt-server-driven-intent-rendering.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
7
|
+
import { PluginIntentCache } from "../plugin-intent-cache.js";
|
|
8
|
+
import type { IntentNode } from "@blackbelt-technology/pi-dashboard-shared/dashboard-plugin/intent-types.js";
|
|
9
|
+
|
|
10
|
+
const sampleIntent: IntentNode = {
|
|
11
|
+
primitive: "ui:action-list",
|
|
12
|
+
props: { actions: [{ label: "Run X" }] },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("PluginIntentCache", () => {
|
|
16
|
+
let cache: PluginIntentCache;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
cache = new PluginIntentCache();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("set + getForSession returns the cached intent", () => {
|
|
23
|
+
cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
|
|
24
|
+
const entries = cache.getForSession("abc");
|
|
25
|
+
expect(entries).toHaveLength(1);
|
|
26
|
+
expect(entries[0].pluginId).toBe("flows");
|
|
27
|
+
expect(entries[0].intent).toEqual(sampleIntent);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("setting null intent clears the slot", () => {
|
|
31
|
+
cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
|
|
32
|
+
cache.set("flows", "abc", "session-card-action-bar", null);
|
|
33
|
+
const entries = cache.getForSession("abc");
|
|
34
|
+
expect(entries).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("clearForSession removes only entries for that session", () => {
|
|
38
|
+
cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
|
|
39
|
+
cache.set("flows", "xyz", "session-card-action-bar", sampleIntent);
|
|
40
|
+
cache.clearForSession("abc");
|
|
41
|
+
expect(cache.getForSession("abc")).toHaveLength(0);
|
|
42
|
+
expect(cache.getForSession("xyz")).toHaveLength(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("supports global slots with sessionId=null", () => {
|
|
46
|
+
cache.set("honcho", null, "settings-section", sampleIntent);
|
|
47
|
+
const entries = cache.getForSession(null);
|
|
48
|
+
expect(entries).toHaveLength(1);
|
|
49
|
+
expect(entries[0].sessionId).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("two plugins can occupy the same slot for the same session", () => {
|
|
53
|
+
cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
|
|
54
|
+
cache.set("jj", "abc", "session-card-action-bar", sampleIntent);
|
|
55
|
+
const entries = cache.getForSession("abc");
|
|
56
|
+
expect(entries).toHaveLength(2);
|
|
57
|
+
expect(new Set(entries.map((e) => e.pluginId))).toEqual(new Set(["flows", "jj"]));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("same (pluginId, sessionId, slot) overwrites", () => {
|
|
61
|
+
const newer: IntentNode = { primitive: "ui:status-pill", props: { text: "updated" } };
|
|
62
|
+
cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
|
|
63
|
+
cache.set("flows", "abc", "session-card-action-bar", newer);
|
|
64
|
+
const entries = cache.getForSession("abc");
|
|
65
|
+
expect(entries).toHaveLength(1);
|
|
66
|
+
expect(entries[0].intent).toEqual(newer);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("reset clears everything", () => {
|
|
70
|
+
cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
|
|
71
|
+
cache.set("honcho", null, "settings-section", sampleIntent);
|
|
72
|
+
cache.reset();
|
|
73
|
+
expect(cache.getAll()).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
});
|