|
1 | 1 | import { describe, it, expect, beforeEach, afterEach } from "vitest"; |
2 | | -import { installProject, mergeClaudeMd, isInitialized } from "../src/installer.js"; |
| 2 | +import { installProject, copyBundledAssets, mergeClaudeMd, isInitialized } from "../src/installer.js"; |
3 | 3 | import { mkdir, rm, access, readFile, writeFile } from "fs/promises"; |
4 | 4 | import { join } from "path"; |
5 | 5 | import { tmpdir } from "os"; |
@@ -119,6 +119,96 @@ describe("installer", () => { |
119 | 119 | }); |
120 | 120 | }); |
121 | 121 |
|
| 122 | + describe("copyBundledAssets", { timeout: 30000 }, () => { |
| 123 | + it("copies all expected files", async () => { |
| 124 | + // Create minimal directory structure (simulating existing init) |
| 125 | + await mkdir(join(testDir, ".ralph"), { recursive: true }); |
| 126 | + await mkdir(join(testDir, ".claude/commands"), { recursive: true }); |
| 127 | + |
| 128 | + const result = await copyBundledAssets(testDir); |
| 129 | + |
| 130 | + await expect(access(join(testDir, "_bmad/core"))).resolves.toBeUndefined(); |
| 131 | + await expect(access(join(testDir, "_bmad/config.yaml"))).resolves.toBeUndefined(); |
| 132 | + await expect(access(join(testDir, ".ralph/ralph_loop.sh"))).resolves.toBeUndefined(); |
| 133 | + await expect(access(join(testDir, ".ralph/lib"))).resolves.toBeUndefined(); |
| 134 | + await expect(access(join(testDir, ".ralph/PROMPT.md"))).resolves.toBeUndefined(); |
| 135 | + await expect(access(join(testDir, ".ralph/@AGENT.md"))).resolves.toBeUndefined(); |
| 136 | + await expect(access(join(testDir, ".claude/commands/bmalph.md"))).resolves.toBeUndefined(); |
| 137 | + expect(result.updatedPaths.length).toBeGreaterThan(0); |
| 138 | + }); |
| 139 | + |
| 140 | + it("does NOT create bmalph/state/ or .ralph/logs/", async () => { |
| 141 | + const result = await copyBundledAssets(testDir); |
| 142 | + |
| 143 | + await expect(access(join(testDir, "bmalph/state"))).rejects.toThrow(); |
| 144 | + await expect(access(join(testDir, ".ralph/logs"))).rejects.toThrow(); |
| 145 | + expect(result.updatedPaths).not.toContain("bmalph/state/"); |
| 146 | + }); |
| 147 | + |
| 148 | + it("preserves existing .ralph/@fix_plan.md", async () => { |
| 149 | + await mkdir(join(testDir, ".ralph"), { recursive: true }); |
| 150 | + await writeFile(join(testDir, ".ralph/@fix_plan.md"), "# My Plan\n- task 1"); |
| 151 | + |
| 152 | + await copyBundledAssets(testDir); |
| 153 | + |
| 154 | + const content = await readFile(join(testDir, ".ralph/@fix_plan.md"), "utf-8"); |
| 155 | + expect(content).toBe("# My Plan\n- task 1"); |
| 156 | + }); |
| 157 | + |
| 158 | + it("preserves existing .ralph/logs/ content", async () => { |
| 159 | + await mkdir(join(testDir, ".ralph/logs"), { recursive: true }); |
| 160 | + await writeFile(join(testDir, ".ralph/logs/run-001.log"), "log content"); |
| 161 | + |
| 162 | + await copyBundledAssets(testDir); |
| 163 | + |
| 164 | + const content = await readFile(join(testDir, ".ralph/logs/run-001.log"), "utf-8"); |
| 165 | + expect(content).toBe("log content"); |
| 166 | + }); |
| 167 | + |
| 168 | + it("preserves existing bmalph/config.json", async () => { |
| 169 | + await mkdir(join(testDir, "bmalph"), { recursive: true }); |
| 170 | + await writeFile( |
| 171 | + join(testDir, "bmalph/config.json"), |
| 172 | + JSON.stringify({ name: "my-project", level: 3 }), |
| 173 | + ); |
| 174 | + |
| 175 | + await copyBundledAssets(testDir); |
| 176 | + |
| 177 | + const config = JSON.parse( |
| 178 | + await readFile(join(testDir, "bmalph/config.json"), "utf-8"), |
| 179 | + ); |
| 180 | + expect(config.name).toBe("my-project"); |
| 181 | + expect(config.level).toBe(3); |
| 182 | + }); |
| 183 | + |
| 184 | + it("is idempotent (twice = same result)", async () => { |
| 185 | + await copyBundledAssets(testDir); |
| 186 | + const firstRun = await readFile(join(testDir, ".ralph/ralph_loop.sh"), "utf-8"); |
| 187 | + |
| 188 | + await copyBundledAssets(testDir); |
| 189 | + const secondRun = await readFile(join(testDir, ".ralph/ralph_loop.sh"), "utf-8"); |
| 190 | + |
| 191 | + expect(firstRun).toBe(secondRun); |
| 192 | + |
| 193 | + // .gitignore should not duplicate entries |
| 194 | + const gitignore = await readFile(join(testDir, ".gitignore"), "utf-8"); |
| 195 | + const matches = gitignore.match(/\.ralph\/logs\//g); |
| 196 | + expect(matches).toHaveLength(1); |
| 197 | + }); |
| 198 | + |
| 199 | + it("returns list of updated paths", async () => { |
| 200 | + const result = await copyBundledAssets(testDir); |
| 201 | + |
| 202 | + expect(result.updatedPaths).toContain("_bmad/"); |
| 203 | + expect(result.updatedPaths).toContain(".ralph/ralph_loop.sh"); |
| 204 | + expect(result.updatedPaths).toContain(".ralph/lib/"); |
| 205 | + expect(result.updatedPaths).toContain(".ralph/PROMPT.md"); |
| 206 | + expect(result.updatedPaths).toContain(".ralph/@AGENT.md"); |
| 207 | + expect(result.updatedPaths).toContain(".claude/commands/bmalph.md"); |
| 208 | + expect(result.updatedPaths).toContain(".gitignore"); |
| 209 | + }); |
| 210 | + }); |
| 211 | + |
122 | 212 | describe("mergeClaudeMd", () => { |
123 | 213 | it("creates CLAUDE.md if it does not exist", async () => { |
124 | 214 | await mergeClaudeMd(testDir); |
|
0 commit comments