@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.
Files changed (212) hide show
  1. package/AGENTS.md +19 -30
  2. package/README.md +69 -1
  3. package/docs/architecture.md +89 -165
  4. package/package.json +10 -7
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
  7. package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
  8. package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
  9. package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
  10. package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
  11. package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
  12. package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
  13. package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
  14. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
  15. package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
  16. package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
  17. package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
  19. package/packages/extension/src/bridge-default-model-gate.ts +32 -0
  20. package/packages/extension/src/bridge.ts +299 -20
  21. package/packages/extension/src/command-handler.ts +53 -7
  22. package/packages/extension/src/dashboard-default-adapter.ts +5 -0
  23. package/packages/extension/src/prompt-bus.ts +15 -0
  24. package/packages/extension/src/slash-dispatch.ts +30 -15
  25. package/packages/extension/src/source-detector.ts +13 -5
  26. package/packages/extension/src/usage-limit-orderer.ts +18 -1
  27. package/packages/server/bin/pi-dashboard.mjs +62 -14
  28. package/packages/server/package.json +9 -5
  29. package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
  30. package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
  31. package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
  32. package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
  33. package/packages/server/src/__tests__/cli-version.test.ts +151 -0
  34. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
  35. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
  36. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
  37. package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
  38. package/packages/server/src/__tests__/directory-service.test.ts +9 -0
  39. package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
  40. package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
  41. package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
  42. package/packages/server/src/__tests__/health-shape.test.ts +35 -12
  43. package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
  44. package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
  45. package/packages/server/src/__tests__/package-routes.test.ts +6 -2
  46. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
  47. package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
  48. package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
  49. package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
  50. package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
  51. package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
  52. package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
  53. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  54. package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
  55. package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
  56. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
  57. package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
  58. package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
  59. package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
  60. package/packages/server/src/browser-gateway.ts +83 -5
  61. package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
  63. package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
  64. package/packages/server/src/changelog-parser.ts +1 -1
  65. package/packages/server/src/cli.ts +68 -250
  66. package/packages/server/src/event-status-extraction.ts +14 -62
  67. package/packages/server/src/event-wiring.ts +23 -10
  68. package/packages/server/src/memory-session-manager.ts +4 -0
  69. package/packages/server/src/pi-core-checker.ts +1 -1
  70. package/packages/server/src/pi-dev-version-check.ts +1 -1
  71. package/packages/server/src/pi-version-skew.ts +24 -46
  72. package/packages/server/src/plugin-intent-cache.ts +67 -0
  73. package/packages/server/src/preferences-store.ts +199 -13
  74. package/packages/server/src/recovery-server.ts +366 -0
  75. package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
  76. package/packages/server/src/routes/doctor-routes.ts +26 -21
  77. package/packages/server/src/routes/manifest-route.ts +162 -0
  78. package/packages/server/src/routes/openspec-routes.ts +4 -25
  79. package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
  80. package/packages/server/src/routes/pi-core-routes.ts +3 -23
  81. package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
  82. package/packages/server/src/routes/recommended-routes.ts +21 -0
  83. package/packages/server/src/routes/system-routes.ts +73 -11
  84. package/packages/server/src/server.ts +105 -307
  85. package/packages/server/src/session-api.ts +5 -63
  86. package/packages/shared/package.json +1 -1
  87. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
  88. package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
  89. package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
  90. package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
  91. package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
  92. package/packages/shared/src/__tests__/config.test.ts +40 -0
  93. package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
  94. package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
  95. package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
  96. package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
  97. package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
  98. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
  99. package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
  100. package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
  101. package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
  102. package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
  103. package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
  104. package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
  105. package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
  106. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
  107. package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
  108. package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
  109. package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
  110. package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
  111. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
  112. package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
  113. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
  114. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
  115. package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
  116. package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
  117. package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
  118. package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
  119. package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
  120. package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
  121. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
  122. package/packages/shared/src/bridge-register.ts +35 -2
  123. package/packages/shared/src/browser-protocol.ts +176 -2
  124. package/packages/shared/src/config.ts +12 -0
  125. package/packages/shared/src/dashboard-paths.ts +69 -0
  126. package/packages/shared/src/dashboard-plugin/index.ts +2 -0
  127. package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
  128. package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
  129. package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
  130. package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
  131. package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
  132. package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
  133. package/packages/shared/src/dashboard-starter.ts +22 -0
  134. package/packages/shared/src/doctor-core.ts +49 -27
  135. package/packages/shared/src/launch-source-types.ts +9 -9
  136. package/packages/shared/src/legacy-managed-dir.ts +97 -0
  137. package/packages/shared/src/mdns-discovery.ts +4 -1
  138. package/packages/shared/src/pi-package-resolver.ts +388 -0
  139. package/packages/shared/src/platform/binary-lookup.ts +27 -3
  140. package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
  141. package/packages/shared/src/platform/exec.ts +22 -0
  142. package/packages/shared/src/platform/node-spawn.ts +42 -41
  143. package/packages/shared/src/plugin-bridge-register.ts +275 -18
  144. package/packages/shared/src/protocol.ts +94 -2
  145. package/packages/shared/src/recommended-extensions.ts +34 -10
  146. package/packages/shared/src/server-identity.ts +74 -5
  147. package/packages/shared/src/server-launcher.ts +20 -0
  148. package/packages/shared/src/source-matching.ts +1 -1
  149. package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
  150. package/packages/shared/src/tool-registry/definitions.ts +91 -7
  151. package/packages/shared/src/types.ts +12 -8
  152. package/scripts/maybe-patch-package.cjs +44 -0
  153. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
  154. package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
  155. package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
  156. package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
  157. package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
  158. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
  159. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
  160. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
  161. package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
  162. package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
  163. package/packages/server/src/bootstrap-install-from-list.ts +0 -232
  164. package/packages/server/src/bootstrap-queue.ts +0 -130
  165. package/packages/server/src/bootstrap-state.ts +0 -159
  166. package/packages/server/src/legacy-pi-cleanup.ts +0 -151
  167. package/packages/server/src/routes/bootstrap-routes.ts +0 -125
  168. package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
  169. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
  170. package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
  171. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
  172. package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
  173. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
  174. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
  175. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
  176. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
  177. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
  178. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
  179. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
  180. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
  181. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
  182. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
  183. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
  184. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
  185. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
  186. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
  187. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
  188. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
  189. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
  190. package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
  191. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
  192. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
  193. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
  194. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
  195. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
  196. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
  197. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
  198. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
  199. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
  200. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
  201. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
  202. package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
  203. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
  204. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
  205. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
  206. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
  207. package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
  208. package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
  209. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
  210. package/packages/shared/src/bootstrap-install.ts +0 -406
  211. package/packages/shared/src/installable-list.ts +0 -152
  212. 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, NO dispatchCommandstopgap error, no sendUserMessage, started+error", async () => {
84
- // regression: see openspec/changes/fix-extension-slash-commands-in-dashboard/
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(["started", "error"]);
92
- expect(evs[1].message).toMatch(/pi 0\.71\+/);
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("never duplicates command_feedback on stopgap path", async () => {
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.filter((e) => e.status === "started")).toHaveLength(1);
177
- expect(evs.filter((e) => e.status === "completed" || e.status === "error")).toHaveLength(1);
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 NEVER reaches sendUserMessage", async () => {
181
- // regression: see openspec/changes/fix-extension-slash-commands-in-dashboard/
182
- for (const withDispatch of [true, false]) {
183
- const stub = makeStubPi({ withDispatch });
184
- await drive("/ctx-stats", stub);
185
- expect(stub.sendUserMessage, `withDispatch=${withDispatch}`).not.toHaveBeenCalled();
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 + connection stopgap error; no connection.send", async () => {
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(2);
286
- expect(evs[0].event.data.status).toBe("started");
287
- expect(evs[1].event.data.status).toBe("error");
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 C degrades to Path D when connection arg is undefined", async () => {
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(true);
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
- // started + error (Path D fallback) — NOT just started.
302
- expect(evs.map((e: any) => e.event.data.status)).toEqual(["started", "error"]);
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 errorD = sink.mock.calls.some((c: any[]) =>
340
- (c[0] as any)?.event?.data?.status === "error");
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", errorD && "D"].filter(Boolean);
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
- expect(pi.sendUserMessage).toHaveBeenCalledWith("/some-command");
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
+ });