@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,266 @@
1
+ /**
2
+ * Tests for the extracted turn spawning pipeline (claude-turn-starter.ts).
3
+ * Covers the key branches of startTurnForChat without touching agent.ts internals.
4
+ */
5
+ import { describe, test, expect, mock } from "bun:test"
6
+ import { startTurnForChat, type StartTurnDeps, type StartTurnForChatArgs } from "./claude-turn-starter"
7
+ import { OAuthPoolUnavailableError } from "./oauth-errors"
8
+ import type { ActiveTurn, ClaudeSessionState } from "./claude-session-state"
9
+ import type { HarnessTurn } from "./harness-types"
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function makeFakeTurn(): HarnessTurn {
16
+ return {
17
+ provider: "codex",
18
+ stream: { async *[Symbol.asyncIterator]() {} },
19
+ interrupt: async () => {},
20
+ close: () => {},
21
+ }
22
+ }
23
+
24
+ function makeFakeChatRecord(overrides: Record<string, unknown> = {}) {
25
+ return {
26
+ id: "chat-1",
27
+ projectId: "proj-1",
28
+ provider: "codex" as const,
29
+ title: "New Chat",
30
+ sessionTokensByProvider: {},
31
+ pendingForkSessionToken: null,
32
+ ...overrides,
33
+ }
34
+ }
35
+
36
+ function makeFakeProjectRecord() {
37
+ return {
38
+ id: "proj-1",
39
+ localPath: "/tmp/project",
40
+ title: "Test Project",
41
+ }
42
+ }
43
+
44
+ function makeDeps(overrides: Partial<StartTurnDeps> = {}): StartTurnDeps {
45
+ const activeTurns = new Map<string, ActiveTurn>()
46
+ const claudeSessions = new Map<string, ClaudeSessionState>()
47
+ const drainingStreams = new Map<string, { turn: HarnessTurn }>()
48
+ const mentionedSubagentIdsByChat = new Map<string, string[]>()
49
+
50
+ const chat = makeFakeChatRecord()
51
+ const project = makeFakeProjectRecord()
52
+
53
+ const fakeTurn = makeFakeTurn()
54
+
55
+ const deps: StartTurnDeps = {
56
+ activeTurns,
57
+ claudeSessions,
58
+ drainingStreams,
59
+ mentionedSubagentIdsByChat,
60
+
61
+ store: {
62
+ requireChat: mock(() => chat),
63
+ getMessages: mock(() => []),
64
+ getProject: mock(() => project),
65
+ appendMessage: mock(async () => {}),
66
+ setChatProvider: mock(async () => {}),
67
+ setPlanMode: mock(async () => {}),
68
+ renameChat: mock(async () => {}),
69
+ recordTurnStarted: mock(async () => {}),
70
+ recordTurnFailed: mock(async () => {}),
71
+ setPendingForkSessionToken: mock(async () => {}),
72
+ } as unknown as StartTurnDeps["store"],
73
+
74
+ codexManager: {
75
+ startSession: mock(async () => null),
76
+ startTurn: mock(async () => fakeTurn),
77
+ } as unknown as StartTurnDeps["codexManager"],
78
+
79
+ subagentOrchestrator: {
80
+ clearChatCancellation: mock(() => {}),
81
+ },
82
+
83
+ clearDrainingStream: mock(() => {}),
84
+ emitStateChange: mock(() => {}),
85
+ resolveClaudeDriverPreference: mock(() => "sdk" as const),
86
+ getSubagents: mock(() => []),
87
+ getAppSettingsSnapshot: mock(() => ({ globalPromptAppend: undefined })),
88
+ generateTitleInBackground: mock(async () => {}),
89
+ recreateActiveTurnFromSession: mock(() => undefined),
90
+ startClaudeTurn: mock(async () => fakeTurn),
91
+ findLastUserMessageId: mock(() => null),
92
+ runTurn: mock(() => {}),
93
+
94
+ ...overrides,
95
+ }
96
+
97
+ return deps
98
+ }
99
+
100
+ function makeArgs(overrides: Partial<StartTurnForChatArgs> = {}): StartTurnForChatArgs {
101
+ return {
102
+ chatId: "chat-1",
103
+ provider: "codex",
104
+ content: "hello world",
105
+ attachments: [],
106
+ model: "gpt-4o",
107
+ planMode: false,
108
+ appendUserPrompt: true,
109
+ ...overrides,
110
+ }
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Tests
115
+ // ---------------------------------------------------------------------------
116
+
117
+ describe("startTurnForChat", () => {
118
+ test("1. closes a draining stream before starting a new turn", async () => {
119
+ const deps = makeDeps()
120
+ const closeFn = mock(() => {})
121
+ deps.drainingStreams.set("chat-1", { turn: { ...makeFakeTurn(), close: closeFn } })
122
+
123
+ await startTurnForChat(deps, makeArgs())
124
+
125
+ expect(closeFn).toHaveBeenCalledTimes(1)
126
+ expect(deps.clearDrainingStream as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1")
127
+ })
128
+
129
+ test("2. throws when chat is already running (activeTurns has the chatId)", async () => {
130
+ const deps = makeDeps()
131
+ // Pre-populate activeTurns to simulate an in-flight turn
132
+ deps.activeTurns.set("chat-1", {} as ActiveTurn)
133
+
134
+ await expect(startTurnForChat(deps, makeArgs())).rejects.toThrow("Chat is already running")
135
+ })
136
+
137
+ test("3. clears cancellation on subagentOrchestrator at the start", async () => {
138
+ const deps = makeDeps()
139
+ await startTurnForChat(deps, makeArgs())
140
+ expect((deps.subagentOrchestrator.clearChatCancellation as ReturnType<typeof mock>)).toHaveBeenCalledWith("chat-1")
141
+ })
142
+
143
+ test("4. appends user_prompt entry when appendUserPrompt is true", async () => {
144
+ const deps = makeDeps()
145
+ await startTurnForChat(deps, makeArgs({ appendUserPrompt: true }))
146
+ expect(deps.store.appendMessage as ReturnType<typeof mock>).toHaveBeenCalled()
147
+ })
148
+
149
+ test("5. does NOT append user_prompt entry when appendUserPrompt is false", async () => {
150
+ const deps = makeDeps()
151
+ await startTurnForChat(deps, makeArgs({ appendUserPrompt: false }))
152
+ // appendMessage may be called for other things (account_info etc), but
153
+ // we can check that user_prompt was not the kind persisted.
154
+ const appendCalls = (deps.store.appendMessage as ReturnType<typeof mock>).mock.calls
155
+ const userPromptCalls = appendCalls.filter(
156
+ (call: unknown[]) => (call[1] as { kind?: string })?.kind === "user_prompt"
157
+ )
158
+ expect(userPromptCalls).toHaveLength(0)
159
+ })
160
+
161
+ test("6. calls recordTurnStarted with correct fields", async () => {
162
+ const deps = makeDeps()
163
+ await startTurnForChat(deps, makeArgs({ provider: "codex", model: "gpt-4o", planMode: true }))
164
+ expect(deps.store.recordTurnStarted as ReturnType<typeof mock>).toHaveBeenCalledWith(
165
+ "chat-1",
166
+ expect.objectContaining({ provider: "codex", model: "gpt-4o", planMode: true })
167
+ )
168
+ })
169
+
170
+ test("7. swallows OAuthPoolUnavailableError and persists a result error entry", async () => {
171
+ const oauthError = new OAuthPoolUnavailableError("pool is full")
172
+ const deps = makeDeps({
173
+ startClaudeTurn: mock(async () => { throw oauthError }),
174
+ // Make it a claude provider so startClaudeTurn gets called
175
+ })
176
+ // Use claude provider so isClaudeSdkProvider returns true → startClaudeTurn called
177
+ const args = makeArgs({ provider: "claude", model: "claude-opus-4-5" })
178
+
179
+ // Should NOT throw (swallowed)
180
+ await expect(startTurnForChat(deps, args)).resolves.toBeUndefined()
181
+
182
+ // Should persist the error as a result entry
183
+ const appendCalls = (deps.store.appendMessage as ReturnType<typeof mock>).mock.calls
184
+ const resultEntries = appendCalls.filter(
185
+ (call: unknown[]) => {
186
+ const entry = call[1] as { kind?: string; isError?: boolean }
187
+ return entry?.kind === "result" && entry?.isError === true
188
+ }
189
+ )
190
+ expect(resultEntries.length).toBeGreaterThan(0)
191
+ })
192
+
193
+ test("8. rethrows non-OAuth errors after cleanup (recordTurnFailed, emitStateChange)", async () => {
194
+ const boom = new Error("unexpected failure")
195
+ const deps = makeDeps({
196
+ startClaudeTurn: mock(async () => { throw boom }),
197
+ })
198
+ const args = makeArgs({ provider: "claude", model: "claude-opus-4-5" })
199
+
200
+ await expect(startTurnForChat(deps, args)).rejects.toThrow("unexpected failure")
201
+
202
+ // Cleanup should still run
203
+ expect(deps.store.recordTurnFailed as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1", "unexpected failure")
204
+ expect(deps.emitStateChange as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-1", { immediate: true })
205
+ })
206
+
207
+ test("9. routes to codexManager.startTurn for non-claude providers", async () => {
208
+ const deps = makeDeps()
209
+ await startTurnForChat(deps, makeArgs({ provider: "codex", model: "gpt-4o" }))
210
+ expect(deps.codexManager.startTurn as ReturnType<typeof mock>).toHaveBeenCalledTimes(1)
211
+ expect(deps.startClaudeTurn as ReturnType<typeof mock>).not.toHaveBeenCalled()
212
+ })
213
+
214
+ test("10. routes to startClaudeTurn for claude provider", async () => {
215
+ const deps = makeDeps()
216
+ const fakeTurn = makeFakeTurn()
217
+ // The real startClaudeTurn populates claudeSessions as a side effect;
218
+ // our mock must do the same so the SDK-session prompt-send path can proceed.
219
+ deps.startClaudeTurn = mock(async () => {
220
+ deps.claudeSessions.set("chat-1", {
221
+ id: "sess-1",
222
+ chatId: "chat-1",
223
+ session: { sendPrompt: mock(async () => {}), getAccountInfo: undefined },
224
+ localPath: "/tmp/project",
225
+ additionalDirectories: [],
226
+ model: "claude-opus-4-5",
227
+ effort: undefined,
228
+ planMode: false,
229
+ sessionToken: null,
230
+ accountInfoLoaded: false,
231
+ nextPromptSeq: 0,
232
+ pendingPromptSeqs: [],
233
+ activeTokenId: null,
234
+ oauthKeyMasked: null,
235
+ oauthLabel: null,
236
+ openrouterKeyMasked: null,
237
+ openrouterModel: null,
238
+ lastUsedAt: Date.now(),
239
+ backgroundTaskIds: new Set<string>(),
240
+ backgroundTaskDeadlineAt: 0,
241
+ loopArmedAtSpawn: false,
242
+ cancelledResultPending: 0,
243
+ suppressSessionTokenPersist: false,
244
+ } as unknown as ClaudeSessionState)
245
+ return fakeTurn
246
+ })
247
+
248
+ await startTurnForChat(deps, makeArgs({ provider: "claude", model: "claude-opus-4-5" }))
249
+ expect(deps.startClaudeTurn as ReturnType<typeof mock>).toHaveBeenCalledTimes(1)
250
+ expect(deps.codexManager.startTurn as ReturnType<typeof mock>).not.toHaveBeenCalled()
251
+ })
252
+
253
+ test("11. registers ActiveTurn in activeTurns map after turn starts", async () => {
254
+ const deps = makeDeps()
255
+ await startTurnForChat(deps, makeArgs({ provider: "codex" }))
256
+ expect(deps.activeTurns.has("chat-1")).toBe(true)
257
+ const active = deps.activeTurns.get("chat-1")
258
+ expect(active?.provider).toBe("codex")
259
+ })
260
+
261
+ test("12. calls runTurn for Codex (non-SDK-session) provider", async () => {
262
+ const deps = makeDeps()
263
+ await startTurnForChat(deps, makeArgs({ provider: "codex" }))
264
+ expect(deps.runTurn as ReturnType<typeof mock>).toHaveBeenCalledTimes(1)
265
+ })
266
+ })