@cuongtran001/kanna 1.3.0 → 1.4.0

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 (200) hide show
  1. package/dist/client/assets/{arc-CAysfmX5.js → arc-BHj370W7.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-1G88q1PY.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-D0L3Co5v.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-CQtERl3H.js} +1 -1
  5. package/dist/client/assets/channel-BP9IMEvd.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-DxOwt09f.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-Wry66y9R.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-D6p-Hi4x.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-CRQZt23N.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-BzgTKOM9.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CKfD-91C.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-a71B27wc.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-vv9gJwx0.js} +1 -1
  14. package/dist/client/assets/classDiagram-4FO5ZUOK--W6Wt7FF.js +1 -0
  15. package/dist/client/assets/classDiagram-v2-Q7XG4LA2--W6Wt7FF.js +1 -0
  16. package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-Cu-dCCaI.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-D8coOWLK.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-NIwZ65jE.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-d46aE_le.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-xdm2UxWc.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-hDbLwC5K.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-MHUGG7ki.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-Ro6n9X70.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-DOZqvRMu.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-Bn2NXYIy.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DK44bfbC.js} +1 -1
  27. package/dist/client/assets/{index-C0U2kwuB.js → index-CWJpHyQy.js} +115 -115
  28. package/dist/client/assets/{index-CLsOWXff.css → index-Cyzd4zZx.css} +1 -1
  29. package/dist/client/assets/{index-CbSY9HfS.js → index-DU56d4oC.js} +1 -1
  30. package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-C2tW_Dwj.js} +1 -1
  31. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-C__4Z1wz.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-Bbn5EawR.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-RgS5IRWl.js} +1 -1
  34. package/dist/client/assets/{linear-DLR6miy3.js → linear-DfI0qBO_.js} +1 -1
  35. package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-BiN10VHc.js} +4 -4
  36. package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-BQhibhh3.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-BEfLRn7a.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-ba641Y2x.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-BGtl-tVO.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-DyziaSFr.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BfYBAYga.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-BfUfmLYA.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdGcgYEq.js +1 -0
  44. package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-T6z7dEFl.js} +1 -1
  45. package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-BAwQbFFd.js} +1 -1
  46. package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-DF1cRKsP.js} +1 -1
  47. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BRtAZ8za.js} +1 -1
  48. package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-Brb-q76h.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.test.ts +22 -6
  55. package/src/server/agent.ts +35 -4987
  56. package/src/server/chat-op-log.test.ts +63 -0
  57. package/src/server/chat-op-log.ts +71 -0
  58. package/src/server/chat-ops-diff.test.ts +120 -0
  59. package/src/server/chat-ops-diff.ts +97 -0
  60. package/src/server/chat-ops-parity.test.ts +140 -0
  61. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  62. package/src/server/claude-autocontinue-commands.ts +256 -0
  63. package/src/server/claude-cancel-handler.test.ts +472 -0
  64. package/src/server/claude-cancel-handler.ts +241 -0
  65. package/src/server/claude-chat-management.test.ts +395 -0
  66. package/src/server/claude-chat-management.ts +251 -0
  67. package/src/server/claude-harness-stream.test.ts +273 -0
  68. package/src/server/claude-harness-stream.ts +158 -0
  69. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  70. package/src/server/claude-loop-orch-commands.ts +466 -0
  71. package/src/server/claude-message-normalizer.test.ts +213 -0
  72. package/src/server/claude-message-normalizer.ts +436 -0
  73. package/src/server/claude-prompt-helpers.test.ts +248 -0
  74. package/src/server/claude-prompt-helpers.ts +139 -0
  75. package/src/server/claude-sdk-queue.test.ts +169 -0
  76. package/src/server/claude-sdk-queue.ts +82 -0
  77. package/src/server/claude-send-command.test.ts +414 -0
  78. package/src/server/claude-send-command.ts +420 -0
  79. package/src/server/claude-session-config-helpers.test.ts +279 -0
  80. package/src/server/claude-session-config-helpers.ts +151 -0
  81. package/src/server/claude-session-config.test.ts +250 -0
  82. package/src/server/claude-session-config.ts +159 -0
  83. package/src/server/claude-session-error-handler.test.ts +498 -0
  84. package/src/server/claude-session-error-handler.ts +361 -0
  85. package/src/server/claude-session-lifecycle.test.ts +669 -0
  86. package/src/server/claude-session-lifecycle.ts +283 -0
  87. package/src/server/claude-session-rebuild.test.ts +224 -0
  88. package/src/server/claude-session-rebuild.ts +101 -0
  89. package/src/server/claude-session-runner.test.ts +523 -0
  90. package/src/server/claude-session-runner.ts +521 -0
  91. package/src/server/claude-session-spawner.test.ts +401 -0
  92. package/src/server/claude-session-spawner.ts +401 -0
  93. package/src/server/claude-session-start.test.ts +205 -0
  94. package/src/server/claude-session-start.ts +277 -0
  95. package/src/server/claude-session-state-queries.test.ts +344 -0
  96. package/src/server/claude-session-state-queries.ts +160 -0
  97. package/src/server/claude-session-state.ts +104 -0
  98. package/src/server/claude-slash-commands.test.ts +277 -0
  99. package/src/server/claude-slash-commands.ts +234 -0
  100. package/src/server/claude-spawn-helpers.test.ts +221 -0
  101. package/src/server/claude-spawn-helpers.ts +179 -0
  102. package/src/server/claude-steer-log.test.ts +243 -0
  103. package/src/server/claude-steer-log.ts +58 -0
  104. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  105. package/src/server/claude-subagent-tool-response.ts +165 -0
  106. package/src/server/claude-subagent-wiring.test.ts +263 -0
  107. package/src/server/claude-subagent-wiring.ts +324 -0
  108. package/src/server/claude-tool-respond.test.ts +321 -0
  109. package/src/server/claude-tool-respond.ts +142 -0
  110. package/src/server/claude-turn-runner.test.ts +310 -0
  111. package/src/server/claude-turn-runner.ts +228 -0
  112. package/src/server/claude-turn-starter.test.ts +266 -0
  113. package/src/server/claude-turn-starter.ts +566 -0
  114. package/src/server/claude-usage-math.test.ts +169 -0
  115. package/src/server/claude-usage-math.ts +132 -0
  116. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  117. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  118. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  119. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  120. package/src/server/diff-store-errors.ts +85 -0
  121. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  122. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  123. package/src/server/diff-store-io.adapter.ts +27 -0
  124. package/src/server/diff-store-parse.test.ts +212 -0
  125. package/src/server/diff-store-parse.ts +122 -0
  126. package/src/server/diff-store-state.ts +82 -0
  127. package/src/server/diff-store.ts +76 -1854
  128. package/src/server/event-store-apply.test.ts +280 -0
  129. package/src/server/event-store-apply.ts +132 -0
  130. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  131. package/src/server/event-store-chat-lifecycle.ts +441 -0
  132. package/src/server/event-store-entity-write.ts +356 -0
  133. package/src/server/event-store-helpers.test.ts +230 -0
  134. package/src/server/event-store-helpers.ts +235 -0
  135. package/src/server/event-store-init.ts +276 -0
  136. package/src/server/event-store-messages.adapter.test.ts +532 -0
  137. package/src/server/event-store-messages.adapter.ts +455 -0
  138. package/src/server/event-store-orch-subscription.ts +124 -0
  139. package/src/server/event-store-orch.test.ts +462 -0
  140. package/src/server/event-store-orch.ts +312 -0
  141. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  142. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  143. package/src/server/event-store-snapshot.test.ts +597 -0
  144. package/src/server/event-store-snapshot.ts +658 -0
  145. package/src/server/event-store-subagent.test.ts +408 -0
  146. package/src/server/event-store-subagent.ts +198 -0
  147. package/src/server/event-store-tool-requests.test.ts +210 -0
  148. package/src/server/event-store-tool-requests.ts +131 -0
  149. package/src/server/event-store-transcript-write.adapter.ts +291 -0
  150. package/src/server/event-store-write-ops.test.ts +554 -0
  151. package/src/server/event-store-write-ops.ts +529 -0
  152. package/src/server/event-store.test.ts +75 -0
  153. package/src/server/event-store.ts +257 -2285
  154. package/src/server/events.ts +1 -1
  155. package/src/server/harness-types.ts +15 -1
  156. package/src/server/oauth-errors.ts +19 -0
  157. package/src/server/storage/backend.ts +7 -0
  158. package/src/server/storage/fs-storage.adapter.ts +21 -1
  159. package/src/server/storage/in-memory-storage.ts +12 -0
  160. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  161. package/src/server/ws-router-agent-ctrl.ts +137 -0
  162. package/src/server/ws-router-broadcast.ts +560 -0
  163. package/src/server/ws-router-chat.test.ts +461 -0
  164. package/src/server/ws-router-chat.ts +279 -0
  165. package/src/server/ws-router-defaults.test.ts +283 -0
  166. package/src/server/ws-router-defaults.ts +434 -0
  167. package/src/server/ws-router-diff.test.ts +167 -0
  168. package/src/server/ws-router-diff.ts +250 -0
  169. package/src/server/ws-router-envelope.ts +360 -0
  170. package/src/server/ws-router-misc.test.ts +422 -0
  171. package/src/server/ws-router-misc.ts +265 -0
  172. package/src/server/ws-router-orch.test.ts +190 -0
  173. package/src/server/ws-router-orch.ts +93 -0
  174. package/src/server/ws-router-project.test.ts +403 -0
  175. package/src/server/ws-router-project.ts +256 -0
  176. package/src/server/ws-router-push.test.ts +193 -0
  177. package/src/server/ws-router-push.ts +120 -0
  178. package/src/server/ws-router-settings.test.ts +173 -0
  179. package/src/server/ws-router-settings.ts +416 -0
  180. package/src/server/ws-router-skills.test.ts +194 -0
  181. package/src/server/ws-router-skills.ts +204 -0
  182. package/src/server/ws-router-utils.ts +238 -0
  183. package/src/server/ws-router.test.ts +90 -0
  184. package/src/server/ws-router.ts +340 -2254
  185. package/src/shared/app-settings-types.ts +365 -0
  186. package/src/shared/chat-ops.test.ts +138 -0
  187. package/src/shared/chat-ops.ts +71 -0
  188. package/src/shared/core-types.ts +27 -0
  189. package/src/shared/git-diff-types.ts +165 -0
  190. package/src/shared/mcp-types.ts +105 -0
  191. package/src/shared/protocol.ts +3 -2
  192. package/src/shared/provider-model-types.ts +437 -0
  193. package/src/shared/subagent-types.ts +198 -0
  194. package/src/shared/tool-call-types.ts +314 -0
  195. package/src/shared/transcript-types.ts +270 -0
  196. package/src/shared/types.ts +31 -1711
  197. package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
  198. package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
  199. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
  200. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Tests for the extracted auto-continue command handlers.
3
+ *
4
+ * Each test builds a minimal `AutoContinueCommandDeps` 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 { AUTO_CONTINUE_EVENT_VERSION } from "./auto-continue/events"
10
+ import type { AutoContinueEvent } from "./auto-continue/events"
11
+ import {
12
+ resolveAutoResumeFor,
13
+ emitAutoContinueEvent,
14
+ getChatSchedule,
15
+ requireFuture,
16
+ fireAutoContinue,
17
+ acceptAutoContinue,
18
+ rescheduleAutoContinue,
19
+ cancelAutoContinue,
20
+ type AutoContinueCommandDeps,
21
+ } from "./claude-autocontinue-commands"
22
+ import type { QueuedChatMessage, TranscriptEntry } from "../shared/types"
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Minimal fake builder
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function makeQueuedMessage(): QueuedChatMessage {
29
+ return {
30
+ id: "q-1",
31
+ content: "continue",
32
+ attachments: [],
33
+ createdAt: Date.now(),
34
+ autoContinue: { scheduleId: "sched-1" },
35
+ }
36
+ }
37
+
38
+ type StoredEvent = AutoContinueEvent
39
+
40
+ interface FakeStore {
41
+ events: StoredEvent[]
42
+ messages: { chatId: string; entry: TranscriptEntry }[]
43
+ chatExists: boolean
44
+ appendAutoContinueEvent(event: AutoContinueEvent): Promise<void>
45
+ getAutoContinueEvents(chatId: string): AutoContinueEvent[]
46
+ getChat(chatId: string): { id: string } | null | undefined
47
+ appendMessage(chatId: string, entry: TranscriptEntry): Promise<void>
48
+ }
49
+
50
+ function makeStore(overrides: Partial<FakeStore> = {}): FakeStore {
51
+ const store: FakeStore = {
52
+ events: [],
53
+ messages: [],
54
+ chatExists: true,
55
+ async appendAutoContinueEvent(event) {
56
+ store.events.push(event)
57
+ },
58
+ getAutoContinueEvents() {
59
+ return store.events
60
+ },
61
+ getChat(chatId) {
62
+ return store.chatExists ? { id: chatId } : null
63
+ },
64
+ async appendMessage(chatId, entry) {
65
+ store.messages.push({ chatId, entry })
66
+ },
67
+ ...overrides,
68
+ }
69
+ return store
70
+ }
71
+
72
+ function makeDeps(overrides: Partial<AutoContinueCommandDeps> = {}): AutoContinueCommandDeps {
73
+ const store = makeStore()
74
+ const autoResumeByChat = new Map<string, boolean>()
75
+
76
+ return {
77
+ autoResumeByChat,
78
+ getAutoResumePreference: () => false,
79
+ store,
80
+ scheduleManager: null,
81
+ emitStateChange: () => {},
82
+ enqueueMessage: async () => makeQueuedMessage(),
83
+ maybeStartNextQueuedMessage: async () => false,
84
+ ...overrides,
85
+ }
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // resolveAutoResumeFor
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe("resolveAutoResumeFor", () => {
93
+ test("returns per-chat override when present", () => {
94
+ const autoResumeByChat = new Map<string, boolean>([["chat-1", true]])
95
+ const deps = makeDeps({ autoResumeByChat, getAutoResumePreference: () => false })
96
+ expect(resolveAutoResumeFor(deps, "chat-1")).toBe(true)
97
+ })
98
+
99
+ test("falls back to global preference when no override", () => {
100
+ const deps = makeDeps({ getAutoResumePreference: () => true })
101
+ expect(resolveAutoResumeFor(deps, "chat-missing")).toBe(true)
102
+ })
103
+
104
+ test("per-chat false wins over global true", () => {
105
+ const autoResumeByChat = new Map<string, boolean>([["chat-1", false]])
106
+ const deps = makeDeps({ autoResumeByChat, getAutoResumePreference: () => true })
107
+ expect(resolveAutoResumeFor(deps, "chat-1")).toBe(false)
108
+ })
109
+ })
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // emitAutoContinueEvent
113
+ // ---------------------------------------------------------------------------
114
+
115
+ describe("emitAutoContinueEvent", () => {
116
+ test("appends event to store", async () => {
117
+ const store = makeStore()
118
+ const deps = makeDeps({ store })
119
+ const event: AutoContinueEvent = {
120
+ v: AUTO_CONTINUE_EVENT_VERSION,
121
+ kind: "auto_continue_fired",
122
+ timestamp: Date.now(),
123
+ chatId: "chat-1",
124
+ scheduleId: "sched-1",
125
+ }
126
+ await emitAutoContinueEvent(deps, event)
127
+ expect(store.events).toHaveLength(1)
128
+ expect(store.events[0]).toEqual(event)
129
+ })
130
+
131
+ test("notifies schedule manager", async () => {
132
+ const notified: AutoContinueEvent[] = []
133
+ const deps = makeDeps({
134
+ scheduleManager: { onEvent: (e) => { notified.push(e) } },
135
+ })
136
+ const event: AutoContinueEvent = {
137
+ v: AUTO_CONTINUE_EVENT_VERSION,
138
+ kind: "auto_continue_fired",
139
+ timestamp: Date.now(),
140
+ chatId: "chat-1",
141
+ scheduleId: "sched-1",
142
+ }
143
+ await emitAutoContinueEvent(deps, event)
144
+ expect(notified).toHaveLength(1)
145
+ })
146
+
147
+ test("emits state change for chat", async () => {
148
+ const changes: string[] = []
149
+ const deps = makeDeps({ emitStateChange: (id) => { changes.push(id) } })
150
+ const event: AutoContinueEvent = {
151
+ v: AUTO_CONTINUE_EVENT_VERSION,
152
+ kind: "auto_continue_fired",
153
+ timestamp: Date.now(),
154
+ chatId: "chat-42",
155
+ scheduleId: "sched-1",
156
+ }
157
+ await emitAutoContinueEvent(deps, event)
158
+ expect(changes).toContain("chat-42")
159
+ })
160
+ })
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // getChatSchedule
164
+ // ---------------------------------------------------------------------------
165
+
166
+ describe("getChatSchedule", () => {
167
+ test("returns undefined when no events", () => {
168
+ const deps = makeDeps()
169
+ expect(getChatSchedule(deps, "chat-1", "sched-1")).toBeUndefined()
170
+ })
171
+
172
+ test("returns the schedule when a proposed event exists", async () => {
173
+ const deps = makeDeps()
174
+ const event: AutoContinueEvent = {
175
+ v: AUTO_CONTINUE_EVENT_VERSION,
176
+ kind: "auto_continue_proposed",
177
+ timestamp: Date.now(),
178
+ chatId: "chat-1",
179
+ scheduleId: "sched-1",
180
+ detectedAt: Date.now() - 100,
181
+ resetAt: Date.now() + 3_600_000,
182
+ tz: "UTC",
183
+ }
184
+ await deps.store.appendAutoContinueEvent(event)
185
+ const result = getChatSchedule(deps, "chat-1", "sched-1")
186
+ expect(result?.state).toBe("proposed")
187
+ })
188
+ })
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // requireFuture
192
+ // ---------------------------------------------------------------------------
193
+
194
+ describe("requireFuture", () => {
195
+ test("does not throw when scheduledAt is in the future", () => {
196
+ expect(() => requireFuture(Date.now() + 60_000)).not.toThrow()
197
+ })
198
+
199
+ test("throws when scheduledAt is now", () => {
200
+ expect(() => requireFuture(Date.now() - 1)).toThrow("scheduledAt must be in the future")
201
+ })
202
+
203
+ test("throws when scheduledAt is in the past", () => {
204
+ expect(() => requireFuture(Date.now() - 5_000)).toThrow()
205
+ })
206
+ })
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // fireAutoContinue
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe("fireAutoContinue", () => {
213
+ test("no-ops when chat does not exist", async () => {
214
+ const store = makeStore({ chatExists: false })
215
+ const enqueued: string[] = []
216
+ const deps = makeDeps({
217
+ store,
218
+ enqueueMessage: async (_, content) => { enqueued.push(content); return makeQueuedMessage() },
219
+ })
220
+ await fireAutoContinue(deps, "chat-1", "sched-1")
221
+ expect(enqueued).toHaveLength(0)
222
+ expect(store.events).toHaveLength(0)
223
+ })
224
+
225
+ test("appends fired event and enqueues prompt", async () => {
226
+ const store = makeStore()
227
+ const enqueued: string[] = []
228
+ const deps = makeDeps({
229
+ store,
230
+ enqueueMessage: async (_, content) => { enqueued.push(content); return makeQueuedMessage() },
231
+ })
232
+ await fireAutoContinue(deps, "chat-1", "sched-1")
233
+ expect(store.events).toHaveLength(1)
234
+ expect(store.events[0]?.kind).toBe("auto_continue_fired")
235
+ expect(enqueued).toContain("continue") // fallback when no schedule prompt
236
+ })
237
+
238
+ test("uses schedule prompt when present", async () => {
239
+ const store = makeStore()
240
+ // Seed a proposed event so getChatSchedule finds it
241
+ await store.appendAutoContinueEvent({
242
+ v: AUTO_CONTINUE_EVENT_VERSION,
243
+ kind: "auto_continue_proposed",
244
+ timestamp: Date.now(),
245
+ chatId: "chat-1",
246
+ scheduleId: "sched-1",
247
+ detectedAt: Date.now() - 100,
248
+ resetAt: Date.now() + 3_600_000,
249
+ tz: "UTC",
250
+ })
251
+ // Accept it so the schedule has the prompt
252
+ await store.appendAutoContinueEvent({
253
+ v: AUTO_CONTINUE_EVENT_VERSION,
254
+ kind: "auto_continue_accepted",
255
+ timestamp: Date.now(),
256
+ chatId: "chat-1",
257
+ scheduleId: "sched-1",
258
+ scheduledAt: Date.now() + 5_000,
259
+ tz: "UTC",
260
+ source: "subagent_background",
261
+ resetAt: Date.now() + 3_600_000,
262
+ detectedAt: Date.now() - 100,
263
+ prompt: "Read PROGRESS.md and decide next step",
264
+ })
265
+ const enqueued: string[] = []
266
+ const deps = makeDeps({
267
+ store,
268
+ enqueueMessage: async (_, content) => { enqueued.push(content); return makeQueuedMessage() },
269
+ })
270
+ await fireAutoContinue(deps, "chat-1", "sched-1")
271
+ expect(enqueued).toContain("Read PROGRESS.md and decide next step")
272
+ })
273
+
274
+ test("appends error message when enqueueMessage throws", async () => {
275
+ const store = makeStore()
276
+ const deps = makeDeps({
277
+ store,
278
+ enqueueMessage: async () => { throw new Error("queue full") },
279
+ })
280
+ await fireAutoContinue(deps, "chat-1", "sched-1")
281
+ expect(store.messages).toHaveLength(1)
282
+ const msg = store.messages[0]?.entry
283
+ expect(msg && "result" in msg ? msg.result : "").toContain("Auto-continue failed: queue full")
284
+ })
285
+ })
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // acceptAutoContinue
289
+ // ---------------------------------------------------------------------------
290
+
291
+ describe("acceptAutoContinue", () => {
292
+ async function seedProposed(store: FakeStore) {
293
+ await store.appendAutoContinueEvent({
294
+ v: AUTO_CONTINUE_EVENT_VERSION,
295
+ kind: "auto_continue_proposed",
296
+ timestamp: Date.now(),
297
+ chatId: "chat-1",
298
+ scheduleId: "sched-1",
299
+ detectedAt: Date.now() - 100,
300
+ resetAt: Date.now() + 3_600_000,
301
+ tz: "UTC",
302
+ })
303
+ }
304
+
305
+ test("throws when schedule not found", async () => {
306
+ const deps = makeDeps()
307
+ await expect(
308
+ acceptAutoContinue(deps, "chat-1", "sched-missing", Date.now() + 60_000),
309
+ ).rejects.toThrow("Schedule not found")
310
+ })
311
+
312
+ test("throws when schedule not in proposed state", async () => {
313
+ const store = makeStore()
314
+ await seedProposed(store)
315
+ // Cancel it first
316
+ await store.appendAutoContinueEvent({
317
+ v: AUTO_CONTINUE_EVENT_VERSION,
318
+ kind: "auto_continue_cancelled",
319
+ timestamp: Date.now(),
320
+ chatId: "chat-1",
321
+ scheduleId: "sched-1",
322
+ reason: "user",
323
+ })
324
+ const deps = makeDeps({ store })
325
+ await expect(
326
+ acceptAutoContinue(deps, "chat-1", "sched-1", Date.now() + 60_000),
327
+ ).rejects.toThrow("Schedule not pending")
328
+ })
329
+
330
+ test("throws when scheduledAt is in the past", async () => {
331
+ const store = makeStore()
332
+ await seedProposed(store)
333
+ const deps = makeDeps({ store })
334
+ await expect(
335
+ acceptAutoContinue(deps, "chat-1", "sched-1", Date.now() - 1),
336
+ ).rejects.toThrow("scheduledAt must be in the future")
337
+ })
338
+
339
+ test("appends accepted event when valid", async () => {
340
+ const store = makeStore()
341
+ await seedProposed(store)
342
+ const deps = makeDeps({ store })
343
+ await acceptAutoContinue(deps, "chat-1", "sched-1", Date.now() + 60_000)
344
+ const accepted = store.events.find((e) => e.kind === "auto_continue_accepted")
345
+ expect(accepted).toBeDefined()
346
+ })
347
+ })
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // rescheduleAutoContinue
351
+ // ---------------------------------------------------------------------------
352
+
353
+ describe("rescheduleAutoContinue", () => {
354
+ async function seedScheduled(store: FakeStore) {
355
+ await store.appendAutoContinueEvent({
356
+ v: AUTO_CONTINUE_EVENT_VERSION,
357
+ kind: "auto_continue_proposed",
358
+ timestamp: Date.now(),
359
+ chatId: "chat-1",
360
+ scheduleId: "sched-1",
361
+ detectedAt: Date.now() - 200,
362
+ resetAt: Date.now() + 3_600_000,
363
+ tz: "UTC",
364
+ })
365
+ await store.appendAutoContinueEvent({
366
+ v: AUTO_CONTINUE_EVENT_VERSION,
367
+ kind: "auto_continue_accepted",
368
+ timestamp: Date.now(),
369
+ chatId: "chat-1",
370
+ scheduleId: "sched-1",
371
+ scheduledAt: Date.now() + 30_000,
372
+ tz: "UTC",
373
+ source: "user",
374
+ resetAt: Date.now() + 3_600_000,
375
+ detectedAt: Date.now() - 200,
376
+ })
377
+ }
378
+
379
+ test("throws when schedule not found", async () => {
380
+ const deps = makeDeps()
381
+ await expect(
382
+ rescheduleAutoContinue(deps, "chat-1", "sched-missing", Date.now() + 60_000),
383
+ ).rejects.toThrow("Schedule not active")
384
+ })
385
+
386
+ test("throws when scheduledAt is in the past", async () => {
387
+ const store = makeStore()
388
+ await seedScheduled(store)
389
+ const deps = makeDeps({ store })
390
+ await expect(
391
+ rescheduleAutoContinue(deps, "chat-1", "sched-1", Date.now() - 1),
392
+ ).rejects.toThrow("scheduledAt must be in the future")
393
+ })
394
+
395
+ test("appends rescheduled event when valid", async () => {
396
+ const store = makeStore()
397
+ await seedScheduled(store)
398
+ const deps = makeDeps({ store })
399
+ await rescheduleAutoContinue(deps, "chat-1", "sched-1", Date.now() + 60_000)
400
+ const rescheduled = store.events.find((e) => e.kind === "auto_continue_rescheduled")
401
+ expect(rescheduled).toBeDefined()
402
+ })
403
+ })
404
+
405
+ // ---------------------------------------------------------------------------
406
+ // cancelAutoContinue
407
+ // ---------------------------------------------------------------------------
408
+
409
+ describe("cancelAutoContinue", () => {
410
+ test("no-ops silently when schedule not found", async () => {
411
+ const store = makeStore()
412
+ const deps = makeDeps({ store })
413
+ await expect(
414
+ cancelAutoContinue(deps, "chat-1", "sched-missing", "user"),
415
+ ).resolves.toBeUndefined()
416
+ expect(store.events).toHaveLength(0)
417
+ })
418
+
419
+ test("no-ops when schedule already fired", async () => {
420
+ const store = makeStore()
421
+ await store.appendAutoContinueEvent({
422
+ v: AUTO_CONTINUE_EVENT_VERSION,
423
+ kind: "auto_continue_proposed",
424
+ timestamp: Date.now(),
425
+ chatId: "chat-1",
426
+ scheduleId: "sched-1",
427
+ detectedAt: Date.now() - 100,
428
+ resetAt: Date.now() + 3_600_000,
429
+ tz: "UTC",
430
+ })
431
+ await store.appendAutoContinueEvent({
432
+ v: AUTO_CONTINUE_EVENT_VERSION,
433
+ kind: "auto_continue_fired",
434
+ timestamp: Date.now(),
435
+ chatId: "chat-1",
436
+ scheduleId: "sched-1",
437
+ })
438
+ const eventsBefore = store.events.length
439
+ const deps = makeDeps({ store })
440
+ await cancelAutoContinue(deps, "chat-1", "sched-1", "user")
441
+ expect(store.events).toHaveLength(eventsBefore)
442
+ })
443
+
444
+ test("appends cancelled event when proposed", async () => {
445
+ const store = makeStore()
446
+ await store.appendAutoContinueEvent({
447
+ v: AUTO_CONTINUE_EVENT_VERSION,
448
+ kind: "auto_continue_proposed",
449
+ timestamp: Date.now(),
450
+ chatId: "chat-1",
451
+ scheduleId: "sched-1",
452
+ detectedAt: Date.now() - 100,
453
+ resetAt: Date.now() + 3_600_000,
454
+ tz: "UTC",
455
+ })
456
+ const deps = makeDeps({ store })
457
+ await cancelAutoContinue(deps, "chat-1", "sched-1", "user")
458
+ const cancelled = store.events.find((e) => e.kind === "auto_continue_cancelled")
459
+ expect(cancelled).toBeDefined()
460
+ })
461
+
462
+ test("appends cancelled event when scheduled", async () => {
463
+ const store = makeStore()
464
+ await store.appendAutoContinueEvent({
465
+ v: AUTO_CONTINUE_EVENT_VERSION,
466
+ kind: "auto_continue_proposed",
467
+ timestamp: Date.now(),
468
+ chatId: "chat-1",
469
+ scheduleId: "sched-1",
470
+ detectedAt: Date.now() - 200,
471
+ resetAt: Date.now() + 3_600_000,
472
+ tz: "UTC",
473
+ })
474
+ await store.appendAutoContinueEvent({
475
+ v: AUTO_CONTINUE_EVENT_VERSION,
476
+ kind: "auto_continue_accepted",
477
+ timestamp: Date.now(),
478
+ chatId: "chat-1",
479
+ scheduleId: "sched-1",
480
+ scheduledAt: Date.now() + 30_000,
481
+ tz: "UTC",
482
+ source: "user",
483
+ resetAt: Date.now() + 3_600_000,
484
+ detectedAt: Date.now() - 200,
485
+ })
486
+ const deps = makeDeps({ store })
487
+ await cancelAutoContinue(deps, "chat-1", "sched-1", "chat_deleted")
488
+ const cancelled = store.events.find((e) => e.kind === "auto_continue_cancelled")
489
+ expect(cancelled).toBeDefined()
490
+ if (cancelled && cancelled.kind === "auto_continue_cancelled") {
491
+ expect(cancelled.reason).toBe("chat_deleted")
492
+ }
493
+ })
494
+ })