mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: remove codex support
This commit is contained in:
@@ -40,19 +40,8 @@ describe("model-resolver.ts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass through OpenAI gpt-* models", () => {
|
||||
const models = ["gpt-5.2", "gpt-5.1-codex", "gpt-4"];
|
||||
models.forEach((model) => {
|
||||
const result = resolveModelString(model);
|
||||
expect(result).toBe(model);
|
||||
});
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Using OpenAI/Codex model")
|
||||
);
|
||||
});
|
||||
|
||||
it("should treat o-series models as unknown (Codex CLI doesn't support them)", () => {
|
||||
const models = ["o1", "o1-mini", "o3"];
|
||||
it("should treat unknown models as falling back to default", () => {
|
||||
const models = ["o1", "o1-mini", "o3", "gpt-5.2", "unknown-model"];
|
||||
models.forEach((model) => {
|
||||
const result = resolveModelString(model);
|
||||
// Should fall back to default since these aren't supported
|
||||
@@ -143,14 +132,12 @@ describe("model-resolver.ts", () => {
|
||||
});
|
||||
|
||||
describe("DEFAULT_MODELS", () => {
|
||||
it("should have claude and openai defaults", () => {
|
||||
it("should have claude default", () => {
|
||||
expect(DEFAULT_MODELS).toHaveProperty("claude");
|
||||
expect(DEFAULT_MODELS).toHaveProperty("openai");
|
||||
});
|
||||
|
||||
it("should have valid default models", () => {
|
||||
it("should have valid default model", () => {
|
||||
expect(DEFAULT_MODELS.claude).toContain("claude");
|
||||
expect(DEFAULT_MODELS.openai).toContain("gpt");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,430 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { CodexConfigManager } from "@/providers/codex-config-manager.js";
|
||||
import * as fs from "fs/promises";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { tomlConfigFixture } from "../../fixtures/configs.js";
|
||||
|
||||
vi.mock("fs/promises");
|
||||
|
||||
describe("codex-config-manager.ts", () => {
|
||||
let manager: CodexConfigManager;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
manager = new CodexConfigManager();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should initialize with user config path", () => {
|
||||
const expectedPath = path.join(os.homedir(), ".codex", "config.toml");
|
||||
expect(manager["userConfigPath"]).toBe(expectedPath);
|
||||
});
|
||||
|
||||
it("should initialize with null project config path", () => {
|
||||
expect(manager["projectConfigPath"]).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setProjectPath", () => {
|
||||
it("should set project config path", () => {
|
||||
manager.setProjectPath("/my/project");
|
||||
const configPath = manager["projectConfigPath"];
|
||||
expect(configPath).toContain("my");
|
||||
expect(configPath).toContain("project");
|
||||
expect(configPath).toContain(".codex");
|
||||
expect(configPath).toContain("config.toml");
|
||||
});
|
||||
|
||||
it("should handle paths with special characters", () => {
|
||||
manager.setProjectPath("/path with spaces/project");
|
||||
expect(manager["projectConfigPath"]).toContain("path with spaces");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getConfigPath", () => {
|
||||
it("should return user config path when no project path set", async () => {
|
||||
const result = await manager.getConfigPath();
|
||||
expect(result).toBe(manager["userConfigPath"]);
|
||||
});
|
||||
|
||||
it("should return project config path when it exists", async () => {
|
||||
manager.setProjectPath("/my/project");
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
|
||||
const result = await manager.getConfigPath();
|
||||
expect(result).toContain("my");
|
||||
expect(result).toContain("project");
|
||||
expect(result).toContain(".codex");
|
||||
expect(result).toContain("config.toml");
|
||||
});
|
||||
|
||||
it("should fall back to user config when project config doesn't exist", async () => {
|
||||
manager.setProjectPath("/my/project");
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT"));
|
||||
|
||||
const result = await manager.getConfigPath();
|
||||
expect(result).toBe(manager["userConfigPath"]);
|
||||
});
|
||||
|
||||
it("should create user config directory if it doesn't exist", async () => {
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
|
||||
await manager.getConfigPath();
|
||||
|
||||
const expectedDir = path.dirname(manager["userConfigPath"]);
|
||||
expect(fs.mkdir).toHaveBeenCalledWith(expectedDir, { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseToml", () => {
|
||||
it("should parse simple key-value pairs", () => {
|
||||
const toml = `
|
||||
key1 = "value1"
|
||||
key2 = "value2"
|
||||
`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.key1).toBe("value1");
|
||||
expect(result.key2).toBe("value2");
|
||||
});
|
||||
|
||||
it("should parse boolean values", () => {
|
||||
const toml = `
|
||||
enabled = true
|
||||
disabled = false
|
||||
`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.enabled).toBe(true);
|
||||
expect(result.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should parse integer values", () => {
|
||||
const toml = `
|
||||
count = 42
|
||||
negative = -10
|
||||
`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.count).toBe(42);
|
||||
expect(result.negative).toBe(-10);
|
||||
});
|
||||
|
||||
it("should parse float values", () => {
|
||||
const toml = `
|
||||
pi = 3.14
|
||||
negative = -2.5
|
||||
`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.pi).toBe(3.14);
|
||||
expect(result.negative).toBe(-2.5);
|
||||
});
|
||||
|
||||
it("should skip comments", () => {
|
||||
const toml = `
|
||||
# This is a comment
|
||||
key = "value"
|
||||
# Another comment
|
||||
`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.key).toBe("value");
|
||||
expect(Object.keys(result)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should skip empty lines", () => {
|
||||
const toml = `
|
||||
key1 = "value1"
|
||||
|
||||
key2 = "value2"
|
||||
|
||||
|
||||
`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.key1).toBe("value1");
|
||||
expect(result.key2).toBe("value2");
|
||||
});
|
||||
|
||||
it("should parse sections", () => {
|
||||
const toml = `
|
||||
[section1]
|
||||
key1 = "value1"
|
||||
key2 = "value2"
|
||||
`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.section1).toBeDefined();
|
||||
expect(result.section1.key1).toBe("value1");
|
||||
expect(result.section1.key2).toBe("value2");
|
||||
});
|
||||
|
||||
it("should parse nested sections", () => {
|
||||
const toml = `
|
||||
[section.subsection]
|
||||
key = "value"
|
||||
`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.section).toBeDefined();
|
||||
expect(result.section.subsection).toBeDefined();
|
||||
expect(result.section.subsection.key).toBe("value");
|
||||
});
|
||||
|
||||
it("should parse MCP server configuration", () => {
|
||||
const result = manager.parseToml(tomlConfigFixture);
|
||||
|
||||
expect(result.experimental_use_rmcp_client).toBe(true);
|
||||
expect(result.mcp_servers).toBeDefined();
|
||||
expect(result.mcp_servers["automaker-tools"]).toBeDefined();
|
||||
expect(result.mcp_servers["automaker-tools"].command).toBe("node");
|
||||
});
|
||||
|
||||
it("should handle quoted strings with spaces", () => {
|
||||
const toml = `key = "value with spaces"`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.key).toBe("value with spaces");
|
||||
});
|
||||
|
||||
it("should handle single-quoted strings", () => {
|
||||
const toml = `key = 'single quoted'`;
|
||||
const result = manager.parseToml(toml);
|
||||
|
||||
expect(result.key).toBe("single quoted");
|
||||
});
|
||||
|
||||
it("should return empty object for empty input", () => {
|
||||
const result = manager.parseToml("");
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("readConfig", () => {
|
||||
it("should read and parse existing config", async () => {
|
||||
vi.mocked(fs.readFile).mockResolvedValue(tomlConfigFixture);
|
||||
|
||||
const result = await manager.readConfig("/path/to/config.toml");
|
||||
|
||||
expect(result.experimental_use_rmcp_client).toBe(true);
|
||||
expect(result.mcp_servers).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return empty object when file doesn't exist", async () => {
|
||||
const error: any = new Error("ENOENT");
|
||||
error.code = "ENOENT";
|
||||
vi.mocked(fs.readFile).mockRejectedValue(error);
|
||||
|
||||
const result = await manager.readConfig("/nonexistent.toml");
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should throw other errors", async () => {
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("Permission denied"));
|
||||
|
||||
await expect(manager.readConfig("/path.toml")).rejects.toThrow(
|
||||
"Permission denied"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("escapeTomlString", () => {
|
||||
it("should escape backslashes", () => {
|
||||
const result = manager.escapeTomlString("path\\to\\file");
|
||||
expect(result).toBe("path\\\\to\\\\file");
|
||||
});
|
||||
|
||||
it("should escape double quotes", () => {
|
||||
const result = manager.escapeTomlString('say "hello"');
|
||||
expect(result).toBe('say \\"hello\\"');
|
||||
});
|
||||
|
||||
it("should escape newlines", () => {
|
||||
const result = manager.escapeTomlString("line1\nline2");
|
||||
expect(result).toBe("line1\\nline2");
|
||||
});
|
||||
|
||||
it("should escape carriage returns", () => {
|
||||
const result = manager.escapeTomlString("line1\rline2");
|
||||
expect(result).toBe("line1\\rline2");
|
||||
});
|
||||
|
||||
it("should escape tabs", () => {
|
||||
const result = manager.escapeTomlString("col1\tcol2");
|
||||
expect(result).toBe("col1\\tcol2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatValue", () => {
|
||||
it("should format strings with quotes", () => {
|
||||
const result = manager.formatValue("test");
|
||||
expect(result).toBe('"test"');
|
||||
});
|
||||
|
||||
it("should format booleans as strings", () => {
|
||||
expect(manager.formatValue(true)).toBe("true");
|
||||
expect(manager.formatValue(false)).toBe("false");
|
||||
});
|
||||
|
||||
it("should format numbers as strings", () => {
|
||||
expect(manager.formatValue(42)).toBe("42");
|
||||
expect(manager.formatValue(3.14)).toBe("3.14");
|
||||
});
|
||||
|
||||
it("should escape special characters in strings", () => {
|
||||
const result = manager.formatValue('path\\with"quotes');
|
||||
expect(result).toBe('"path\\\\with\\"quotes"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeConfig", () => {
|
||||
it("should write TOML config to file", async () => {
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const config = {
|
||||
experimental_use_rmcp_client: true,
|
||||
mcp_servers: {
|
||||
"test-server": {
|
||||
command: "node",
|
||||
args: ["server.js"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await manager.writeConfig("/path/config.toml", config);
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
"/path/config.toml",
|
||||
expect.stringContaining("experimental_use_rmcp_client = true"),
|
||||
"utf-8"
|
||||
);
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
"/path/config.toml",
|
||||
expect.stringContaining("[mcp_servers.test-server]"),
|
||||
"utf-8"
|
||||
);
|
||||
});
|
||||
|
||||
it("should create config directory if it doesn't exist", async () => {
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
await manager.writeConfig("/path/to/config.toml", {});
|
||||
|
||||
expect(fs.mkdir).toHaveBeenCalledWith("/path/to", { recursive: true });
|
||||
});
|
||||
|
||||
it("should include env section for MCP servers", async () => {
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const config = {
|
||||
mcp_servers: {
|
||||
"test-server": {
|
||||
command: "node",
|
||||
env: {
|
||||
MY_VAR: "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await manager.writeConfig("/path/config.toml", config);
|
||||
|
||||
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
|
||||
expect(writtenContent).toContain("[mcp_servers.test-server.env]");
|
||||
expect(writtenContent).toContain('MY_VAR = "value"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("configureMcpServer", () => {
|
||||
it("should configure automaker-tools MCP server", async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT"));
|
||||
vi.mocked(fs.readFile).mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const result = await manager.configureMcpServer(
|
||||
"/my/project",
|
||||
"/path/to/mcp-server.js"
|
||||
);
|
||||
|
||||
expect(result).toContain("config.toml");
|
||||
|
||||
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
|
||||
expect(writtenContent).toContain("[mcp_servers.automaker-tools]");
|
||||
expect(writtenContent).toContain('command = "node"');
|
||||
expect(writtenContent).toContain("/path/to/mcp-server.js");
|
||||
expect(writtenContent).toContain("AUTOMAKER_PROJECT_PATH");
|
||||
});
|
||||
|
||||
it("should preserve existing MCP servers", async () => {
|
||||
const existingConfig = `
|
||||
[mcp_servers.other-server]
|
||||
command = "other"
|
||||
`;
|
||||
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT"));
|
||||
vi.mocked(fs.readFile).mockResolvedValue(existingConfig);
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
await manager.configureMcpServer("/project", "/server.js");
|
||||
|
||||
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
|
||||
expect(writtenContent).toContain("[mcp_servers.other-server]");
|
||||
expect(writtenContent).toContain("[mcp_servers.automaker-tools]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeMcpServer", () => {
|
||||
it("should remove automaker-tools MCP server", async () => {
|
||||
const configWithServer = `
|
||||
[mcp_servers.automaker-tools]
|
||||
command = "node"
|
||||
|
||||
[mcp_servers.other-server]
|
||||
command = "other"
|
||||
`;
|
||||
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT"));
|
||||
vi.mocked(fs.readFile).mockResolvedValue(configWithServer);
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
await manager.removeMcpServer("/project");
|
||||
|
||||
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
|
||||
expect(writtenContent).not.toContain("automaker-tools");
|
||||
expect(writtenContent).toContain("other-server");
|
||||
});
|
||||
|
||||
it("should remove mcp_servers section if empty", async () => {
|
||||
const configWithOnlyAutomaker = `
|
||||
[mcp_servers.automaker-tools]
|
||||
command = "node"
|
||||
`;
|
||||
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT"));
|
||||
vi.mocked(fs.readFile).mockResolvedValue(configWithOnlyAutomaker);
|
||||
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
await manager.removeMcpServer("/project");
|
||||
|
||||
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
|
||||
expect(writtenContent).not.toContain("mcp_servers");
|
||||
});
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("Read error"));
|
||||
|
||||
// Should not throw
|
||||
await expect(manager.removeMcpServer("/project")).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { ProviderFactory } from "@/providers/provider-factory.js";
|
||||
import { ClaudeProvider } from "@/providers/claude-provider.js";
|
||||
import { CodexProvider } from "@/providers/codex-provider.js";
|
||||
|
||||
describe("provider-factory.ts", () => {
|
||||
let consoleSpy: any;
|
||||
@@ -17,48 +16,6 @@ describe("provider-factory.ts", () => {
|
||||
});
|
||||
|
||||
describe("getProviderForModel", () => {
|
||||
describe("OpenAI/Codex models (gpt-*)", () => {
|
||||
it("should return CodexProvider for gpt-5.2", () => {
|
||||
const provider = ProviderFactory.getProviderForModel("gpt-5.2");
|
||||
expect(provider).toBeInstanceOf(CodexProvider);
|
||||
});
|
||||
|
||||
it("should return CodexProvider for gpt-5.1-codex", () => {
|
||||
const provider = ProviderFactory.getProviderForModel("gpt-5.1-codex");
|
||||
expect(provider).toBeInstanceOf(CodexProvider);
|
||||
});
|
||||
|
||||
it("should return CodexProvider for gpt-4", () => {
|
||||
const provider = ProviderFactory.getProviderForModel("gpt-4");
|
||||
expect(provider).toBeInstanceOf(CodexProvider);
|
||||
});
|
||||
|
||||
it("should be case-insensitive for gpt models", () => {
|
||||
const provider1 = ProviderFactory.getProviderForModel("GPT-5.2");
|
||||
const provider2 = ProviderFactory.getProviderForModel("Gpt-5.1");
|
||||
expect(provider1).toBeInstanceOf(CodexProvider);
|
||||
expect(provider2).toBeInstanceOf(CodexProvider);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Unsupported o-series models", () => {
|
||||
it("should default to ClaudeProvider for o1 (not supported by Codex CLI)", () => {
|
||||
const provider = ProviderFactory.getProviderForModel("o1");
|
||||
expect(provider).toBeInstanceOf(ClaudeProvider);
|
||||
expect(consoleSpy.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to ClaudeProvider for o3", () => {
|
||||
const provider = ProviderFactory.getProviderForModel("o3");
|
||||
expect(provider).toBeInstanceOf(ClaudeProvider);
|
||||
});
|
||||
|
||||
it("should default to ClaudeProvider for o1-mini", () => {
|
||||
const provider = ProviderFactory.getProviderForModel("o1-mini");
|
||||
expect(provider).toBeInstanceOf(ClaudeProvider);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Claude models (claude-* prefix)", () => {
|
||||
it("should return ClaudeProvider for claude-opus-4-5-20251101", () => {
|
||||
const provider = ProviderFactory.getProviderForModel(
|
||||
@@ -138,6 +95,18 @@ describe("provider-factory.ts", () => {
|
||||
expect(provider).toBeInstanceOf(ClaudeProvider);
|
||||
expect(consoleSpy.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to ClaudeProvider for gpt models (not supported)", () => {
|
||||
const provider = ProviderFactory.getProviderForModel("gpt-5.2");
|
||||
expect(provider).toBeInstanceOf(ClaudeProvider);
|
||||
expect(consoleSpy.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to ClaudeProvider for o-series models (not supported)", () => {
|
||||
const provider = ProviderFactory.getProviderForModel("o1");
|
||||
expect(provider).toBeInstanceOf(ClaudeProvider);
|
||||
expect(consoleSpy.warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,15 +124,9 @@ describe("provider-factory.ts", () => {
|
||||
expect(hasClaudeProvider).toBe(true);
|
||||
});
|
||||
|
||||
it("should include CodexProvider", () => {
|
||||
it("should return exactly 1 provider", () => {
|
||||
const providers = ProviderFactory.getAllProviders();
|
||||
const hasCodexProvider = providers.some((p) => p instanceof CodexProvider);
|
||||
expect(hasCodexProvider).toBe(true);
|
||||
});
|
||||
|
||||
it("should return exactly 2 providers", () => {
|
||||
const providers = ProviderFactory.getAllProviders();
|
||||
expect(providers).toHaveLength(2);
|
||||
expect(providers).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should create new instances each time", () => {
|
||||
@@ -171,7 +134,6 @@ describe("provider-factory.ts", () => {
|
||||
const providers2 = ProviderFactory.getAllProviders();
|
||||
|
||||
expect(providers1[0]).not.toBe(providers2[0]);
|
||||
expect(providers1[1]).not.toBe(providers2[1]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,14 +142,12 @@ describe("provider-factory.ts", () => {
|
||||
const statuses = await ProviderFactory.checkAllProviders();
|
||||
|
||||
expect(statuses).toHaveProperty("claude");
|
||||
expect(statuses).toHaveProperty("codex");
|
||||
});
|
||||
|
||||
it("should call detectInstallation on each provider", async () => {
|
||||
const statuses = await ProviderFactory.checkAllProviders();
|
||||
|
||||
expect(statuses.claude).toHaveProperty("installed");
|
||||
expect(statuses.codex).toHaveProperty("installed");
|
||||
});
|
||||
|
||||
it("should return correct provider names as keys", async () => {
|
||||
@@ -195,8 +155,7 @@ describe("provider-factory.ts", () => {
|
||||
const keys = Object.keys(statuses);
|
||||
|
||||
expect(keys).toContain("claude");
|
||||
expect(keys).toContain("codex");
|
||||
expect(keys).toHaveLength(2);
|
||||
expect(keys).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -211,24 +170,12 @@ describe("provider-factory.ts", () => {
|
||||
expect(provider).toBeInstanceOf(ClaudeProvider);
|
||||
});
|
||||
|
||||
it("should return CodexProvider for 'codex'", () => {
|
||||
const provider = ProviderFactory.getProviderByName("codex");
|
||||
expect(provider).toBeInstanceOf(CodexProvider);
|
||||
});
|
||||
|
||||
it("should return CodexProvider for 'openai'", () => {
|
||||
const provider = ProviderFactory.getProviderByName("openai");
|
||||
expect(provider).toBeInstanceOf(CodexProvider);
|
||||
});
|
||||
|
||||
it("should be case-insensitive", () => {
|
||||
const provider1 = ProviderFactory.getProviderByName("CLAUDE");
|
||||
const provider2 = ProviderFactory.getProviderByName("Codex");
|
||||
const provider3 = ProviderFactory.getProviderByName("ANTHROPIC");
|
||||
const provider2 = ProviderFactory.getProviderByName("ANTHROPIC");
|
||||
|
||||
expect(provider1).toBeInstanceOf(ClaudeProvider);
|
||||
expect(provider2).toBeInstanceOf(CodexProvider);
|
||||
expect(provider3).toBeInstanceOf(ClaudeProvider);
|
||||
expect(provider2).toBeInstanceOf(ClaudeProvider);
|
||||
});
|
||||
|
||||
it("should return null for unknown provider", () => {
|
||||
@@ -273,7 +220,7 @@ describe("provider-factory.ts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should aggregate models from both Claude and Codex", () => {
|
||||
it("should include Claude models", () => {
|
||||
const models = ProviderFactory.getAllAvailableModels();
|
||||
|
||||
// Claude models should include claude-* in their IDs
|
||||
@@ -281,13 +228,7 @@ describe("provider-factory.ts", () => {
|
||||
m.id.toLowerCase().includes("claude")
|
||||
);
|
||||
|
||||
// Codex models should include gpt-* in their IDs
|
||||
const hasCodexModels = models.some((m) =>
|
||||
m.id.toLowerCase().includes("gpt")
|
||||
);
|
||||
|
||||
expect(hasClaudeModels).toBe(true);
|
||||
expect(hasCodexModels).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,7 +245,7 @@ describe("agent-service.ts", () => {
|
||||
|
||||
it("should use custom model if provided", async () => {
|
||||
const mockProvider = {
|
||||
getName: () => "codex",
|
||||
getName: () => "claude",
|
||||
executeQuery: async function* () {
|
||||
yield {
|
||||
type: "result",
|
||||
@@ -266,10 +266,10 @@ describe("agent-service.ts", () => {
|
||||
await service.sendMessage({
|
||||
sessionId: "session-1",
|
||||
message: "Hello",
|
||||
model: "gpt-5.2",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
});
|
||||
|
||||
expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith("gpt-5.2");
|
||||
expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith("claude-sonnet-4-20250514");
|
||||
});
|
||||
|
||||
it("should save session messages", async () => {
|
||||
|
||||
Reference in New Issue
Block a user