Files
automaker/apps/server/tests/unit/providers/codex-config-manager.test.ts
Kacper 23ff99d2e2 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>
2025-12-13 13:34:27 +01:00

431 lines
13 KiB
TypeScript

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();
});
});
});