@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.1
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 +87 -114
- package/README.md +408 -430
- package/docs/architecture.md +465 -12
- package/package.json +10 -5
- package/packages/extension/package.json +14 -4
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +40 -8
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +5 -4
- package/packages/extension/src/bridge.ts +171 -17
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +43 -0
- 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 +83 -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__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- 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 +237 -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 +111 -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 +310 -39
- 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 +207 -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 +141 -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__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -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__/resolve-tool-cli.test.ts +105 -0
- 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__/state-replay-entry-id.test.ts +69 -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 +16 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -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/protocol.ts +23 -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/state-replay.ts +9 -0
- package/packages/shared/src/tool-registry/definitions.ts +434 -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
|
@@ -1,100 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for
|
|
2
|
+
* Tests for bridge entryId stamping on message_end events.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* HISTORY: Originally this file modelled pi <0.69's synchronous emit pattern,
|
|
5
|
+
* where the bridge's `queueMicrotask` deferral ran BEFORE
|
|
6
|
+
* sessionManager.appendMessage. That design no longer reflects pi 0.70.x:
|
|
7
|
+
* pi awaits extension handlers inside _emitExtensionEvent, so the microtask
|
|
8
|
+
* resolves *inside* the awaited dispatcher, before persistence. The fix
|
|
9
|
+
* (see change: fix-per-message-fork) is `setTimeout(0)` (a macrotask)
|
|
10
|
+
* combined with reading `event.message.id` (which pi mutates in place
|
|
11
|
+
* during appendMessage) or a WeakMap populated by the wrapped appendMessage.
|
|
7
12
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
13
|
+
* The previous test "message_start should still capture entryId immediately
|
|
14
|
+
* (no deferral)" codified the off-by-one bug as expected behavior — it has
|
|
15
|
+
* been REMOVED. The current test suite below models pi 0.70.x semantics
|
|
16
|
+
* directly. Detailed pi-0.70-specific scenarios live in
|
|
17
|
+
* `bridge-entry-id-pi-070.test.ts`.
|
|
12
18
|
*/
|
|
13
|
-
import { describe, it, expect
|
|
19
|
+
import { describe, it, expect } from "vitest";
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* 1. _emit(event) — bridge handler called (async, not awaited)
|
|
20
|
-
* 2. appendMessage(msg) — updates leafId synchronously
|
|
21
|
-
*
|
|
22
|
-
* The bridge handler (async) should yield via queueMicrotask before reading
|
|
23
|
-
* getLeafId(), so that appendMessage has already run.
|
|
24
|
-
*/
|
|
25
|
-
describe("message_end entryId timing", () => {
|
|
26
|
-
it("deferred getLeafId() captures the post-persist entry ID", async () => {
|
|
27
|
-
// Simulate sessionManager with mutable leafId
|
|
28
|
-
let leafId = "user-entry-100"; // stale leaf before appendMessage
|
|
21
|
+
describe("message_end entryId timing on pi 0.70.x", () => {
|
|
22
|
+
it("setTimeout(0) deferral captures the post-persist entry ID", async () => {
|
|
23
|
+
// Simulate pi 0.70.x: bridge handler runs awaited, THEN appendMessage runs.
|
|
24
|
+
let leafId = "user-entry-100";
|
|
29
25
|
const sessionManager = {
|
|
30
26
|
getLeafId: () => leafId,
|
|
27
|
+
appendMessage: (msg: any) => {
|
|
28
|
+
msg.id = "assistant-entry-101";
|
|
29
|
+
leafId = msg.id;
|
|
30
|
+
return msg.id;
|
|
31
|
+
},
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
const event = { type: "message_end", message: { role: "assistant" } as any };
|
|
35
|
+
let captured: string | undefined;
|
|
36
|
+
let sendDone!: () => void;
|
|
37
|
+
const sentP = new Promise<void>((r) => { sendDone = r; });
|
|
38
|
+
|
|
39
|
+
// Bridge handler: schedules a setTimeout(0) and returns synchronously.
|
|
40
|
+
// The awaited dispatcher then unwinds, appendMessage runs, AND finally
|
|
41
|
+
// the timeout fires.
|
|
42
|
+
const bridgeHandler = (ev: any) => {
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
captured = ev.message.id ?? sessionManager.getLeafId();
|
|
45
|
+
sendDone();
|
|
46
|
+
}, 0);
|
|
40
47
|
};
|
|
41
48
|
|
|
42
|
-
// Simulate
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
leafId = "assistant-entry-101";
|
|
47
|
-
|
|
48
|
-
// Wait for the deferred handler to complete
|
|
49
|
-
await handlerPromise;
|
|
49
|
+
// Simulate the dispatcher: await handler, then call appendMessage.
|
|
50
|
+
await bridgeHandler(event);
|
|
51
|
+
sessionManager.appendMessage(event.message);
|
|
52
|
+
await sentP;
|
|
50
53
|
|
|
51
|
-
expect(
|
|
54
|
+
expect(captured).toBe("assistant-entry-101");
|
|
52
55
|
});
|
|
53
56
|
|
|
54
|
-
it("
|
|
57
|
+
it("queueMicrotask deferral would NOT work on pi 0.70.x (regression demonstration)", async () => {
|
|
58
|
+
// Reproduces why we abandoned queueMicrotask: it resolves inside the
|
|
59
|
+
// awaited dispatcher, before appendMessage runs.
|
|
55
60
|
let leafId = "user-entry-100";
|
|
56
|
-
|
|
57
|
-
getLeafId: () => leafId,
|
|
58
|
-
};
|
|
61
|
+
let captured: string | undefined;
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const buggyBridgeHandler = async () => {
|
|
64
|
-
// No deferral — reads leafId before appendMessage runs
|
|
65
|
-
capturedEntryId = sessionManager.getLeafId();
|
|
63
|
+
const buggyBridge = async () => {
|
|
64
|
+
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
65
|
+
captured = leafId;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await handlerPromise;
|
|
68
|
+
// Pi 0.70.x: await the bridge, THEN persist. Mirroring the real ordering:
|
|
69
|
+
const handlerP = buggyBridge();
|
|
70
|
+
await handlerP;
|
|
71
|
+
leafId = "assistant-entry-101"; // appendMessage runs after await
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
expect(
|
|
73
|
+
expect(captured).toBe("user-entry-100");
|
|
74
|
+
expect(captured).not.toBe("assistant-entry-101");
|
|
76
75
|
});
|
|
77
76
|
|
|
78
|
-
it("
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Simulate bridge handler for message_start (immediate, no deferral)
|
|
87
|
-
const messageStartHandler = async () => {
|
|
88
|
-
capturedEntryId = sessionManager.getLeafId();
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const handlerPromise = messageStartHandler();
|
|
92
|
-
// User entry gets written after message_start
|
|
93
|
-
leafId = "user-entry-100";
|
|
94
|
-
|
|
95
|
-
await handlerPromise;
|
|
77
|
+
it("entry_persisted is the back-fill mechanism for user message_start (where event.message.id is unavailable)", () => {
|
|
78
|
+
// Behavioural assertion (pure data shape): when the bridge sends a
|
|
79
|
+
// user message_start, it stamps a nonce; later when pi persists the
|
|
80
|
+
// user entry, the bridge sends entry_persisted { nonce, entryId }.
|
|
81
|
+
// The reducer pairs them by nonce. See change: fix-per-message-fork.
|
|
82
|
+
const start = { type: "message_start", message: { role: "user" }, nonce: "n-1" };
|
|
83
|
+
const persisted = { type: "entry_persisted", entryId: "user-200", nonce: "n-1" };
|
|
96
84
|
|
|
97
|
-
|
|
98
|
-
expect(
|
|
85
|
+
expect(start.nonce).toBe(persisted.nonce);
|
|
86
|
+
expect(persisted.entryId).toBe("user-200");
|
|
99
87
|
});
|
|
100
88
|
});
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { MultiSelectList } from "../multiselect-list.js";
|
|
3
|
+
|
|
4
|
+
function make(options: string[] = ["A", "B", "C"], title = "Pick", message?: string) {
|
|
5
|
+
const list = new MultiSelectList(title, options, message);
|
|
6
|
+
const onConfirm = vi.fn();
|
|
7
|
+
const onCancel = vi.fn();
|
|
8
|
+
list.onConfirm = onConfirm;
|
|
9
|
+
list.onCancel = onCancel;
|
|
10
|
+
return { list, onConfirm, onCancel };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("MultiSelectList", () => {
|
|
14
|
+
describe("keybindings", () => {
|
|
15
|
+
it("space toggles the checked state of the current item and nothing else", () => {
|
|
16
|
+
const { list, onConfirm } = make();
|
|
17
|
+
list.handleInput(" "); // toggle index 0 → A checked
|
|
18
|
+
list.handleInput("\r");
|
|
19
|
+
expect(onConfirm).toHaveBeenCalledWith(["A"]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("arrow down moves cursor and space toggles the new current item", () => {
|
|
23
|
+
const { list, onConfirm } = make();
|
|
24
|
+
list.handleInput("\u001b[B"); // cursor → 1 (B)
|
|
25
|
+
list.handleInput(" "); // B checked
|
|
26
|
+
list.handleInput("\r");
|
|
27
|
+
expect(onConfirm).toHaveBeenCalledWith(["B"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("j / k navigation works like arrows", () => {
|
|
31
|
+
const { list, onConfirm } = make();
|
|
32
|
+
list.handleInput("j"); // cursor → 1
|
|
33
|
+
list.handleInput("j"); // cursor → 2
|
|
34
|
+
list.handleInput(" "); // C checked
|
|
35
|
+
list.handleInput("k"); // cursor → 1
|
|
36
|
+
list.handleInput(" "); // B checked
|
|
37
|
+
list.handleInput("\r");
|
|
38
|
+
// selected values returned in ORIGINAL order
|
|
39
|
+
expect(onConfirm).toHaveBeenCalledWith(["B", "C"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("enter with nothing checked confirms with []", () => {
|
|
43
|
+
const { list, onConfirm } = make();
|
|
44
|
+
list.handleInput("\r");
|
|
45
|
+
expect(onConfirm).toHaveBeenCalledWith([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("escape cancels; no confirm is fired", () => {
|
|
49
|
+
const { list, onConfirm, onCancel } = make();
|
|
50
|
+
list.handleInput(" "); // check A
|
|
51
|
+
list.handleInput("\u001b");
|
|
52
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("pressing 'a' does NOT bulk-toggle (no select-all in TUI)", () => {
|
|
57
|
+
const { list, onConfirm } = make();
|
|
58
|
+
list.handleInput("a");
|
|
59
|
+
list.handleInput("\r");
|
|
60
|
+
expect(onConfirm).toHaveBeenCalledWith([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("cursor does not go below 0", () => {
|
|
64
|
+
const { list } = make();
|
|
65
|
+
list.handleInput("k");
|
|
66
|
+
list.handleInput("k");
|
|
67
|
+
expect(list.getCursor()).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("cursor does not go past last item", () => {
|
|
71
|
+
const { list } = make(["A", "B"]);
|
|
72
|
+
list.handleInput("j");
|
|
73
|
+
list.handleInput("j");
|
|
74
|
+
list.handleInput("j");
|
|
75
|
+
expect(list.getCursor()).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("toggling twice returns item to unchecked", () => {
|
|
79
|
+
const { list, onConfirm } = make();
|
|
80
|
+
list.handleInput(" ");
|
|
81
|
+
list.handleInput(" ");
|
|
82
|
+
list.handleInput("\r");
|
|
83
|
+
expect(onConfirm).toHaveBeenCalledWith([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("selected order follows original option order, not toggle order", () => {
|
|
87
|
+
const { list, onConfirm } = make(["A", "B", "C", "D"]);
|
|
88
|
+
// toggle D first, then A, then C
|
|
89
|
+
list.handleInput("j");
|
|
90
|
+
list.handleInput("j");
|
|
91
|
+
list.handleInput("j");
|
|
92
|
+
list.handleInput(" "); // D
|
|
93
|
+
list.handleInput("k");
|
|
94
|
+
list.handleInput("k");
|
|
95
|
+
list.handleInput("k");
|
|
96
|
+
list.handleInput(" "); // A
|
|
97
|
+
list.handleInput("j");
|
|
98
|
+
list.handleInput("j");
|
|
99
|
+
list.handleInput(" "); // C
|
|
100
|
+
list.handleInput("\r");
|
|
101
|
+
expect(onConfirm).toHaveBeenCalledWith(["A", "C", "D"]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("render", () => {
|
|
106
|
+
it("includes footer hint", () => {
|
|
107
|
+
const { list } = make();
|
|
108
|
+
const lines = list.render(80);
|
|
109
|
+
expect(lines.some((l) => l.includes("space toggle"))).toBe(true);
|
|
110
|
+
expect(lines.some((l) => l.includes("enter confirm"))).toBe(true);
|
|
111
|
+
expect(lines.some((l) => l.includes("esc cancel"))).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("renders [ ] for unchecked and [x] for checked items", () => {
|
|
115
|
+
const { list } = make();
|
|
116
|
+
list.handleInput(" "); // check A
|
|
117
|
+
const lines = list.render(80);
|
|
118
|
+
expect(lines.some((l) => l.includes("[x] A"))).toBe(true);
|
|
119
|
+
expect(lines.some((l) => l.includes("[ ] B"))).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("renders cursor marker on current item", () => {
|
|
123
|
+
const { list } = make();
|
|
124
|
+
list.handleInput("j"); // cursor → 1
|
|
125
|
+
const lines = list.render(80);
|
|
126
|
+
// Cursor line should start with "▸ " somewhere
|
|
127
|
+
expect(lines.some((l) => l.startsWith("▸ ") && l.includes("B"))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("includes title and message when provided", () => {
|
|
131
|
+
const { list } = make(["A", "B"], "Pick one or more", "Some context");
|
|
132
|
+
const lines = list.render(80);
|
|
133
|
+
expect(lines.some((l) => l.includes("Pick one or more"))).toBe(true);
|
|
134
|
+
expect(lines.some((l) => l.includes("Some context"))).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: the bridge MUST NOT call pi's session-replacement
|
|
3
|
+
* APIs (`pi.newSession(...)`, `ctx.fork(...)`, `ctx.switchSession(...)`)
|
|
4
|
+
* from any code under `packages/extension/src/`.
|
|
5
|
+
*
|
|
6
|
+
* Rationale: pi 0.69.0+ invalidates captured pre-replacement `pi`/`ctx`/
|
|
7
|
+
* session-bound objects on next access after these calls. The bridge
|
|
8
|
+
* holds long-lived caches (`cachedCtx`, `cachedModelRegistry`,
|
|
9
|
+
* `cachedHasUI` in `bridge.ts`; `modelRegistry` in `provider-register.ts`)
|
|
10
|
+
* that depend on pi being the ONLY originator of session replacement, so
|
|
11
|
+
* we can re-capture inside the resulting `session_start` handler keyed on
|
|
12
|
+
* `event.reason ∈ {"new","fork","resume"}`.
|
|
13
|
+
*
|
|
14
|
+
* If this test fails: do NOT add the call. Either drive the user-facing
|
|
15
|
+
* action through the dashboard server (which prompts the user, who
|
|
16
|
+
* triggers replacement via pi's UI), or wrap your post-switch work in
|
|
17
|
+
* the `withSession` callback that pi 0.69+ exposes on each replacement
|
|
18
|
+
* API and capture the freshly-emitted ReplacedSessionContext there.
|
|
19
|
+
*
|
|
20
|
+
* See change: pi-zero-seventy-compat.
|
|
21
|
+
*/
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import fs from "node:fs/promises";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import url from "node:url";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Each pattern matches `<receiver>.<method>(` allowing for whitespace and
|
|
29
|
+
* tolerating common variations like `await pi.newSession(...)`. Prefixed
|
|
30
|
+
* with a non-word boundary so we don't flag method names embedded in
|
|
31
|
+
* longer identifiers.
|
|
32
|
+
*/
|
|
33
|
+
const PATTERNS: ReadonlyArray<{ name: string; re: RegExp }> = [
|
|
34
|
+
{ name: "pi.newSession", re: /(?:^|[^.\w])pi\.newSession\s*\(/ },
|
|
35
|
+
{ name: "ctx.fork", re: /(?:^|[^.\w])ctx\.fork\s*\(/ },
|
|
36
|
+
{ name: "ctx.switchSession", re: /(?:^|[^.\w])ctx\.switchSession\s*\(/ },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Per-line opt-out marker. Use only for documented exceptions (e.g. a
|
|
41
|
+
* future migration cell that intentionally drives a replacement and
|
|
42
|
+
* fully re-binds via `withSession`):
|
|
43
|
+
* await pi.newSession({ withSession: ... }); // ban:session-replacement-ok
|
|
44
|
+
*/
|
|
45
|
+
const OPT_OUT_MARKER = "ban:session-replacement-ok";
|
|
46
|
+
|
|
47
|
+
async function* walk(dir: string): AsyncGenerator<string> {
|
|
48
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const full = path.join(dir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__") continue;
|
|
53
|
+
yield* walk(full);
|
|
54
|
+
} else if (entry.isFile() && /\.(ts|tsx|mts|cts)$/.test(entry.name)) {
|
|
55
|
+
yield full;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("no session-replacement API calls in packages/extension/src/", () => {
|
|
61
|
+
it("bridge code never invokes pi.newSession / ctx.fork / ctx.switchSession", async () => {
|
|
62
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
63
|
+
const srcDir = path.resolve(here, "..");
|
|
64
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
65
|
+
|
|
66
|
+
const violations: Array<{ file: string; line: number; pattern: string; text: string }> = [];
|
|
67
|
+
|
|
68
|
+
for await (const file of walk(srcDir)) {
|
|
69
|
+
const content = await fs.readFile(file, "utf-8");
|
|
70
|
+
const lines = content.split(/\r?\n/);
|
|
71
|
+
lines.forEach((line, idx) => {
|
|
72
|
+
if (line.includes(OPT_OUT_MARKER)) return;
|
|
73
|
+
for (const { name, re } of PATTERNS) {
|
|
74
|
+
if (re.test(line)) {
|
|
75
|
+
violations.push({
|
|
76
|
+
file: path.relative(repoRoot, file),
|
|
77
|
+
line: idx + 1,
|
|
78
|
+
pattern: name,
|
|
79
|
+
text: line.trim(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (violations.length > 0) {
|
|
87
|
+
const msg =
|
|
88
|
+
`Bridge code MUST NOT call pi session-replacement APIs.\n` +
|
|
89
|
+
`pi 0.69.0+ invalidates captured pre-replacement pi/ctx after these calls;\n` +
|
|
90
|
+
`the bridge relies on pi being the sole originator of replacement so it can\n` +
|
|
91
|
+
`re-capture state inside the resulting session_start handler.\n\n` +
|
|
92
|
+
`Offenders (${violations.length}):\n` +
|
|
93
|
+
violations
|
|
94
|
+
.map((v) => ` ${v.file}:${v.line} [${v.pattern}] ${v.text}`)
|
|
95
|
+
.join("\n");
|
|
96
|
+
expect(violations, msg).toEqual([]);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|