Skip to content

Commit c34e725

Browse files
committed
test: add LocalRuntime unit tests
Add comprehensive test coverage for LocalRuntime: - Constructor and getWorkspacePath behavior (ignores args, returns project path) - createWorkspace success/failure cases - deleteWorkspace no-op behavior (doesn't delete anything) - renameWorkspace/forkWorkspace unsupported operation errors - Inherited LocalBaseRuntime methods (exec, stat, resolvePath, normalizePath) 12 new tests covering the project-dir runtime behavior.
1 parent 56094dc commit c34e725

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { describe, expect, it, beforeAll, afterAll } from "bun:test";
2+
import * as os from "os";
3+
import * as path from "path";
4+
import * as fs from "fs/promises";
5+
import { LocalRuntime } from "./LocalRuntime";
6+
import type { InitLogger } from "./Runtime";
7+
8+
// Minimal mock logger - matches pattern in initHook.test.ts
9+
function createMockLogger(): InitLogger & { steps: string[] } {
10+
const steps: string[] = [];
11+
return {
12+
steps,
13+
logStep: (msg: string) => steps.push(msg),
14+
logStdout: () => {
15+
/* no-op for test */
16+
},
17+
logStderr: () => {
18+
/* no-op for test */
19+
},
20+
logComplete: () => {
21+
/* no-op for test */
22+
},
23+
};
24+
}
25+
26+
describe("LocalRuntime", () => {
27+
// Use a temp directory for tests
28+
let testDir: string;
29+
30+
beforeAll(async () => {
31+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), "localruntime-test-"));
32+
});
33+
34+
afterAll(async () => {
35+
await fs.rm(testDir, { recursive: true, force: true });
36+
});
37+
38+
describe("constructor and getWorkspacePath", () => {
39+
it("stores projectPath and returns it regardless of arguments", () => {
40+
const runtime = new LocalRuntime("/home/user/my-project");
41+
// Both arguments are ignored - always returns the project path
42+
expect(runtime.getWorkspacePath("/other/path", "some-branch")).toBe("/home/user/my-project");
43+
expect(runtime.getWorkspacePath("", "")).toBe("/home/user/my-project");
44+
});
45+
46+
it("does not expand tilde (unlike WorktreeRuntime)", () => {
47+
// LocalRuntime stores the path as-is; callers must pass expanded paths
48+
const runtime = new LocalRuntime("~/my-project");
49+
expect(runtime.getWorkspacePath("", "")).toBe("~/my-project");
50+
});
51+
});
52+
53+
describe("createWorkspace", () => {
54+
it("succeeds when directory exists", async () => {
55+
const runtime = new LocalRuntime(testDir);
56+
const logger = createMockLogger();
57+
58+
const result = await runtime.createWorkspace({
59+
projectPath: testDir,
60+
branchName: "main",
61+
trunkBranch: "main",
62+
directoryName: "main",
63+
initLogger: logger,
64+
});
65+
66+
expect(result.success).toBe(true);
67+
expect(result.workspacePath).toBe(testDir);
68+
expect(logger.steps.length).toBeGreaterThan(0);
69+
expect(logger.steps.some((s) => s.includes("project directory"))).toBe(true);
70+
});
71+
72+
it("fails when directory does not exist", async () => {
73+
const nonExistentPath = path.join(testDir, "does-not-exist");
74+
const runtime = new LocalRuntime(nonExistentPath);
75+
const logger = createMockLogger();
76+
77+
const result = await runtime.createWorkspace({
78+
projectPath: nonExistentPath,
79+
branchName: "main",
80+
trunkBranch: "main",
81+
directoryName: "main",
82+
initLogger: logger,
83+
});
84+
85+
expect(result.success).toBe(false);
86+
expect(result.error).toContain("does not exist");
87+
});
88+
});
89+
90+
describe("deleteWorkspace", () => {
91+
it("returns success without deleting anything", async () => {
92+
const runtime = new LocalRuntime(testDir);
93+
94+
// Create a test file to verify it isn't deleted
95+
const testFile = path.join(testDir, "delete-test.txt");
96+
await fs.writeFile(testFile, "should not be deleted");
97+
98+
const result = await runtime.deleteWorkspace(testDir, "main", false);
99+
100+
expect(result.success).toBe(true);
101+
if (result.success) {
102+
expect(result.deletedPath).toBe(testDir);
103+
}
104+
105+
// Verify file still exists
106+
const fileStillExists = await fs.access(testFile).then(
107+
() => true,
108+
() => false
109+
);
110+
expect(fileStillExists).toBe(true);
111+
112+
// Cleanup
113+
await fs.unlink(testFile);
114+
});
115+
116+
it("returns success even with force=true (still no-op)", async () => {
117+
const runtime = new LocalRuntime(testDir);
118+
119+
const result = await runtime.deleteWorkspace(testDir, "main", true);
120+
121+
expect(result.success).toBe(true);
122+
if (result.success) {
123+
expect(result.deletedPath).toBe(testDir);
124+
}
125+
// Directory should still exist
126+
const dirExists = await fs.access(testDir).then(
127+
() => true,
128+
() => false
129+
);
130+
expect(dirExists).toBe(true);
131+
});
132+
});
133+
134+
describe("renameWorkspace", () => {
135+
it("returns error - operation not supported", async () => {
136+
const runtime = new LocalRuntime(testDir);
137+
138+
const result = await runtime.renameWorkspace(testDir, "old", "new");
139+
140+
expect(result.success).toBe(false);
141+
if (!result.success) {
142+
expect(result.error).toContain("Cannot rename");
143+
expect(result.error).toContain("project-dir");
144+
}
145+
});
146+
});
147+
148+
describe("forkWorkspace", () => {
149+
it("returns error - operation not supported", async () => {
150+
const runtime = new LocalRuntime(testDir);
151+
const logger = createMockLogger();
152+
153+
const result = await runtime.forkWorkspace({
154+
projectPath: testDir,
155+
sourceWorkspaceName: "main",
156+
newWorkspaceName: "feature",
157+
initLogger: logger,
158+
});
159+
160+
expect(result.success).toBe(false);
161+
expect(result.error).toContain("Cannot fork");
162+
expect(result.error).toContain("project-dir");
163+
});
164+
});
165+
166+
describe("inherited LocalBaseRuntime methods", () => {
167+
it("exec runs commands in projectPath", async () => {
168+
const runtime = new LocalRuntime(testDir);
169+
170+
const stream = await runtime.exec("pwd", {
171+
cwd: testDir,
172+
timeout: 10,
173+
});
174+
175+
const reader = stream.stdout.getReader();
176+
let output = "";
177+
while (true) {
178+
const { done, value } = await reader.read();
179+
if (done) break;
180+
output += new TextDecoder().decode(value);
181+
}
182+
183+
const exitCode = await stream.exitCode;
184+
expect(exitCode).toBe(0);
185+
expect(output.trim()).toBe(testDir);
186+
});
187+
188+
it("stat works on projectPath", async () => {
189+
const runtime = new LocalRuntime(testDir);
190+
191+
const stat = await runtime.stat(testDir);
192+
193+
expect(stat.isDirectory).toBe(true);
194+
});
195+
196+
it("resolvePath expands tilde", async () => {
197+
const runtime = new LocalRuntime(testDir);
198+
199+
const resolved = await runtime.resolvePath("~");
200+
201+
expect(resolved).toBe(os.homedir());
202+
});
203+
204+
it("normalizePath resolves relative paths", () => {
205+
const runtime = new LocalRuntime(testDir);
206+
207+
const result = runtime.normalizePath(".", testDir);
208+
209+
expect(result).toBe(testDir);
210+
});
211+
});
212+
});

0 commit comments

Comments
 (0)