@cuongtran001/kanna 1.2.0 → 1.3.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.
Files changed (196) hide show
  1. package/dist/client/assets/{arc-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
  5. package/dist/client/assets/channel-BEYTrUAm.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
  14. package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
  15. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
  16. package/dist/client/assets/{cose-bilkent-S5V4N54A-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
  27. package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
  28. package/dist/client/assets/index-CLsOWXff.css +32 -0
  29. package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
  30. package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
  31. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
  34. package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
  35. package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
  36. package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
  44. package/dist/client/assets/{timeline-definition-PNZ67QCA-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
  45. package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
  46. package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
  47. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
  48. package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
  49. package/dist/client/index.html +2 -2
  50. package/package.json +1 -1
  51. package/src/server/agent-coordinator-types.ts +158 -0
  52. package/src/server/agent-coordinator.ts +919 -0
  53. package/src/server/agent-deps-builders.ts +425 -0
  54. package/src/server/agent.oauth-rotation.test.ts +101 -0
  55. package/src/server/agent.test.ts +24 -8
  56. package/src/server/agent.ts +35 -4963
  57. package/src/server/auto-continue/read-model.test.ts +2 -2
  58. package/src/server/auto-continue/read-model.ts +4 -1
  59. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  60. package/src/server/claude-autocontinue-commands.ts +256 -0
  61. package/src/server/claude-cancel-handler.test.ts +472 -0
  62. package/src/server/claude-cancel-handler.ts +241 -0
  63. package/src/server/claude-chat-management.test.ts +395 -0
  64. package/src/server/claude-chat-management.ts +251 -0
  65. package/src/server/claude-harness-stream.test.ts +273 -0
  66. package/src/server/claude-harness-stream.ts +158 -0
  67. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  68. package/src/server/claude-loop-orch-commands.ts +466 -0
  69. package/src/server/claude-message-normalizer.test.ts +213 -0
  70. package/src/server/claude-message-normalizer.ts +436 -0
  71. package/src/server/claude-prompt-helpers.test.ts +248 -0
  72. package/src/server/claude-prompt-helpers.ts +139 -0
  73. package/src/server/claude-sdk-queue.test.ts +169 -0
  74. package/src/server/claude-sdk-queue.ts +82 -0
  75. package/src/server/claude-send-command.test.ts +414 -0
  76. package/src/server/claude-send-command.ts +420 -0
  77. package/src/server/claude-session-config-helpers.test.ts +279 -0
  78. package/src/server/claude-session-config-helpers.ts +151 -0
  79. package/src/server/claude-session-config.test.ts +250 -0
  80. package/src/server/claude-session-config.ts +159 -0
  81. package/src/server/claude-session-error-handler.test.ts +498 -0
  82. package/src/server/claude-session-error-handler.ts +361 -0
  83. package/src/server/claude-session-lifecycle.test.ts +669 -0
  84. package/src/server/claude-session-lifecycle.ts +283 -0
  85. package/src/server/claude-session-rebuild.test.ts +224 -0
  86. package/src/server/claude-session-rebuild.ts +101 -0
  87. package/src/server/claude-session-runner.test.ts +523 -0
  88. package/src/server/claude-session-runner.ts +521 -0
  89. package/src/server/claude-session-spawner.test.ts +401 -0
  90. package/src/server/claude-session-spawner.ts +401 -0
  91. package/src/server/claude-session-start.test.ts +205 -0
  92. package/src/server/claude-session-start.ts +277 -0
  93. package/src/server/claude-session-state-queries.test.ts +344 -0
  94. package/src/server/claude-session-state-queries.ts +160 -0
  95. package/src/server/claude-session-state.ts +104 -0
  96. package/src/server/claude-slash-commands.test.ts +277 -0
  97. package/src/server/claude-slash-commands.ts +234 -0
  98. package/src/server/claude-spawn-helpers.test.ts +221 -0
  99. package/src/server/claude-spawn-helpers.ts +179 -0
  100. package/src/server/claude-steer-log.test.ts +243 -0
  101. package/src/server/claude-steer-log.ts +58 -0
  102. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  103. package/src/server/claude-subagent-tool-response.ts +165 -0
  104. package/src/server/claude-subagent-wiring.test.ts +263 -0
  105. package/src/server/claude-subagent-wiring.ts +324 -0
  106. package/src/server/claude-tool-respond.test.ts +321 -0
  107. package/src/server/claude-tool-respond.ts +142 -0
  108. package/src/server/claude-turn-runner.test.ts +310 -0
  109. package/src/server/claude-turn-runner.ts +228 -0
  110. package/src/server/claude-turn-starter.test.ts +266 -0
  111. package/src/server/claude-turn-starter.ts +566 -0
  112. package/src/server/claude-usage-math.test.ts +169 -0
  113. package/src/server/claude-usage-math.ts +132 -0
  114. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  115. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  116. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  117. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  118. package/src/server/diff-store-errors.ts +85 -0
  119. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  120. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  121. package/src/server/diff-store-io.adapter.ts +27 -0
  122. package/src/server/diff-store-parse.test.ts +212 -0
  123. package/src/server/diff-store-parse.ts +122 -0
  124. package/src/server/diff-store-state.ts +82 -0
  125. package/src/server/diff-store.ts +76 -1854
  126. package/src/server/event-store-apply.test.ts +280 -0
  127. package/src/server/event-store-apply.ts +132 -0
  128. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  129. package/src/server/event-store-chat-lifecycle.ts +441 -0
  130. package/src/server/event-store-entity-write.ts +356 -0
  131. package/src/server/event-store-helpers.test.ts +230 -0
  132. package/src/server/event-store-helpers.ts +235 -0
  133. package/src/server/event-store-init.ts +276 -0
  134. package/src/server/event-store-messages.adapter.test.ts +365 -0
  135. package/src/server/event-store-messages.adapter.ts +213 -0
  136. package/src/server/event-store-orch-subscription.ts +124 -0
  137. package/src/server/event-store-orch.test.ts +462 -0
  138. package/src/server/event-store-orch.ts +312 -0
  139. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  140. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  141. package/src/server/event-store-snapshot.test.ts +597 -0
  142. package/src/server/event-store-snapshot.ts +658 -0
  143. package/src/server/event-store-subagent.test.ts +408 -0
  144. package/src/server/event-store-subagent.ts +198 -0
  145. package/src/server/event-store-tool-requests.test.ts +210 -0
  146. package/src/server/event-store-tool-requests.ts +131 -0
  147. package/src/server/event-store-transcript-write.adapter.ts +285 -0
  148. package/src/server/event-store-write-ops.test.ts +554 -0
  149. package/src/server/event-store-write-ops.ts +529 -0
  150. package/src/server/event-store.test.ts +35 -0
  151. package/src/server/event-store.ts +251 -2284
  152. package/src/server/events.ts +3 -0
  153. package/src/server/harness-types.ts +15 -1
  154. package/src/server/oauth-errors.ts +19 -0
  155. package/src/server/read-models.ts +26 -1
  156. package/src/server/subagent-orchestrator.ts +3 -0
  157. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  158. package/src/server/ws-router-agent-ctrl.ts +137 -0
  159. package/src/server/ws-router-broadcast.ts +488 -0
  160. package/src/server/ws-router-chat.test.ts +461 -0
  161. package/src/server/ws-router-chat.ts +279 -0
  162. package/src/server/ws-router-defaults.test.ts +283 -0
  163. package/src/server/ws-router-defaults.ts +434 -0
  164. package/src/server/ws-router-diff.test.ts +167 -0
  165. package/src/server/ws-router-diff.ts +250 -0
  166. package/src/server/ws-router-envelope.ts +329 -0
  167. package/src/server/ws-router-misc.test.ts +422 -0
  168. package/src/server/ws-router-misc.ts +265 -0
  169. package/src/server/ws-router-orch.test.ts +190 -0
  170. package/src/server/ws-router-orch.ts +93 -0
  171. package/src/server/ws-router-project.test.ts +403 -0
  172. package/src/server/ws-router-project.ts +256 -0
  173. package/src/server/ws-router-push.test.ts +193 -0
  174. package/src/server/ws-router-push.ts +120 -0
  175. package/src/server/ws-router-settings.test.ts +173 -0
  176. package/src/server/ws-router-settings.ts +416 -0
  177. package/src/server/ws-router-skills.test.ts +194 -0
  178. package/src/server/ws-router-skills.ts +204 -0
  179. package/src/server/ws-router-utils.ts +224 -0
  180. package/src/server/ws-router.ts +339 -2254
  181. package/src/shared/app-settings-types.ts +365 -0
  182. package/src/shared/core-types.ts +27 -0
  183. package/src/shared/git-diff-types.ts +165 -0
  184. package/src/shared/loop-progress.test.ts +106 -0
  185. package/src/shared/loop-progress.ts +88 -0
  186. package/src/shared/mcp-types.ts +105 -0
  187. package/src/shared/provider-model-types.ts +437 -0
  188. package/src/shared/subagent-types.ts +198 -0
  189. package/src/shared/tool-call-types.ts +314 -0
  190. package/src/shared/transcript-types.ts +270 -0
  191. package/src/shared/types.ts +27 -1668
  192. package/dist/client/assets/channel-BMkhjiLI.js +0 -1
  193. package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
  194. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
  195. package/dist/client/assets/index-H6C-f2Vp.css +0 -32
  196. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
@@ -0,0 +1,461 @@
1
+ /**
2
+ * ws-router-chat.test.ts
3
+ *
4
+ * Unit tests for the chat lifecycle WS command handlers.
5
+ */
6
+ import { describe, expect, mock, test } from "bun:test"
7
+ import type {
8
+ ChatAgentDep,
9
+ ChatAnalyticsDep,
10
+ ChatCommandDeps,
11
+ ChatStoreDep,
12
+ ChatToolCallbackServiceDep,
13
+ } from "./ws-router-chat"
14
+ import { handleChatCommand } from "./ws-router-chat"
15
+ import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function makeStore(overrides: Partial<ChatStoreDep> = {}): ChatStoreDep {
22
+ return {
23
+ createChat: mock(async () => ({ id: "chat-1" })),
24
+ renameChat: mock(async () => {}),
25
+ archiveChat: mock(async () => {}),
26
+ unarchiveChat: mock(async () => {}),
27
+ deleteChat: mock(async () => {}),
28
+ setChatReadState: mock(async () => {}),
29
+ setChatPolicyOverride: mock(async () => {}),
30
+ getChat: mock(() => ({ id: "chat-1" })),
31
+ getMessagesPageBefore: mock(() => ({ messages: [], hasOlder: false, olderCursor: null })),
32
+ getToolRequest: mock(() => ({ chatId: "chat-1" })),
33
+ ...overrides,
34
+ }
35
+ }
36
+
37
+ function makeToolCallbackService(
38
+ overrides: Partial<ChatToolCallbackServiceDep> = {},
39
+ ): ChatToolCallbackServiceDep {
40
+ return {
41
+ cancelAllForChat: mock(async () => {}),
42
+ answer: mock(async () => {}),
43
+ ...overrides,
44
+ }
45
+ }
46
+
47
+ function makeAgent(overrides: Partial<ChatAgentDep> = {}): ChatAgentDep {
48
+ return {
49
+ send: mock(async () => ({ chatId: "chat-1" })),
50
+ forkChat: mock(async () => ({ chatId: "forked-1" })),
51
+ cancel: mock(async () => {}),
52
+ cancelAutoContinue: mock(async () => {}),
53
+ listLiveSchedules: mock(() => []),
54
+ closeChat: mock(async () => {}),
55
+ stopDraining: mock(async () => {}),
56
+ respondTool: mock(async () => {}),
57
+ respondSubagentTool: mock(async () => {}),
58
+ cancelSubagentRun: mock(async () => {}),
59
+ getActiveTurnProfile: mock(() => null),
60
+ toolCallbackService: makeToolCallbackService(),
61
+ ...overrides,
62
+ }
63
+ }
64
+
65
+ function makeAnalytics(): ChatAnalyticsDep & { events: string[] } {
66
+ const events: string[] = []
67
+ return { events, track: (e) => { events.push(e) } }
68
+ }
69
+
70
+ interface TestDeps extends ChatCommandDeps {
71
+ sent: ServerEnvelope[]
72
+ sidebarBroadcasts: number
73
+ chatSidebarBroadcasts: string[]
74
+ allBroadcasts: number
75
+ draftProtectionCalls: string[][]
76
+ logCalls: Array<{ stage: string }>
77
+ }
78
+
79
+ function makeDeps(
80
+ opts: {
81
+ storeOverrides?: Partial<ChatStoreDep>
82
+ agentOverrides?: Partial<ChatAgentDep>
83
+ } = {},
84
+ ): TestDeps {
85
+ const sent: ServerEnvelope[] = []
86
+ let sidebarBroadcasts = 0
87
+ const chatSidebarBroadcasts: string[] = []
88
+ let allBroadcasts = 0
89
+ const draftProtectionCalls: string[][] = []
90
+ const logCalls: Array<{ stage: string }> = []
91
+ const analytics = makeAnalytics()
92
+
93
+ return {
94
+ store: makeStore(opts.storeOverrides),
95
+ agent: makeAgent(opts.agentOverrides),
96
+ analytics,
97
+ setDraftProtection: (chatIds) => { draftProtectionCalls.push(chatIds) },
98
+ logSendProfilingFn: (_traceId, _startedAt, stage) => { logCalls.push({ stage }) },
99
+ send: (envelope) => { sent.push(envelope); return JSON.stringify(envelope).length },
100
+ broadcastChatAndSidebar: async (chatId) => { chatSidebarBroadcasts.push(chatId) },
101
+ broadcastSidebar: async () => { sidebarBroadcasts++ },
102
+ broadcastAll: async () => { allBroadcasts++ },
103
+ sent,
104
+ get sidebarBroadcasts() { return sidebarBroadcasts },
105
+ chatSidebarBroadcasts,
106
+ get allBroadcasts() { return allBroadcasts },
107
+ draftProtectionCalls,
108
+ logCalls,
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Tests
114
+ // ---------------------------------------------------------------------------
115
+
116
+ describe("handleChatCommand", () => {
117
+ // -------------------------------------------------------------------------
118
+ // Unknown / out-of-scope
119
+ // -------------------------------------------------------------------------
120
+
121
+ test("returns false for a non-chat command", async () => {
122
+ const deps = makeDeps()
123
+ const handled = await handleChatCommand(
124
+ deps,
125
+ { type: "system.ping" } as unknown as ClientCommand,
126
+ "r0",
127
+ )
128
+ expect(handled).toBe(false)
129
+ expect(deps.sent).toHaveLength(0)
130
+ })
131
+
132
+ // -------------------------------------------------------------------------
133
+ // chat.create
134
+ // -------------------------------------------------------------------------
135
+
136
+ test("chat.create — creates chat, acks, tracks analytics, broadcasts chat+sidebar", async () => {
137
+ const deps = makeDeps()
138
+ const cmd: ClientCommand = { type: "chat.create", projectId: "proj-1" } as ClientCommand
139
+ const handled = await handleChatCommand(deps, cmd, "r1")
140
+ expect(handled).toBe(true)
141
+ expect(deps.store.createChat as ReturnType<typeof mock>).toHaveBeenCalledWith("proj-1", expect.any(Object))
142
+ const ack = deps.sent[0] as { type: string; result: { chatId: string } }
143
+ expect(ack.result.chatId).toBe("chat-1")
144
+ expect((deps.analytics as ReturnType<typeof makeAnalytics>).events).toContain("chat_created")
145
+ expect(deps.chatSidebarBroadcasts).toContain("chat-1")
146
+ })
147
+
148
+ // -------------------------------------------------------------------------
149
+ // chat.fork
150
+ // -------------------------------------------------------------------------
151
+
152
+ test("chat.fork — forks chat, acks with result, broadcasts sidebar", async () => {
153
+ const deps = makeDeps()
154
+ const handled = await handleChatCommand(
155
+ deps,
156
+ { type: "chat.fork", chatId: "chat-1" },
157
+ "r2",
158
+ )
159
+ expect(handled).toBe(true)
160
+ expect(deps.agent.forkChat as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1")
161
+ expect(deps.sidebarBroadcasts).toBe(1)
162
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r2" })
163
+ })
164
+
165
+ // -------------------------------------------------------------------------
166
+ // chat.rename
167
+ // -------------------------------------------------------------------------
168
+
169
+ test("chat.rename — renames, acks, broadcasts chat+sidebar", async () => {
170
+ const deps = makeDeps()
171
+ const handled = await handleChatCommand(
172
+ deps,
173
+ { type: "chat.rename", chatId: "chat-1", title: "New Title" },
174
+ "r3",
175
+ )
176
+ expect(handled).toBe(true)
177
+ expect(deps.store.renameChat as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1", "New Title")
178
+ expect(deps.chatSidebarBroadcasts).toContain("chat-1")
179
+ })
180
+
181
+ // -------------------------------------------------------------------------
182
+ // chat.archive / chat.unarchive
183
+ // -------------------------------------------------------------------------
184
+
185
+ test("chat.archive — archives, acks, broadcasts sidebar only", async () => {
186
+ const deps = makeDeps()
187
+ const handled = await handleChatCommand(
188
+ deps,
189
+ { type: "chat.archive", chatId: "chat-1" },
190
+ "r4",
191
+ )
192
+ expect(handled).toBe(true)
193
+ expect(deps.store.archiveChat as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1")
194
+ expect(deps.sidebarBroadcasts).toBe(1)
195
+ expect(deps.chatSidebarBroadcasts).toHaveLength(0)
196
+ })
197
+
198
+ // -------------------------------------------------------------------------
199
+ // chat.delete
200
+ // -------------------------------------------------------------------------
201
+
202
+ test("chat.delete — cancels, closes schedules, closes chat, cancels tool callbacks, deletes, tracks analytics", async () => {
203
+ const scheduleId = "sched-1"
204
+ const cancelAutoContinue = mock(async () => {})
205
+ const cancelAllForChat = mock(async () => {})
206
+ const deleteChat = mock(async () => {})
207
+ const deps = makeDeps({
208
+ agentOverrides: {
209
+ listLiveSchedules: mock(() => [scheduleId]),
210
+ cancelAutoContinue,
211
+ toolCallbackService: makeToolCallbackService({ cancelAllForChat }),
212
+ },
213
+ storeOverrides: { deleteChat },
214
+ })
215
+
216
+ const handled = await handleChatCommand(
217
+ deps,
218
+ { type: "chat.delete", chatId: "chat-1" },
219
+ "r5",
220
+ )
221
+ expect(handled).toBe(true)
222
+ expect(deps.agent.cancel as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1")
223
+ expect(cancelAutoContinue as ReturnType<typeof mock>).toHaveBeenCalledWith(
224
+ "chat-1",
225
+ scheduleId,
226
+ "chat_deleted",
227
+ )
228
+ expect(deps.agent.closeChat as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1")
229
+ expect(cancelAllForChat as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1", "chat_deleted")
230
+ expect(deleteChat as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1")
231
+ expect((deps.analytics as ReturnType<typeof makeAnalytics>).events).toContain("chat_deleted")
232
+ expect(deps.sidebarBroadcasts).toBe(1)
233
+ })
234
+
235
+ // -------------------------------------------------------------------------
236
+ // chat.setDraftProtection
237
+ // -------------------------------------------------------------------------
238
+
239
+ test("chat.setDraftProtection — sets protection, acks, broadcasts all", async () => {
240
+ const deps = makeDeps()
241
+ const handled = await handleChatCommand(
242
+ deps,
243
+ { type: "chat.setDraftProtection", chatIds: ["chat-a", "chat-b"] },
244
+ "r6",
245
+ )
246
+ expect(handled).toBe(true)
247
+ expect(deps.draftProtectionCalls).toHaveLength(1)
248
+ expect(deps.draftProtectionCalls[0]).toEqual(["chat-a", "chat-b"])
249
+ expect(deps.allBroadcasts).toBe(1)
250
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r6" })
251
+ })
252
+
253
+ // -------------------------------------------------------------------------
254
+ // chat.send
255
+ // -------------------------------------------------------------------------
256
+
257
+ test("chat.send — calls agent.send, acks with result, calls profiling hooks", async () => {
258
+ const sendResult = { chatId: "chat-sent" }
259
+ const profile = { traceId: "trace-1", startedAt: 100 }
260
+ const agentSend = mock(async () => sendResult)
261
+ const getActiveTurnProfile = mock(() => profile)
262
+ const deps = makeDeps({
263
+ agentOverrides: { send: agentSend, getActiveTurnProfile },
264
+ })
265
+ const cmd: ClientCommand = {
266
+ type: "chat.send",
267
+ content: "hello",
268
+ clientTraceId: "trace-1",
269
+ } as unknown as ClientCommand
270
+ const handled = await handleChatCommand(deps, cmd, "r7")
271
+ expect(handled).toBe(true)
272
+ expect(agentSend as ReturnType<typeof mock>).toHaveBeenCalledWith(cmd)
273
+ const ack = deps.sent[0] as { type: string; result: typeof sendResult }
274
+ expect(ack.result).toEqual(sendResult)
275
+ // profiling hooks should have been called twice (ack + ack_completed)
276
+ expect(deps.logCalls.some((c) => c.stage === "ws.chat_send_ack")).toBe(true)
277
+ expect(deps.logCalls.some((c) => c.stage === "ws.chat_send_ack_completed")).toBe(true)
278
+ })
279
+
280
+ // -------------------------------------------------------------------------
281
+ // chat.cancel
282
+ // -------------------------------------------------------------------------
283
+
284
+ test("chat.cancel — cancels agent, cancels tool callbacks, acks", async () => {
285
+ const cancelAllForChat = mock(async () => {})
286
+ const deps = makeDeps({
287
+ agentOverrides: {
288
+ toolCallbackService: makeToolCallbackService({ cancelAllForChat }),
289
+ },
290
+ })
291
+ const handled = await handleChatCommand(
292
+ deps,
293
+ { type: "chat.cancel", chatId: "chat-1" },
294
+ "r8",
295
+ )
296
+ expect(handled).toBe(true)
297
+ expect(deps.agent.cancel as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1")
298
+ expect(cancelAllForChat as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1", "chat_cancelled")
299
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r8" })
300
+ })
301
+
302
+ test("chat.cancel — no toolCallbackService — still acks", async () => {
303
+ const deps = makeDeps({
304
+ agentOverrides: { toolCallbackService: null },
305
+ })
306
+ const handled = await handleChatCommand(
307
+ deps,
308
+ { type: "chat.cancel", chatId: "chat-2" },
309
+ "r9",
310
+ )
311
+ expect(handled).toBe(true)
312
+ expect(deps.sent[0]).toMatchObject({ type: "ack" })
313
+ })
314
+
315
+ // -------------------------------------------------------------------------
316
+ // chat.loadHistory
317
+ // -------------------------------------------------------------------------
318
+
319
+ test("chat.loadHistory — returns messages page", async () => {
320
+ const page: import("../shared/types").ChatHistoryPage = { messages: [] as import("../shared/types").TranscriptEntry[], hasOlder: false, olderCursor: null }
321
+ const deps = makeDeps({
322
+ storeOverrides: {
323
+ getMessagesPageBefore: mock(() => page),
324
+ getChat: mock(() => ({ id: "chat-1" })),
325
+ },
326
+ })
327
+ const handled = await handleChatCommand(
328
+ deps,
329
+ { type: "chat.loadHistory", chatId: "chat-1", beforeCursor: "cursor-0", limit: 50 },
330
+ "r10",
331
+ )
332
+ expect(handled).toBe(true)
333
+ const ack = deps.sent[0] as { type: string; result: typeof page }
334
+ expect(ack.result).toEqual(page)
335
+ })
336
+
337
+ test("chat.loadHistory — chat not found — throws", async () => {
338
+ const deps = makeDeps({
339
+ storeOverrides: { getChat: mock(() => undefined) },
340
+ })
341
+ await expect(
342
+ handleChatCommand(
343
+ deps,
344
+ { type: "chat.loadHistory", chatId: "ghost", beforeCursor: "", limit: 50 },
345
+ "r11",
346
+ ),
347
+ ).rejects.toThrow("Chat not found")
348
+ })
349
+
350
+ // -------------------------------------------------------------------------
351
+ // chat.toolRequestAnswer
352
+ // -------------------------------------------------------------------------
353
+
354
+ test("chat.toolRequestAnswer — valid decision — answers, acks, broadcasts chat+sidebar", async () => {
355
+ const answer = mock(async () => {})
356
+ const deps = makeDeps({
357
+ agentOverrides: {
358
+ toolCallbackService: makeToolCallbackService({ answer }),
359
+ },
360
+ storeOverrides: {
361
+ getToolRequest: mock(() => ({ chatId: "chat-1" })),
362
+ },
363
+ })
364
+ const handled = await handleChatCommand(
365
+ deps,
366
+ {
367
+ type: "chat.toolRequestAnswer",
368
+ chatId: "chat-1",
369
+ toolRequestId: "req-1",
370
+ decision: { kind: "allow" },
371
+ } as unknown as ClientCommand,
372
+ "r12",
373
+ )
374
+ expect(handled).toBe(true)
375
+ expect(answer as ReturnType<typeof mock>).toHaveBeenCalledWith("req-1", { kind: "allow" })
376
+ expect(deps.chatSidebarBroadcasts).toContain("chat-1")
377
+ })
378
+
379
+ test("chat.toolRequestAnswer — no toolCallbackService — throws", async () => {
380
+ const deps = makeDeps({ agentOverrides: { toolCallbackService: null } })
381
+ await expect(
382
+ handleChatCommand(
383
+ deps,
384
+ {
385
+ type: "chat.toolRequestAnswer",
386
+ chatId: "chat-1",
387
+ toolRequestId: "req-1",
388
+ decision: { kind: "allow" },
389
+ } as unknown as ClientCommand,
390
+ "r13",
391
+ ),
392
+ ).rejects.toThrow("tool callback service unavailable")
393
+ })
394
+
395
+ test("chat.toolRequestAnswer — invalid decision kind — throws", async () => {
396
+ const deps = makeDeps()
397
+ await expect(
398
+ handleChatCommand(
399
+ deps,
400
+ {
401
+ type: "chat.toolRequestAnswer",
402
+ chatId: "chat-1",
403
+ toolRequestId: "req-1",
404
+ decision: { kind: "INVALID" },
405
+ } as unknown as ClientCommand,
406
+ "r14",
407
+ ),
408
+ ).rejects.toThrow("Invalid tool request decision kind")
409
+ })
410
+
411
+ test("chat.toolRequestAnswer — tool request belongs to different chat — throws", async () => {
412
+ const deps = makeDeps({
413
+ storeOverrides: {
414
+ getToolRequest: mock(() => ({ chatId: "different-chat" })),
415
+ },
416
+ })
417
+ await expect(
418
+ handleChatCommand(
419
+ deps,
420
+ {
421
+ type: "chat.toolRequestAnswer",
422
+ chatId: "chat-1",
423
+ toolRequestId: "req-1",
424
+ decision: { kind: "allow" },
425
+ } as unknown as ClientCommand,
426
+ "r15",
427
+ ),
428
+ ).rejects.toThrow("Tool request does not belong to this chat")
429
+ })
430
+
431
+ // -------------------------------------------------------------------------
432
+ // chat.respondTool / chat.respondSubagentTool / chat.cancelSubagentRun
433
+ // -------------------------------------------------------------------------
434
+
435
+ test("chat.respondTool — calls respondTool, acks", async () => {
436
+ const respondTool = mock(async () => {})
437
+ const deps = makeDeps({ agentOverrides: { respondTool } })
438
+ const cmd: ClientCommand = {
439
+ type: "chat.respondTool",
440
+ chatId: "chat-1",
441
+ toolUseId: "t1",
442
+ result: "ok",
443
+ } as unknown as ClientCommand
444
+ const handled = await handleChatCommand(deps, cmd, "r16")
445
+ expect(handled).toBe(true)
446
+ expect(respondTool as ReturnType<typeof mock>).toHaveBeenCalledWith(cmd)
447
+ expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r16" })
448
+ })
449
+
450
+ test("chat.stopDraining — stops draining, acks", async () => {
451
+ const stopDraining = mock(async () => {})
452
+ const deps = makeDeps({ agentOverrides: { stopDraining } })
453
+ const handled = await handleChatCommand(
454
+ deps,
455
+ { type: "chat.stopDraining", chatId: "chat-1" },
456
+ "r17",
457
+ )
458
+ expect(handled).toBe(true)
459
+ expect(stopDraining as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1")
460
+ })
461
+ })