@blackbelt-technology/pi-agent-dashboard 0.5.3 → 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 +10 -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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the bridge's shutdown-time shadow-queue reset.
|
|
3
|
+
*
|
|
4
|
+
* Pure-model mirror of bridge.ts:786 `shutdown` extension command. If
|
|
5
|
+
* production drifts from this shape, this test drifts in lockstep.
|
|
6
|
+
*
|
|
7
|
+
* Spec: openspec/specs/mid-turn-prompt-queue/spec.md — requirement
|
|
8
|
+
* "Session shutdown resets shadow queues and clears pi's native queues"
|
|
9
|
+
* (added by change reset-shadow-queues-on-shutdown).
|
|
10
|
+
*
|
|
11
|
+
* See change: reset-shadow-queues-on-shutdown.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, vi } from "vitest";
|
|
14
|
+
|
|
15
|
+
interface ShadowQueue {
|
|
16
|
+
steering: string[];
|
|
17
|
+
followUp: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type QueueUpdateEmit = { steering: string[]; followUp: string[] };
|
|
21
|
+
|
|
22
|
+
interface PiLike {
|
|
23
|
+
clearSteeringQueue?: () => void;
|
|
24
|
+
clearFollowUpQueue?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CachedCtxLike {
|
|
28
|
+
shutdown?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pure version of the shutdown extension command. Mirrors bridge.ts
|
|
33
|
+
* 1:1 — defensive pi clears (unconditional), conditional shadow reset
|
|
34
|
+
* + emit, then cachedCtx.shutdown, then process.exit safety net.
|
|
35
|
+
*/
|
|
36
|
+
function makeShutdown(opts: {
|
|
37
|
+
pi: PiLike;
|
|
38
|
+
cachedCtx: CachedCtxLike | null;
|
|
39
|
+
queue: ShadowQueue;
|
|
40
|
+
onEmit: (snapshot: QueueUpdateEmit) => void;
|
|
41
|
+
onProcessExit: () => void;
|
|
42
|
+
callLog: string[];
|
|
43
|
+
}) {
|
|
44
|
+
return () => {
|
|
45
|
+
try {
|
|
46
|
+
if (typeof opts.pi.clearSteeringQueue === "function") {
|
|
47
|
+
opts.callLog.push("pi.clearSteeringQueue");
|
|
48
|
+
opts.pi.clearSteeringQueue();
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// swallow — teardown must not throw
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
if (typeof opts.pi.clearFollowUpQueue === "function") {
|
|
55
|
+
opts.callLog.push("pi.clearFollowUpQueue");
|
|
56
|
+
opts.pi.clearFollowUpQueue();
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// swallow
|
|
60
|
+
}
|
|
61
|
+
if (opts.queue.steering.length > 0 || opts.queue.followUp.length > 0) {
|
|
62
|
+
opts.queue.steering = [];
|
|
63
|
+
opts.queue.followUp = [];
|
|
64
|
+
opts.callLog.push("emitQueueUpdate");
|
|
65
|
+
opts.onEmit({ steering: [], followUp: [] });
|
|
66
|
+
}
|
|
67
|
+
if (opts.cachedCtx?.shutdown) {
|
|
68
|
+
opts.callLog.push("cachedCtx.shutdown");
|
|
69
|
+
opts.cachedCtx.shutdown();
|
|
70
|
+
}
|
|
71
|
+
opts.callLog.push("setTimeout(process.exit)");
|
|
72
|
+
opts.onProcessExit();
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("bridge shutdown: shadow-queue reset", () => {
|
|
77
|
+
it("non-empty steering: resets shadow, emits one final queue_update with empty arrays", () => {
|
|
78
|
+
const queue: ShadowQueue = { steering: ["focus on X"], followUp: [] };
|
|
79
|
+
const onEmit = vi.fn();
|
|
80
|
+
const callLog: string[] = [];
|
|
81
|
+
const pi: PiLike = { clearSteeringQueue: vi.fn(), clearFollowUpQueue: vi.fn() };
|
|
82
|
+
const cachedCtx: CachedCtxLike = { shutdown: vi.fn() };
|
|
83
|
+
|
|
84
|
+
makeShutdown({ pi, cachedCtx, queue, onEmit, onProcessExit: vi.fn(), callLog })();
|
|
85
|
+
|
|
86
|
+
expect(queue.steering).toEqual([]);
|
|
87
|
+
expect(queue.followUp).toEqual([]);
|
|
88
|
+
expect(onEmit).toHaveBeenCalledTimes(1);
|
|
89
|
+
expect(onEmit).toHaveBeenCalledWith({ steering: [], followUp: [] });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("non-empty followUp: resets shadow, emits one final queue_update", () => {
|
|
93
|
+
const queue: ShadowQueue = { steering: [], followUp: ["run tests when done"] };
|
|
94
|
+
const onEmit = vi.fn();
|
|
95
|
+
const pi: PiLike = { clearSteeringQueue: vi.fn(), clearFollowUpQueue: vi.fn() };
|
|
96
|
+
const cachedCtx: CachedCtxLike = { shutdown: vi.fn() };
|
|
97
|
+
|
|
98
|
+
makeShutdown({ pi, cachedCtx, queue, onEmit, onProcessExit: vi.fn(), callLog: [] })();
|
|
99
|
+
|
|
100
|
+
expect(queue).toEqual({ steering: [], followUp: [] });
|
|
101
|
+
expect(onEmit).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("both queues non-empty: emits exactly once (not twice)", () => {
|
|
105
|
+
const queue: ShadowQueue = { steering: ["a", "b"], followUp: ["c"] };
|
|
106
|
+
const onEmit = vi.fn();
|
|
107
|
+
const pi: PiLike = { clearSteeringQueue: vi.fn(), clearFollowUpQueue: vi.fn() };
|
|
108
|
+
|
|
109
|
+
makeShutdown({
|
|
110
|
+
pi,
|
|
111
|
+
cachedCtx: { shutdown: vi.fn() },
|
|
112
|
+
queue,
|
|
113
|
+
onEmit,
|
|
114
|
+
onProcessExit: vi.fn(),
|
|
115
|
+
callLog: [],
|
|
116
|
+
})();
|
|
117
|
+
|
|
118
|
+
expect(queue).toEqual({ steering: [], followUp: [] });
|
|
119
|
+
expect(onEmit).toHaveBeenCalledTimes(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("both queues empty: does NOT emit queue_update, still calls pi.clear* defensively", () => {
|
|
123
|
+
const queue: ShadowQueue = { steering: [], followUp: [] };
|
|
124
|
+
const onEmit = vi.fn();
|
|
125
|
+
const clearSteer = vi.fn();
|
|
126
|
+
const clearFollow = vi.fn();
|
|
127
|
+
const pi: PiLike = { clearSteeringQueue: clearSteer, clearFollowUpQueue: clearFollow };
|
|
128
|
+
|
|
129
|
+
makeShutdown({
|
|
130
|
+
pi,
|
|
131
|
+
cachedCtx: { shutdown: vi.fn() },
|
|
132
|
+
queue,
|
|
133
|
+
onEmit,
|
|
134
|
+
onProcessExit: vi.fn(),
|
|
135
|
+
callLog: [],
|
|
136
|
+
})();
|
|
137
|
+
|
|
138
|
+
expect(onEmit).not.toHaveBeenCalled();
|
|
139
|
+
// Defensive clears run unconditionally — pi's queues may be non-empty
|
|
140
|
+
// from non-dashboard sources.
|
|
141
|
+
expect(clearSteer).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(clearFollow).toHaveBeenCalledTimes(1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("pi missing clearSteeringQueue / clearFollowUpQueue: still resets shadow + emits + does not throw", () => {
|
|
146
|
+
const queue: ShadowQueue = { steering: ["a"], followUp: ["b"] };
|
|
147
|
+
const onEmit = vi.fn();
|
|
148
|
+
const pi: PiLike = {}; // both functions absent — pi version skew
|
|
149
|
+
const cachedCtx: CachedCtxLike = { shutdown: vi.fn() };
|
|
150
|
+
|
|
151
|
+
expect(() => {
|
|
152
|
+
makeShutdown({ pi, cachedCtx, queue, onEmit, onProcessExit: vi.fn(), callLog: [] })();
|
|
153
|
+
}).not.toThrow();
|
|
154
|
+
|
|
155
|
+
expect(queue).toEqual({ steering: [], followUp: [] });
|
|
156
|
+
expect(onEmit).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("pi.clearSteeringQueue throws: teardown continues (shadow still reset, emit still fires, cachedCtx.shutdown still called)", () => {
|
|
160
|
+
const queue: ShadowQueue = { steering: ["a"], followUp: [] };
|
|
161
|
+
const onEmit = vi.fn();
|
|
162
|
+
const cachedShutdown = vi.fn();
|
|
163
|
+
const pi: PiLike = {
|
|
164
|
+
clearSteeringQueue: () => {
|
|
165
|
+
throw new Error("boom");
|
|
166
|
+
},
|
|
167
|
+
clearFollowUpQueue: vi.fn(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
expect(() => {
|
|
171
|
+
makeShutdown({
|
|
172
|
+
pi,
|
|
173
|
+
cachedCtx: { shutdown: cachedShutdown },
|
|
174
|
+
queue,
|
|
175
|
+
onEmit,
|
|
176
|
+
onProcessExit: vi.fn(),
|
|
177
|
+
callLog: [],
|
|
178
|
+
})();
|
|
179
|
+
}).not.toThrow();
|
|
180
|
+
|
|
181
|
+
expect(queue).toEqual({ steering: [], followUp: [] });
|
|
182
|
+
expect(onEmit).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(cachedShutdown).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("order of operations: pi.clearSteeringQueue → pi.clearFollowUpQueue → emitQueueUpdate → cachedCtx.shutdown → process.exit", () => {
|
|
187
|
+
const queue: ShadowQueue = { steering: ["a"], followUp: ["b"] };
|
|
188
|
+
const callLog: string[] = [];
|
|
189
|
+
const pi: PiLike = { clearSteeringQueue: vi.fn(), clearFollowUpQueue: vi.fn() };
|
|
190
|
+
const cachedCtx: CachedCtxLike = { shutdown: vi.fn() };
|
|
191
|
+
|
|
192
|
+
makeShutdown({ pi, cachedCtx, queue, onEmit: vi.fn(), onProcessExit: vi.fn(), callLog })();
|
|
193
|
+
|
|
194
|
+
expect(callLog).toEqual([
|
|
195
|
+
"pi.clearSteeringQueue",
|
|
196
|
+
"pi.clearFollowUpQueue",
|
|
197
|
+
"emitQueueUpdate",
|
|
198
|
+
"cachedCtx.shutdown",
|
|
199
|
+
"setTimeout(process.exit)",
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("safety-net: cachedCtx.shutdown still called and process.exit scheduled when shadows are empty", () => {
|
|
204
|
+
const queue: ShadowQueue = { steering: [], followUp: [] };
|
|
205
|
+
const cachedShutdown = vi.fn();
|
|
206
|
+
const onProcessExit = vi.fn();
|
|
207
|
+
const pi: PiLike = { clearSteeringQueue: vi.fn(), clearFollowUpQueue: vi.fn() };
|
|
208
|
+
|
|
209
|
+
makeShutdown({
|
|
210
|
+
pi,
|
|
211
|
+
cachedCtx: { shutdown: cachedShutdown },
|
|
212
|
+
queue,
|
|
213
|
+
onEmit: vi.fn(),
|
|
214
|
+
onProcessExit,
|
|
215
|
+
callLog: [],
|
|
216
|
+
})();
|
|
217
|
+
|
|
218
|
+
expect(cachedShutdown).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(onProcessExit).toHaveBeenCalledTimes(1);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("safety-net: process.exit scheduled even when cachedCtx is null", () => {
|
|
223
|
+
const queue: ShadowQueue = { steering: ["a"], followUp: [] };
|
|
224
|
+
const onProcessExit = vi.fn();
|
|
225
|
+
const pi: PiLike = { clearSteeringQueue: vi.fn(), clearFollowUpQueue: vi.fn() };
|
|
226
|
+
|
|
227
|
+
makeShutdown({
|
|
228
|
+
pi,
|
|
229
|
+
cachedCtx: null,
|
|
230
|
+
queue,
|
|
231
|
+
onEmit: vi.fn(),
|
|
232
|
+
onProcessExit,
|
|
233
|
+
callLog: [],
|
|
234
|
+
})();
|
|
235
|
+
|
|
236
|
+
expect(onProcessExit).toHaveBeenCalledTimes(1);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -59,10 +59,10 @@ function feedbackEvents(sink: ReturnType<typeof vi.fn>, command: string) {
|
|
|
59
59
|
.map((m) => (m as any).event.data);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
async function drive(text: string, stub: ReturnType<typeof makeStubPi
|
|
62
|
+
async function drive(text: string, stub: ReturnType<typeof makeStubPi>, delivery?: "steer" | "followUp") {
|
|
63
63
|
const sink = vi.fn();
|
|
64
64
|
const handler = createCommandHandler(stub.pi as any, "s1", { eventSink: sink });
|
|
65
|
-
await handler.handle({ type: "send_prompt", sessionId: "s1", text } as any);
|
|
65
|
+
await handler.handle({ type: "send_prompt", sessionId: "s1", text, delivery } as any);
|
|
66
66
|
return sink;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -80,16 +80,32 @@ describe("bridge slash command routing (regression contract)", () => {
|
|
|
80
80
|
expect(evs.map((e) => e.status)).toEqual(["started", "completed"]);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
it("extension cmd
|
|
84
|
-
|
|
83
|
+
it("extension cmd with delivery: steer → dispatchCommand called with streamingBehavior: steer", async () => {
|
|
84
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
85
|
+
const sink = await drive("/ctx-stats", stub, "steer");
|
|
86
|
+
|
|
87
|
+
expect(stub.dispatchCommand).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(stub.dispatchCommand).toHaveBeenCalledWith("/ctx-stats", { streamingBehavior: "steer" });
|
|
89
|
+
expect(stub.sendUserMessage).not.toHaveBeenCalled();
|
|
90
|
+
|
|
91
|
+
const evs = feedbackEvents(sink, "/ctx-stats");
|
|
92
|
+
expect(evs.map((e) => e.status)).toEqual(["started", "completed"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("extension cmd, NO dispatchCommand, not headless → error feedback with rpc-keeper hint, no sendUserMessage", async () => {
|
|
96
|
+
// Path D: extension commands cannot be dispatched for non-headless sessions.
|
|
97
|
+
// Emits error with hint to enable useRpcKeeper for headless mode.
|
|
98
|
+
// See change: fix-slash-dispatch-delivery.
|
|
85
99
|
const stub = makeStubPi({ withDispatch: false });
|
|
86
100
|
const sink = await drive("/ctx-stats", stub);
|
|
87
101
|
|
|
102
|
+
// sendUserMessage is NOT called — the command is handled (with error).
|
|
88
103
|
expect(stub.sendUserMessage).not.toHaveBeenCalled();
|
|
89
104
|
|
|
105
|
+
// Error feedback emitted with rpc-keeper hint.
|
|
90
106
|
const evs = feedbackEvents(sink, "/ctx-stats");
|
|
91
|
-
expect(evs.map((e) => e.status)).toEqual(["
|
|
92
|
-
expect(evs[
|
|
107
|
+
expect(evs.map((e) => e.status)).toEqual(["error"]);
|
|
108
|
+
expect(evs[0].message).toContain("useRpcKeeper");
|
|
93
109
|
});
|
|
94
110
|
|
|
95
111
|
it("extension cmd dispatch rejects → started+error with err.message, no sendUserMessage", async () => {
|
|
@@ -169,21 +185,28 @@ describe("bridge slash command routing (regression contract)", () => {
|
|
|
169
185
|
expect(evs.filter((e) => e.status === "completed" || e.status === "error")).toHaveLength(1);
|
|
170
186
|
});
|
|
171
187
|
|
|
172
|
-
it("
|
|
188
|
+
it("error feedback on fallthrough path (dispatchCommand absent, non-headless)", async () => {
|
|
189
|
+
// Path D returns true with error feedback (including rpc-keeper hint).
|
|
190
|
+
// See change: fix-slash-dispatch-delivery.
|
|
173
191
|
const stub = makeStubPi({ withDispatch: false });
|
|
174
192
|
const sink = await drive("/ctx-stats", stub);
|
|
175
193
|
const evs = feedbackEvents(sink, "/ctx-stats");
|
|
176
|
-
expect(evs
|
|
177
|
-
expect(evs.
|
|
194
|
+
expect(evs).toHaveLength(1);
|
|
195
|
+
expect(evs[0].status).toBe("error");
|
|
196
|
+
expect(evs[0].message).toContain("useRpcKeeper");
|
|
178
197
|
});
|
|
179
198
|
|
|
180
|
-
it("anti-regression: /ctx-stats
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
199
|
+
it("anti-regression: /ctx-stats does NOT reach sendUserMessage when dispatchCommand absent", async () => {
|
|
200
|
+
// Path D now emits error feedback instead of falling through silently.
|
|
201
|
+
// Extension commands can only be dispatched for headless sessions with
|
|
202
|
+
// the RPC keeper enabled. See change: fix-slash-dispatch-delivery.
|
|
203
|
+
const stub = makeStubPi({ withDispatch: false });
|
|
204
|
+
const sink = await drive("/ctx-stats", stub);
|
|
205
|
+
// sendUserMessage is NOT called — command handled with error feedback.
|
|
206
|
+
expect(stub.sendUserMessage).not.toHaveBeenCalled();
|
|
207
|
+
const evs = feedbackEvents(sink, "/ctx-stats");
|
|
208
|
+
expect(evs).toHaveLength(1);
|
|
209
|
+
expect(evs[0].status).toBe("error");
|
|
187
210
|
});
|
|
188
211
|
});
|
|
189
212
|
|
|
@@ -270,36 +293,37 @@ describe("tryDispatchExtensionCommand: Path B/C/D mutual exclusion", () => {
|
|
|
270
293
|
expect(evs).toEqual(["started"]);
|
|
271
294
|
});
|
|
272
295
|
|
|
273
|
-
it("Path D: no dispatchCommand + non-headless
|
|
296
|
+
it("Path D: no dispatchCommand + non-headless → returns true with error feedback including rpc-keeper hint", async () => {
|
|
274
297
|
const { pi } = makePi({ withDispatch: false });
|
|
275
298
|
const sink = vi.fn();
|
|
276
299
|
const { conn, sent } = makeConn();
|
|
277
300
|
setHeadless(false);
|
|
278
301
|
|
|
279
302
|
const handled = await tryDispatchExtensionCommand(pi, "/ctx-stats", "sid", sink, conn);
|
|
280
|
-
expect(handled).toBe(true);
|
|
303
|
+
expect(handled).toBe(true); // handled with error feedback
|
|
281
304
|
expect(sent.filter((m) => m.type === "dispatch_extension_command")).toEqual([]);
|
|
305
|
+
// Error feedback emitted with rpc-keeper hint.
|
|
282
306
|
const evs = sink.mock.calls
|
|
283
307
|
.map((c: any[]) => c[0])
|
|
284
308
|
.filter((m: any) => m?.event?.eventType === "command_feedback");
|
|
285
|
-
expect(evs).toHaveLength(
|
|
286
|
-
expect(evs[0].event.data.status).toBe("
|
|
287
|
-
expect(evs[
|
|
288
|
-
expect(evs[1].event.data.message).toMatch(/pi 0\.71\+/);
|
|
309
|
+
expect(evs).toHaveLength(1);
|
|
310
|
+
expect((evs[0] as any).event.data.status).toBe("error");
|
|
311
|
+
expect((evs[0] as any).event.data.message).toContain("useRpcKeeper");
|
|
289
312
|
});
|
|
290
313
|
|
|
291
|
-
it("Path
|
|
314
|
+
it("Path D: no dispatchCommand + no connection → returns true with error feedback", async () => {
|
|
292
315
|
const { pi } = makePi({ withDispatch: false });
|
|
293
316
|
const sink = vi.fn();
|
|
294
|
-
setHeadless(
|
|
317
|
+
setHeadless(false);
|
|
295
318
|
|
|
296
319
|
const handled = await tryDispatchExtensionCommand(pi, "/ctx-stats", "sid", sink, undefined);
|
|
297
|
-
expect(handled).toBe(true);
|
|
320
|
+
expect(handled).toBe(true); // handled with error feedback
|
|
321
|
+
// Error feedback emitted with rpc-keeper hint.
|
|
298
322
|
const evs = sink.mock.calls
|
|
299
323
|
.map((c: any[]) => c[0])
|
|
300
324
|
.filter((m: any) => m?.event?.eventType === "command_feedback");
|
|
301
|
-
|
|
302
|
-
expect(evs
|
|
325
|
+
expect(evs).toHaveLength(1);
|
|
326
|
+
expect((evs[0] as any).event.data.status).toBe("error");
|
|
303
327
|
});
|
|
304
328
|
|
|
305
329
|
it("non-extension /skill:foo → returns false; no path fires; no events", async () => {
|
|
@@ -332,19 +356,91 @@ describe("tryDispatchExtensionCommand: Path B/C/D mutual exclusion", () => {
|
|
|
332
356
|
const { conn, sent } = makeConn();
|
|
333
357
|
setHeadless(s.headless);
|
|
334
358
|
|
|
335
|
-
await tryDispatchExtensionCommand(pi, "/ctx-stats", "sid", sink, conn);
|
|
359
|
+
const handled = await tryDispatchExtensionCommand(pi, "/ctx-stats", "sid", sink, conn);
|
|
336
360
|
|
|
337
361
|
const dispatchedB = !!dispatchCommand && dispatchCommand.mock.calls.length > 0;
|
|
338
362
|
const dispatchedC = sent.some((m) => m.type === "dispatch_extension_command");
|
|
339
|
-
const
|
|
340
|
-
(c[
|
|
363
|
+
const dispatchedD = sink.mock.calls
|
|
364
|
+
.map((c: any[]) => c[0])
|
|
365
|
+
.some((m: any) => m?.event?.eventType === "command_feedback" && m?.event?.data?.status === "error");
|
|
366
|
+
|
|
367
|
+
expect(handled, JSON.stringify(s)).toBe(true); // all paths now handle the command
|
|
341
368
|
|
|
342
|
-
const fired = [dispatchedB && "B", dispatchedC && "C",
|
|
369
|
+
const fired = [dispatchedB && "B", dispatchedC && "C", dispatchedD && "D"].filter(Boolean);
|
|
343
370
|
expect(fired, JSON.stringify(s)).toEqual([s.expect]);
|
|
344
371
|
}
|
|
345
372
|
});
|
|
346
373
|
});
|
|
347
374
|
|
|
375
|
+
// See change: add-steering-message (task 4.4).
|
|
376
|
+
// Verify the slash-routing fallback paths that call sendUserMessage honor the
|
|
377
|
+
// delivery field — `"steer"` → deliverAs:"steer"; absent/"followUp" → deliverAs:"followUp".
|
|
378
|
+
describe("bridge slash routing: delivery field → sendUserMessage deliverAs", () => {
|
|
379
|
+
function lastDeliverAs(sendUserMessage: ReturnType<typeof vi.fn>): string | undefined {
|
|
380
|
+
const lastCall = sendUserMessage.mock.calls.at(-1);
|
|
381
|
+
if (!lastCall) return undefined;
|
|
382
|
+
const opts = lastCall[1];
|
|
383
|
+
return opts?.deliverAs;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
it("skill command + delivery:'steer' → sendUserMessage called with deliverAs:'steer'", async () => {
|
|
387
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
388
|
+
await drive("/skill:foo", stub, "steer");
|
|
389
|
+
expect(stub.sendUserMessage).toHaveBeenCalledTimes(1);
|
|
390
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("steer");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("skill command + delivery:'followUp' → sendUserMessage called with deliverAs:'followUp'", async () => {
|
|
394
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
395
|
+
await drive("/skill:foo", stub, "followUp");
|
|
396
|
+
expect(stub.sendUserMessage).toHaveBeenCalledTimes(1);
|
|
397
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("followUp");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("skill command + delivery omitted → sendUserMessage defaults to deliverAs:'followUp'", async () => {
|
|
401
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
402
|
+
await drive("/skill:foo", stub);
|
|
403
|
+
expect(stub.sendUserMessage).toHaveBeenCalledTimes(1);
|
|
404
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("followUp");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("prompt template + delivery:'steer' → deliverAs:'steer'", async () => {
|
|
408
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
409
|
+
await drive("/review", stub, "steer");
|
|
410
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("steer");
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("passthrough text + delivery:'steer' → deliverAs:'steer'", async () => {
|
|
414
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
415
|
+
await drive("hello world", stub, "steer");
|
|
416
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("steer");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("passthrough text + delivery omitted → deliverAs:'followUp'", async () => {
|
|
420
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
421
|
+
await drive("hello world", stub);
|
|
422
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("followUp");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("unrecognized slash + delivery:'steer' → deliverAs:'steer'", async () => {
|
|
426
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
427
|
+
await drive("/totally-unknown-command", stub, "steer");
|
|
428
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("steer");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("bridge-native /__dashboard_reload fallback + delivery:'steer' → deliverAs:'steer'", async () => {
|
|
432
|
+
const stub = makeStubPi({ withDispatch: true });
|
|
433
|
+
await drive("/__dashboard_reload", stub, "steer");
|
|
434
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("steer");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("getCommands throws fallback + delivery:'steer' → deliverAs:'steer'", async () => {
|
|
438
|
+
const stub = makeStubPi({ withDispatch: true, getCommandsThrows: true });
|
|
439
|
+
await drive("/ctx-stats", stub, "steer");
|
|
440
|
+
expect(lastDeliverAs(stub.sendUserMessage)).toBe("steer");
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
348
444
|
describe("hasDispatchCommand", () => {
|
|
349
445
|
it("returns true when field is a function", () => {
|
|
350
446
|
expect(hasDispatchCommand({ dispatchCommand: () => {} })).toBe(true);
|
|
@@ -12,6 +12,7 @@ describe("CommandHandler", () => {
|
|
|
12
12
|
setSessionName: vi.fn(),
|
|
13
13
|
getSessionName: vi.fn(),
|
|
14
14
|
on: vi.fn(),
|
|
15
|
+
exec: vi.fn(),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -481,7 +482,7 @@ describe("CommandHandler", () => {
|
|
|
481
482
|
|
|
482
483
|
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "/some-command args" });
|
|
483
484
|
|
|
484
|
-
expect(sessionPrompt).toHaveBeenCalledWith("/some-command args");
|
|
485
|
+
expect(sessionPrompt).toHaveBeenCalledWith("/some-command args", undefined);
|
|
485
486
|
expect(pi.sendUserMessage).not.toHaveBeenCalled();
|
|
486
487
|
});
|
|
487
488
|
|
|
@@ -513,7 +514,8 @@ describe("CommandHandler", () => {
|
|
|
513
514
|
|
|
514
515
|
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "/some-command" });
|
|
515
516
|
|
|
516
|
-
|
|
517
|
+
// Slash fallback forwards delivery (default 'followUp'). See change: add-steering-message.
|
|
518
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith("/some-command", { deliverAs: "followUp" });
|
|
517
519
|
const feedbackCalls = eventSink.mock.calls.filter(
|
|
518
520
|
(c) => (c[0] as any)?.event?.eventType === "command_feedback",
|
|
519
521
|
);
|
|
@@ -526,7 +528,7 @@ describe("CommandHandler", () => {
|
|
|
526
528
|
|
|
527
529
|
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "/some-command args" });
|
|
528
530
|
|
|
529
|
-
expect(pi.sendUserMessage).toHaveBeenCalledWith("/some-command args");
|
|
531
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith("/some-command args", { deliverAs: "followUp" });
|
|
530
532
|
});
|
|
531
533
|
|
|
532
534
|
it("should route /quit to shutdown", async () => {
|
|
@@ -780,3 +782,103 @@ describe("parseSendPrompt", () => {
|
|
|
780
782
|
});
|
|
781
783
|
});
|
|
782
784
|
});
|
|
785
|
+
|
|
786
|
+
describe("CommandHandler delivery routing (pi-native queues)", () => {
|
|
787
|
+
// After change: add-followup-edit-and-steer-cancel, the bridge no longer
|
|
788
|
+
// owns a parallel queue. All passthrough sends go directly to pi.sendUserMessage;
|
|
789
|
+
// followUp delivery additionally calls pi.clearFollowUpQueue() first to enforce
|
|
790
|
+
// capacity-1 on the slot.
|
|
791
|
+
function createMockPi() {
|
|
792
|
+
return {
|
|
793
|
+
sendUserMessage: vi.fn(),
|
|
794
|
+
getCommands: vi.fn().mockReturnValue([]),
|
|
795
|
+
setSessionName: vi.fn(),
|
|
796
|
+
getSessionName: vi.fn(),
|
|
797
|
+
on: vi.fn(),
|
|
798
|
+
exec: vi.fn(),
|
|
799
|
+
clearFollowUpQueue: vi.fn(),
|
|
800
|
+
clearSteeringQueue: vi.fn(),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
it("passthrough followUp APPENDS to pi's queue (v2: no pre-clear)", async () => {
|
|
805
|
+
const pi = createMockPi();
|
|
806
|
+
const handler = createCommandHandler(pi as any, "s1");
|
|
807
|
+
|
|
808
|
+
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "after done", delivery: "followUp" });
|
|
809
|
+
|
|
810
|
+
// v2: cap-1 invariant dropped. clearFollowUpQueue is only called by explicit
|
|
811
|
+
// promote/remove/edit operations, not on every send.
|
|
812
|
+
expect(pi.clearFollowUpQueue).not.toHaveBeenCalled();
|
|
813
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith("after done", { deliverAs: "followUp" });
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it("passthrough delivery absent defaults to followUp and APPENDS (v2)", async () => {
|
|
817
|
+
const pi = createMockPi();
|
|
818
|
+
const handler = createCommandHandler(pi as any, "s1");
|
|
819
|
+
|
|
820
|
+
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "plain" });
|
|
821
|
+
|
|
822
|
+
expect(pi.clearFollowUpQueue).not.toHaveBeenCalled();
|
|
823
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith("plain", { deliverAs: "followUp" });
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it("passthrough delivery steer does NOT call clearFollowUpQueue or clearSteeringQueue", async () => {
|
|
827
|
+
const pi = createMockPi();
|
|
828
|
+
const handler = createCommandHandler(pi as any, "s1");
|
|
829
|
+
|
|
830
|
+
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "focus on X", delivery: "steer" });
|
|
831
|
+
|
|
832
|
+
expect(pi.clearFollowUpQueue).not.toHaveBeenCalled();
|
|
833
|
+
expect(pi.clearSteeringQueue).not.toHaveBeenCalled();
|
|
834
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith("focus on X", { deliverAs: "steer" });
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("passthrough with images preserves image content (v2: no pre-clear)", async () => {
|
|
838
|
+
const pi = createMockPi();
|
|
839
|
+
const images = [{ type: "image" as const, data: "AAA", mimeType: "image/png" }];
|
|
840
|
+
const handler = createCommandHandler(pi as any, "s1");
|
|
841
|
+
|
|
842
|
+
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "img", images, delivery: "followUp" });
|
|
843
|
+
|
|
844
|
+
expect(pi.clearFollowUpQueue).not.toHaveBeenCalled();
|
|
845
|
+
expect(pi.sendUserMessage).toHaveBeenCalledTimes(1);
|
|
846
|
+
const [content, opts] = pi.sendUserMessage.mock.calls[0];
|
|
847
|
+
expect(opts).toEqual({ deliverAs: "followUp" });
|
|
848
|
+
expect(Array.isArray(content)).toBe(true);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it("bash commands bypass delivery routing entirely (no clearFollowUpQueue call)", async () => {
|
|
852
|
+
const pi = createMockPi();
|
|
853
|
+
pi.exec = vi.fn().mockResolvedValue({ stdout: "hi", stderr: "", exitCode: 0 });
|
|
854
|
+
const handler = createCommandHandler(pi as any, "s1");
|
|
855
|
+
|
|
856
|
+
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "!ls" });
|
|
857
|
+
|
|
858
|
+
// Bash handler forwards stdout via sendUserMessage as its result, but
|
|
859
|
+
// delivery routing is not involved — no clearFollowUpQueue or deliverAs option.
|
|
860
|
+
expect(pi.clearFollowUpQueue).not.toHaveBeenCalled();
|
|
861
|
+
expect(pi.clearSteeringQueue).not.toHaveBeenCalled();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("slash command with delivery=steer passes delivery to sessionPrompt; no pi call from handler", async () => {
|
|
865
|
+
const pi = createMockPi();
|
|
866
|
+
const sessionPrompt = vi.fn();
|
|
867
|
+
const handler = createCommandHandler(pi as any, "s1", { sessionPrompt });
|
|
868
|
+
|
|
869
|
+
await handler.handle({ type: "send_prompt", sessionId: "s1", text: "/some-command args", delivery: "steer" });
|
|
870
|
+
|
|
871
|
+
expect(sessionPrompt).toHaveBeenCalledWith("/some-command args", "steer");
|
|
872
|
+
expect(pi.sendUserMessage).not.toHaveBeenCalled();
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it("abort no longer requires clearQueueOnAbort option", async () => {
|
|
876
|
+
const pi = createMockPi();
|
|
877
|
+
const abort = vi.fn();
|
|
878
|
+
const handler = createCommandHandler(pi as any, "s1", { abort });
|
|
879
|
+
|
|
880
|
+
await handler.handle({ type: "abort", sessionId: "s1" });
|
|
881
|
+
|
|
882
|
+
expect(abort).toHaveBeenCalledTimes(1);
|
|
883
|
+
});
|
|
884
|
+
});
|