|
1 | 1 | import { afterEach, beforeEach, expect, test } from "bun:test"; |
2 | | -import { mkdtemp, rm } from "node:fs/promises"; |
| 2 | +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; |
3 | 3 | import { tmpdir } from "node:os"; |
4 | 4 | import { join } from "node:path"; |
5 | 5 |
|
@@ -116,6 +116,78 @@ test("usage errors under --json emit an E_USAGE envelope instead of help text", |
116 | 116 | expect(result.stdout).not.toContain("Usage:"); |
117 | 117 | }); |
118 | 118 |
|
| 119 | +for (const action of ["up", "down", "restart"] as const) { |
| 120 | + test(`${action} --json preserves E_USAGE for a detached linked worktree`, async () => { |
| 121 | + const worktreeRoot = await createDetachedWorktreeFixture(); |
| 122 | + const result = await runCliWithCapturedOutput([ |
| 123 | + action, |
| 124 | + "--json", |
| 125 | + "--path", |
| 126 | + worktreeRoot, |
| 127 | + ]); |
| 128 | + |
| 129 | + expect(result.exitCode).toBe(1); |
| 130 | + const parsed = JSON.parse(result.stdout) as { |
| 131 | + ok: boolean; |
| 132 | + error?: { code: string; message: string }; |
| 133 | + }; |
| 134 | + expect(parsed.ok).toBe(false); |
| 135 | + expect(parsed.error?.code).toBe("E_USAGE"); |
| 136 | + expect(parsed.error?.message).toContain("Detached linked worktree"); |
| 137 | + expect(parsed.error?.message).toContain("--branch <name>"); |
| 138 | + }); |
| 139 | +} |
| 140 | + |
| 141 | +async function createDetachedWorktreeFixture(): Promise<string> { |
| 142 | + if (!tempDir) { |
| 143 | + throw new Error("Missing temp directory"); |
| 144 | + } |
| 145 | + |
| 146 | + const primaryRoot = join(tempDir, "repo-primary"); |
| 147 | + const hackDir = join(primaryRoot, ".hack"); |
| 148 | + await mkdir(hackDir, { recursive: true }); |
| 149 | + await writeFile( |
| 150 | + join(hackDir, "hack.config.json"), |
| 151 | + `${JSON.stringify({ name: "json-worktree", dev_host: "json-worktree.hack" }, null, 2)}\n` |
| 152 | + ); |
| 153 | + await writeFile( |
| 154 | + join(hackDir, "docker-compose.yml"), |
| 155 | + ["services:", " api:", " image: alpine:3.20", ""].join("\n") |
| 156 | + ); |
| 157 | + await writeFile(join(primaryRoot, "README.md"), "# fixture\n"); |
| 158 | + |
| 159 | + runGit({ cwd: primaryRoot, args: ["init", "-b", "main"] }); |
| 160 | + runGit({ |
| 161 | + cwd: primaryRoot, |
| 162 | + args: ["config", "user.email", "test@example.com"], |
| 163 | + }); |
| 164 | + runGit({ cwd: primaryRoot, args: ["config", "user.name", "Test User"] }); |
| 165 | + runGit({ cwd: primaryRoot, args: ["add", "."] }); |
| 166 | + runGit({ cwd: primaryRoot, args: ["commit", "-m", "init"] }); |
| 167 | + |
| 168 | + const worktreeRoot = join(tempDir, "repo-worktree"); |
| 169 | + runGit({ |
| 170 | + cwd: primaryRoot, |
| 171 | + args: ["worktree", "add", "-b", "feature/json", worktreeRoot], |
| 172 | + }); |
| 173 | + runGit({ cwd: worktreeRoot, args: ["checkout", "--detach"] }); |
| 174 | + return worktreeRoot; |
| 175 | +} |
| 176 | + |
| 177 | +function runGit(opts: { |
| 178 | + readonly cwd: string; |
| 179 | + readonly args: readonly string[]; |
| 180 | +}): void { |
| 181 | + const result = Bun.spawnSync(["git", "-C", opts.cwd, ...opts.args], { |
| 182 | + stdin: "ignore", |
| 183 | + stdout: "pipe", |
| 184 | + stderr: "pipe", |
| 185 | + }); |
| 186 | + if (result.exitCode !== 0) { |
| 187 | + throw new Error(Buffer.from(result.stderr).toString("utf8")); |
| 188 | + } |
| 189 | +} |
| 190 | + |
119 | 191 | async function runCliWithCapturedOutput( |
120 | 192 | args: readonly string[] |
121 | 193 | ): Promise<CapturedRunResult> { |
|
0 commit comments