Skip to content

Commit e2ee558

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 e2ee558

File tree

1 file changed

+217
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)