@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,523 @@
1
+ /**
2
+ * Tests for the extracted runClaudeSession event-loop function.
3
+ *
4
+ * Each test builds a fake `RunClaudeSessionDeps` object, a fake `ClaudeSessionState`,
5
+ * and an async-iterable stream of `HarnessEvent`s, then asserts the side effects
6
+ * produced by the function.
7
+ */
8
+
9
+ // NOTE: do NOT mock.module("../shared/log") here — Bun's mock.module mutates
10
+ // the global registry for the whole test run, turning shared/log into noops
11
+ // for every later test file (analytics.test.ts asserts real log output).
12
+
13
+ import { describe, test, expect } from "bun:test"
14
+ import { runClaudeSession } from "./claude-session-runner"
15
+ import type { RunClaudeSessionDeps } from "./claude-session-runner"
16
+ import type { ClaudeSessionState, ActiveTurn } from "./claude-session-state"
17
+ import type { HarnessEvent } from "./harness-types"
18
+ import type { TranscriptEntry } from "../shared/types"
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /** Build a minimal ClaudeSessionState. Override individual fields as needed. */
25
+ function makeSession(overrides: Partial<ClaudeSessionState> = {}): ClaudeSessionState {
26
+ const fakeHandle = {
27
+ provider: "claude" as const,
28
+ stream: (async function* () {})() as AsyncIterable<HarnessEvent>,
29
+ interrupt: async () => {},
30
+ close: () => {},
31
+ sendPrompt: async () => {},
32
+ setModel: async () => {},
33
+ setPermissionMode: async () => {},
34
+ getSupportedCommands: async () => [],
35
+ }
36
+ return {
37
+ id: "sess-1",
38
+ chatId: "chat-1",
39
+ session: fakeHandle,
40
+ localPath: "/tmp/test",
41
+ additionalDirectories: [],
42
+ model: "claude-opus-4",
43
+ planMode: false,
44
+ sessionToken: null,
45
+ accountInfoLoaded: false,
46
+ nextPromptSeq: 1,
47
+ pendingPromptSeqs: [],
48
+ activeTokenId: null,
49
+ oauthKeyMasked: null,
50
+ oauthLabel: null,
51
+ openrouterKeyMasked: null,
52
+ openrouterModel: null,
53
+ lastUsedAt: 0,
54
+ backgroundTaskIds: new Set(),
55
+ backgroundTaskDeadlineAt: 0,
56
+ loopArmedAtSpawn: false,
57
+ cancelledResultPending: 0,
58
+ suppressSessionTokenPersist: false,
59
+ ...overrides,
60
+ }
61
+ }
62
+
63
+ /** Build a minimal ActiveTurn backed by a fake HarnessTurn. */
64
+ function makeActiveTurn(chatId: string, overrides: Partial<ActiveTurn> = {}): ActiveTurn {
65
+ const fakeTurn = {
66
+ provider: "claude" as const,
67
+ stream: (async function* () {})() as AsyncIterable<HarnessEvent>,
68
+ interrupt: async () => {},
69
+ close: () => {},
70
+ }
71
+ return {
72
+ chatId,
73
+ provider: "claude",
74
+ turn: fakeTurn,
75
+ model: "claude-opus-4",
76
+ planMode: false,
77
+ status: "starting",
78
+ pendingTool: null,
79
+ postToolFollowUp: null,
80
+ hasFinalResult: false,
81
+ cancelRequested: false,
82
+ cancelRecorded: false,
83
+ waitStartedAt: null,
84
+ userMessageId: null,
85
+ claudePromptSeq: 1,
86
+ ...overrides,
87
+ }
88
+ }
89
+
90
+ /** Create a fake stream from a list of HarnessEvents. */
91
+ async function* fakeStream(events: HarnessEvent[]): AsyncIterable<HarnessEvent> {
92
+ for (const e of events) yield e
93
+ }
94
+
95
+ /** Build a fake result TranscriptEntry. */
96
+ function fakeResultEntry(isError: boolean, result = "ok"): TranscriptEntry {
97
+ return {
98
+ _id: "entry-1",
99
+ createdAt: Date.now(),
100
+ kind: "result",
101
+ subtype: isError ? "error" : "success",
102
+ isError,
103
+ durationMs: 0,
104
+ result,
105
+ } as unknown as TranscriptEntry
106
+ }
107
+
108
+ /** Build a fake system_init TranscriptEntry. */
109
+ function fakeSystemInitEntry(): TranscriptEntry {
110
+ return {
111
+ _id: "entry-sys",
112
+ createdAt: Date.now(),
113
+ kind: "system_init",
114
+ provider: "claude",
115
+ model: "claude-opus-4",
116
+ tools: [],
117
+ agents: [],
118
+ slashCommands: [],
119
+ mcpServers: [],
120
+ } as unknown as TranscriptEntry
121
+ }
122
+
123
+ /** Build a minimal RunClaudeSessionDeps with all fields as no-ops. */
124
+ function makeDeps(session: ClaudeSessionState, overrides: Partial<RunClaudeSessionDeps> = {}): RunClaudeSessionDeps {
125
+ const sessions = new Map<string, ClaudeSessionState>()
126
+ sessions.set(session.chatId, session)
127
+
128
+ return {
129
+ openrouterFirstEntryTimeoutMs: 30000,
130
+ claudeSessions: sessions,
131
+ activeTurns: new Map(),
132
+ oauthPool: null,
133
+ claudeLimitDetector: {
134
+ detect: () => null,
135
+ detectFromResultText: () => null,
136
+ },
137
+ claudeAuthErrorDetector: {
138
+ detect: () => null,
139
+ detectFromResultText: () => null,
140
+ },
141
+ throwOnClaudeSessionStart: false,
142
+ store: {
143
+ appendMessage: async () => {},
144
+ recordTurnFailed: async () => {},
145
+ setSessionTokenForProvider: async () => {},
146
+ setPendingForkSessionToken: async () => {},
147
+ recordTurnFinished: async () => {},
148
+ setCompactFailureCount: async () => {},
149
+ recordTurnCancelled: async () => {},
150
+ getChat: () => null,
151
+ recordSessionCommandsLoaded: async () => {},
152
+ },
153
+ emitStateChange: () => {},
154
+ handleLimitDetection: async () => false,
155
+ maybeRegisterSdkWorkflowsDir: () => {},
156
+ getSubagents: () => [],
157
+ resolveBackgroundTaskMaxMs: () => 3_600_000,
158
+ mergeLocalCatalog: (cmds) => cmds,
159
+ handleLimitError: async () => false,
160
+ handleAuthFailure: async () => false,
161
+ closeClaudeSession: () => {},
162
+ maybeStartNextQueuedMessage: async () => {},
163
+ resolveClaudeDriverPreference: () => "sdk",
164
+ ...overrides,
165
+ }
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Tests
170
+ // ---------------------------------------------------------------------------
171
+
172
+ describe("runClaudeSession", () => {
173
+ test("empty stream → session closed and emitStateChange called", async () => {
174
+ const session = makeSession()
175
+ const closeCalls: string[] = []
176
+ const stateChangeChatIds: (string | undefined)[] = []
177
+
178
+ session.session.close = () => closeCalls.push("closed")
179
+ const deps = makeDeps(session, {
180
+ emitStateChange: (chatId) => stateChangeChatIds.push(chatId),
181
+ })
182
+ // The session stream is empty (returns immediately)
183
+ session.session.stream = fakeStream([])
184
+
185
+ await runClaudeSession(deps, session)
186
+
187
+ expect(closeCalls).toHaveLength(1)
188
+ expect(stateChangeChatIds).toContain(session.chatId)
189
+ })
190
+
191
+ test("session_token event → setSessionTokenForProvider called when session is current", async () => {
192
+ const session = makeSession({ pendingPromptSeqs: [] })
193
+ const tokenCalls: { chatId: string; provider: string; token: string | null }[] = []
194
+
195
+ const deps = makeDeps(session, {
196
+ store: {
197
+ ...makeDeps(session).store,
198
+ setSessionTokenForProvider: async (chatId, provider, token) => {
199
+ tokenCalls.push({ chatId, provider: String(provider), token })
200
+ },
201
+ },
202
+ })
203
+ session.session.stream = fakeStream([
204
+ { type: "session_token", sessionToken: "tok-abc" },
205
+ ])
206
+
207
+ await runClaudeSession(deps, session)
208
+
209
+ expect(tokenCalls).toHaveLength(1)
210
+ expect(tokenCalls[0]).toMatchObject({ chatId: "chat-1", provider: "claude", token: "tok-abc" })
211
+ expect(session.sessionToken).toBe("tok-abc")
212
+ })
213
+
214
+ test("session_token NOT persisted when session is no longer current", async () => {
215
+ const session = makeSession()
216
+ const tokenCalls: string[] = []
217
+
218
+ // Remove the session from the map so it's "not current"
219
+ const sessions = new Map<string, ClaudeSessionState>()
220
+ // Don't add session — so get(session.chatId) returns undefined, !== session
221
+
222
+ const deps = makeDeps(session, {
223
+ claudeSessions: sessions,
224
+ store: {
225
+ ...makeDeps(session).store,
226
+ setSessionTokenForProvider: async () => { tokenCalls.push("called") },
227
+ },
228
+ })
229
+ session.session.stream = fakeStream([
230
+ { type: "session_token", sessionToken: "tok-xyz" },
231
+ ])
232
+
233
+ await runClaudeSession(deps, session)
234
+
235
+ expect(tokenCalls).toHaveLength(0)
236
+ })
237
+
238
+ test("session_token NOT persisted when cancelledResultPending > 0", async () => {
239
+ const session = makeSession({ cancelledResultPending: 1 })
240
+ const tokenCalls: string[] = []
241
+
242
+ const deps = makeDeps(session, {
243
+ store: {
244
+ ...makeDeps(session).store,
245
+ setSessionTokenForProvider: async () => { tokenCalls.push("called") },
246
+ },
247
+ })
248
+ session.session.stream = fakeStream([
249
+ { type: "session_token", sessionToken: "tok-skip" },
250
+ ])
251
+
252
+ await runClaudeSession(deps, session)
253
+
254
+ expect(tokenCalls).toHaveLength(0)
255
+ })
256
+
257
+ test("session_token NOT persisted when suppressSessionTokenPersist is true", async () => {
258
+ const session = makeSession({ suppressSessionTokenPersist: true })
259
+ const tokenCalls: string[] = []
260
+
261
+ const deps = makeDeps(session, {
262
+ store: {
263
+ ...makeDeps(session).store,
264
+ setSessionTokenForProvider: async () => { tokenCalls.push("called") },
265
+ },
266
+ })
267
+ session.session.stream = fakeStream([
268
+ { type: "session_token", sessionToken: "tok-suppressed" },
269
+ ])
270
+
271
+ await runClaudeSession(deps, session)
272
+
273
+ expect(tokenCalls).toHaveLength(0)
274
+ })
275
+
276
+ test("rate_limit event → handleLimitDetection called", async () => {
277
+ const session = makeSession()
278
+ const detectionCalls: { chatId: string; resetAt: number }[] = []
279
+
280
+ const deps = makeDeps(session, {
281
+ handleLimitDetection: async (chatId, detection) => {
282
+ detectionCalls.push({ chatId, resetAt: detection.resetAt })
283
+ return false
284
+ },
285
+ })
286
+ const resetAt = Date.now() + 60000
287
+ session.session.stream = fakeStream([
288
+ { type: "rate_limit", rateLimit: { resetAt, tz: "UTC" } },
289
+ ])
290
+
291
+ await runClaudeSession(deps, session)
292
+
293
+ expect(detectionCalls).toHaveLength(1)
294
+ expect(detectionCalls[0]).toMatchObject({ chatId: "chat-1", resetAt })
295
+ })
296
+
297
+ test("cancelled result entry is suppressed and counter decremented", async () => {
298
+ const session = makeSession({ cancelledResultPending: 1 })
299
+ const appendCalls: string[] = []
300
+
301
+ const deps = makeDeps(session, {
302
+ store: {
303
+ ...makeDeps(session).store,
304
+ appendMessage: async (_chatId, _entry) => { appendCalls.push("appended") },
305
+ },
306
+ })
307
+ session.session.stream = fakeStream([
308
+ { type: "transcript", entry: fakeResultEntry(true, "") },
309
+ ])
310
+
311
+ await runClaudeSession(deps, session)
312
+
313
+ // The cancelled result entry should be swallowed — appendMessage NOT called
314
+ expect(appendCalls).toHaveLength(0)
315
+ // Counter decremented
316
+ expect(session.cancelledResultPending).toBe(0)
317
+ })
318
+
319
+ test("system_init entry sets active turn status to running", async () => {
320
+ const session = makeSession()
321
+ const active = makeActiveTurn(session.chatId)
322
+ const activeTurns = new Map([[session.chatId, active]])
323
+
324
+ const deps = makeDeps(session, {
325
+ activeTurns,
326
+ store: {
327
+ ...makeDeps(session).store,
328
+ getChat: () => ({ compactFailureCount: 0, pendingForkSessionToken: null }),
329
+ },
330
+ })
331
+ session.session.stream = fakeStream([
332
+ { type: "transcript", entry: fakeSystemInitEntry() },
333
+ ])
334
+
335
+ await runClaudeSession(deps, session)
336
+
337
+ expect(active.status).toBe("running")
338
+ })
339
+
340
+ test("successful result entry calls recordTurnFinished and clears activeTurns", async () => {
341
+ const session = makeSession()
342
+ session.pendingPromptSeqs = [1] // so completedClaudePromptSeq shifts to 1
343
+
344
+ const active = makeActiveTurn(session.chatId, { claudePromptSeq: 1 })
345
+ const activeTurns = new Map([[session.chatId, active]])
346
+
347
+ const finishedCalls: string[] = []
348
+ const releaseCalls: string[] = []
349
+
350
+ const deps = makeDeps(session, {
351
+ activeTurns,
352
+ oauthPool: { release: (chatId) => releaseCalls.push(chatId) },
353
+ store: {
354
+ ...makeDeps(session).store,
355
+ recordTurnFinished: async (chatId) => { finishedCalls.push(chatId) },
356
+ },
357
+ })
358
+ session.session.stream = fakeStream([
359
+ { type: "transcript", entry: fakeResultEntry(false, "success") },
360
+ ])
361
+
362
+ await runClaudeSession(deps, session)
363
+
364
+ expect(finishedCalls).toHaveLength(1)
365
+ expect(finishedCalls[0]).toBe("chat-1")
366
+ // activeTurns should be cleared after result
367
+ expect(activeTurns.size).toBe(0)
368
+ // oauthPool.release called once (from result handling) + once (finally if still current)
369
+ // Actually only result path, finally only if isCurrentSession & active?.provider=="claude" & not already deleted
370
+ expect(releaseCalls.length).toBeGreaterThan(0)
371
+ })
372
+
373
+ test("error result entry (unhandled) calls recordTurnFailed with result text", async () => {
374
+ const session = makeSession()
375
+ session.pendingPromptSeqs = [1]
376
+
377
+ const active = makeActiveTurn(session.chatId, { claudePromptSeq: 1 })
378
+ const activeTurns = new Map([[session.chatId, active]])
379
+ const failedCalls: { chatId: string; reason: string }[] = []
380
+
381
+ const deps = makeDeps(session, {
382
+ activeTurns,
383
+ store: {
384
+ ...makeDeps(session).store,
385
+ recordTurnFailed: async (chatId, reason) => { failedCalls.push({ chatId, reason }) },
386
+ getChat: () => ({ compactFailureCount: 0, pendingForkSessionToken: null }),
387
+ },
388
+ })
389
+ session.session.stream = fakeStream([
390
+ { type: "transcript", entry: fakeResultEntry(true, "Something went wrong") },
391
+ ])
392
+
393
+ await runClaudeSession(deps, session)
394
+
395
+ expect(failedCalls.length).toBeGreaterThan(0)
396
+ expect(failedCalls[0].chatId).toBe("chat-1")
397
+ expect(failedCalls[0].reason).toBe("Something went wrong")
398
+ })
399
+
400
+ test("compact_boundary with proactiveCompactInjection (PTY driver) finalizes the turn", async () => {
401
+ const session = makeSession()
402
+ session.pendingPromptSeqs = [1]
403
+
404
+ const active = makeActiveTurn(session.chatId, {
405
+ claudePromptSeq: 1,
406
+ proactiveCompactInjection: true,
407
+ })
408
+ const activeTurns = new Map([[session.chatId, active]])
409
+ const finishedCalls: string[] = []
410
+ const releaseCalls: string[] = []
411
+
412
+ const deps = makeDeps(session, {
413
+ activeTurns,
414
+ oauthPool: { release: (chatId) => releaseCalls.push(chatId) },
415
+ store: {
416
+ ...makeDeps(session).store,
417
+ recordTurnFinished: async (chatId) => { finishedCalls.push(chatId) },
418
+ setCompactFailureCount: async () => {},
419
+ },
420
+ resolveClaudeDriverPreference: () => "pty",
421
+ })
422
+ const compactBoundaryEntry = {
423
+ _id: "compact-1",
424
+ createdAt: Date.now(),
425
+ kind: "compact_boundary",
426
+ } as unknown as TranscriptEntry
427
+ session.session.stream = fakeStream([
428
+ { type: "transcript", entry: compactBoundaryEntry },
429
+ ])
430
+
431
+ await runClaudeSession(deps, session)
432
+
433
+ expect(finishedCalls).toHaveLength(1)
434
+ expect(active.hasFinalResult).toBe(true)
435
+ // activeTurns cleared in compact_boundary path
436
+ expect(activeTurns.size).toBe(0)
437
+ })
438
+
439
+ test("tool_result with background task ID updates backgroundTaskIds and deadline", async () => {
440
+ const session = makeSession()
441
+ const taskId = "bgtask42"
442
+ const toolResultContent = `\nCommand running in background with ID: ${taskId}\nSome other output`
443
+
444
+ const bgToolResultEntry = {
445
+ _id: "tool-res-1",
446
+ createdAt: Date.now(),
447
+ kind: "tool_result",
448
+ content: toolResultContent,
449
+ } as unknown as TranscriptEntry
450
+
451
+ let resolveBackgroundCalled = 0
452
+ const deps = makeDeps(session, {
453
+ resolveBackgroundTaskMaxMs: () => {
454
+ resolveBackgroundCalled++
455
+ return 1_800_000
456
+ },
457
+ })
458
+ session.session.stream = fakeStream([
459
+ { type: "transcript", entry: bgToolResultEntry },
460
+ ])
461
+
462
+ await runClaudeSession(deps, session)
463
+
464
+ expect(session.backgroundTaskIds.has(taskId)).toBe(true)
465
+ expect(session.backgroundTaskDeadlineAt).toBeGreaterThan(0)
466
+ expect(resolveBackgroundCalled).toBeGreaterThan(0)
467
+ })
468
+
469
+ test("thrown exception with no limit/auth detection → error result appended and turn failed", async () => {
470
+ const session = makeSession()
471
+ const active = makeActiveTurn(session.chatId)
472
+ const activeTurns = new Map([[session.chatId, active]])
473
+ const appendedEntries: TranscriptEntry[] = []
474
+ const failedReasons: string[] = []
475
+
476
+ const error = new Error("network dropped")
477
+ const throwingStream: AsyncIterable<HarnessEvent> = {
478
+ [Symbol.asyncIterator]: () => ({
479
+ next: () => Promise.reject(error) as Promise<IteratorResult<HarnessEvent>>,
480
+ return: () => Promise.resolve({ done: true as const, value: undefined as unknown as HarnessEvent }),
481
+ }),
482
+ }
483
+
484
+ const deps = makeDeps(session, {
485
+ activeTurns,
486
+ store: {
487
+ ...makeDeps(session).store,
488
+ appendMessage: async (_chatId, entry) => { appendedEntries.push(entry) },
489
+ recordTurnFailed: async (_chatId, reason) => { failedReasons.push(reason) },
490
+ },
491
+ })
492
+ session.session.stream = throwingStream
493
+
494
+ await runClaudeSession(deps, session)
495
+
496
+ expect(appendedEntries.length).toBeGreaterThan(0)
497
+ const errEntry = appendedEntries.find((e) => (e as { kind: string }).kind === "result")
498
+ expect(errEntry).toBeDefined()
499
+ expect(failedReasons).toContain("network dropped")
500
+ })
501
+
502
+ test("stream ends without hasFinalResult → fail-close records turn failure", async () => {
503
+ const session = makeSession()
504
+ const active = makeActiveTurn(session.chatId, { hasFinalResult: false })
505
+ const activeTurns = new Map([[session.chatId, active]])
506
+ const failedReasons: string[] = []
507
+
508
+ const deps = makeDeps(session, {
509
+ activeTurns,
510
+ store: {
511
+ ...makeDeps(session).store,
512
+ recordTurnFailed: async (_chatId, reason) => { failedReasons.push(reason) },
513
+ },
514
+ })
515
+ // Stream produces NO result entry (empty stream), so hasFinalResult stays false
516
+ session.session.stream = fakeStream([])
517
+
518
+ await runClaudeSession(deps, session)
519
+
520
+ // The finally block should fail-close since hasFinalResult is false and provider is "claude"
521
+ expect(failedReasons).toContain("session stream ended without a result")
522
+ })
523
+ })