Compare commits

...

2 Commits

Author SHA1 Message Date
aibrahim-oai
efccf74170 Add Rust CLI test 2025-07-11 13:10:26 -07:00
aibrahim-oai
b1fd1bd96f Add CLI integration tests 2025-07-11 12:04:02 -07:00
5 changed files with 364 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";
const originalArgv = process.argv.slice();
const originalCwd = process.cwd();
const originalEnv = { ...process.env };
const originalNode = process.versions.node;
const originalPlatform = process.platform;
function setupCommonMocks(tmpDir: string) {
vi.doMock("../src/utils/logger/log.js", () => ({
__esModule: true,
initLogger: () => ({ isLoggingEnabled: () => false, log: () => {} }),
}));
vi.doMock("../src/utils/check-updates.js", () => ({
__esModule: true,
checkForUpdates: vi.fn(),
}));
vi.doMock("../src/utils/get-api-key.js", () => ({
__esModule: true,
getApiKey: () => "test-key",
maybeRedeemCredits: async () => {},
}));
vi.doMock("../src/approvals.js", () => ({
__esModule: true,
alwaysApprovedCommands: new Set<string>(),
canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false }),
isSafeCommand: () => null,
}));
vi.doMock("src/approvals.js", () => ({
__esModule: true,
alwaysApprovedCommands: new Set<string>(),
canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false }),
isSafeCommand: () => null,
}));
vi.doMock("../src/format-command.js", () => ({
__esModule: true,
formatCommandForDisplay: (cmd: Array<string>) => cmd.join(" "),
}));
vi.doMock("src/format-command.js", () => ({
__esModule: true,
formatCommandForDisplay: (cmd: Array<string>) => cmd.join(" "),
}));
vi.doMock("../src/utils/agent/log.js", () => ({
__esModule: true,
log: () => {},
isLoggingEnabled: () => false,
}));
vi.doMock("../src/utils/config.js", () => ({
__esModule: true,
loadConfig: () => ({
model: "test-model",
instructions: "",
provider: "openai",
notify: false,
approvalMode: undefined,
tools: { shell: { maxBytes: 1024, maxLines: 100 } },
disableResponseStorage: false,
reasoningEffort: "medium",
}),
PRETTY_PRINT: true,
INSTRUCTIONS_FILEPATH: path.join(tmpDir, "instructions.md"),
}));
}
describe("CLI Full Conversation Integration", () => {
let tmpDir: string;
beforeEach(() => {
vi.resetModules();
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-cli-test-"));
process.chdir(tmpDir);
process.env.CODEX_UNSAFE_ALLOW_NO_SANDBOX = "1";
Object.defineProperty(process.versions, "node", { value: "22.0.0" });
Object.defineProperty(process, "platform", { value: "win32" });
setupCommonMocks(tmpDir);
});
afterEach(() => {
process.argv = originalArgv.slice();
process.chdir(originalCwd);
process.env = { ...originalEnv };
Object.defineProperty(process.versions, "node", { value: originalNode });
Object.defineProperty(process, "platform", { value: originalPlatform });
vi.restoreAllMocks();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("prints the assistant response and exits", async () => {
class MsgStream {
async *[Symbol.asyncIterator]() {
yield {
type: "response.output_item.done",
item: {
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Hello, world." }],
},
} as any;
yield {
type: "response.completed",
response: {
id: "resp1",
status: "completed",
output: [
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Hello, world." }],
},
],
},
} as any;
}
}
vi.mock("openai", () => ({
__esModule: true,
default: class FakeOpenAI {
public responses = { create: async () => new MsgStream() };
},
APIConnectionTimeoutError: class APIConnectionTimeoutError extends Error {},
}));
const logs: Array<string> = [];
vi.spyOn(console, "log").mockImplementation((...args) => {
logs.push(args.join(" "));
});
const exitSpy = vi
.spyOn(process, "exit")
.mockImplementation(((code?: number) => {
throw new Error(`exit:${code}`);
}) as any);
process.argv = ["node", "codex", "-q", "Hello", "--full-auto"];
await expect(import("../src/cli.tsx")).rejects.toThrow("exit:0");
const hasText = logs.some((l) => l.includes("assistant: Hello, world."));
expect(hasText).toBe(true);
expect(exitSpy).toHaveBeenCalledWith(0);
});
});

View File

@@ -0,0 +1,181 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";
const originalArgv = process.argv.slice();
const originalCwd = process.cwd();
const originalEnv = { ...process.env };
const originalNode = process.versions.node;
const originalPlatform = process.platform;
// Utility to setup mocks common to CLI tests
function setupCommonMocks(tmpDir: string) {
vi.doMock("../src/utils/logger/log.js", () => ({
__esModule: true,
initLogger: () => ({ isLoggingEnabled: () => false, log: () => {} }),
}));
vi.doMock("../src/utils/check-updates.js", () => ({
__esModule: true,
checkForUpdates: vi.fn(),
}));
vi.doMock("../src/utils/get-api-key.js", () => ({
__esModule: true,
getApiKey: () => "test-key",
maybeRedeemCredits: async () => {},
}));
vi.doMock("../src/approvals.js", () => ({
__esModule: true,
alwaysApprovedCommands: new Set<string>(),
canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false }),
isSafeCommand: () => null,
}));
vi.doMock("src/approvals.js", () => ({
__esModule: true,
alwaysApprovedCommands: new Set<string>(),
canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false }),
isSafeCommand: () => null,
}));
vi.doMock("../src/format-command.js", () => ({
__esModule: true,
formatCommandForDisplay: (cmd: Array<string>) => cmd.join(" "),
}));
vi.doMock("src/format-command.js", () => ({
__esModule: true,
formatCommandForDisplay: (cmd: Array<string>) => cmd.join(" "),
}));
vi.doMock("../src/utils/agent/log.js", () => ({
__esModule: true,
log: () => {},
isLoggingEnabled: () => false,
}));
vi.doMock("../src/utils/config.js", () => ({
__esModule: true,
loadConfig: () => ({
model: "test-model",
instructions: "",
provider: "openai",
notify: false,
approvalMode: undefined,
tools: { shell: { maxBytes: 1024, maxLines: 100 } },
disableResponseStorage: false,
reasoningEffort: "medium",
}),
PRETTY_PRINT: true,
INSTRUCTIONS_FILEPATH: path.join(tmpDir, "instructions.md"),
}));
}
describe("CLI Tool Invocation Flow", () => {
let tmpDir: string;
beforeEach(() => {
vi.resetModules();
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-cli-test-"));
process.chdir(tmpDir);
process.env.CODEX_UNSAFE_ALLOW_NO_SANDBOX = "1";
Object.defineProperty(process.versions, "node", { value: "22.0.0" });
Object.defineProperty(process, "platform", { value: "win32" });
setupCommonMocks(tmpDir);
});
afterEach(() => {
process.argv = originalArgv.slice();
process.chdir(originalCwd);
process.env = { ...originalEnv };
Object.defineProperty(process.versions, "node", { value: originalNode });
Object.defineProperty(process, "platform", { value: originalPlatform });
vi.restoreAllMocks();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("executes a shell command returned by the model", async () => {
class CallStream {
async *[Symbol.asyncIterator]() {
yield {
type: "response.output_item.done",
item: {
type: "function_call",
id: "call_1",
name: "shell",
arguments: JSON.stringify({ cmd: ["echo", "Hello"] }),
},
} as any;
yield {
type: "response.completed",
response: {
id: "resp1",
status: "completed",
output: [
{
type: "function_call",
id: "call_1",
name: "shell",
arguments: JSON.stringify({ cmd: ["echo", "Hello"] }),
},
],
},
} as any;
}
}
class DoneStream {
async *[Symbol.asyncIterator]() {
yield {
type: "response.output_item.done",
item: {
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "done" }],
},
} as any;
yield {
type: "response.completed",
response: {
id: "resp2",
status: "completed",
output: [
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "done" }],
},
],
},
} as any;
}
}
let call = 0;
vi.mock("openai", () => {
return {
__esModule: true,
default: class FakeOpenAI {
public responses = {
create: async () => {
call += 1;
return call === 1 ? new CallStream() : new DoneStream();
},
};
},
APIConnectionTimeoutError: class APIConnectionTimeoutError extends Error {},
};
});
const logs: Array<string> = [];
vi.spyOn(console, "log").mockImplementation((...args) => {
logs.push(args.join(" "));
});
vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`exit:${code}`);
}) as any);
process.argv = ["node", "codex", "--full-auto", "-q", "hi"];
await expect(import("../src/cli.tsx")).rejects.toThrow("exit:0");
const hasOutput = logs.some((l) => l.includes("command.stdout") && l.includes("Hello"));
expect(hasOutput).toBe(true);
});
});

3
codex-rs/Cargo.lock generated
View File

@@ -669,14 +669,17 @@ name = "codex-exec"
version = "0.0.0"
dependencies = [
"anyhow",
"assert_cmd",
"chrono",
"clap",
"codex-common",
"codex-core",
"codex-linux-sandbox",
"owo-colors",
"predicates",
"serde_json",
"shlex",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",

View File

@@ -37,3 +37,8 @@ tokio = { version = "1", features = [
] }
tracing = { version = "0.1.41", features = ["log"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
[dev-dependencies]
assert_cmd = "2"
predicates = "3"
tempfile = "3"

View File

@@ -0,0 +1,29 @@
#![allow(clippy::unwrap_used, clippy::expect_used)]
use codex_core::exec::{ExecParams, SandboxType, process_exec_tool_call};
use codex_core::protocol::SandboxPolicy;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Notify;
#[tokio::test]
async fn echo_command_outputs_text() {
let params = ExecParams {
command: vec!["echo".into(), "Hello".into()],
cwd: std::env::current_dir().unwrap(),
timeout_ms: None,
env: HashMap::new(),
};
let policy = SandboxPolicy::new_workspace_write_policy();
let output = process_exec_tool_call(
params,
SandboxType::None,
Arc::new(Notify::new()),
&policy,
&None,
)
.await
.expect("exec failed");
assert_eq!(output.exit_code, 0);
assert!(output.stdout.contains("Hello"));
}