@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,347 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { cleanupTempDir, createTempGitRepo, runGitInDir } from "../test-helpers.ts";
|
|
5
|
+
import { initCommand, OVERSTORY_GITIGNORE, OVERSTORY_README } from "./init.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tests for `overstory init` -- agent definition deployment.
|
|
9
|
+
*
|
|
10
|
+
* Uses real temp git repos. Suppresses stdout to keep test output clean.
|
|
11
|
+
* process.cwd() is saved/restored because initCommand uses it to find the project root.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const AGENT_DEF_FILES = [
|
|
15
|
+
"scout.md",
|
|
16
|
+
"builder.md",
|
|
17
|
+
"reviewer.md",
|
|
18
|
+
"lead.md",
|
|
19
|
+
"merger.md",
|
|
20
|
+
"supervisor.md",
|
|
21
|
+
"coordinator.md",
|
|
22
|
+
"monitor.md",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Resolve the source agents directory (same logic as init.ts). */
|
|
26
|
+
const SOURCE_AGENTS_DIR = join(import.meta.dir, "..", "..", "agents");
|
|
27
|
+
|
|
28
|
+
describe("initCommand: agent-defs deployment", () => {
|
|
29
|
+
let tempDir: string;
|
|
30
|
+
let originalCwd: string;
|
|
31
|
+
let originalWrite: typeof process.stdout.write;
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
tempDir = await createTempGitRepo();
|
|
35
|
+
originalCwd = process.cwd();
|
|
36
|
+
process.chdir(tempDir);
|
|
37
|
+
|
|
38
|
+
// Suppress stdout noise from initCommand
|
|
39
|
+
originalWrite = process.stdout.write;
|
|
40
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
process.chdir(originalCwd);
|
|
45
|
+
process.stdout.write = originalWrite;
|
|
46
|
+
await cleanupTempDir(tempDir);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("creates .overstory/agent-defs/ with all 8 agent definition files", async () => {
|
|
50
|
+
await initCommand([]);
|
|
51
|
+
|
|
52
|
+
const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
|
|
53
|
+
const files = await readdir(agentDefsDir);
|
|
54
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
55
|
+
|
|
56
|
+
expect(mdFiles).toEqual(AGENT_DEF_FILES.slice().sort());
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("copied files match source content", async () => {
|
|
60
|
+
await initCommand([]);
|
|
61
|
+
|
|
62
|
+
for (const fileName of AGENT_DEF_FILES) {
|
|
63
|
+
const sourcePath = join(SOURCE_AGENTS_DIR, fileName);
|
|
64
|
+
const targetPath = join(tempDir, ".overstory", "agent-defs", fileName);
|
|
65
|
+
|
|
66
|
+
const sourceContent = await Bun.file(sourcePath).text();
|
|
67
|
+
const targetContent = await Bun.file(targetPath).text();
|
|
68
|
+
|
|
69
|
+
expect(targetContent).toBe(sourceContent);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("--force reinit overwrites existing agent def files", async () => {
|
|
74
|
+
// First init
|
|
75
|
+
await initCommand([]);
|
|
76
|
+
|
|
77
|
+
// Tamper with one of the deployed files
|
|
78
|
+
const tamperPath = join(tempDir, ".overstory", "agent-defs", "scout.md");
|
|
79
|
+
await Bun.write(tamperPath, "# tampered content\n");
|
|
80
|
+
|
|
81
|
+
// Verify tamper worked
|
|
82
|
+
const tampered = await Bun.file(tamperPath).text();
|
|
83
|
+
expect(tampered).toBe("# tampered content\n");
|
|
84
|
+
|
|
85
|
+
// Reinit with --force
|
|
86
|
+
await initCommand(["--force"]);
|
|
87
|
+
|
|
88
|
+
// Verify the file was overwritten with the original source
|
|
89
|
+
const sourceContent = await Bun.file(join(SOURCE_AGENTS_DIR, "scout.md")).text();
|
|
90
|
+
const restored = await Bun.file(tamperPath).text();
|
|
91
|
+
expect(restored).toBe(sourceContent);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("Stop hook includes mulch learn command", async () => {
|
|
95
|
+
await initCommand([]);
|
|
96
|
+
|
|
97
|
+
const hooksPath = join(tempDir, ".overstory", "hooks.json");
|
|
98
|
+
const content = await Bun.file(hooksPath).text();
|
|
99
|
+
const parsed = JSON.parse(content);
|
|
100
|
+
const stopHooks = parsed.hooks.Stop[0].hooks;
|
|
101
|
+
|
|
102
|
+
expect(stopHooks.length).toBe(2);
|
|
103
|
+
expect(stopHooks[0].command).toContain("overstory log session-end");
|
|
104
|
+
expect(stopHooks[1].command).toBe("mulch learn");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("PostToolUse hooks include Bash-matched mulch diff hook", async () => {
|
|
108
|
+
await initCommand([]);
|
|
109
|
+
|
|
110
|
+
const hooksPath = join(tempDir, ".overstory", "hooks.json");
|
|
111
|
+
const content = await Bun.file(hooksPath).text();
|
|
112
|
+
const parsed = JSON.parse(content);
|
|
113
|
+
const postToolUseHooks = parsed.hooks.PostToolUse;
|
|
114
|
+
|
|
115
|
+
// Should have the generic tool-end logger plus the new Bash-specific hook
|
|
116
|
+
expect(postToolUseHooks.length).toBe(2);
|
|
117
|
+
|
|
118
|
+
const bashHookEntry = postToolUseHooks[1];
|
|
119
|
+
expect(bashHookEntry.matcher).toBe("Bash");
|
|
120
|
+
expect(bashHookEntry.hooks.length).toBe(1);
|
|
121
|
+
|
|
122
|
+
const command = bashHookEntry.hooks[0].command;
|
|
123
|
+
expect(command).toContain("git commit");
|
|
124
|
+
expect(command).toContain("mulch diff HEAD~1");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("initCommand: .overstory/.gitignore", () => {
|
|
129
|
+
let tempDir: string;
|
|
130
|
+
let originalCwd: string;
|
|
131
|
+
let originalWrite: typeof process.stdout.write;
|
|
132
|
+
|
|
133
|
+
beforeEach(async () => {
|
|
134
|
+
tempDir = await createTempGitRepo();
|
|
135
|
+
originalCwd = process.cwd();
|
|
136
|
+
process.chdir(tempDir);
|
|
137
|
+
|
|
138
|
+
// Suppress stdout noise from initCommand
|
|
139
|
+
originalWrite = process.stdout.write;
|
|
140
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
afterEach(async () => {
|
|
144
|
+
process.chdir(originalCwd);
|
|
145
|
+
process.stdout.write = originalWrite;
|
|
146
|
+
await cleanupTempDir(tempDir);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("creates .overstory/.gitignore with wildcard+whitelist model", async () => {
|
|
150
|
+
await initCommand([]);
|
|
151
|
+
|
|
152
|
+
const gitignorePath = join(tempDir, ".overstory", ".gitignore");
|
|
153
|
+
const content = await Bun.file(gitignorePath).text();
|
|
154
|
+
|
|
155
|
+
// Verify wildcard+whitelist pattern
|
|
156
|
+
expect(content).toContain("*\n");
|
|
157
|
+
expect(content).toContain("!.gitignore\n");
|
|
158
|
+
expect(content).toContain("!config.yaml\n");
|
|
159
|
+
expect(content).toContain("!agent-manifest.json\n");
|
|
160
|
+
expect(content).toContain("!hooks.json\n");
|
|
161
|
+
expect(content).toContain("!groups.json\n");
|
|
162
|
+
expect(content).toContain("!agent-defs/\n");
|
|
163
|
+
|
|
164
|
+
// Verify it matches the exported constant
|
|
165
|
+
expect(content).toBe(OVERSTORY_GITIGNORE);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("gitignore is always written when init completes", async () => {
|
|
169
|
+
// Init should write gitignore
|
|
170
|
+
await initCommand([]);
|
|
171
|
+
|
|
172
|
+
const gitignorePath = join(tempDir, ".overstory", ".gitignore");
|
|
173
|
+
const content = await Bun.file(gitignorePath).text();
|
|
174
|
+
|
|
175
|
+
// Verify gitignore was written with correct content
|
|
176
|
+
expect(content).toBe(OVERSTORY_GITIGNORE);
|
|
177
|
+
|
|
178
|
+
// Verify the file exists
|
|
179
|
+
const exists = await Bun.file(gitignorePath).exists();
|
|
180
|
+
expect(exists).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("--force reinit overwrites stale .overstory/.gitignore", async () => {
|
|
184
|
+
// First init
|
|
185
|
+
await initCommand([]);
|
|
186
|
+
|
|
187
|
+
const gitignorePath = join(tempDir, ".overstory", ".gitignore");
|
|
188
|
+
|
|
189
|
+
// Tamper with the gitignore file (simulate old deny-list format)
|
|
190
|
+
await Bun.write(gitignorePath, "# old format\nworktrees/\nlogs/\nmail.db\n");
|
|
191
|
+
|
|
192
|
+
// Verify tamper worked
|
|
193
|
+
const tampered = await Bun.file(gitignorePath).text();
|
|
194
|
+
expect(tampered).not.toContain("*\n");
|
|
195
|
+
expect(tampered).not.toContain("!.gitignore\n");
|
|
196
|
+
|
|
197
|
+
// Reinit with --force
|
|
198
|
+
await initCommand(["--force"]);
|
|
199
|
+
|
|
200
|
+
// Verify the file was overwritten with the new wildcard+whitelist format
|
|
201
|
+
const restored = await Bun.file(gitignorePath).text();
|
|
202
|
+
expect(restored).toBe(OVERSTORY_GITIGNORE);
|
|
203
|
+
expect(restored).toContain("*\n");
|
|
204
|
+
expect(restored).toContain("!.gitignore\n");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("subsequent init without --force does not overwrite gitignore", async () => {
|
|
208
|
+
// First init
|
|
209
|
+
await initCommand([]);
|
|
210
|
+
|
|
211
|
+
const gitignorePath = join(tempDir, ".overstory", ".gitignore");
|
|
212
|
+
|
|
213
|
+
// Tamper with the gitignore file
|
|
214
|
+
await Bun.write(gitignorePath, "# custom content\n");
|
|
215
|
+
|
|
216
|
+
// Verify tamper worked
|
|
217
|
+
const tampered = await Bun.file(gitignorePath).text();
|
|
218
|
+
expect(tampered).toBe("# custom content\n");
|
|
219
|
+
|
|
220
|
+
// Second init without --force should return early (not overwrite)
|
|
221
|
+
await initCommand([]);
|
|
222
|
+
|
|
223
|
+
// Verify the file was NOT overwritten (early return prevented it)
|
|
224
|
+
const afterSecondInit = await Bun.file(gitignorePath).text();
|
|
225
|
+
expect(afterSecondInit).toBe("# custom content\n");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("initCommand: .overstory/README.md", () => {
|
|
230
|
+
let tempDir: string;
|
|
231
|
+
let originalCwd: string;
|
|
232
|
+
let originalWrite: typeof process.stdout.write;
|
|
233
|
+
|
|
234
|
+
beforeEach(async () => {
|
|
235
|
+
tempDir = await createTempGitRepo();
|
|
236
|
+
originalCwd = process.cwd();
|
|
237
|
+
process.chdir(tempDir);
|
|
238
|
+
|
|
239
|
+
// Suppress stdout noise from initCommand
|
|
240
|
+
originalWrite = process.stdout.write;
|
|
241
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
afterEach(async () => {
|
|
245
|
+
process.chdir(originalCwd);
|
|
246
|
+
process.stdout.write = originalWrite;
|
|
247
|
+
await cleanupTempDir(tempDir);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("creates .overstory/README.md with expected content", async () => {
|
|
251
|
+
await initCommand([]);
|
|
252
|
+
|
|
253
|
+
const readmePath = join(tempDir, ".overstory", "README.md");
|
|
254
|
+
const exists = await Bun.file(readmePath).exists();
|
|
255
|
+
expect(exists).toBe(true);
|
|
256
|
+
|
|
257
|
+
const content = await Bun.file(readmePath).text();
|
|
258
|
+
expect(content).toBe(OVERSTORY_README);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("README.md is whitelisted in gitignore", () => {
|
|
262
|
+
expect(OVERSTORY_GITIGNORE).toContain("!README.md\n");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("--force reinit overwrites README.md", async () => {
|
|
266
|
+
// First init
|
|
267
|
+
await initCommand([]);
|
|
268
|
+
|
|
269
|
+
const readmePath = join(tempDir, ".overstory", "README.md");
|
|
270
|
+
|
|
271
|
+
// Tamper with the README
|
|
272
|
+
await Bun.write(readmePath, "# tampered\n");
|
|
273
|
+
const tampered = await Bun.file(readmePath).text();
|
|
274
|
+
expect(tampered).toBe("# tampered\n");
|
|
275
|
+
|
|
276
|
+
// Reinit with --force
|
|
277
|
+
await initCommand(["--force"]);
|
|
278
|
+
|
|
279
|
+
// Verify restored to canonical content
|
|
280
|
+
const restored = await Bun.file(readmePath).text();
|
|
281
|
+
expect(restored).toBe(OVERSTORY_README);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("subsequent init without --force does not overwrite README.md", async () => {
|
|
285
|
+
// First init
|
|
286
|
+
await initCommand([]);
|
|
287
|
+
|
|
288
|
+
const readmePath = join(tempDir, ".overstory", "README.md");
|
|
289
|
+
|
|
290
|
+
// Tamper with the README
|
|
291
|
+
await Bun.write(readmePath, "# custom content\n");
|
|
292
|
+
const tampered = await Bun.file(readmePath).text();
|
|
293
|
+
expect(tampered).toBe("# custom content\n");
|
|
294
|
+
|
|
295
|
+
// Second init without --force returns early
|
|
296
|
+
await initCommand([]);
|
|
297
|
+
|
|
298
|
+
// Verify tampered content preserved (early return)
|
|
299
|
+
const afterSecondInit = await Bun.file(readmePath).text();
|
|
300
|
+
expect(afterSecondInit).toBe("# custom content\n");
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("initCommand: canonical branch detection", () => {
|
|
305
|
+
let tempDir: string;
|
|
306
|
+
let originalCwd: string;
|
|
307
|
+
let originalWrite: typeof process.stdout.write;
|
|
308
|
+
|
|
309
|
+
beforeEach(async () => {
|
|
310
|
+
tempDir = await createTempGitRepo();
|
|
311
|
+
originalCwd = process.cwd();
|
|
312
|
+
// Remove origin remote so detectCanonicalBranch falls through to
|
|
313
|
+
// current-branch check (otherwise remote HEAD resolves to main regardless)
|
|
314
|
+
await runGitInDir(tempDir, ["remote", "remove", "origin"]);
|
|
315
|
+
process.chdir(tempDir);
|
|
316
|
+
|
|
317
|
+
// Suppress stdout noise from initCommand
|
|
318
|
+
originalWrite = process.stdout.write;
|
|
319
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
afterEach(async () => {
|
|
323
|
+
process.chdir(originalCwd);
|
|
324
|
+
process.stdout.write = originalWrite;
|
|
325
|
+
await cleanupTempDir(tempDir);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("non-standard branch names are accepted as canonicalBranch", async () => {
|
|
329
|
+
// Switch to a non-standard branch name
|
|
330
|
+
await runGitInDir(tempDir, ["switch", "-c", "trunk"]);
|
|
331
|
+
|
|
332
|
+
await initCommand([]);
|
|
333
|
+
|
|
334
|
+
const configPath = join(tempDir, ".overstory", "config.yaml");
|
|
335
|
+
const content = await Bun.file(configPath).text();
|
|
336
|
+
expect(content).toContain("canonicalBranch: trunk");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("standard branch names (main) still work as canonicalBranch", async () => {
|
|
340
|
+
// createTempGitRepo defaults to main branch
|
|
341
|
+
await initCommand([]);
|
|
342
|
+
|
|
343
|
+
const configPath = join(tempDir, ".overstory", "config.yaml");
|
|
344
|
+
const content = await Bun.file(configPath).text();
|
|
345
|
+
expect(content).toContain("canonicalBranch: main");
|
|
346
|
+
});
|
|
347
|
+
});
|