mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
feat: add comprehensive integration tests for auto-mode-service
- Created git-test-repo helper for managing test git repositories - Added 13 integration tests covering: - Worktree operations (create, error handling, non-worktree mode) - Feature execution (status updates, model selection, duplicate prevention) - Auto loop (start/stop, pending features, max concurrency, events) - Error handling (provider errors, continue after failures) - Integration tests use real git operations with temporary repos - All 416 tests passing with 72.65% overall coverage - Service coverage improved: agent-service 58%, auto-mode-service 44%, feature-loader 66% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
362
apps/server/tests/unit/providers/codex-cli-detector.test.ts
Normal file
362
apps/server/tests/unit/providers/codex-cli-detector.test.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { CodexCliDetector } from "@/providers/codex-cli-detector.js";
|
||||
import * as cp from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
|
||||
vi.mock("child_process");
|
||||
vi.mock("fs");
|
||||
|
||||
describe("codex-cli-detector.ts", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
});
|
||||
|
||||
describe("getConfigDir", () => {
|
||||
it("should return .codex directory in user home", () => {
|
||||
const homeDir = os.homedir();
|
||||
const configDir = CodexCliDetector.getConfigDir();
|
||||
expect(configDir).toBe(path.join(homeDir, ".codex"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuthPath", () => {
|
||||
it("should return auth.json path in config directory", () => {
|
||||
const authPath = CodexCliDetector.getAuthPath();
|
||||
expect(authPath).toContain(".codex");
|
||||
expect(authPath).toContain("auth.json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkAuth", () => {
|
||||
const mockAuthPath = "/home/user/.codex/auth.json";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(CodexCliDetector, "getAuthPath").mockReturnValue(mockAuthPath);
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should detect token object authentication", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||
JSON.stringify({
|
||||
token: {
|
||||
access_token: "test_access",
|
||||
refresh_token: "test_refresh",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(true);
|
||||
expect(result.method).toBe("cli_tokens");
|
||||
expect(result.hasAuthFile).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect token with Id_token field", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||
JSON.stringify({
|
||||
token: {
|
||||
Id_token: "test_id_token",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(true);
|
||||
expect(result.method).toBe("cli_tokens");
|
||||
});
|
||||
|
||||
it("should detect root-level tokens", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||
JSON.stringify({
|
||||
access_token: "test_access",
|
||||
refresh_token: "test_refresh",
|
||||
})
|
||||
);
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(true);
|
||||
expect(result.method).toBe("cli_tokens");
|
||||
});
|
||||
|
||||
it("should detect API key in auth file", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||
JSON.stringify({
|
||||
api_key: "test-api-key",
|
||||
})
|
||||
);
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(true);
|
||||
expect(result.method).toBe("auth_file");
|
||||
});
|
||||
|
||||
it("should detect openai_api_key field", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||
JSON.stringify({
|
||||
openai_api_key: "test-key",
|
||||
})
|
||||
);
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(true);
|
||||
expect(result.method).toBe("auth_file");
|
||||
});
|
||||
|
||||
it("should detect environment variable authentication", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
process.env.OPENAI_API_KEY = "env-api-key";
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(true);
|
||||
expect(result.method).toBe("env");
|
||||
expect(result.hasEnvKey).toBe(true);
|
||||
expect(result.hasAuthFile).toBe(false);
|
||||
});
|
||||
|
||||
it("should return not authenticated when no auth found", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(false);
|
||||
expect(result.method).toBe("none");
|
||||
expect(result.hasAuthFile).toBe(false);
|
||||
expect(result.hasEnvKey).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle malformed auth file", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue("invalid json");
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(false);
|
||||
expect(result.method).toBe("none");
|
||||
});
|
||||
|
||||
it("should return auth result with required fields", () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
const result = CodexCliDetector.checkAuth();
|
||||
|
||||
expect(result).toHaveProperty("authenticated");
|
||||
expect(result).toHaveProperty("method");
|
||||
expect(typeof result.authenticated).toBe("boolean");
|
||||
expect(typeof result.method).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectCodexInstallation", () => {
|
||||
// Note: Full detection logic involves OS-specific commands (which/where, npm, brew)
|
||||
// and is better tested in integration tests. Here we test the basic structure.
|
||||
|
||||
it("should return hasApiKey when OPENAI_API_KEY is set and CLI not found", () => {
|
||||
vi.mocked(cp.execSync).mockImplementation(() => {
|
||||
throw new Error("command not found");
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
process.env.OPENAI_API_KEY = "test-key";
|
||||
|
||||
const result = CodexCliDetector.detectCodexInstallation();
|
||||
|
||||
expect(result.installed).toBe(false);
|
||||
expect(result.hasApiKey).toBe(true);
|
||||
});
|
||||
|
||||
it("should return not installed when nothing found", () => {
|
||||
vi.mocked(cp.execSync).mockImplementation(() => {
|
||||
throw new Error("command failed");
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
|
||||
const result = CodexCliDetector.detectCodexInstallation();
|
||||
|
||||
expect(result.installed).toBe(false);
|
||||
expect(result.hasApiKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return installation status object with installed boolean", () => {
|
||||
vi.mocked(cp.execSync).mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
const result = CodexCliDetector.detectCodexInstallation();
|
||||
|
||||
expect(result).toHaveProperty("installed");
|
||||
expect(typeof result.installed).toBe("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCodexVersion", () => {
|
||||
// Note: Testing execSync calls is difficult in unit tests and better suited for integration tests
|
||||
// The method structure and error handling can be verified indirectly through other tests
|
||||
|
||||
it("should return null when given invalid path", () => {
|
||||
const version = CodexCliDetector.getCodexVersion("/nonexistent/path");
|
||||
expect(version).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInstallationInfo", () => {
|
||||
it("should return installed status when CLI is detected", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: true,
|
||||
path: "/usr/bin/codex",
|
||||
version: "0.5.0",
|
||||
method: "cli",
|
||||
});
|
||||
|
||||
const info = CodexCliDetector.getInstallationInfo();
|
||||
|
||||
expect(info.status).toBe("installed");
|
||||
expect(info.method).toBe("cli");
|
||||
expect(info.version).toBe("0.5.0");
|
||||
expect(info.path).toBe("/usr/bin/codex");
|
||||
expect(info.recommendation).toContain("ready for GPT-5.1/5.2");
|
||||
});
|
||||
|
||||
it("should return api_key_only when API key is set but CLI not installed", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
hasApiKey: true,
|
||||
});
|
||||
|
||||
const info = CodexCliDetector.getInstallationInfo();
|
||||
|
||||
expect(info.status).toBe("api_key_only");
|
||||
expect(info.method).toBe("api-key-only");
|
||||
expect(info.recommendation).toContain("OPENAI_API_KEY detected");
|
||||
expect(info.recommendation).toContain("Install Codex CLI");
|
||||
expect(info.installCommands).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return not_installed when nothing detected", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
|
||||
const info = CodexCliDetector.getInstallationInfo();
|
||||
|
||||
expect(info.status).toBe("not_installed");
|
||||
expect(info.recommendation).toContain("Install OpenAI Codex CLI");
|
||||
expect(info.installCommands).toBeDefined();
|
||||
});
|
||||
|
||||
it("should include install commands for all platforms", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: false,
|
||||
});
|
||||
|
||||
const info = CodexCliDetector.getInstallationInfo();
|
||||
|
||||
expect(info.installCommands).toHaveProperty("npm");
|
||||
expect(info.installCommands).toHaveProperty("macos");
|
||||
expect(info.installCommands).toHaveProperty("linux");
|
||||
expect(info.installCommands).toHaveProperty("windows");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInstallCommands", () => {
|
||||
it("should return installation commands for all platforms", () => {
|
||||
const commands = CodexCliDetector.getInstallCommands();
|
||||
|
||||
expect(commands.npm).toContain("npm install");
|
||||
expect(commands.npm).toContain("@openai/codex");
|
||||
expect(commands.macos).toContain("brew install");
|
||||
expect(commands.linux).toContain("npm install");
|
||||
expect(commands.windows).toContain("npm install");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isModelSupported", () => {
|
||||
it("should return true for supported models", () => {
|
||||
expect(CodexCliDetector.isModelSupported("gpt-5.1-codex-max")).toBe(true);
|
||||
expect(CodexCliDetector.isModelSupported("gpt-5.1-codex")).toBe(true);
|
||||
expect(CodexCliDetector.isModelSupported("gpt-5.1-codex-mini")).toBe(true);
|
||||
expect(CodexCliDetector.isModelSupported("gpt-5.1")).toBe(true);
|
||||
expect(CodexCliDetector.isModelSupported("gpt-5.2")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for unsupported models", () => {
|
||||
expect(CodexCliDetector.isModelSupported("gpt-4")).toBe(false);
|
||||
expect(CodexCliDetector.isModelSupported("claude-opus")).toBe(false);
|
||||
expect(CodexCliDetector.isModelSupported("unknown-model")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultModel", () => {
|
||||
it("should return gpt-5.2 as default", () => {
|
||||
const defaultModel = CodexCliDetector.getDefaultModel();
|
||||
expect(defaultModel).toBe("gpt-5.2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFullStatus", () => {
|
||||
it("should include installation, auth, and info", () => {
|
||||
vi.spyOn(CodexCliDetector, "detectCodexInstallation").mockReturnValue({
|
||||
installed: true,
|
||||
path: "/usr/bin/codex",
|
||||
});
|
||||
vi.spyOn(CodexCliDetector, "checkAuth").mockReturnValue({
|
||||
authenticated: true,
|
||||
method: "cli_verified",
|
||||
hasAuthFile: true,
|
||||
hasEnvKey: false,
|
||||
});
|
||||
|
||||
const status = CodexCliDetector.getFullStatus();
|
||||
|
||||
expect(status).toHaveProperty("status");
|
||||
expect(status).toHaveProperty("auth");
|
||||
expect(status).toHaveProperty("installation");
|
||||
expect(status.auth.authenticated).toBe(true);
|
||||
expect(status.installation.installed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user