@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,414 @@
1
+ /**
2
+ * Tests for the extracted send/queue handler cluster.
3
+ *
4
+ * Each test builds a minimal `SendCommandDeps` fake and asserts correct
5
+ * behaviour without any real IO or OS calls.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach } from "bun:test"
9
+ import {
10
+ resolveProvider,
11
+ getProviderSettings,
12
+ shouldInjectProactiveCompact,
13
+ enqueueMessage,
14
+ dequeueAndStartQueuedMessage,
15
+ maybeStartNextQueuedMessage,
16
+ sendCommand,
17
+ type SendCommandDeps,
18
+ } from "./claude-send-command"
19
+ import type { QueuedChatMessage, ChatAttachment, CustomModelEntry } from "../shared/types"
20
+ import type { StartTurnForChatArgs } from "./claude-turn-starter"
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Minimal fixture types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function makeQueuedMessage(overrides: Partial<QueuedChatMessage> = {}): QueuedChatMessage {
27
+ return {
28
+ id: "qm-1",
29
+ content: "hello",
30
+ attachments: [],
31
+ createdAt: Date.now(),
32
+ provider: undefined,
33
+ model: undefined,
34
+ ...overrides,
35
+ }
36
+ }
37
+
38
+ const NO_ATTACHMENTS: ChatAttachment[] = []
39
+
40
+ type DepsOptions = {
41
+ activeChatIds?: string[]
42
+ queuedMessages?: QueuedChatMessage[]
43
+ chatProvider?: "claude" | "openrouter" | "codex" | null
44
+ chatCompactFailures?: number
45
+ messages?: { chatId: string; count: number }[]
46
+ stopLoopCalled?: string[]
47
+ emitStateCalled?: string[]
48
+ startTurnCalled?: StartTurnForChatArgs[]
49
+ enqueuedMessages?: Array<{ chatId: string; content: string }>
50
+ removedMessages?: Array<{ chatId: string; id: string }>
51
+ createdChats?: string[]
52
+ analyticsEvents?: string[]
53
+ session?: { backgroundTaskIds: Set<string>; backgroundTaskDeadlineAt: number } | null
54
+ customModels?: readonly CustomModelEntry[]
55
+ }
56
+
57
+ function makeDeps(opts: DepsOptions = {}): SendCommandDeps & { startTurnCalled: StartTurnForChatArgs[] } {
58
+ const activeChatIds = new Set(opts.activeChatIds ?? [])
59
+ const queuedMessages: QueuedChatMessage[] = opts.queuedMessages ?? []
60
+ const stopLoopCalled = opts.stopLoopCalled ?? []
61
+ const emitStateCalled = opts.emitStateCalled ?? []
62
+ const startTurnCalled: StartTurnForChatArgs[] = opts.startTurnCalled ?? []
63
+ const enqueuedMessages = opts.enqueuedMessages ?? []
64
+ const removedMessages = opts.removedMessages ?? []
65
+ const createdChats = opts.createdChats ?? []
66
+ const analyticsEvents = opts.analyticsEvents ?? []
67
+ const customModels = opts.customModels ?? []
68
+
69
+ return {
70
+ startTurnCalled,
71
+ store: {
72
+ createChat: async (projectId: string) => {
73
+ const id = `chat-new-${projectId}`
74
+ createdChats.push(id)
75
+ return { id }
76
+ },
77
+ requireChat: (_chatId: string) => ({
78
+ provider: opts.chatProvider ?? null,
79
+ }),
80
+ getChat: (_chatId: string) => ({
81
+ compactFailureCount: opts.chatCompactFailures ?? 0,
82
+ }),
83
+ enqueueMessage: async (chatId: string, msg: Omit<QueuedChatMessage, "id" | "createdAt">) => {
84
+ const queued: QueuedChatMessage = {
85
+ id: `qm-enqueued-${enqueuedMessages.length}`,
86
+ createdAt: Date.now(),
87
+ content: msg.content,
88
+ attachments: msg.attachments,
89
+ provider: msg.provider,
90
+ model: msg.model,
91
+ modelOptions: msg.modelOptions,
92
+ planMode: msg.planMode,
93
+ autoContinue: msg.autoContinue,
94
+ }
95
+ enqueuedMessages.push({ chatId, content: msg.content })
96
+ return queued
97
+ },
98
+ removeQueuedMessage: async (chatId: string, id: string) => {
99
+ removedMessages.push({ chatId, id })
100
+ },
101
+ getQueuedMessages: (_chatId: string) => queuedMessages,
102
+ getMessages: (_chatId: string) => [],
103
+ },
104
+ activeTurns: {
105
+ has: (chatId: string) => activeChatIds.has(chatId),
106
+ get: (_chatId: string) => undefined,
107
+ },
108
+ claudeSessions: {
109
+ get: (_chatId: string) => opts.session ?? undefined,
110
+ },
111
+ autoResumeByChat: {
112
+ set: (_chatId: string, _value: boolean) => {},
113
+ },
114
+ analytics: {
115
+ track: (event: string) => { analyticsEvents.push(event) },
116
+ },
117
+ getAppSettingsSnapshot: () => ({ customModels }),
118
+ stopLoop: async (chatId: string, reason: string) => {
119
+ stopLoopCalled.push(`${chatId}:${reason}`)
120
+ },
121
+ emitStateChange: (chatId: string) => { emitStateCalled.push(chatId) },
122
+ startTurnForChat: async (args: StartTurnForChatArgs) => { startTurnCalled.push(args) },
123
+ }
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // resolveProvider — pure
128
+ // ---------------------------------------------------------------------------
129
+
130
+ describe("resolveProvider", () => {
131
+ test("returns command provider when set", () => {
132
+ expect(resolveProvider({ provider: "codex" }, null)).toBe("codex")
133
+ })
134
+
135
+ test("falls back to chat provider when command has none", () => {
136
+ expect(resolveProvider({}, "openrouter")).toBe("openrouter")
137
+ })
138
+
139
+ test("falls back to claude when both are null", () => {
140
+ expect(resolveProvider({}, null)).toBe("claude")
141
+ })
142
+
143
+ test("command provider overrides chat provider", () => {
144
+ expect(resolveProvider({ provider: "codex" }, "claude")).toBe("codex")
145
+ })
146
+ })
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // getProviderSettings — pure
150
+ // ---------------------------------------------------------------------------
151
+
152
+ describe("getProviderSettings", () => {
153
+ test("claude provider returns model string", () => {
154
+ const settings = getProviderSettings("claude", { model: "claude-opus-4-5" }, [])
155
+ expect(typeof settings.model).toBe("string")
156
+ expect(settings.model.length).toBeGreaterThan(0)
157
+ })
158
+
159
+ test("claude provider sets planMode false when not set", () => {
160
+ const settings = getProviderSettings("claude", {}, [])
161
+ expect(settings.planMode).toBe(false)
162
+ })
163
+
164
+ test("openrouter provider uses model from options directly", () => {
165
+ const settings = getProviderSettings("openrouter", { model: "anthropic/claude-3.5-sonnet" }, [])
166
+ expect(settings.model).toBe("anthropic/claude-3.5-sonnet")
167
+ })
168
+
169
+ test("openrouter provider falls back to default model when blank", () => {
170
+ const settings = getProviderSettings("openrouter", { model: " " }, [])
171
+ expect(typeof settings.model).toBe("string")
172
+ expect(settings.model.length).toBeGreaterThan(0)
173
+ })
174
+
175
+ test("codex provider returns a model string", () => {
176
+ const settings = getProviderSettings("codex", {}, [])
177
+ expect(typeof settings.model).toBe("string")
178
+ })
179
+ })
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // shouldInjectProactiveCompact
183
+ // ---------------------------------------------------------------------------
184
+
185
+ describe("shouldInjectProactiveCompact", () => {
186
+ test("returns false for slash commands", () => {
187
+ const deps = makeDeps()
188
+ expect(shouldInjectProactiveCompact(deps, "chat-1", "/compact")).toBe(false)
189
+ expect(shouldInjectProactiveCompact(deps, "chat-1", " /clear")).toBe(false)
190
+ })
191
+
192
+ test("returns false when getMessages returns empty (no usage data)", () => {
193
+ // With no context window usage data, shouldProactivelyCompact returns false
194
+ const deps = makeDeps({ chatCompactFailures: 0 })
195
+ expect(shouldInjectProactiveCompact(deps, "chat-1", "hello")).toBe(false)
196
+ })
197
+
198
+ test("returns false when failure count is at or above threshold", () => {
199
+ const deps = makeDeps({ chatCompactFailures: 5 }) // MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 5
200
+ expect(shouldInjectProactiveCompact(deps, "chat-1", "hello")).toBe(false)
201
+ })
202
+ })
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // enqueueMessage
206
+ // ---------------------------------------------------------------------------
207
+
208
+ describe("enqueueMessage", () => {
209
+ test("calls store.enqueueMessage with the right content", async () => {
210
+ const enqueuedMessages: Array<{ chatId: string; content: string }> = []
211
+ const deps = makeDeps({ enqueuedMessages })
212
+ await enqueueMessage(deps, "chat-1", "test content", NO_ATTACHMENTS)
213
+ expect(enqueuedMessages).toEqual([{ chatId: "chat-1", content: "test content" }])
214
+ })
215
+
216
+ test("emits state change after enqueue", async () => {
217
+ const emitStateCalled: string[] = []
218
+ const deps = makeDeps({ emitStateCalled })
219
+ await enqueueMessage(deps, "chat-1", "msg", NO_ATTACHMENTS)
220
+ expect(emitStateCalled).toContain("chat-1")
221
+ })
222
+
223
+ test("returns the queued message with an id", async () => {
224
+ const deps = makeDeps()
225
+ const result = await enqueueMessage(deps, "chat-1", "content", NO_ATTACHMENTS)
226
+ expect(result.id).toMatch(/^qm-enqueued-/)
227
+ expect(result.content).toBe("content")
228
+ })
229
+
230
+ test("passes provider and model from options", async () => {
231
+ const enqueuedMessages: Array<{ chatId: string; content: string }> = []
232
+ const deps = makeDeps({ enqueuedMessages })
233
+ await enqueueMessage(deps, "chat-1", "msg", NO_ATTACHMENTS, {
234
+ provider: "claude",
235
+ model: "claude-opus-4-5",
236
+ })
237
+ expect(enqueuedMessages[0]).toEqual({ chatId: "chat-1", content: "msg" })
238
+ })
239
+ })
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // dequeueAndStartQueuedMessage
243
+ // ---------------------------------------------------------------------------
244
+
245
+ describe("dequeueAndStartQueuedMessage", () => {
246
+ test("removes message from store before starting turn", async () => {
247
+ const removedMessages: Array<{ chatId: string; id: string }> = []
248
+ const deps = makeDeps({ removedMessages })
249
+ const msg = makeQueuedMessage({ id: "qm-abc" })
250
+ await dequeueAndStartQueuedMessage(deps, "chat-1", msg)
251
+ expect(removedMessages).toEqual([{ chatId: "chat-1", id: "qm-abc" }])
252
+ })
253
+
254
+ test("calls startTurnForChat with resolved provider", async () => {
255
+ const deps = makeDeps({ chatProvider: "claude" })
256
+ const msg = makeQueuedMessage({ content: "user message" })
257
+ await dequeueAndStartQueuedMessage(deps, "chat-1", msg)
258
+ expect(deps.startTurnCalled.length).toBe(1)
259
+ expect(deps.startTurnCalled[0]?.provider).toBe("claude")
260
+ expect(deps.startTurnCalled[0]?.content).toBe("user message")
261
+ })
262
+
263
+ test("wraps content in steered format when steered=true", async () => {
264
+ const deps = makeDeps({ chatProvider: "claude" })
265
+ const msg = makeQueuedMessage({ content: "original" })
266
+ await dequeueAndStartQueuedMessage(deps, "chat-1", msg, { steered: true })
267
+ const startedContent = deps.startTurnCalled[0]?.content ?? ""
268
+ // Steered messages are wrapped (not the original string)
269
+ expect(startedContent).not.toBe("original")
270
+ expect(startedContent.length).toBeGreaterThan(0)
271
+ })
272
+
273
+ test("suppresses user_prompt entry for rate-limit fallback", async () => {
274
+ const deps = makeDeps({ chatProvider: "claude" })
275
+ const msg = makeQueuedMessage({
276
+ content: "continue",
277
+ autoContinue: { scheduleId: "sched-1" },
278
+ })
279
+ await dequeueAndStartQueuedMessage(deps, "chat-1", msg)
280
+ expect(deps.startTurnCalled[0]?.appendUserPrompt).toBe(false)
281
+ })
282
+
283
+ test("appends user_prompt for non-rate-limit message", async () => {
284
+ const deps = makeDeps({ chatProvider: "claude" })
285
+ const msg = makeQueuedMessage({ content: "hello" })
286
+ await dequeueAndStartQueuedMessage(deps, "chat-1", msg)
287
+ expect(deps.startTurnCalled[0]?.appendUserPrompt).toBe(true)
288
+ })
289
+ })
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // maybeStartNextQueuedMessage
293
+ // ---------------------------------------------------------------------------
294
+
295
+ describe("maybeStartNextQueuedMessage", () => {
296
+ test("returns false and does nothing when a turn is active", async () => {
297
+ const deps = makeDeps({ activeChatIds: ["chat-1"], queuedMessages: [makeQueuedMessage()] })
298
+ const result = await maybeStartNextQueuedMessage(deps, "chat-1")
299
+ expect(result).toBe(false)
300
+ expect(deps.startTurnCalled.length).toBe(0)
301
+ })
302
+
303
+ test("returns false when no queued messages", async () => {
304
+ const deps = makeDeps({ queuedMessages: [] })
305
+ const result = await maybeStartNextQueuedMessage(deps, "chat-1")
306
+ expect(result).toBe(false)
307
+ })
308
+
309
+ test("dequeues and starts when idle and messages exist", async () => {
310
+ const queuedMessages = [makeQueuedMessage({ content: "queued msg" })]
311
+ const deps = makeDeps({ queuedMessages })
312
+ const result = await maybeStartNextQueuedMessage(deps, "chat-1")
313
+ expect(result).toBe(true)
314
+ expect(deps.startTurnCalled.length).toBe(1)
315
+ expect(deps.startTurnCalled[0]?.content).toBe("queued msg")
316
+ })
317
+ })
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // sendCommand — integration
321
+ // ---------------------------------------------------------------------------
322
+
323
+ describe("sendCommand", () => {
324
+ let stopLoopCalled: string[]
325
+ let analyticsEvents: string[]
326
+ let deps: SendCommandDeps & { startTurnCalled: StartTurnForChatArgs[] }
327
+
328
+ beforeEach(() => {
329
+ stopLoopCalled = []
330
+ analyticsEvents = []
331
+ deps = makeDeps({ stopLoopCalled, analyticsEvents, chatProvider: "claude" })
332
+ })
333
+
334
+ test("throws when no chatId and no projectId", async () => {
335
+ expect(sendCommand(deps, {
336
+ type: "chat.send",
337
+ content: "hello",
338
+ chatId: undefined,
339
+ projectId: undefined,
340
+ } as Parameters<typeof sendCommand>[1])).rejects.toThrow("Missing projectId")
341
+ })
342
+
343
+ test("creates a new chat when chatId is absent", async () => {
344
+ const createdChats: string[] = []
345
+ const d = makeDeps({ createdChats, analyticsEvents })
346
+ const result = await sendCommand(d, {
347
+ type: "chat.send",
348
+ content: "hi",
349
+ chatId: undefined,
350
+ projectId: "proj-1",
351
+ } as Parameters<typeof sendCommand>[1])
352
+ expect(createdChats.length).toBe(1)
353
+ expect(result.chatId).toMatch(/^chat-new-/)
354
+ })
355
+
356
+ test("calls stopLoop with user_send before processing", async () => {
357
+ await sendCommand(deps, {
358
+ type: "chat.send",
359
+ content: "hi",
360
+ chatId: "chat-1",
361
+ } as Parameters<typeof sendCommand>[1])
362
+ expect(stopLoopCalled).toContain("chat-1:user_send")
363
+ })
364
+
365
+ test("enqueues message and returns queued:true when turn is active", async () => {
366
+ const d = makeDeps({
367
+ activeChatIds: ["chat-1"],
368
+ chatProvider: "claude",
369
+ analyticsEvents,
370
+ })
371
+ const result = await sendCommand(d, {
372
+ type: "chat.send",
373
+ content: "queued message",
374
+ chatId: "chat-1",
375
+ } as Parameters<typeof sendCommand>[1])
376
+ expect(result.queued).toBe(true)
377
+ expect(result.queuedMessageId).toMatch(/^qm-enqueued-/)
378
+ })
379
+
380
+ test("starts a turn directly when idle", async () => {
381
+ const result = await sendCommand(deps, {
382
+ type: "chat.send",
383
+ content: "start now",
384
+ chatId: "chat-1",
385
+ } as Parameters<typeof sendCommand>[1])
386
+ expect(result.queued).toBeUndefined()
387
+ expect(deps.startTurnCalled.length).toBe(1)
388
+ expect(deps.startTurnCalled[0]?.content).toBe("start now")
389
+ })
390
+
391
+ test("clears background task state on existing session", async () => {
392
+ const session = {
393
+ backgroundTaskIds: new Set(["task-1"]),
394
+ backgroundTaskDeadlineAt: 9999,
395
+ }
396
+ const d = makeDeps({ session })
397
+ await sendCommand(d, {
398
+ type: "chat.send",
399
+ content: "hi",
400
+ chatId: "chat-1",
401
+ } as Parameters<typeof sendCommand>[1])
402
+ expect(session.backgroundTaskIds.size).toBe(0)
403
+ expect(session.backgroundTaskDeadlineAt).toBe(0)
404
+ })
405
+
406
+ test("tracks message_sent analytics", async () => {
407
+ await sendCommand(deps, {
408
+ type: "chat.send",
409
+ content: "track me",
410
+ chatId: "chat-1",
411
+ } as Parameters<typeof sendCommand>[1])
412
+ expect(analyticsEvents).toContain("message_sent")
413
+ })
414
+ })