@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,579 @@
1
+ /**
2
+ * Tests for the extracted loop-orchestration command handlers.
3
+ *
4
+ * Each test builds a minimal `LoopOrchCommandDeps` fake and asserts the
5
+ * correct behaviour of the function under test. No real IO or OS calls.
6
+ */
7
+
8
+ import { describe, test, expect } from "bun:test"
9
+ import type { AutoContinueEvent } from "./auto-continue/events"
10
+ import { AUTO_CONTINUE_EVENT_VERSION } from "./auto-continue/events"
11
+ import type { TranscriptEntry } from "../shared/types"
12
+ import type { OrchRunConfig, OrchTaskSpec, OrchRunSnapshot, OrchPhaseSpec } from "../shared/orchestration-types"
13
+ import type { ClaudeSessionState } from "./claude-session-state"
14
+ import type { EnsureTrackingFileArgs, EnsureTrackingFileResult } from "./loop-template-io.adapter"
15
+ import type { ProviderRunStart, BackgroundRunOutcome } from "./subagent-orchestrator"
16
+ import type { WorkerSpawnArgs } from "./orchestration-queue"
17
+ import type { Subagent } from "../shared/types"
18
+ import {
19
+ isLoopArmed,
20
+ listLiveSchedules,
21
+ getOrchRunDetail,
22
+ cancelOrchRun,
23
+ buildOrchRunContext,
24
+ runOrchestration,
25
+ clearClaudeSessionContext,
26
+ deliverSubagentToMain,
27
+ stopLoop,
28
+ buildOrchWorker,
29
+ type LoopOrchCommandDeps,
30
+ } from "./claude-loop-orch-commands"
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Fake store builder
34
+ // ---------------------------------------------------------------------------
35
+
36
+ interface FakeStore {
37
+ events: AutoContinueEvent[]
38
+ messages: { chatId: string; entry: TranscriptEntry }[]
39
+ chats: Map<string, { id: string; projectId: string }>
40
+ projects: Map<string, { id: string; localPath: string }>
41
+ orchRuns: Map<string, OrchRunSnapshot>
42
+ sessionTokensSet: { chatId: string; provider: string; token: string | null }[]
43
+ getAutoContinueEvents(chatId: string): AutoContinueEvent[]
44
+ getChat(chatId: string): { id: string; projectId: string } | null
45
+ getProject(projectId: string): { id: string; localPath: string } | null
46
+ getOrchRun(runId: string): OrchRunSnapshot | null
47
+ setSessionTokenForProvider(chatId: string, provider: "claude", token: string | null): Promise<void>
48
+ appendMessage(chatId: string, entry: TranscriptEntry): Promise<void>
49
+ }
50
+
51
+ function makeStore(overrides: Partial<FakeStore> = {}): FakeStore {
52
+ const store: FakeStore = {
53
+ events: [],
54
+ messages: [],
55
+ chats: new Map([["chat-1", { id: "chat-1", projectId: "proj-1" }]]),
56
+ projects: new Map([["proj-1", { id: "proj-1", localPath: "/repo" }]]),
57
+ orchRuns: new Map(),
58
+ sessionTokensSet: [],
59
+ getAutoContinueEvents() {
60
+ return store.events
61
+ },
62
+ getChat(chatId) {
63
+ return store.chats.get(chatId) ?? null
64
+ },
65
+ getProject(projectId) {
66
+ return store.projects.get(projectId) ?? null
67
+ },
68
+ getOrchRun(runId) {
69
+ return store.orchRuns.get(runId) ?? null
70
+ },
71
+ async setSessionTokenForProvider(chatId, provider, token) {
72
+ store.sessionTokensSet.push({ chatId, provider, token })
73
+ },
74
+ async appendMessage(chatId, entry) {
75
+ store.messages.push({ chatId, entry })
76
+ },
77
+ ...overrides,
78
+ }
79
+ return store
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Fake orchestration queue
84
+ // ---------------------------------------------------------------------------
85
+
86
+ interface FakeOrchQueue {
87
+ created: { config: OrchRunConfig; tasks: OrchTaskSpec[] }[]
88
+ cancelled: string[]
89
+ runIdToReturn: string
90
+ createRun(config: OrchRunConfig, tasks: OrchTaskSpec[]): Promise<string>
91
+ cancelRun(runId: string): Promise<void>
92
+ }
93
+
94
+ function makeOrchQueue(overrides: Partial<FakeOrchQueue> = {}): FakeOrchQueue {
95
+ const q: FakeOrchQueue = {
96
+ created: [],
97
+ cancelled: [],
98
+ runIdToReturn: "run-1",
99
+ async createRun(config, tasks) {
100
+ q.created.push({ config, tasks })
101
+ return q.runIdToReturn
102
+ },
103
+ async cancelRun(runId) {
104
+ q.cancelled.push(runId)
105
+ },
106
+ ...overrides,
107
+ }
108
+ return q
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Fake dep builder
113
+ // ---------------------------------------------------------------------------
114
+
115
+ function makeDeps(overrides: Partial<LoopOrchCommandDeps> = {}): LoopOrchCommandDeps {
116
+ const store = makeStore()
117
+ const orchQueue = makeOrchQueue()
118
+ const emittedEvents: AutoContinueEvent[] = []
119
+ const closedSessions: string[] = []
120
+
121
+ return {
122
+ store,
123
+ orchestrationQueue: orchQueue,
124
+ claudeSessions: new Map<string, ClaudeSessionState>(),
125
+ activeTurns: new Map<string, unknown>(),
126
+ getSubagents: () => [],
127
+ getAppSettingsSnapshot: () => ({}),
128
+ buildSubagentProviderRunForChat: () => {
129
+ throw new Error("not implemented")
130
+ },
131
+ closeClaudeSession: (chatId) => {
132
+ closedSessions.push(chatId)
133
+ },
134
+ emitAutoContinueEvent: async (event) => {
135
+ emittedEvents.push(event)
136
+ store.events.push(event)
137
+ },
138
+ ensureTrackingFile: async (_args: EnsureTrackingFileArgs): Promise<EnsureTrackingFileResult> => {
139
+ return { created: true, reconciled: false, actions: [], absPath: _args.absPath }
140
+ },
141
+ ...overrides,
142
+ // isLoopArmed MUST follow the spread: Partial<...> widens it to T|undefined,
143
+ // placing it after with ?? fallback ensures TS7 always sees a concrete function.
144
+ isLoopArmed: overrides.isLoopArmed ?? ((_chatId: string) => null),
145
+ }
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // isLoopArmed
150
+ // ---------------------------------------------------------------------------
151
+
152
+ describe("isLoopArmed", () => {
153
+ test("returns null when no events", () => {
154
+ const deps = makeDeps()
155
+ expect(isLoopArmed(deps, "chat-1")).toBeNull()
156
+ })
157
+
158
+ test("returns LoopState when loop_armed event is present", () => {
159
+ const deps = makeDeps()
160
+ const armEvent: AutoContinueEvent = {
161
+ v: AUTO_CONTINUE_EVENT_VERSION,
162
+ kind: "loop_armed",
163
+ timestamp: Date.now(),
164
+ chatId: "chat-1",
165
+ scheduleId: "sched-1",
166
+ subagentId: "agent-1",
167
+ prompt: "Read PROGRESS.md",
168
+ }
169
+ deps.store.getAutoContinueEvents = () => [armEvent]
170
+ const result = isLoopArmed(deps, "chat-1")
171
+ expect(result).not.toBeNull()
172
+ expect(result?.prompt).toBe("Read PROGRESS.md")
173
+ })
174
+
175
+ test("returns null after loop_disarmed event", () => {
176
+ const deps = makeDeps()
177
+ const now = Date.now()
178
+ const events: AutoContinueEvent[] = [
179
+ {
180
+ v: AUTO_CONTINUE_EVENT_VERSION,
181
+ kind: "loop_armed",
182
+ timestamp: now,
183
+ chatId: "chat-1",
184
+ scheduleId: "sched-1",
185
+ subagentId: "agent-1",
186
+ prompt: "Read PROGRESS.md",
187
+ },
188
+ {
189
+ v: AUTO_CONTINUE_EVENT_VERSION,
190
+ kind: "loop_disarmed",
191
+ timestamp: now + 1,
192
+ chatId: "chat-1",
193
+ scheduleId: "sched-2",
194
+ reason: "goal_met",
195
+ },
196
+ ]
197
+ deps.store.getAutoContinueEvents = () => events
198
+ expect(isLoopArmed(deps, "chat-1")).toBeNull()
199
+ })
200
+ })
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // listLiveSchedules
204
+ // ---------------------------------------------------------------------------
205
+
206
+ describe("listLiveSchedules", () => {
207
+ test("returns empty array when no events", () => {
208
+ const deps = makeDeps()
209
+ expect(listLiveSchedules(deps, "chat-1")).toEqual([])
210
+ })
211
+
212
+ test("returns scheduleIds for proposed/scheduled events", () => {
213
+ const deps = makeDeps()
214
+ const now = Date.now() + 10_000
215
+ const events: AutoContinueEvent[] = [
216
+ {
217
+ v: AUTO_CONTINUE_EVENT_VERSION,
218
+ kind: "auto_continue_accepted",
219
+ timestamp: Date.now(),
220
+ chatId: "chat-1",
221
+ scheduleId: "sched-a",
222
+ scheduledAt: now,
223
+ tz: "system",
224
+ source: "auto_setting",
225
+ resetAt: now,
226
+ detectedAt: Date.now(),
227
+ },
228
+ ]
229
+ deps.store.getAutoContinueEvents = () => events
230
+ const schedules = listLiveSchedules(deps, "chat-1")
231
+ expect(schedules).toContain("sched-a")
232
+ })
233
+ })
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // getOrchRunDetail
237
+ // ---------------------------------------------------------------------------
238
+
239
+ describe("getOrchRunDetail", () => {
240
+ test("returns null when run not found", () => {
241
+ const deps = makeDeps()
242
+ expect(getOrchRunDetail(deps, "missing-run")).toBeNull()
243
+ })
244
+ })
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // cancelOrchRun
248
+ // ---------------------------------------------------------------------------
249
+
250
+ describe("cancelOrchRun", () => {
251
+ test("delegates to orchestrationQueue.cancelRun", async () => {
252
+ const queue = makeOrchQueue()
253
+ const deps = makeDeps({ orchestrationQueue: queue })
254
+ await cancelOrchRun(deps, "run-42")
255
+ expect(queue.cancelled).toContain("run-42")
256
+ })
257
+ })
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // buildOrchRunContext
261
+ // ---------------------------------------------------------------------------
262
+
263
+ describe("buildOrchRunContext", () => {
264
+ test("returns null for unknown chat", () => {
265
+ const deps = makeDeps()
266
+ expect(buildOrchRunContext(deps, "unknown-chat")).toBeNull()
267
+ })
268
+
269
+ test("returns null when project not found", () => {
270
+ const store = makeStore()
271
+ store.chats.set("chat-1", { id: "chat-1", projectId: "missing-proj" })
272
+ const deps = makeDeps({ store })
273
+ expect(buildOrchRunContext(deps, "chat-1")).toBeNull()
274
+ })
275
+
276
+ test("returns context for known chat+project", () => {
277
+ const subagent: Subagent = {
278
+ id: "agent-1",
279
+ name: "Agent 1",
280
+ provider: "claude",
281
+ model: "claude-opus-4-5",
282
+ modelOptions: { reasoningEffort: "high", contextWindow: "200k" },
283
+ systemPrompt: "You are helpful",
284
+ contextScope: "full-transcript",
285
+ triggerMode: "manual",
286
+ createdAt: 0,
287
+ updatedAt: 0,
288
+ }
289
+ const deps = makeDeps({ getSubagents: () => [subagent] })
290
+ const ctx = buildOrchRunContext(deps, "chat-1")
291
+ expect(ctx).not.toBeNull()
292
+ expect(ctx?.chatId).toBe("chat-1")
293
+ expect(ctx?.repoRoot).toBe("/repo")
294
+ expect(ctx?.roster).toEqual([{ id: "agent-1", name: "Agent 1" }])
295
+ })
296
+ })
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // runOrchestration
300
+ // ---------------------------------------------------------------------------
301
+
302
+ describe("runOrchestration", () => {
303
+ test("returns error when chat not found", async () => {
304
+ const deps = makeDeps()
305
+ const result = await runOrchestration(deps, "unknown-chat", { tasks: [] })
306
+ expect(result.ok).toBe(false)
307
+ if (!result.ok) {
308
+ expect(result.errors.some((e) => e.includes("unknown-chat"))).toBe(true)
309
+ }
310
+ })
311
+
312
+ test("returns error when validation fails (empty tasks)", async () => {
313
+ // No subagents → roster is empty → validation will fail on subagentId
314
+ const deps = makeDeps()
315
+ // Empty tasks → should fail validation
316
+ const result = await runOrchestration(deps, "chat-1", { tasks: [] })
317
+ expect(result.ok).toBe(false)
318
+ })
319
+ })
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // clearClaudeSessionContext
323
+ // ---------------------------------------------------------------------------
324
+
325
+ describe("clearClaudeSessionContext", () => {
326
+ test("sets session token to null", async () => {
327
+ const store = makeStore()
328
+ const deps = makeDeps({ store })
329
+ await clearClaudeSessionContext(deps, "chat-1")
330
+ expect(store.sessionTokensSet.some((e) => e.chatId === "chat-1" && e.token === null)).toBe(true)
331
+ })
332
+
333
+ test("sets suppressSessionTokenPersist on live session when no active turn", async () => {
334
+ const store = makeStore()
335
+ const fakeSession = {
336
+ chatId: "chat-1",
337
+ suppressSessionTokenPersist: false,
338
+ } as unknown as ClaudeSessionState
339
+ const claudeSessions = new Map<string, ClaudeSessionState>([["chat-1", fakeSession]])
340
+ const activeTurns = new Map<string, unknown>() // no active turn
341
+
342
+ let closed = false
343
+ const deps = makeDeps({
344
+ store,
345
+ claudeSessions,
346
+ activeTurns,
347
+ closeClaudeSession: () => {
348
+ closed = true
349
+ },
350
+ })
351
+ await clearClaudeSessionContext(deps, "chat-1")
352
+ expect(fakeSession.suppressSessionTokenPersist).toBe(true)
353
+ expect(closed).toBe(true)
354
+ })
355
+
356
+ test("does not close session when active turn exists", async () => {
357
+ const store = makeStore()
358
+ const fakeSession = {
359
+ chatId: "chat-1",
360
+ suppressSessionTokenPersist: false,
361
+ } as unknown as ClaudeSessionState
362
+ const claudeSessions = new Map<string, ClaudeSessionState>([["chat-1", fakeSession]])
363
+ const activeTurns = new Map<string, unknown>([["chat-1", {}]])
364
+
365
+ let closed = false
366
+ const deps = makeDeps({
367
+ store,
368
+ claudeSessions,
369
+ activeTurns,
370
+ closeClaudeSession: () => {
371
+ closed = true
372
+ },
373
+ })
374
+ await clearClaudeSessionContext(deps, "chat-1")
375
+ expect(fakeSession.suppressSessionTokenPersist).toBe(true)
376
+ expect(closed).toBe(false)
377
+ })
378
+ })
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // stopLoop
382
+ // ---------------------------------------------------------------------------
383
+
384
+ describe("stopLoop", () => {
385
+ test("is a no-op when loop is not armed", async () => {
386
+ const emitted: AutoContinueEvent[] = []
387
+ const deps = makeDeps({
388
+ emitAutoContinueEvent: async (event) => {
389
+ emitted.push(event)
390
+ },
391
+ })
392
+ await stopLoop(deps, "chat-1", "goal_met")
393
+ expect(emitted).toHaveLength(0)
394
+ })
395
+
396
+ test("emits loop_disarmed when loop is armed", async () => {
397
+ const emitted: AutoContinueEvent[] = []
398
+ const armEvent: AutoContinueEvent = {
399
+ v: AUTO_CONTINUE_EVENT_VERSION,
400
+ kind: "loop_armed",
401
+ timestamp: Date.now(),
402
+ chatId: "chat-1",
403
+ scheduleId: "sched-1",
404
+ subagentId: "agent-1",
405
+ prompt: "Read PROGRESS.md",
406
+ }
407
+ const deps = makeDeps({
408
+ emitAutoContinueEvent: async (event) => {
409
+ emitted.push(event)
410
+ },
411
+ })
412
+ deps.store.getAutoContinueEvents = () => [armEvent]
413
+ await stopLoop(deps, "chat-1", "goal_met")
414
+ expect(emitted.some((e) => e.kind === "loop_disarmed")).toBe(true)
415
+ })
416
+ })
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // deliverSubagentToMain
420
+ // ---------------------------------------------------------------------------
421
+
422
+ describe("deliverSubagentToMain", () => {
423
+ test("no-ops when chat not found", async () => {
424
+ const store = makeStore()
425
+ store.chats.clear()
426
+ const emitted: AutoContinueEvent[] = []
427
+ const deps = makeDeps({
428
+ store,
429
+ emitAutoContinueEvent: async (event) => { emitted.push(event) },
430
+ })
431
+ const outcome: BackgroundRunOutcome = { status: "completed", runId: "run-1", text: "done" }
432
+ await deliverSubagentToMain(deps, "unknown-chat", "run-1", outcome)
433
+ expect(emitted).toHaveLength(0)
434
+ })
435
+
436
+ test("emits auto_continue_accepted on completed outcome", async () => {
437
+ const emitted: AutoContinueEvent[] = []
438
+ const store = makeStore()
439
+ const deps = makeDeps({
440
+ store,
441
+ emitAutoContinueEvent: async (event) => {
442
+ emitted.push(event)
443
+ store.events.push(event)
444
+ },
445
+ })
446
+ const outcome: BackgroundRunOutcome = { status: "completed", runId: "run-1", text: "done" }
447
+ await deliverSubagentToMain(deps, "chat-1", "run-1", outcome)
448
+ expect(emitted.some((e) => e.kind === "auto_continue_accepted")).toBe(true)
449
+ })
450
+ })
451
+
452
+ // ---------------------------------------------------------------------------
453
+ // buildOrchWorker
454
+ // ---------------------------------------------------------------------------
455
+
456
+ describe("buildOrchWorker", () => {
457
+ const fakePhase: OrchPhaseSpec = {
458
+ name: "implement",
459
+ kind: "implement",
460
+ parallel: 1,
461
+ promptTemplate: "{{TASK}}",
462
+ }
463
+
464
+ function makeSpawnArgs(): WorkerSpawnArgs {
465
+ return {
466
+ runId: "run-1",
467
+ workerId: "wt-0",
468
+ taskId: "task-1",
469
+ phase: fakePhase,
470
+ phaseIndex: 0,
471
+ prompt: "Do the work",
472
+ cwd: "/worktree",
473
+ abortSignal: new AbortController().signal,
474
+ }
475
+ }
476
+
477
+ test("returns failed when run not found", async () => {
478
+ const deps = makeDeps()
479
+ const result = await buildOrchWorker(deps, makeSpawnArgs())
480
+ expect(result.kind).toBe("failed")
481
+ if (result.kind === "failed") {
482
+ expect(result.error).toContain("originChatId")
483
+ }
484
+ })
485
+
486
+ test("returns failed when subagent not found", async () => {
487
+ const store = makeStore()
488
+ const orchRun: OrchRunSnapshot = {
489
+ runId: "run-1",
490
+ config: {
491
+ title: "Test run",
492
+ repoRoot: "/repo",
493
+ baseBranch: "main",
494
+ maxParallelTasks: 1,
495
+ worktreePoolSize: 1,
496
+ maxAttempts: 3,
497
+ phases: [],
498
+ gates: [],
499
+ contextPrompt: null,
500
+ verify: null,
501
+ init: null,
502
+ workerSubagentId: "missing-agent",
503
+ originChatId: "chat-1",
504
+ },
505
+ tasks: [],
506
+ worktrees: [],
507
+ status: "running",
508
+ createdAt: 0,
509
+ updatedAt: 0,
510
+ }
511
+ store.orchRuns.set("run-1", orchRun)
512
+ const deps = makeDeps({ store, getSubagents: () => [] })
513
+ const result = await buildOrchWorker(deps, makeSpawnArgs())
514
+ expect(result.kind).toBe("failed")
515
+ if (result.kind === "failed") {
516
+ expect(result.error).toContain("missing-agent")
517
+ }
518
+ })
519
+
520
+ test("returns completed on successful provider run", async () => {
521
+ const store = makeStore()
522
+ const subagent: Subagent = {
523
+ id: "agent-1",
524
+ name: "Agent 1",
525
+ provider: "claude",
526
+ model: "claude-opus-4-5",
527
+ modelOptions: { reasoningEffort: "high", contextWindow: "200k" },
528
+ systemPrompt: "You are helpful",
529
+ contextScope: "full-transcript",
530
+ triggerMode: "manual",
531
+ createdAt: 0,
532
+ updatedAt: 0,
533
+ }
534
+ const orchRun: OrchRunSnapshot = {
535
+ runId: "run-1",
536
+ config: {
537
+ title: "Test run",
538
+ repoRoot: "/repo",
539
+ baseBranch: "main",
540
+ maxParallelTasks: 1,
541
+ worktreePoolSize: 1,
542
+ maxAttempts: 3,
543
+ phases: [],
544
+ gates: [],
545
+ contextPrompt: null,
546
+ verify: null,
547
+ init: null,
548
+ workerSubagentId: "agent-1",
549
+ originChatId: "chat-1",
550
+ },
551
+ tasks: [],
552
+ worktrees: [],
553
+ status: "running",
554
+ createdAt: 0,
555
+ updatedAt: 0,
556
+ }
557
+ store.orchRuns.set("run-1", orchRun)
558
+
559
+ const fakeProviderRun: ProviderRunStart = {
560
+ provider: "claude",
561
+ model: "claude-opus-4-5",
562
+ systemPrompt: "",
563
+ preamble: null,
564
+ authReady: async () => true,
565
+ start: async () => ({ text: "Worker output" }),
566
+ }
567
+
568
+ const deps = makeDeps({
569
+ store,
570
+ getSubagents: () => [subagent],
571
+ buildSubagentProviderRunForChat: () => fakeProviderRun,
572
+ })
573
+ const result = await buildOrchWorker(deps, makeSpawnArgs())
574
+ expect(result.kind).toBe("completed")
575
+ if (result.kind === "completed") {
576
+ expect(result.text).toBe("Worker output")
577
+ }
578
+ })
579
+ })