Skip to content

Commit 50f39f3

Browse files
committed
refactor: improve handling of working directory in execCommand
- Introduced effectiveWorkdir to manage the working directory more robustly. - Updated logging to reflect the effective working directory used during command execution. - Added a new test suite for handleExecCommand to ensure proper functionality and error handling.
1 parent 45b266e commit 50f39f3

File tree

2 files changed

+217
-8
lines changed

2 files changed

+217
-8
lines changed

codex-cli/src/utils/agent/handle-exec-command.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,22 @@ async function execCommand(
218218
abortSignal?: AbortSignal,
219219
config?: AppConfig,
220220
): Promise<ExecCommandSummary> {
221-
let { workdir } = execInput;
222-
if (workdir) {
221+
// Use a separate variable to track the effective working directory
222+
let effectiveWorkdir = execInput.workdir;
223+
if (effectiveWorkdir) {
223224
try {
224-
await access(workdir);
225+
await access(effectiveWorkdir);
225226
} catch (e) {
226-
log(`EXEC workdir=${workdir} not found, use process.cwd() instead`);
227-
workdir = process.cwd();
227+
log(
228+
`EXEC workdir=${effectiveWorkdir} not found, use process.cwd() instead`,
229+
);
230+
// Update the effective working directory if access fails
231+
effectiveWorkdir = process.cwd();
228232
}
233+
} else {
234+
// If no workdir was provided, default to cwd
235+
effectiveWorkdir = process.cwd();
236+
log(`EXEC workdir not provided, using process.cwd() = ${effectiveWorkdir}`);
229237
}
230238

231239
if (applyPatchCommand != null) {
@@ -241,7 +249,7 @@ async function execCommand(
241249
log(
242250
`EXEC running \`${formatCommandForDisplay(
243251
cmd,
244-
)}\` in workdir=${workdir} with timeout=${timeout}s`,
252+
)}\` in workdir=${effectiveWorkdir} with timeout=${timeout}s`, // Log the effective workdir
245253
);
246254
}
247255

@@ -251,9 +259,15 @@ async function execCommand(
251259
const start = Date.now();
252260
const execResult =
253261
applyPatchCommand != null
254-
? execApplyPatch(applyPatchCommand.patch, workdir)
262+
? execApplyPatch(applyPatchCommand.patch, effectiveWorkdir) // Use effective workdir
255263
: await exec(
256-
{ ...execInput, additionalWritableRoots },
264+
{
265+
// Construct the input using the effective workdir
266+
cmd: execInput.cmd,
267+
timeoutInMillis: execInput.timeoutInMillis,
268+
workdir: effectiveWorkdir, // Ensure the updated workdir is passed
269+
additionalWritableRoots,
270+
},
257271
await getSandbox(runInSandbox),
258272
abortSignal,
259273
config,
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
2+
import { handleExecCommand } from "../src/utils/agent/handle-exec-command"; // Adjust path as necessary
3+
import * as execModule from "../src/utils/agent/exec"; // Import the module to mock
4+
import * as approvalsModule from "../src/approvals"; // Import the module to mock canAutoApprove
5+
import * as fsPromises from "fs/promises";
6+
import type { AppConfig } from "../src/utils/config";
7+
import type { ApprovalPolicy, SafetyAssessment } from "../src/approvals";
8+
import type { ExecInput } from "../src/utils/agent/sandbox/interface";
9+
import { SandboxType } from "../src/utils/agent/sandbox/interface";
10+
import { ReviewDecision } from "../src/utils/agent/review";
11+
import type { CommandConfirmation } from "../src/utils/agent/agent-loop";
12+
13+
// Mock dependencies
14+
vi.mock("../src/utils/agent/exec", async (importOriginal) => {
15+
const actual = await importOriginal<typeof execModule>();
16+
return {
17+
...actual,
18+
exec: vi
19+
.fn()
20+
.mockResolvedValue({ stdout: "mock stdout", stderr: "", exitCode: 0 }),
21+
};
22+
});
23+
24+
vi.mock("../src/approvals", async (importOriginal) => {
25+
const actual = await importOriginal<typeof approvalsModule>();
26+
return {
27+
...actual,
28+
canAutoApprove: vi.fn().mockReturnValue({
29+
type: "auto-approve",
30+
runInSandbox: false,
31+
applyPatch: undefined,
32+
reason: "Mocked auto-approval",
33+
group: "mock_group",
34+
} as SafetyAssessment),
35+
};
36+
});
37+
38+
vi.mock("fs/promises", async (importOriginal) => {
39+
const actual = await importOriginal<typeof fsPromises>();
40+
return {
41+
...actual,
42+
access: vi.fn().mockResolvedValue(undefined), // Mock access to succeed
43+
};
44+
});
45+
46+
// Mock logger to prevent console output during tests
47+
vi.mock("../src/utils/logger/log", () => ({
48+
log: vi.fn(),
49+
isLoggingEnabled: vi.fn().mockReturnValue(false),
50+
}));
51+
52+
describe("handleExecCommand", () => {
53+
let mockConfig: AppConfig;
54+
let mockPolicy: ApprovalPolicy;
55+
let mockExecInput: ExecInput;
56+
let mockGetCommandConfirmation: Mock<
57+
(
58+
command: Array<string>,
59+
applyPatch: approvalsModule.ApplyPatchCommand | undefined,
60+
) => Promise<CommandConfirmation>
61+
>;
62+
63+
const mockedExec = vi.mocked(execModule.exec);
64+
const mockedCanAutoApprove = vi.mocked(approvalsModule.canAutoApprove);
65+
const mockedFsAccess = vi.mocked(fsPromises.access);
66+
67+
beforeEach(() => {
68+
vi.clearAllMocks(); // Clear mocks before each test
69+
70+
mockConfig = {
71+
model: "test-model",
72+
instructions: "test-instructions",
73+
tools: {
74+
shell: {
75+
maxBytes: 1024,
76+
maxLines: 100,
77+
},
78+
},
79+
};
80+
81+
mockPolicy = "full-auto";
82+
83+
mockExecInput = {
84+
cmd: ["echo", "hello"],
85+
workdir: "/test/dir",
86+
timeoutInMillis: 5000,
87+
};
88+
89+
mockGetCommandConfirmation = vi.fn().mockResolvedValue({
90+
review: ReviewDecision.YES,
91+
customDenyMessage: "",
92+
});
93+
94+
mockedCanAutoApprove.mockReturnValue({
95+
type: "auto-approve",
96+
runInSandbox: false,
97+
applyPatch: undefined,
98+
reason: "Mocked auto-approval",
99+
group: "mock_group",
100+
} as SafetyAssessment);
101+
mockedExec.mockResolvedValue({
102+
stdout: "mock stdout",
103+
stderr: "",
104+
exitCode: 0,
105+
});
106+
mockedFsAccess.mockResolvedValue(undefined);
107+
});
108+
109+
it("should propagate AppConfig correctly to the exec function", async () => {
110+
await handleExecCommand(
111+
mockExecInput,
112+
mockConfig,
113+
mockPolicy,
114+
["/additional/writable"],
115+
mockGetCommandConfirmation,
116+
undefined,
117+
);
118+
119+
expect(mockedFsAccess).toHaveBeenCalledWith(mockExecInput.workdir);
120+
121+
expect(mockedCanAutoApprove).toHaveBeenCalledWith(
122+
mockExecInput.cmd,
123+
mockExecInput.workdir,
124+
mockPolicy,
125+
[expect.any(String)],
126+
);
127+
128+
expect(mockedExec).toHaveBeenCalledTimes(1);
129+
130+
const execCallArgs = mockedExec.mock.calls[0];
131+
expect(execCallArgs).toBeDefined();
132+
133+
const passedExecInput = execCallArgs![0];
134+
const passedSandboxType = execCallArgs![1];
135+
const passedAbortSignal = execCallArgs![2];
136+
const passedConfig = execCallArgs![3];
137+
138+
expect(passedConfig).toBeDefined();
139+
expect(passedConfig).toMatchObject(mockConfig);
140+
expect(passedConfig?.tools?.shell?.maxBytes).toBe(
141+
mockConfig.tools?.shell?.maxBytes,
142+
);
143+
expect(passedConfig?.tools?.shell?.maxLines).toBe(
144+
mockConfig.tools?.shell?.maxLines,
145+
);
146+
147+
expect(passedExecInput).toEqual({
148+
...mockExecInput,
149+
additionalWritableRoots: ["/additional/writable"],
150+
});
151+
expect(passedSandboxType).toBe(SandboxType.NONE);
152+
expect(passedAbortSignal).toBeUndefined();
153+
});
154+
155+
it("should use process.cwd() if workdir access fails", async () => {
156+
const mockCwd = "/mock/cwd";
157+
const spyCwd = vi.spyOn(process, "cwd").mockReturnValue(mockCwd);
158+
mockedFsAccess.mockRejectedValue(new Error("Access denied"));
159+
160+
await handleExecCommand(
161+
mockExecInput,
162+
mockConfig,
163+
mockPolicy,
164+
[],
165+
mockGetCommandConfirmation,
166+
undefined,
167+
);
168+
169+
expect(mockedFsAccess).toHaveBeenCalledWith(mockExecInput.workdir);
170+
expect(mockedCanAutoApprove).toHaveBeenCalledWith(
171+
mockExecInput.cmd,
172+
mockExecInput.workdir,
173+
mockPolicy,
174+
[mockCwd],
175+
);
176+
expect(mockedExec).toHaveBeenCalledTimes(1);
177+
178+
const execCallArgs = mockedExec.mock.calls[0];
179+
expect(execCallArgs).toBeDefined();
180+
const passedExecInput = execCallArgs![0];
181+
182+
expect(passedExecInput.workdir).toBe(mockCwd);
183+
expect(passedExecInput.cmd).toEqual(mockExecInput.cmd);
184+
185+
spyCwd.mockRestore();
186+
});
187+
188+
// Add more tests for other scenarios:
189+
// - Sandbox usage (runInSandbox = true)
190+
// - User rejection (`askUserPermission` path)
191+
// - Always approved commands path
192+
// - Policy rejection path
193+
// - Error handling during exec
194+
// - `fullAutoErrorMode = ASK_USER` path
195+
});

0 commit comments

Comments
 (0)