@os-eco/overstory-cli 0.6.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.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { DEFAULT_CONFIG, DEFAULT_QUALITY_GATES, loadConfig, resolveProjectRoot } from "./config.ts";
|
|
6
|
+
import { ValidationError } from "./errors.ts";
|
|
7
|
+
import { cleanupTempDir, createTempGitRepo, runGitInDir } from "./test-helpers.ts";
|
|
8
|
+
|
|
9
|
+
describe("loadConfig", () => {
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tempDir = await mkdtemp(join(tmpdir(), "overstory-test-"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function writeConfig(yaml: string): Promise<void> {
|
|
21
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
22
|
+
await Bun.write(join(overstoryDir, "config.yaml"), yaml);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function ensureOverstoryDir(): Promise<void> {
|
|
26
|
+
const { mkdir } = await import("node:fs/promises");
|
|
27
|
+
await mkdir(join(tempDir, ".overstory"), { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("returns defaults when no config file exists", async () => {
|
|
31
|
+
const config = await loadConfig(tempDir);
|
|
32
|
+
|
|
33
|
+
expect(config.project.root).toBe(tempDir);
|
|
34
|
+
expect(config.project.canonicalBranch).toBe("main");
|
|
35
|
+
expect(config.agents.maxConcurrent).toBe(25);
|
|
36
|
+
expect(config.agents.maxDepth).toBe(2);
|
|
37
|
+
expect(config.taskTracker.enabled).toBe(true);
|
|
38
|
+
expect(config.mulch.enabled).toBe(true);
|
|
39
|
+
expect(config.mulch.primeFormat).toBe("markdown");
|
|
40
|
+
expect(config.logging.verbose).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("sets project.name from directory name", async () => {
|
|
44
|
+
const config = await loadConfig(tempDir);
|
|
45
|
+
const parts = tempDir.split("/");
|
|
46
|
+
const expectedName = parts[parts.length - 1] ?? "unknown";
|
|
47
|
+
expect(config.project.name).toBe(expectedName);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("merges config file values over defaults", async () => {
|
|
51
|
+
await ensureOverstoryDir();
|
|
52
|
+
await writeConfig(`
|
|
53
|
+
project:
|
|
54
|
+
canonicalBranch: develop
|
|
55
|
+
agents:
|
|
56
|
+
maxConcurrent: 10
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
const config = await loadConfig(tempDir);
|
|
60
|
+
|
|
61
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
62
|
+
expect(config.agents.maxConcurrent).toBe(10);
|
|
63
|
+
// Non-overridden values keep defaults
|
|
64
|
+
expect(config.agents.maxDepth).toBe(2);
|
|
65
|
+
expect(config.taskTracker.enabled).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("always sets project.root to the actual projectRoot", async () => {
|
|
69
|
+
await ensureOverstoryDir();
|
|
70
|
+
await writeConfig(`
|
|
71
|
+
project:
|
|
72
|
+
root: /some/wrong/path
|
|
73
|
+
`);
|
|
74
|
+
|
|
75
|
+
const config = await loadConfig(tempDir);
|
|
76
|
+
expect(config.project.root).toBe(tempDir);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("parses boolean values correctly", async () => {
|
|
80
|
+
await ensureOverstoryDir();
|
|
81
|
+
await writeConfig(`
|
|
82
|
+
beads:
|
|
83
|
+
enabled: false
|
|
84
|
+
mulch:
|
|
85
|
+
enabled: true
|
|
86
|
+
logging:
|
|
87
|
+
verbose: true
|
|
88
|
+
redactSecrets: false
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
const config = await loadConfig(tempDir);
|
|
92
|
+
|
|
93
|
+
expect(config.taskTracker.enabled).toBe(false);
|
|
94
|
+
expect(config.mulch.enabled).toBe(true);
|
|
95
|
+
expect(config.logging.verbose).toBe(true);
|
|
96
|
+
expect(config.logging.redactSecrets).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("parses empty array literal", async () => {
|
|
100
|
+
await ensureOverstoryDir();
|
|
101
|
+
await writeConfig(`
|
|
102
|
+
mulch:
|
|
103
|
+
domains: []
|
|
104
|
+
`);
|
|
105
|
+
|
|
106
|
+
const config = await loadConfig(tempDir);
|
|
107
|
+
expect(config.mulch.domains).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("parses numeric values including underscore-separated", async () => {
|
|
111
|
+
await ensureOverstoryDir();
|
|
112
|
+
await writeConfig(`
|
|
113
|
+
agents:
|
|
114
|
+
staggerDelayMs: 5000
|
|
115
|
+
watchdog:
|
|
116
|
+
tier0IntervalMs: 60000
|
|
117
|
+
staleThresholdMs: 120000
|
|
118
|
+
zombieThresholdMs: 300000
|
|
119
|
+
`);
|
|
120
|
+
|
|
121
|
+
const config = await loadConfig(tempDir);
|
|
122
|
+
expect(config.agents.staggerDelayMs).toBe(5000);
|
|
123
|
+
expect(config.watchdog.tier0IntervalMs).toBe(60000);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("handles quoted string values", async () => {
|
|
127
|
+
await ensureOverstoryDir();
|
|
128
|
+
await writeConfig(`
|
|
129
|
+
project:
|
|
130
|
+
canonicalBranch: "develop"
|
|
131
|
+
`);
|
|
132
|
+
|
|
133
|
+
const config = await loadConfig(tempDir);
|
|
134
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("ignores comments and empty lines", async () => {
|
|
138
|
+
await ensureOverstoryDir();
|
|
139
|
+
await writeConfig(`
|
|
140
|
+
# This is a comment
|
|
141
|
+
project:
|
|
142
|
+
canonicalBranch: develop # inline comment
|
|
143
|
+
|
|
144
|
+
# Another comment
|
|
145
|
+
agents:
|
|
146
|
+
maxConcurrent: 3
|
|
147
|
+
`);
|
|
148
|
+
|
|
149
|
+
const config = await loadConfig(tempDir);
|
|
150
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
151
|
+
expect(config.agents.maxConcurrent).toBe(3);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("config.local.yaml overrides values from config.yaml", async () => {
|
|
155
|
+
await ensureOverstoryDir();
|
|
156
|
+
await writeConfig(`
|
|
157
|
+
project:
|
|
158
|
+
canonicalBranch: develop
|
|
159
|
+
agents:
|
|
160
|
+
maxConcurrent: 10
|
|
161
|
+
`);
|
|
162
|
+
await Bun.write(
|
|
163
|
+
join(tempDir, ".overstory", "config.local.yaml"),
|
|
164
|
+
`agents:\n maxConcurrent: 4\n`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const config = await loadConfig(tempDir);
|
|
168
|
+
// Local override wins
|
|
169
|
+
expect(config.agents.maxConcurrent).toBe(4);
|
|
170
|
+
// Non-overridden value from config.yaml preserved
|
|
171
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("config.local.yaml works when config.yaml does not exist", async () => {
|
|
175
|
+
await ensureOverstoryDir();
|
|
176
|
+
// No config.yaml, only config.local.yaml
|
|
177
|
+
await Bun.write(
|
|
178
|
+
join(tempDir, ".overstory", "config.local.yaml"),
|
|
179
|
+
`agents:\n maxConcurrent: 3\n`,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const config = await loadConfig(tempDir);
|
|
183
|
+
expect(config.agents.maxConcurrent).toBe(3);
|
|
184
|
+
// Defaults still applied
|
|
185
|
+
expect(config.project.canonicalBranch).toBe("main");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("values from config.local.yaml are validated", async () => {
|
|
189
|
+
await ensureOverstoryDir();
|
|
190
|
+
await writeConfig(`
|
|
191
|
+
project:
|
|
192
|
+
canonicalBranch: main
|
|
193
|
+
`);
|
|
194
|
+
await Bun.write(
|
|
195
|
+
join(tempDir, ".overstory", "config.local.yaml"),
|
|
196
|
+
`agents:\n maxConcurrent: -1\n`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("config.local.yaml deep merges nested objects", async () => {
|
|
203
|
+
await ensureOverstoryDir();
|
|
204
|
+
await writeConfig(`
|
|
205
|
+
watchdog:
|
|
206
|
+
tier0Enabled: false
|
|
207
|
+
staleThresholdMs: 120000
|
|
208
|
+
`);
|
|
209
|
+
await Bun.write(
|
|
210
|
+
join(tempDir, ".overstory", "config.local.yaml"),
|
|
211
|
+
`watchdog:\n tier0Enabled: true\n`,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const config = await loadConfig(tempDir);
|
|
215
|
+
// Local override
|
|
216
|
+
expect(config.watchdog.tier0Enabled).toBe(true);
|
|
217
|
+
// Non-overridden value from config.yaml preserved
|
|
218
|
+
expect(config.watchdog.staleThresholdMs).toBe(120000);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("parses providers section from config.yaml", async () => {
|
|
222
|
+
await ensureOverstoryDir();
|
|
223
|
+
await writeConfig(`
|
|
224
|
+
providers:
|
|
225
|
+
openrouter:
|
|
226
|
+
type: gateway
|
|
227
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
228
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
229
|
+
`);
|
|
230
|
+
const config = await loadConfig(tempDir);
|
|
231
|
+
expect(config.providers.openrouter).toEqual({
|
|
232
|
+
type: "gateway",
|
|
233
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
234
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
235
|
+
});
|
|
236
|
+
// Default anthropic provider preserved via deep merge
|
|
237
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("config.local.yaml overrides provider settings", async () => {
|
|
241
|
+
await ensureOverstoryDir();
|
|
242
|
+
await writeConfig(`
|
|
243
|
+
providers:
|
|
244
|
+
anthropic:
|
|
245
|
+
type: native
|
|
246
|
+
`);
|
|
247
|
+
await Bun.write(
|
|
248
|
+
join(tempDir, ".overstory", "config.local.yaml"),
|
|
249
|
+
`providers:\n anthropic:\n type: gateway\n baseUrl: http://localhost:8080\n authTokenEnv: ANTHROPIC_GATEWAY_KEY\n`,
|
|
250
|
+
);
|
|
251
|
+
const config = await loadConfig(tempDir);
|
|
252
|
+
expect(config.providers.anthropic).toEqual({
|
|
253
|
+
type: "gateway",
|
|
254
|
+
baseUrl: "http://localhost:8080",
|
|
255
|
+
authTokenEnv: "ANTHROPIC_GATEWAY_KEY",
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("empty providers section preserves defaults", async () => {
|
|
260
|
+
await ensureOverstoryDir();
|
|
261
|
+
await writeConfig(`
|
|
262
|
+
providers:
|
|
263
|
+
`);
|
|
264
|
+
const config = await loadConfig(tempDir);
|
|
265
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("migrates deprecated watchdog tier1/tier2 keys to tier0/tier1", async () => {
|
|
269
|
+
await ensureOverstoryDir();
|
|
270
|
+
await writeConfig(`
|
|
271
|
+
watchdog:
|
|
272
|
+
tier1Enabled: true
|
|
273
|
+
tier1IntervalMs: 45000
|
|
274
|
+
tier2Enabled: true
|
|
275
|
+
`);
|
|
276
|
+
|
|
277
|
+
const config = await loadConfig(tempDir);
|
|
278
|
+
// Old tier1 (mechanical daemon) → new tier0
|
|
279
|
+
expect(config.watchdog.tier0Enabled).toBe(true);
|
|
280
|
+
expect(config.watchdog.tier0IntervalMs).toBe(45000);
|
|
281
|
+
// Old tier2 (AI triage) → new tier1
|
|
282
|
+
expect(config.watchdog.tier1Enabled).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("new-style tier keys take precedence over deprecated keys", async () => {
|
|
286
|
+
await ensureOverstoryDir();
|
|
287
|
+
await writeConfig(`
|
|
288
|
+
watchdog:
|
|
289
|
+
tier0Enabled: false
|
|
290
|
+
tier0IntervalMs: 20000
|
|
291
|
+
tier1Enabled: true
|
|
292
|
+
`);
|
|
293
|
+
|
|
294
|
+
const config = await loadConfig(tempDir);
|
|
295
|
+
// New keys used directly — no migration needed
|
|
296
|
+
expect(config.watchdog.tier0Enabled).toBe(false);
|
|
297
|
+
expect(config.watchdog.tier0IntervalMs).toBe(20000);
|
|
298
|
+
expect(config.watchdog.tier1Enabled).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("migrates deprecated beads: key to taskTracker:", async () => {
|
|
302
|
+
await ensureOverstoryDir();
|
|
303
|
+
await writeConfig(`
|
|
304
|
+
beads:
|
|
305
|
+
enabled: false
|
|
306
|
+
`);
|
|
307
|
+
|
|
308
|
+
const config = await loadConfig(tempDir);
|
|
309
|
+
expect(config.taskTracker.backend).toBe("beads");
|
|
310
|
+
expect(config.taskTracker.enabled).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("migrates deprecated seeds: key to taskTracker:", async () => {
|
|
314
|
+
await ensureOverstoryDir();
|
|
315
|
+
await writeConfig(`
|
|
316
|
+
seeds:
|
|
317
|
+
enabled: true
|
|
318
|
+
`);
|
|
319
|
+
|
|
320
|
+
const config = await loadConfig(tempDir);
|
|
321
|
+
expect(config.taskTracker.backend).toBe("seeds");
|
|
322
|
+
expect(config.taskTracker.enabled).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("taskTracker: key takes precedence over legacy keys", async () => {
|
|
326
|
+
await ensureOverstoryDir();
|
|
327
|
+
await writeConfig(`
|
|
328
|
+
taskTracker:
|
|
329
|
+
backend: auto
|
|
330
|
+
enabled: true
|
|
331
|
+
beads:
|
|
332
|
+
enabled: false
|
|
333
|
+
`);
|
|
334
|
+
|
|
335
|
+
const config = await loadConfig(tempDir);
|
|
336
|
+
// taskTracker present — beads key ignored
|
|
337
|
+
expect(config.taskTracker.backend).toBe("auto");
|
|
338
|
+
expect(config.taskTracker.enabled).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("defaults taskTracker.backend to auto", async () => {
|
|
342
|
+
const config = await loadConfig(tempDir);
|
|
343
|
+
expect(config.taskTracker.backend).toBe("auto");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("validateConfig", () => {
|
|
348
|
+
let tempDir: string;
|
|
349
|
+
|
|
350
|
+
beforeEach(async () => {
|
|
351
|
+
tempDir = await mkdtemp(join(tmpdir(), "overstory-test-"));
|
|
352
|
+
const { mkdir } = await import("node:fs/promises");
|
|
353
|
+
await mkdir(join(tempDir, ".overstory"), { recursive: true });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
afterEach(async () => {
|
|
357
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
async function writeConfig(yaml: string): Promise<void> {
|
|
361
|
+
await Bun.write(join(tempDir, ".overstory", "config.yaml"), yaml);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
test("rejects negative maxConcurrent", async () => {
|
|
365
|
+
await writeConfig(`
|
|
366
|
+
agents:
|
|
367
|
+
maxConcurrent: -1
|
|
368
|
+
`);
|
|
369
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("rejects zero maxConcurrent", async () => {
|
|
373
|
+
await writeConfig(`
|
|
374
|
+
agents:
|
|
375
|
+
maxConcurrent: 0
|
|
376
|
+
`);
|
|
377
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("rejects negative maxDepth", async () => {
|
|
381
|
+
await writeConfig(`
|
|
382
|
+
agents:
|
|
383
|
+
maxDepth: -1
|
|
384
|
+
`);
|
|
385
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("rejects negative staggerDelayMs", async () => {
|
|
389
|
+
await writeConfig(`
|
|
390
|
+
agents:
|
|
391
|
+
staggerDelayMs: -100
|
|
392
|
+
`);
|
|
393
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("accepts maxSessionsPerRun of 0 (unlimited)", async () => {
|
|
397
|
+
await writeConfig(`
|
|
398
|
+
agents:
|
|
399
|
+
maxSessionsPerRun: 0
|
|
400
|
+
`);
|
|
401
|
+
const config = await loadConfig(tempDir);
|
|
402
|
+
expect(config.agents.maxSessionsPerRun).toBe(0);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("accepts positive maxSessionsPerRun", async () => {
|
|
406
|
+
await writeConfig(`
|
|
407
|
+
agents:
|
|
408
|
+
maxSessionsPerRun: 20
|
|
409
|
+
`);
|
|
410
|
+
const config = await loadConfig(tempDir);
|
|
411
|
+
expect(config.agents.maxSessionsPerRun).toBe(20);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("rejects negative maxSessionsPerRun", async () => {
|
|
415
|
+
await writeConfig(`
|
|
416
|
+
agents:
|
|
417
|
+
maxSessionsPerRun: -1
|
|
418
|
+
`);
|
|
419
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("rejects non-integer maxSessionsPerRun", async () => {
|
|
423
|
+
await writeConfig(`
|
|
424
|
+
agents:
|
|
425
|
+
maxSessionsPerRun: 1.5
|
|
426
|
+
`);
|
|
427
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("rejects invalid mulch.primeFormat", async () => {
|
|
431
|
+
await writeConfig(`
|
|
432
|
+
mulch:
|
|
433
|
+
primeFormat: yaml
|
|
434
|
+
`);
|
|
435
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("rejects invalid taskTracker.backend", async () => {
|
|
439
|
+
await writeConfig(`
|
|
440
|
+
taskTracker:
|
|
441
|
+
backend: invalid
|
|
442
|
+
enabled: true
|
|
443
|
+
`);
|
|
444
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("rejects zombieThresholdMs <= staleThresholdMs", async () => {
|
|
448
|
+
await writeConfig(`
|
|
449
|
+
watchdog:
|
|
450
|
+
staleThresholdMs: 300000
|
|
451
|
+
zombieThresholdMs: 300000
|
|
452
|
+
`);
|
|
453
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("rejects non-positive tier0IntervalMs when tier0 is enabled", async () => {
|
|
457
|
+
await writeConfig(`
|
|
458
|
+
watchdog:
|
|
459
|
+
tier0Enabled: true
|
|
460
|
+
tier0IntervalMs: 0
|
|
461
|
+
`);
|
|
462
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("accepts empty models section", async () => {
|
|
466
|
+
await writeConfig(`
|
|
467
|
+
models:
|
|
468
|
+
`);
|
|
469
|
+
const config = await loadConfig(tempDir);
|
|
470
|
+
expect(config.models).toBeDefined();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("accepts valid model names in models section", async () => {
|
|
474
|
+
await writeConfig(`
|
|
475
|
+
models:
|
|
476
|
+
coordinator: sonnet
|
|
477
|
+
monitor: haiku
|
|
478
|
+
builder: opus
|
|
479
|
+
`);
|
|
480
|
+
const config = await loadConfig(tempDir);
|
|
481
|
+
expect(config.models.coordinator).toBe("sonnet");
|
|
482
|
+
expect(config.models.monitor).toBe("haiku");
|
|
483
|
+
expect(config.models.builder).toBe("opus");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("rejects invalid model name in models section", async () => {
|
|
487
|
+
await writeConfig(`
|
|
488
|
+
models:
|
|
489
|
+
coordinator: gpt4
|
|
490
|
+
`);
|
|
491
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Provider validation tests
|
|
495
|
+
|
|
496
|
+
test("rejects provider with invalid type", async () => {
|
|
497
|
+
await writeConfig(`
|
|
498
|
+
providers:
|
|
499
|
+
custom:
|
|
500
|
+
type: custom
|
|
501
|
+
`);
|
|
502
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("rejects gateway provider without baseUrl", async () => {
|
|
506
|
+
await writeConfig(`
|
|
507
|
+
providers:
|
|
508
|
+
mygateway:
|
|
509
|
+
type: gateway
|
|
510
|
+
authTokenEnv: MY_TOKEN
|
|
511
|
+
`);
|
|
512
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("rejects gateway provider without authTokenEnv", async () => {
|
|
516
|
+
await writeConfig(`
|
|
517
|
+
providers:
|
|
518
|
+
mygateway:
|
|
519
|
+
type: gateway
|
|
520
|
+
baseUrl: https://example.com
|
|
521
|
+
`);
|
|
522
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("accepts native provider without baseUrl or authTokenEnv", async () => {
|
|
526
|
+
await writeConfig(`
|
|
527
|
+
providers:
|
|
528
|
+
mylocal:
|
|
529
|
+
type: native
|
|
530
|
+
`);
|
|
531
|
+
const config = await loadConfig(tempDir);
|
|
532
|
+
expect(config.providers.mylocal).toEqual({ type: "native" });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Model validation tests
|
|
536
|
+
|
|
537
|
+
test("accepts provider-prefixed model ref when provider exists", async () => {
|
|
538
|
+
await writeConfig(`
|
|
539
|
+
providers:
|
|
540
|
+
openrouter:
|
|
541
|
+
type: gateway
|
|
542
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
543
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
544
|
+
models:
|
|
545
|
+
coordinator: openrouter/openai/gpt-5.3
|
|
546
|
+
`);
|
|
547
|
+
const config = await loadConfig(tempDir);
|
|
548
|
+
expect(config.models.coordinator).toBe("openrouter/openai/gpt-5.3");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("rejects provider-prefixed model ref when provider is unknown", async () => {
|
|
552
|
+
await writeConfig(`
|
|
553
|
+
models:
|
|
554
|
+
coordinator: unknown/model
|
|
555
|
+
`);
|
|
556
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("rejects bare invalid model name", async () => {
|
|
560
|
+
await writeConfig(`
|
|
561
|
+
models:
|
|
562
|
+
coordinator: gpt4
|
|
563
|
+
`);
|
|
564
|
+
const err = await loadConfig(tempDir).catch((e: unknown) => e);
|
|
565
|
+
expect(err).toBeInstanceOf(ValidationError);
|
|
566
|
+
expect((err as ValidationError).message).toContain("provider-prefixed ref");
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("warns on non-Anthropic model in tool-heavy role", async () => {
|
|
570
|
+
await writeConfig(`
|
|
571
|
+
providers:
|
|
572
|
+
openrouter:
|
|
573
|
+
type: gateway
|
|
574
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
575
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
576
|
+
models:
|
|
577
|
+
builder: openrouter/openai/gpt-4
|
|
578
|
+
`);
|
|
579
|
+
const origWrite = process.stderr.write;
|
|
580
|
+
let capturedStderr = "";
|
|
581
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
582
|
+
if (typeof s === "string") capturedStderr += s;
|
|
583
|
+
return true;
|
|
584
|
+
}) as typeof process.stderr.write;
|
|
585
|
+
try {
|
|
586
|
+
await loadConfig(tempDir);
|
|
587
|
+
} finally {
|
|
588
|
+
process.stderr.write = origWrite;
|
|
589
|
+
}
|
|
590
|
+
expect(capturedStderr).toContain("WARNING: models.builder uses non-Anthropic model");
|
|
591
|
+
expect(capturedStderr).toContain("openrouter/openai/gpt-4");
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("does not warn for non-Anthropic model in non-tool-heavy role", async () => {
|
|
595
|
+
await writeConfig(`
|
|
596
|
+
providers:
|
|
597
|
+
openrouter:
|
|
598
|
+
type: gateway
|
|
599
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
600
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
601
|
+
models:
|
|
602
|
+
coordinator: openrouter/openai/gpt-4
|
|
603
|
+
`);
|
|
604
|
+
const origWrite = process.stderr.write;
|
|
605
|
+
let capturedStderr = "";
|
|
606
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
607
|
+
if (typeof s === "string") capturedStderr += s;
|
|
608
|
+
return true;
|
|
609
|
+
}) as typeof process.stderr.write;
|
|
610
|
+
try {
|
|
611
|
+
await loadConfig(tempDir);
|
|
612
|
+
} finally {
|
|
613
|
+
process.stderr.write = origWrite;
|
|
614
|
+
}
|
|
615
|
+
expect(capturedStderr).not.toContain("WARNING");
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("custom qualityGates from config.yaml are loaded", async () => {
|
|
619
|
+
await writeConfig(`
|
|
620
|
+
project:
|
|
621
|
+
canonicalBranch: main
|
|
622
|
+
qualityGates:
|
|
623
|
+
- name: Test
|
|
624
|
+
command: pytest
|
|
625
|
+
description: all tests pass
|
|
626
|
+
- name: Lint
|
|
627
|
+
command: ruff check .
|
|
628
|
+
description: no lint errors
|
|
629
|
+
`);
|
|
630
|
+
const config = await loadConfig(tempDir);
|
|
631
|
+
expect(config.project.qualityGates?.length).toBe(2);
|
|
632
|
+
expect(config.project.qualityGates?.[0]?.command).toBe("pytest");
|
|
633
|
+
expect(config.project.qualityGates?.[1]?.command).toBe("ruff check .");
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test("rejects qualityGate with empty name", async () => {
|
|
637
|
+
await writeConfig(`
|
|
638
|
+
project:
|
|
639
|
+
canonicalBranch: main
|
|
640
|
+
qualityGates:
|
|
641
|
+
- name: ""
|
|
642
|
+
command: pytest
|
|
643
|
+
description: all tests pass
|
|
644
|
+
`);
|
|
645
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("rejects qualityGate with empty command", async () => {
|
|
649
|
+
await writeConfig(`
|
|
650
|
+
project:
|
|
651
|
+
canonicalBranch: main
|
|
652
|
+
qualityGates:
|
|
653
|
+
- name: Test
|
|
654
|
+
command: ""
|
|
655
|
+
description: all tests pass
|
|
656
|
+
`);
|
|
657
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("rejects qualityGate with empty description", async () => {
|
|
661
|
+
await writeConfig(`
|
|
662
|
+
project:
|
|
663
|
+
canonicalBranch: main
|
|
664
|
+
qualityGates:
|
|
665
|
+
- name: Test
|
|
666
|
+
command: pytest
|
|
667
|
+
description: ""
|
|
668
|
+
`);
|
|
669
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe("resolveProjectRoot", () => {
|
|
674
|
+
let repoDir: string;
|
|
675
|
+
|
|
676
|
+
afterEach(async () => {
|
|
677
|
+
if (repoDir) {
|
|
678
|
+
// Remove worktrees before cleaning up
|
|
679
|
+
try {
|
|
680
|
+
await runGitInDir(repoDir, ["worktree", "prune"]);
|
|
681
|
+
} catch {
|
|
682
|
+
// Best effort
|
|
683
|
+
}
|
|
684
|
+
await cleanupTempDir(repoDir);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("returns startDir when .overstory/config.yaml exists there", async () => {
|
|
689
|
+
repoDir = await createTempGitRepo();
|
|
690
|
+
await mkdir(join(repoDir, ".overstory"), { recursive: true });
|
|
691
|
+
await Bun.write(
|
|
692
|
+
join(repoDir, ".overstory", "config.yaml"),
|
|
693
|
+
"project:\n canonicalBranch: main\n",
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
const result = await resolveProjectRoot(repoDir);
|
|
697
|
+
expect(result).toBe(repoDir);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("resolves worktree to main project root", async () => {
|
|
701
|
+
repoDir = await createTempGitRepo();
|
|
702
|
+
// Resolve symlinks (macOS /var -> /private/var) to match git's output
|
|
703
|
+
repoDir = await realpath(repoDir);
|
|
704
|
+
await mkdir(join(repoDir, ".overstory"), { recursive: true });
|
|
705
|
+
await Bun.write(
|
|
706
|
+
join(repoDir, ".overstory", "config.yaml"),
|
|
707
|
+
"project:\n canonicalBranch: main\n",
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// Create a worktree like overstory sling does
|
|
711
|
+
const worktreeDir = join(repoDir, ".overstory", "worktrees", "test-agent");
|
|
712
|
+
await mkdir(join(repoDir, ".overstory", "worktrees"), { recursive: true });
|
|
713
|
+
await runGitInDir(repoDir, [
|
|
714
|
+
"worktree",
|
|
715
|
+
"add",
|
|
716
|
+
"-b",
|
|
717
|
+
"overstory/test-agent/task-1",
|
|
718
|
+
worktreeDir,
|
|
719
|
+
]);
|
|
720
|
+
|
|
721
|
+
// resolveProjectRoot from the worktree should return the main repo
|
|
722
|
+
const result = await resolveProjectRoot(worktreeDir);
|
|
723
|
+
expect(result).toBe(repoDir);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("resolves worktree to main root even when config.yaml is committed (regression)", async () => {
|
|
727
|
+
repoDir = await createTempGitRepo();
|
|
728
|
+
repoDir = await realpath(repoDir);
|
|
729
|
+
|
|
730
|
+
// Commit .overstory/config.yaml so the worktree gets a copy via git
|
|
731
|
+
// (this is what overstory init does — the file is tracked)
|
|
732
|
+
await mkdir(join(repoDir, ".overstory"), { recursive: true });
|
|
733
|
+
await Bun.write(
|
|
734
|
+
join(repoDir, ".overstory", "config.yaml"),
|
|
735
|
+
"project:\n canonicalBranch: main\n",
|
|
736
|
+
);
|
|
737
|
+
await runGitInDir(repoDir, ["add", ".overstory/config.yaml"]);
|
|
738
|
+
await runGitInDir(repoDir, ["commit", "-m", "add overstory config"]);
|
|
739
|
+
|
|
740
|
+
// Create a worktree — it will now have .overstory/config.yaml from git
|
|
741
|
+
const worktreeDir = join(repoDir, ".overstory", "worktrees", "mail-scout");
|
|
742
|
+
await mkdir(join(repoDir, ".overstory", "worktrees"), { recursive: true });
|
|
743
|
+
await runGitInDir(repoDir, [
|
|
744
|
+
"worktree",
|
|
745
|
+
"add",
|
|
746
|
+
"-b",
|
|
747
|
+
"overstory/mail-scout/task-1",
|
|
748
|
+
worktreeDir,
|
|
749
|
+
]);
|
|
750
|
+
|
|
751
|
+
// Must resolve to main repo root, NOT the worktree
|
|
752
|
+
// (even though worktree has its own .overstory/config.yaml)
|
|
753
|
+
const result = await resolveProjectRoot(worktreeDir);
|
|
754
|
+
expect(result).toBe(repoDir);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test("loadConfig resolves correct root from worktree", async () => {
|
|
758
|
+
repoDir = await createTempGitRepo();
|
|
759
|
+
// Resolve symlinks (macOS /var -> /private/var) to match git's output
|
|
760
|
+
repoDir = await realpath(repoDir);
|
|
761
|
+
await mkdir(join(repoDir, ".overstory"), { recursive: true });
|
|
762
|
+
await Bun.write(
|
|
763
|
+
join(repoDir, ".overstory", "config.yaml"),
|
|
764
|
+
"project:\n canonicalBranch: develop\n",
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
const worktreeDir = join(repoDir, ".overstory", "worktrees", "agent-2");
|
|
768
|
+
await mkdir(join(repoDir, ".overstory", "worktrees"), { recursive: true });
|
|
769
|
+
await runGitInDir(repoDir, ["worktree", "add", "-b", "overstory/agent-2/task-2", worktreeDir]);
|
|
770
|
+
|
|
771
|
+
// loadConfig from the worktree should resolve to the main project root
|
|
772
|
+
const config = await loadConfig(worktreeDir);
|
|
773
|
+
expect(config.project.root).toBe(repoDir);
|
|
774
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe("DEFAULT_CONFIG", () => {
|
|
779
|
+
test("has all required top-level keys", () => {
|
|
780
|
+
expect(DEFAULT_CONFIG.project).toBeDefined();
|
|
781
|
+
expect(DEFAULT_CONFIG.agents).toBeDefined();
|
|
782
|
+
expect(DEFAULT_CONFIG.worktrees).toBeDefined();
|
|
783
|
+
expect(DEFAULT_CONFIG.taskTracker).toBeDefined();
|
|
784
|
+
expect(DEFAULT_CONFIG.mulch).toBeDefined();
|
|
785
|
+
expect(DEFAULT_CONFIG.merge).toBeDefined();
|
|
786
|
+
expect(DEFAULT_CONFIG.providers).toBeDefined();
|
|
787
|
+
expect(DEFAULT_CONFIG.watchdog).toBeDefined();
|
|
788
|
+
expect(DEFAULT_CONFIG.models).toBeDefined();
|
|
789
|
+
expect(DEFAULT_CONFIG.logging).toBeDefined();
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("has default providers with anthropic native", () => {
|
|
793
|
+
expect(DEFAULT_CONFIG.providers).toBeDefined();
|
|
794
|
+
expect(DEFAULT_CONFIG.providers.anthropic).toEqual({ type: "native" });
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("has sensible default values", () => {
|
|
798
|
+
expect(DEFAULT_CONFIG.project.canonicalBranch).toBe("main");
|
|
799
|
+
expect(DEFAULT_CONFIG.agents.maxConcurrent).toBe(25);
|
|
800
|
+
expect(DEFAULT_CONFIG.agents.maxDepth).toBe(2);
|
|
801
|
+
expect(DEFAULT_CONFIG.agents.staggerDelayMs).toBe(2_000);
|
|
802
|
+
expect(DEFAULT_CONFIG.agents.maxSessionsPerRun).toBe(0);
|
|
803
|
+
expect(DEFAULT_CONFIG.watchdog.tier0IntervalMs).toBe(30_000);
|
|
804
|
+
expect(DEFAULT_CONFIG.watchdog.staleThresholdMs).toBe(300_000);
|
|
805
|
+
expect(DEFAULT_CONFIG.watchdog.zombieThresholdMs).toBe(600_000);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("includes default qualityGates", () => {
|
|
809
|
+
expect(DEFAULT_CONFIG.project.qualityGates).toBeDefined();
|
|
810
|
+
expect(DEFAULT_CONFIG.project.qualityGates?.length).toBe(3);
|
|
811
|
+
expect(DEFAULT_CONFIG.project.qualityGates?.[0]?.command).toBe("bun test");
|
|
812
|
+
expect(DEFAULT_CONFIG.project.qualityGates?.[1]?.command).toBe("bun run lint");
|
|
813
|
+
expect(DEFAULT_CONFIG.project.qualityGates?.[2]?.command).toBe("bun run typecheck");
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("DEFAULT_QUALITY_GATES matches the project default gates", () => {
|
|
817
|
+
expect(DEFAULT_QUALITY_GATES).toHaveLength(3);
|
|
818
|
+
expect(DEFAULT_QUALITY_GATES[0]?.name).toBe("Tests");
|
|
819
|
+
expect(DEFAULT_QUALITY_GATES[1]?.name).toBe("Lint");
|
|
820
|
+
expect(DEFAULT_QUALITY_GATES[2]?.name).toBe("Typecheck");
|
|
821
|
+
});
|
|
822
|
+
});
|