@ronkovic/aad 0.3.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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/aad.js +2 -0
  4. package/package.json +78 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
  6. package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
  7. package/src/__tests__/integration/cli-smoke.test.ts +175 -0
  8. package/src/__tests__/integration/pipeline.test.ts +346 -0
  9. package/src/bun-imports.d.ts +14 -0
  10. package/src/main.ts +52 -0
  11. package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
  12. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
  13. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
  14. package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
  15. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
  16. package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
  17. package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
  18. package/src/modules/claude-provider/claude-provider.port.ts +35 -0
  19. package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
  20. package/src/modules/claude-provider/effort-strategy.ts +94 -0
  21. package/src/modules/claude-provider/index.ts +32 -0
  22. package/src/modules/claude-provider/provider-registry.ts +92 -0
  23. package/src/modules/claude-provider/retry.ts +81 -0
  24. package/src/modules/cli/__tests__/app.test.ts +160 -0
  25. package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
  26. package/src/modules/cli/__tests__/commands.test.ts +186 -0
  27. package/src/modules/cli/__tests__/output.test.ts +329 -0
  28. package/src/modules/cli/__tests__/resume.test.ts +324 -0
  29. package/src/modules/cli/__tests__/run.test.ts +168 -0
  30. package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
  31. package/src/modules/cli/__tests__/status.test.ts +144 -0
  32. package/src/modules/cli/app.ts +241 -0
  33. package/src/modules/cli/commands/cleanup.ts +120 -0
  34. package/src/modules/cli/commands/resume.ts +156 -0
  35. package/src/modules/cli/commands/run.ts +322 -0
  36. package/src/modules/cli/commands/status.ts +101 -0
  37. package/src/modules/cli/index.ts +29 -0
  38. package/src/modules/cli/output.ts +256 -0
  39. package/src/modules/cli/shutdown.ts +122 -0
  40. package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
  41. package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
  42. package/src/modules/dashboard/__tests__/server.test.ts +120 -0
  43. package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
  44. package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
  45. package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
  46. package/src/modules/dashboard/index.ts +8 -0
  47. package/src/modules/dashboard/routes/api.ts +84 -0
  48. package/src/modules/dashboard/routes/sse.ts +37 -0
  49. package/src/modules/dashboard/server.ts +111 -0
  50. package/src/modules/dashboard/services/file-watcher.ts +36 -0
  51. package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
  52. package/src/modules/dashboard/services/state-aggregator.ts +132 -0
  53. package/src/modules/dashboard/ui/dashboard.html +405 -0
  54. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
  55. package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
  56. package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
  57. package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
  58. package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
  59. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
  60. package/src/modules/git-workspace/branch-manager.ts +191 -0
  61. package/src/modules/git-workspace/git-exec.ts +124 -0
  62. package/src/modules/git-workspace/index.ts +17 -0
  63. package/src/modules/git-workspace/memory-sync.ts +89 -0
  64. package/src/modules/git-workspace/merge-service.ts +156 -0
  65. package/src/modules/git-workspace/settings-merge.ts +95 -0
  66. package/src/modules/git-workspace/worktree-manager.ts +199 -0
  67. package/src/modules/logging/__tests__/log-store.test.ts +242 -0
  68. package/src/modules/logging/__tests__/logger.test.ts +81 -0
  69. package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
  70. package/src/modules/logging/index.ts +7 -0
  71. package/src/modules/logging/log-store.ts +80 -0
  72. package/src/modules/logging/logger.ts +55 -0
  73. package/src/modules/logging/transports/sse-transport.ts +28 -0
  74. package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
  75. package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
  76. package/src/modules/multi-repo/index.ts +12 -0
  77. package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
  78. package/src/modules/multi-repo/repo-context.ts +71 -0
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
  133. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
  134. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
  135. package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
  136. package/src/modules/persistence/__tests__/index.test.ts +38 -0
  137. package/src/modules/persistence/__tests__/stores.test.ts +594 -0
  138. package/src/modules/persistence/file-lock.ts +158 -0
  139. package/src/modules/persistence/fs-run-store.ts +73 -0
  140. package/src/modules/persistence/fs-task-store.ts +152 -0
  141. package/src/modules/persistence/fs-worker-store.ts +116 -0
  142. package/src/modules/persistence/in-memory-stores.ts +98 -0
  143. package/src/modules/persistence/index.ts +60 -0
  144. package/src/modules/persistence/stores.port.ts +60 -0
  145. package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
  146. package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
  147. package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
  148. package/src/modules/planning/file-conflict-validator.ts +135 -0
  149. package/src/modules/planning/index.ts +40 -0
  150. package/src/modules/planning/planning.service.ts +262 -0
  151. package/src/modules/planning/project-detection.ts +525 -0
  152. package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
  153. package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
  154. package/src/modules/plugin/index.ts +3 -0
  155. package/src/modules/plugin/plugin-loader.ts +46 -0
  156. package/src/modules/plugin/plugin-manager.ts +90 -0
  157. package/src/modules/plugin/plugin.types.ts +37 -0
  158. package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
  159. package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
  160. package/src/modules/process-manager/index.ts +5 -0
  161. package/src/modules/process-manager/process-manager.ts +193 -0
  162. package/src/modules/process-manager/worker.ts +106 -0
  163. package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
  164. package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
  165. package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
  166. package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
  167. package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
  168. package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
  169. package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
  170. package/src/modules/task-execution/executor.ts +303 -0
  171. package/src/modules/task-execution/index.ts +45 -0
  172. package/src/modules/task-execution/phases/default-spawner.ts +49 -0
  173. package/src/modules/task-execution/phases/implementer-green.ts +100 -0
  174. package/src/modules/task-execution/phases/merge.ts +122 -0
  175. package/src/modules/task-execution/phases/reviewer.ts +160 -0
  176. package/src/modules/task-execution/phases/tester-red.ts +100 -0
  177. package/src/modules/task-execution/phases/tester-verify.ts +120 -0
  178. package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
  179. package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
  180. package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
  181. package/src/modules/task-queue/__tests__/task.test.ts +130 -0
  182. package/src/modules/task-queue/dependency-resolver.ts +171 -0
  183. package/src/modules/task-queue/dispatcher.ts +372 -0
  184. package/src/modules/task-queue/index.ts +16 -0
  185. package/src/modules/task-queue/task-plan.ts +40 -0
  186. package/src/modules/task-queue/task.ts +67 -0
  187. package/src/shared/__tests__/config.test.ts +204 -0
  188. package/src/shared/__tests__/errors.test.ts +285 -0
  189. package/src/shared/__tests__/events.test.ts +496 -0
  190. package/src/shared/__tests__/types.test.ts +360 -0
  191. package/src/shared/config.ts +133 -0
  192. package/src/shared/errors.ts +128 -0
  193. package/src/shared/events.ts +171 -0
  194. package/src/shared/types.ts +143 -0
  195. package/tsconfig.json +30 -0
@@ -0,0 +1,594 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { rm, mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { createTaskId, createWorkerId, createRunId, type Task, type Worker, type RunState } from "@aad/shared/types";
5
+ import { createStores } from "../index";
6
+ import type { Stores } from "../index";
7
+
8
+ // Use a unique directory per process to avoid cross-contamination in parallel runs
9
+ const TEST_DIR = join(import.meta.dir, `.tmp-stores-test-${process.pid}`);
10
+
11
+ describe("InMemory Stores", () => {
12
+ let stores: Stores;
13
+
14
+ beforeEach(() => {
15
+ stores = createStores("memory");
16
+ });
17
+
18
+ describe("TaskStore", () => {
19
+ test("saves and retrieves task", async () => {
20
+ const task: Task = {
21
+ taskId: createTaskId("task-1"),
22
+ title: "Test Task",
23
+ description: "A test task",
24
+ filesToModify: ["file1.ts"],
25
+ dependsOn: [],
26
+ priority: 1,
27
+ status: "pending",
28
+ retryCount: 0,
29
+ };
30
+
31
+ await stores.taskStore.save(task);
32
+ const retrieved = await stores.taskStore.get(createTaskId("task-1"));
33
+
34
+ expect(retrieved).toEqual(task);
35
+ });
36
+
37
+ test("getByStatus returns filtered tasks", async () => {
38
+ const tasks: Task[] = [
39
+ {
40
+ taskId: createTaskId("task-1"),
41
+ title: "Task 1",
42
+ description: "Pending task",
43
+ filesToModify: [],
44
+ dependsOn: [],
45
+ priority: 1,
46
+ status: "pending",
47
+ retryCount: 0,
48
+ },
49
+ {
50
+ taskId: createTaskId("task-2"),
51
+ title: "Task 2",
52
+ description: "Running task",
53
+ filesToModify: [],
54
+ dependsOn: [],
55
+ priority: 2,
56
+ status: "running",
57
+ retryCount: 0,
58
+ },
59
+ ];
60
+
61
+ for (const task of tasks) {
62
+ await stores.taskStore.save(task);
63
+ }
64
+
65
+ const pending = await stores.taskStore.getByStatus("pending");
66
+ const running = await stores.taskStore.getByStatus("running");
67
+
68
+ expect(pending).toHaveLength(1);
69
+ expect(running).toHaveLength(1);
70
+ expect(pending[0]?.taskId as string).toBe("task-1");
71
+ expect(running[0]?.taskId as string).toBe("task-2");
72
+ });
73
+
74
+ test("getAll returns all tasks", async () => {
75
+ const tasks: Task[] = [
76
+ {
77
+ taskId: createTaskId("task-1"),
78
+ title: "Task 1",
79
+ description: "Task 1",
80
+ filesToModify: [],
81
+ dependsOn: [],
82
+ priority: 1,
83
+ status: "pending",
84
+ retryCount: 0,
85
+ },
86
+ {
87
+ taskId: createTaskId("task-2"),
88
+ title: "Task 2",
89
+ description: "Task 2",
90
+ filesToModify: [],
91
+ dependsOn: [],
92
+ priority: 2,
93
+ status: "running",
94
+ retryCount: 0,
95
+ },
96
+ ];
97
+
98
+ for (const task of tasks) {
99
+ await stores.taskStore.save(task);
100
+ }
101
+
102
+ const all = await stores.taskStore.getAll();
103
+ expect(all).toHaveLength(2);
104
+ });
105
+
106
+ test("delete removes task", async () => {
107
+ const task: Task = {
108
+ taskId: createTaskId("task-1"),
109
+ title: "Task",
110
+ description: "Task",
111
+ filesToModify: [],
112
+ dependsOn: [],
113
+ priority: 1,
114
+ status: "pending",
115
+ retryCount: 0,
116
+ };
117
+
118
+ await stores.taskStore.save(task);
119
+ await stores.taskStore.delete(createTaskId("task-1"));
120
+
121
+ const retrieved = await stores.taskStore.get(createTaskId("task-1"));
122
+ expect(retrieved).toBeNull();
123
+ });
124
+ });
125
+
126
+ describe("WorkerStore", () => {
127
+ test("saves and retrieves worker", async () => {
128
+ const worker: Worker = {
129
+ workerId: createWorkerId("worker-1"),
130
+ status: "idle",
131
+ currentTask: null,
132
+ };
133
+
134
+ await stores.workerStore.save(worker);
135
+ const retrieved = await stores.workerStore.get(createWorkerId("worker-1"));
136
+
137
+ expect(retrieved).toEqual(worker);
138
+ });
139
+
140
+ test("getIdle returns only idle workers", async () => {
141
+ const workers: Worker[] = [
142
+ {
143
+ workerId: createWorkerId("worker-1"),
144
+ status: "idle",
145
+ currentTask: null,
146
+ },
147
+ {
148
+ workerId: createWorkerId("worker-2"),
149
+ status: "busy",
150
+ currentTask: createTaskId("task-1"),
151
+ },
152
+ ];
153
+
154
+ for (const worker of workers) {
155
+ await stores.workerStore.save(worker);
156
+ }
157
+
158
+ const idle = await stores.workerStore.getIdle();
159
+ expect(idle).toHaveLength(1);
160
+ expect(idle[0]?.workerId as string).toBe("worker-1");
161
+ });
162
+
163
+ test("getAll returns all workers", async () => {
164
+ const workers: Worker[] = [
165
+ {
166
+ workerId: createWorkerId("worker-1"),
167
+ status: "idle",
168
+ currentTask: null,
169
+ },
170
+ {
171
+ workerId: createWorkerId("worker-2"),
172
+ status: "busy",
173
+ currentTask: createTaskId("task-1"),
174
+ },
175
+ ];
176
+
177
+ for (const worker of workers) {
178
+ await stores.workerStore.save(worker);
179
+ }
180
+
181
+ const all = await stores.workerStore.getAll();
182
+ expect(all).toHaveLength(2);
183
+ });
184
+
185
+ test("delete removes worker", async () => {
186
+ const worker: Worker = {
187
+ workerId: createWorkerId("worker-1"),
188
+ status: "idle",
189
+ currentTask: null,
190
+ };
191
+
192
+ await stores.workerStore.save(worker);
193
+ await stores.workerStore.delete(createWorkerId("worker-1"));
194
+
195
+ const retrieved = await stores.workerStore.get(createWorkerId("worker-1"));
196
+ expect(retrieved).toBeNull();
197
+ });
198
+ });
199
+
200
+ describe("RunStore", () => {
201
+ test("saves and retrieves run state", async () => {
202
+ const runState: RunState = {
203
+ runId: createRunId("run-123"),
204
+ parentBranch: "main",
205
+ totalTasks: 10,
206
+ pending: 5,
207
+ running: 3,
208
+ completed: 2,
209
+ failed: 0,
210
+ startTime: new Date().toISOString(),
211
+ };
212
+
213
+ await stores.runStore.save(runState);
214
+ const retrieved = await stores.runStore.get(createRunId("run-123"));
215
+
216
+ expect(retrieved).toEqual(runState);
217
+ });
218
+
219
+ test("returns null for non-existent run", async () => {
220
+ const retrieved = await stores.runStore.get(createRunId("non-existent"));
221
+ expect(retrieved).toBeNull();
222
+ });
223
+
224
+ test("getLatest returns most recent run by startTime", async () => {
225
+ const runs: RunState[] = [
226
+ {
227
+ runId: createRunId("run-1"),
228
+ parentBranch: "main",
229
+ totalTasks: 5,
230
+ pending: 0,
231
+ running: 0,
232
+ completed: 5,
233
+ failed: 0,
234
+ startTime: "2024-01-01T10:00:00Z",
235
+ },
236
+ {
237
+ runId: createRunId("run-2"),
238
+ parentBranch: "main",
239
+ totalTasks: 3,
240
+ pending: 0,
241
+ running: 0,
242
+ completed: 3,
243
+ failed: 0,
244
+ startTime: "2024-01-02T10:00:00Z", // Most recent
245
+ },
246
+ {
247
+ runId: createRunId("run-3"),
248
+ parentBranch: "main",
249
+ totalTasks: 2,
250
+ pending: 0,
251
+ running: 0,
252
+ completed: 2,
253
+ failed: 0,
254
+ startTime: "2024-01-01T12:00:00Z",
255
+ },
256
+ ];
257
+
258
+ for (const run of runs) {
259
+ await stores.runStore.save(run);
260
+ }
261
+
262
+ const latest = await stores.runStore.getLatest();
263
+ expect(latest).not.toBeNull();
264
+ expect(latest?.runId as string).toBe("run-2");
265
+ });
266
+
267
+ test("getLatest returns null when no runs exist", async () => {
268
+ const latest = await stores.runStore.getLatest();
269
+ expect(latest).toBeNull();
270
+ });
271
+ });
272
+ });
273
+
274
+ describe("FileSystem Stores", () => {
275
+ let stores: Stores;
276
+
277
+ beforeEach(async () => {
278
+ await rm(TEST_DIR, { recursive: true, force: true });
279
+ await mkdir(TEST_DIR, { recursive: true });
280
+ stores = createStores("fs", { basePath: TEST_DIR });
281
+ });
282
+
283
+ afterEach(async () => {
284
+ await rm(TEST_DIR, { recursive: true, force: true });
285
+ });
286
+
287
+ describe("FSTaskStore", () => {
288
+ test("saves and retrieves task", async () => {
289
+ const task: Task = {
290
+ taskId: createTaskId("task-1"),
291
+ title: "FS Test Task",
292
+ description: "A filesystem test task",
293
+ filesToModify: ["file1.ts", "file2.ts"],
294
+ dependsOn: [],
295
+ priority: 1,
296
+ status: "pending",
297
+ retryCount: 0,
298
+ };
299
+
300
+ await stores.taskStore.save(task);
301
+ const retrieved = await stores.taskStore.get(createTaskId("task-1"));
302
+
303
+ expect(retrieved).toEqual(task);
304
+ });
305
+
306
+ test("handles status changes correctly", async () => {
307
+ const task: Task = {
308
+ taskId: createTaskId("task-status-change"),
309
+ title: "Task",
310
+ description: "Task",
311
+ filesToModify: [],
312
+ dependsOn: [],
313
+ priority: 1,
314
+ status: "pending",
315
+ retryCount: 0,
316
+ };
317
+
318
+ await stores.taskStore.save(task);
319
+
320
+ // Verify it's in pending
321
+ let pending = await stores.taskStore.getByStatus("pending");
322
+ expect(pending).toHaveLength(1);
323
+
324
+ // Change status to running
325
+ task.status = "running";
326
+ await stores.taskStore.save(task);
327
+
328
+ // Verify it moved from pending to running
329
+ pending = await stores.taskStore.getByStatus("pending");
330
+ const running = await stores.taskStore.getByStatus("running");
331
+
332
+ expect(pending).toHaveLength(0);
333
+ expect(running).toHaveLength(1);
334
+ });
335
+
336
+ test("getAll returns all tasks from all status dirs", async () => {
337
+ const tasks: Task[] = [
338
+ {
339
+ taskId: createTaskId("task-getall-1"),
340
+ title: "Task 1",
341
+ description: "Task 1",
342
+ filesToModify: [],
343
+ dependsOn: [],
344
+ priority: 1,
345
+ status: "pending",
346
+ retryCount: 0,
347
+ },
348
+ {
349
+ taskId: createTaskId("task-getall-2"),
350
+ title: "Task 2",
351
+ description: "Task 2",
352
+ filesToModify: [],
353
+ dependsOn: [],
354
+ priority: 2,
355
+ status: "completed",
356
+ retryCount: 0,
357
+ },
358
+ ];
359
+
360
+ for (const task of tasks) {
361
+ await stores.taskStore.save(task);
362
+ }
363
+
364
+ const all = await stores.taskStore.getAll();
365
+ expect(all).toHaveLength(2);
366
+ });
367
+ });
368
+
369
+ describe("FSWorkerStore", () => {
370
+ test("saves and retrieves worker", async () => {
371
+ const worker: Worker = {
372
+ workerId: createWorkerId("worker-1"),
373
+ status: "idle",
374
+ currentTask: null,
375
+ pid: process.pid,
376
+ };
377
+
378
+ await stores.workerStore.save(worker);
379
+ const retrieved = await stores.workerStore.get(createWorkerId("worker-1"));
380
+
381
+ expect(retrieved).toEqual(worker);
382
+ });
383
+
384
+ test("getIdle filters idle workers", async () => {
385
+ const workers: Worker[] = [
386
+ {
387
+ workerId: createWorkerId("worker-1"),
388
+ status: "idle",
389
+ currentTask: null,
390
+ },
391
+ {
392
+ workerId: createWorkerId("worker-2"),
393
+ status: "busy",
394
+ currentTask: createTaskId("task-1"),
395
+ },
396
+ ];
397
+
398
+ for (const worker of workers) {
399
+ await stores.workerStore.save(worker);
400
+ }
401
+
402
+ const idle = await stores.workerStore.getIdle();
403
+ expect(idle).toHaveLength(1);
404
+ expect(idle[0]?.status).toBe("idle");
405
+ });
406
+ });
407
+
408
+ describe("FSTaskStore edge cases", () => {
409
+ test("delete then getAll does not contain deleted task", async () => {
410
+ const task: Task = {
411
+ taskId: createTaskId("task-del-getall"),
412
+ title: "Task",
413
+ description: "Task",
414
+ filesToModify: [],
415
+ dependsOn: [],
416
+ priority: 1,
417
+ status: "pending",
418
+ retryCount: 0,
419
+ };
420
+
421
+ await stores.taskStore.save(task);
422
+ await stores.taskStore.delete(createTaskId("task-del-getall"));
423
+
424
+ const all = await stores.taskStore.getAll();
425
+ const found = all.find((t) => (t.taskId as string) === "task-del-getall");
426
+ expect(found).toBeUndefined();
427
+ });
428
+
429
+ test("get non-existent taskId returns null", async () => {
430
+ const result = await stores.taskStore.get(createTaskId("does-not-exist"));
431
+ expect(result).toBeNull();
432
+ });
433
+
434
+ test("delete non-existent taskId does not throw", async () => {
435
+ await expect(
436
+ stores.taskStore.delete(createTaskId("does-not-exist"))
437
+ ).resolves.toBeUndefined();
438
+ });
439
+ });
440
+
441
+ describe("FSWorkerStore edge cases", () => {
442
+ test("delete then getAll does not contain deleted worker", async () => {
443
+ const worker: Worker = {
444
+ workerId: createWorkerId("worker-del"),
445
+ status: "idle",
446
+ currentTask: null,
447
+ };
448
+
449
+ await stores.workerStore.save(worker);
450
+ await stores.workerStore.delete(createWorkerId("worker-del"));
451
+
452
+ const all = await stores.workerStore.getAll();
453
+ const found = all.find((w) => (w.workerId as string) === "worker-del");
454
+ expect(found).toBeUndefined();
455
+ });
456
+
457
+ test("get non-existent workerId returns null", async () => {
458
+ const result = await stores.workerStore.get(createWorkerId("does-not-exist"));
459
+ expect(result).toBeNull();
460
+ });
461
+ });
462
+
463
+ describe("FSRunStore edge cases", () => {
464
+ test("save overwrites previous run state", async () => {
465
+ const run1: RunState = {
466
+ runId: createRunId("run-overwrite"),
467
+ parentBranch: "main",
468
+ totalTasks: 5,
469
+ pending: 5,
470
+ running: 0,
471
+ completed: 0,
472
+ failed: 0,
473
+ startTime: "2024-01-01T10:00:00Z",
474
+ };
475
+
476
+ await stores.runStore.save(run1);
477
+
478
+ const run1Updated: RunState = {
479
+ ...run1,
480
+ pending: 3,
481
+ running: 2,
482
+ };
483
+
484
+ await stores.runStore.save(run1Updated);
485
+ const retrieved = await stores.runStore.get(createRunId("run-overwrite"));
486
+
487
+ expect(retrieved).toEqual(run1Updated);
488
+ expect(retrieved?.pending).toBe(3);
489
+ expect(retrieved?.running).toBe(2);
490
+ });
491
+
492
+ test("getLatest returns last saved run (single file store)", async () => {
493
+ const run1: RunState = {
494
+ runId: createRunId("run-first"),
495
+ parentBranch: "main",
496
+ totalTasks: 5,
497
+ pending: 5,
498
+ running: 0,
499
+ completed: 0,
500
+ failed: 0,
501
+ startTime: "2024-01-01T10:00:00Z",
502
+ };
503
+
504
+ const run2: RunState = {
505
+ runId: createRunId("run-second"),
506
+ parentBranch: "main",
507
+ totalTasks: 3,
508
+ pending: 3,
509
+ running: 0,
510
+ completed: 0,
511
+ failed: 0,
512
+ startTime: "2024-01-02T10:00:00Z",
513
+ };
514
+
515
+ await stores.runStore.save(run1);
516
+ await stores.runStore.save(run2);
517
+
518
+ const latest = await stores.runStore.getLatest();
519
+ expect(latest).not.toBeNull();
520
+ expect(latest?.runId as string).toBe("run-second");
521
+ });
522
+ });
523
+
524
+ describe("FSRunStore", () => {
525
+ test("saves and retrieves run state", async () => {
526
+ const runState: RunState = {
527
+ runId: createRunId("run-456"),
528
+ parentBranch: "develop",
529
+ totalTasks: 20,
530
+ pending: 10,
531
+ running: 5,
532
+ completed: 5,
533
+ failed: 0,
534
+ startTime: new Date().toISOString(),
535
+ };
536
+
537
+ await stores.runStore.save(runState);
538
+ const retrieved = await stores.runStore.get(createRunId("run-456"));
539
+
540
+ expect(retrieved).toEqual(runState);
541
+ });
542
+
543
+ test("returns null for mismatched runId", async () => {
544
+ const runState: RunState = {
545
+ runId: createRunId("run-111"),
546
+ parentBranch: "main",
547
+ totalTasks: 5,
548
+ pending: 5,
549
+ running: 0,
550
+ completed: 0,
551
+ failed: 0,
552
+ startTime: new Date().toISOString(),
553
+ };
554
+
555
+ await stores.runStore.save(runState);
556
+ const retrieved = await stores.runStore.get(createRunId("run-999"));
557
+
558
+ expect(retrieved).toBeNull();
559
+ });
560
+
561
+ test("getLatest returns the saved run state", async () => {
562
+ const runState: RunState = {
563
+ runId: createRunId("run-latest"),
564
+ parentBranch: "main",
565
+ totalTasks: 8,
566
+ pending: 0,
567
+ running: 0,
568
+ completed: 8,
569
+ failed: 0,
570
+ startTime: "2024-01-03T10:00:00Z",
571
+ };
572
+
573
+ await stores.runStore.save(runState);
574
+ const latest = await stores.runStore.getLatest();
575
+
576
+ expect(latest).toEqual(runState);
577
+ });
578
+
579
+ test("getLatest returns null when no run exists", async () => {
580
+ // Create a completely separate directory for this test
581
+ const emptyTestDir = join(import.meta.dir, ".tmp-stores-test-empty");
582
+ await rm(emptyTestDir, { recursive: true, force: true });
583
+ await mkdir(emptyTestDir, { recursive: true });
584
+
585
+ try {
586
+ const freshStores = createStores("fs", { basePath: emptyTestDir });
587
+ const latest = await freshStores.runStore.getLatest();
588
+ expect(latest).toBeNull();
589
+ } finally {
590
+ await rm(emptyTestDir, { recursive: true, force: true });
591
+ }
592
+ });
593
+ });
594
+ });