-
Notifications
You must be signed in to change notification settings - Fork 121
Expand file tree
/
Copy pathLocalBaseRuntime.test.ts
More file actions
188 lines (168 loc) · 6.38 KB
/
Copy pathLocalBaseRuntime.test.ts
File metadata and controls
188 lines (168 loc) · 6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import { describe, expect, it } from "bun:test";
import * as fs from "fs/promises";
import * as os from "os";
import * as path from "path";
import { EXIT_CODE_TIMEOUT } from "@/common/constants/exitCodes";
import { LocalBaseRuntime } from "./LocalBaseRuntime";
import type {
WorkspaceCreationParams,
WorkspaceCreationResult,
WorkspaceInitParams,
WorkspaceInitResult,
WorkspaceForkParams,
WorkspaceForkResult,
} from "./Runtime";
class TestLocalRuntime extends LocalBaseRuntime {
getWorkspacePath(_projectPath: string, _workspaceName: string): string {
return "/tmp/workspace";
}
createWorkspace(_params: WorkspaceCreationParams): Promise<WorkspaceCreationResult> {
return Promise.resolve({ success: true, workspacePath: "/tmp/workspace" });
}
initWorkspace(_params: WorkspaceInitParams): Promise<WorkspaceInitResult> {
return Promise.resolve({ success: true });
}
renameWorkspace(
_projectPath: string,
_oldName: string,
_newName: string
): Promise<
{ success: true; oldPath: string; newPath: string } | { success: false; error: string }
> {
return Promise.resolve({ success: true, oldPath: "/tmp/workspace", newPath: "/tmp/workspace" });
}
deleteWorkspace(
_projectPath: string,
_workspaceName: string,
_force: boolean
): Promise<{ success: true; deletedPath: string } | { success: false; error: string }> {
return Promise.resolve({ success: true, deletedPath: "/tmp/workspace" });
}
forkWorkspace(_params: WorkspaceForkParams): Promise<WorkspaceForkResult> {
return Promise.resolve({
success: true,
workspacePath: "/tmp/workspace",
sourceBranch: "main",
});
}
}
async function readStreamAsString(stream: ReadableStream<Uint8Array>): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let output = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
return output;
}
output += decoder.decode(value, { stream: true });
}
}
describe("LocalBaseRuntime.resolvePath", () => {
it("should expand tilde to home directory", async () => {
const runtime = new TestLocalRuntime();
const resolved = await runtime.resolvePath("~");
expect(resolved).toBe(os.homedir());
});
it("should expand tilde with path", async () => {
const runtime = new TestLocalRuntime();
const resolved = await runtime.resolvePath("~/..");
const expected = path.dirname(os.homedir());
expect(resolved).toBe(expected);
});
it("should resolve absolute paths", async () => {
const runtime = new TestLocalRuntime();
const resolved = await runtime.resolvePath("/tmp");
expect(resolved).toBe("/tmp");
});
it("should resolve non-existent paths without checking existence", async () => {
const runtime = new TestLocalRuntime();
const resolved = await runtime.resolvePath("/this/path/does/not/exist/12345");
// Should resolve to absolute path without checking if it exists
expect(resolved).toBe("/this/path/does/not/exist/12345");
});
it("should resolve relative paths from cwd", async () => {
const runtime = new TestLocalRuntime();
const resolved = await runtime.resolvePath(".");
// Should resolve to absolute path
expect(path.isAbsolute(resolved)).toBe(true);
});
});
describe("LocalBaseRuntime.exec PATH handling", () => {
it("strips mux browser shims and leaked browser env from child shells", async () => {
const runtime = new TestLocalRuntime();
const tempBinDir = await fs.mkdtemp(path.join(os.tmpdir(), "mux-path-probe-"));
const vendoredBinDir = await fs.mkdtemp(path.join(os.tmpdir(), "mux-vendored-bin-"));
const commandPath = path.join(tempBinDir, "mux-path-probe");
const vendoredCommandPath = path.join(vendoredBinDir, "mux-path-probe");
const originalVendoredBinDir = process.env.MUX_VENDORED_BIN_DIR;
const originalAgentBrowserSession = process.env.AGENT_BROWSER_SESSION;
try {
await fs.writeFile(commandPath, "#!/bin/sh\necho caller-path\n", "utf8");
await fs.chmod(commandPath, 0o755);
await fs.writeFile(vendoredCommandPath, "#!/bin/sh\necho vendored-path\n", "utf8");
await fs.chmod(vendoredCommandPath, 0o755);
process.env.MUX_VENDORED_BIN_DIR = vendoredBinDir;
process.env.AGENT_BROWSER_SESSION = "mux-leaked-session";
const stream = await runtime.exec(
'printf "%s\\n" "${AGENT_BROWSER_SESSION-unset}" && mux-path-probe',
{
cwd: os.tmpdir(),
timeout: 5,
env: { PATH: `${vendoredBinDir}:${tempBinDir}:/usr/bin:/bin` },
}
);
const [stdout, exitCode] = await Promise.all([
readStreamAsString(stream.stdout),
stream.exitCode,
]);
expect(exitCode).toBe(0);
expect(stdout.trim()).toBe("unset\ncaller-path");
} finally {
if (originalVendoredBinDir == null) {
delete process.env.MUX_VENDORED_BIN_DIR;
} else {
process.env.MUX_VENDORED_BIN_DIR = originalVendoredBinDir;
}
if (originalAgentBrowserSession == null) {
delete process.env.AGENT_BROWSER_SESSION;
} else {
process.env.AGENT_BROWSER_SESSION = originalAgentBrowserSession;
}
await fs.rm(tempBinDir, { recursive: true, force: true });
await fs.rm(vendoredBinDir, { recursive: true, force: true });
}
});
});
describe("LocalBaseRuntime.exec timeout", () => {
it("should resolve exitCode with EXIT_CODE_TIMEOUT when command exceeds timeout", async () => {
const runtime = new TestLocalRuntime();
const stream = await runtime.exec("sleep 30", {
cwd: os.tmpdir(),
timeout: 1,
});
const exitCode = await stream.exitCode;
expect(exitCode).toBe(EXIT_CODE_TIMEOUT);
});
it("should close stdout/stderr streams on timeout so readers don't hang", async () => {
const runtime = new TestLocalRuntime();
const stream = await runtime.exec("sleep 30", {
cwd: os.tmpdir(),
timeout: 1,
});
// This mimics what bash.ts does: read streams AND await exitCode concurrently.
// Without the fix, consumeStream hangs on Windows because the reader never sees EOF.
const [exitCode] = await Promise.all([
stream.exitCode,
stream.stdout
.getReader()
.read()
.then(({ done }) => done),
stream.stderr
.getReader()
.read()
.then(({ done }) => done),
]);
expect(exitCode).toBe(EXIT_CODE_TIMEOUT);
});
});