2025-04-16 12:56:08 -04:00
|
|
|
|
import type * as fsType from "fs";
|
|
|
|
|
|
|
2025-05-05 22:56:55 +05:30
|
|
|
|
import {
|
|
|
|
|
|
loadConfig,
|
|
|
|
|
|
saveConfig,
|
|
|
|
|
|
DEFAULT_SHELL_MAX_BYTES,
|
|
|
|
|
|
DEFAULT_SHELL_MAX_LINES,
|
|
|
|
|
|
} from "../src/utils/config.js";
|
2025-04-19 10:25:25 -04:00
|
|
|
|
import { AutoApprovalMode } from "../src/utils/auto-approval-mode.js";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import { tmpdir } from "os";
|
|
|
|
|
|
import { join } from "path";
|
|
|
|
|
|
import { test, expect, beforeEach, afterEach, vi } from "vitest";
|
2025-04-23 13:45:56 +08:00
|
|
|
|
import { providers as defaultProviders } from "../src/utils/providers";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
// In‑memory FS store
|
|
|
|
|
|
let memfs: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
|
|
// Mock out the parts of "fs" that our config module uses:
|
|
|
|
|
|
vi.mock("fs", async () => {
|
|
|
|
|
|
// now `real` is the actual fs module
|
|
|
|
|
|
const real = (await vi.importActual("fs")) as typeof fsType;
|
|
|
|
|
|
return {
|
|
|
|
|
|
...real,
|
|
|
|
|
|
existsSync: (path: string) => memfs[path] !== undefined,
|
|
|
|
|
|
readFileSync: (path: string) => {
|
|
|
|
|
|
if (memfs[path] === undefined) {
|
|
|
|
|
|
throw new Error("ENOENT");
|
|
|
|
|
|
}
|
|
|
|
|
|
return memfs[path];
|
|
|
|
|
|
},
|
|
|
|
|
|
writeFileSync: (path: string, data: string) => {
|
|
|
|
|
|
memfs[path] = data;
|
|
|
|
|
|
},
|
|
|
|
|
|
mkdirSync: () => {
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// no-op in in‑memory store
|
2025-04-16 12:56:08 -04:00
|
|
|
|
},
|
|
|
|
|
|
rmSync: (path: string) => {
|
|
|
|
|
|
// recursively delete any key under this prefix
|
|
|
|
|
|
const prefix = path.endsWith("/") ? path : path + "/";
|
|
|
|
|
|
for (const key of Object.keys(memfs)) {
|
|
|
|
|
|
if (key === path || key.startsWith(prefix)) {
|
|
|
|
|
|
delete memfs[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let testDir: string;
|
|
|
|
|
|
let testConfigPath: string;
|
|
|
|
|
|
let testInstructionsPath: string;
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
memfs = {}; // reset in‑memory store
|
|
|
|
|
|
testDir = tmpdir(); // use the OS temp dir as our "cwd"
|
|
|
|
|
|
testConfigPath = join(testDir, "config.json");
|
|
|
|
|
|
testInstructionsPath = join(testDir, "instructions.md");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
memfs = {};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("loads default config if files don't exist", () => {
|
|
|
|
|
|
const config = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
2025-04-17 21:41:54 +02:00
|
|
|
|
// Keep the test focused on just checking that default model and instructions are loaded
|
|
|
|
|
|
// so we need to make sure we check just these properties
|
|
|
|
|
|
expect(config.model).toBe("o4-mini");
|
|
|
|
|
|
expect(config.instructions).toBe("");
|
2025-04-16 12:56:08 -04:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("saves and loads config correctly", () => {
|
|
|
|
|
|
const testConfig = {
|
|
|
|
|
|
model: "test-model",
|
|
|
|
|
|
instructions: "test instructions",
|
2025-04-17 16:19:26 -07:00
|
|
|
|
notify: false,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
};
|
|
|
|
|
|
saveConfig(testConfig, testConfigPath, testInstructionsPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Our in‑memory fs should now contain those keys:
|
|
|
|
|
|
expect(memfs[testConfigPath]).toContain(`"model": "test-model"`);
|
|
|
|
|
|
expect(memfs[testInstructionsPath]).toBe("test instructions");
|
|
|
|
|
|
|
|
|
|
|
|
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
2025-04-17 21:41:54 +02:00
|
|
|
|
// Check just the specified properties that were saved
|
|
|
|
|
|
expect(loadedConfig.model).toBe(testConfig.model);
|
|
|
|
|
|
expect(loadedConfig.instructions).toBe(testConfig.instructions);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("loads user instructions + project doc when codex.md is present", () => {
|
|
|
|
|
|
// 1) seed memfs: a config JSON, an instructions.md, and a codex.md in the cwd
|
|
|
|
|
|
const userInstr = "here are user instructions";
|
|
|
|
|
|
const projectDoc = "# Project Title\n\nSome project‑specific doc";
|
|
|
|
|
|
// first, make config so loadConfig will see storedConfig
|
|
|
|
|
|
memfs[testConfigPath] = JSON.stringify({ model: "mymodel" }, null, 2);
|
|
|
|
|
|
// then user instructions:
|
|
|
|
|
|
memfs[testInstructionsPath] = userInstr;
|
|
|
|
|
|
// and now our fake codex.md in the cwd:
|
|
|
|
|
|
const codexPath = join(testDir, "codex.md");
|
|
|
|
|
|
memfs[codexPath] = projectDoc;
|
|
|
|
|
|
|
|
|
|
|
|
// 2) loadConfig without disabling project‑doc, but with cwd=testDir
|
|
|
|
|
|
const cfg = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
cwd: testDir,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 3) assert we got both pieces concatenated
|
|
|
|
|
|
expect(cfg.model).toBe("mymodel");
|
|
|
|
|
|
expect(cfg.instructions).toBe(
|
|
|
|
|
|
userInstr + "\n\n--- project-doc ---\n\n" + projectDoc,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
2025-04-19 10:25:25 -04:00
|
|
|
|
|
|
|
|
|
|
test("loads and saves approvalMode correctly", () => {
|
|
|
|
|
|
// Setup config with approvalMode
|
|
|
|
|
|
memfs[testConfigPath] = JSON.stringify(
|
|
|
|
|
|
{
|
|
|
|
|
|
model: "mymodel",
|
|
|
|
|
|
approvalMode: AutoApprovalMode.AUTO_EDIT,
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
2,
|
|
|
|
|
|
);
|
|
|
|
|
|
memfs[testInstructionsPath] = "test instructions";
|
|
|
|
|
|
|
|
|
|
|
|
// Load config and verify approvalMode
|
|
|
|
|
|
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Check approvalMode was loaded correctly
|
|
|
|
|
|
expect(loadedConfig.approvalMode).toBe(AutoApprovalMode.AUTO_EDIT);
|
|
|
|
|
|
|
|
|
|
|
|
// Modify approvalMode and save
|
|
|
|
|
|
const updatedConfig = {
|
|
|
|
|
|
...loadedConfig,
|
|
|
|
|
|
approvalMode: AutoApprovalMode.FULL_AUTO,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
saveConfig(updatedConfig, testConfigPath, testInstructionsPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Verify saved config contains updated approvalMode
|
|
|
|
|
|
expect(memfs[testConfigPath]).toContain(
|
|
|
|
|
|
`"approvalMode": "${AutoApprovalMode.FULL_AUTO}"`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Load again and verify updated value
|
|
|
|
|
|
const reloadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(reloadedConfig.approvalMode).toBe(AutoApprovalMode.FULL_AUTO);
|
|
|
|
|
|
});
|
2025-04-23 13:45:56 +08:00
|
|
|
|
|
|
|
|
|
|
test("loads and saves providers correctly", () => {
|
|
|
|
|
|
// Setup custom providers configuration
|
|
|
|
|
|
const customProviders = {
|
|
|
|
|
|
openai: {
|
|
|
|
|
|
name: "Custom OpenAI",
|
|
|
|
|
|
baseURL: "https://custom-api.openai.com/v1",
|
|
|
|
|
|
envKey: "CUSTOM_OPENAI_API_KEY",
|
|
|
|
|
|
},
|
|
|
|
|
|
anthropic: {
|
|
|
|
|
|
name: "Anthropic",
|
|
|
|
|
|
baseURL: "https://api.anthropic.com",
|
|
|
|
|
|
envKey: "ANTHROPIC_API_KEY",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Create config with providers
|
|
|
|
|
|
const testConfig = {
|
|
|
|
|
|
model: "test-model",
|
|
|
|
|
|
provider: "anthropic",
|
|
|
|
|
|
providers: customProviders,
|
|
|
|
|
|
instructions: "test instructions",
|
|
|
|
|
|
notify: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Save the config
|
|
|
|
|
|
saveConfig(testConfig, testConfigPath, testInstructionsPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Verify saved config contains providers
|
|
|
|
|
|
expect(memfs[testConfigPath]).toContain(`"providers"`);
|
|
|
|
|
|
expect(memfs[testConfigPath]).toContain(`"Custom OpenAI"`);
|
|
|
|
|
|
expect(memfs[testConfigPath]).toContain(`"Anthropic"`);
|
|
|
|
|
|
expect(memfs[testConfigPath]).toContain(`"provider": "anthropic"`);
|
|
|
|
|
|
|
|
|
|
|
|
// Load config and verify providers were loaded correctly
|
|
|
|
|
|
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Check providers were loaded correctly
|
|
|
|
|
|
expect(loadedConfig.provider).toBe("anthropic");
|
|
|
|
|
|
expect(loadedConfig.providers).toEqual({
|
|
|
|
|
|
...defaultProviders,
|
|
|
|
|
|
...customProviders,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Test merging with built-in providers
|
|
|
|
|
|
// Create a config with only one custom provider
|
|
|
|
|
|
const partialProviders = {
|
|
|
|
|
|
customProvider: {
|
|
|
|
|
|
name: "Custom Provider",
|
|
|
|
|
|
baseURL: "https://custom-api.example.com",
|
|
|
|
|
|
envKey: "CUSTOM_API_KEY",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const partialConfig = {
|
|
|
|
|
|
model: "test-model",
|
|
|
|
|
|
provider: "customProvider",
|
|
|
|
|
|
providers: partialProviders,
|
|
|
|
|
|
instructions: "test instructions",
|
|
|
|
|
|
notify: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Save the partial config
|
|
|
|
|
|
saveConfig(partialConfig, testConfigPath, testInstructionsPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Load config and verify providers were merged with built-in providers
|
|
|
|
|
|
const mergedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Check providers is defined
|
|
|
|
|
|
expect(mergedConfig.providers).toBeDefined();
|
|
|
|
|
|
|
|
|
|
|
|
// Use bracket notation to access properties
|
|
|
|
|
|
if (mergedConfig.providers) {
|
|
|
|
|
|
expect(mergedConfig.providers["customProvider"]).toBeDefined();
|
|
|
|
|
|
expect(mergedConfig.providers["customProvider"]).toEqual(
|
|
|
|
|
|
partialProviders.customProvider,
|
|
|
|
|
|
);
|
|
|
|
|
|
// Built-in providers should still be there (like openai)
|
|
|
|
|
|
expect(mergedConfig.providers["openai"]).toBeDefined();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-04-25 08:32:33 +08:00
|
|
|
|
|
|
|
|
|
|
test("saves and loads instructions with project doc separator correctly", () => {
|
|
|
|
|
|
const userInstructions = "user specific instructions";
|
|
|
|
|
|
const projectDoc = "project specific documentation";
|
|
|
|
|
|
const combinedInstructions = `${userInstructions}\n\n--- project-doc ---\n\n${projectDoc}`;
|
|
|
|
|
|
|
|
|
|
|
|
const testConfig = {
|
|
|
|
|
|
model: "test-model",
|
|
|
|
|
|
instructions: combinedInstructions,
|
|
|
|
|
|
notify: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
saveConfig(testConfig, testConfigPath, testInstructionsPath);
|
|
|
|
|
|
|
|
|
|
|
|
expect(memfs[testInstructionsPath]).toBe(userInstructions);
|
|
|
|
|
|
|
|
|
|
|
|
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(loadedConfig.instructions).toBe(userInstructions);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("handles empty user instructions when saving with project doc separator", () => {
|
|
|
|
|
|
const projectDoc = "project specific documentation";
|
|
|
|
|
|
const combinedInstructions = `\n\n--- project-doc ---\n\n${projectDoc}`;
|
|
|
|
|
|
|
|
|
|
|
|
const testConfig = {
|
|
|
|
|
|
model: "test-model",
|
|
|
|
|
|
instructions: combinedInstructions,
|
|
|
|
|
|
notify: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
saveConfig(testConfig, testConfigPath, testInstructionsPath);
|
|
|
|
|
|
|
|
|
|
|
|
expect(memfs[testInstructionsPath]).toBe("");
|
|
|
|
|
|
|
|
|
|
|
|
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(loadedConfig.instructions).toBe("");
|
|
|
|
|
|
});
|
2025-05-05 22:56:55 +05:30
|
|
|
|
|
|
|
|
|
|
test("loads default shell config when not specified", () => {
|
|
|
|
|
|
// Setup config without shell settings
|
|
|
|
|
|
memfs[testConfigPath] = JSON.stringify(
|
|
|
|
|
|
{
|
|
|
|
|
|
model: "mymodel",
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
2,
|
|
|
|
|
|
);
|
|
|
|
|
|
memfs[testInstructionsPath] = "test instructions";
|
|
|
|
|
|
|
|
|
|
|
|
// Load config and verify default shell settings
|
|
|
|
|
|
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Check shell settings were loaded with defaults
|
|
|
|
|
|
expect(loadedConfig.tools).toBeDefined();
|
|
|
|
|
|
expect(loadedConfig.tools?.shell).toBeDefined();
|
|
|
|
|
|
expect(loadedConfig.tools?.shell?.maxBytes).toBe(DEFAULT_SHELL_MAX_BYTES);
|
|
|
|
|
|
expect(loadedConfig.tools?.shell?.maxLines).toBe(DEFAULT_SHELL_MAX_LINES);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("loads and saves custom shell config", () => {
|
|
|
|
|
|
// Setup config with custom shell settings
|
|
|
|
|
|
const customMaxBytes = 12_410;
|
|
|
|
|
|
const customMaxLines = 500;
|
|
|
|
|
|
|
|
|
|
|
|
memfs[testConfigPath] = JSON.stringify(
|
|
|
|
|
|
{
|
|
|
|
|
|
model: "mymodel",
|
|
|
|
|
|
tools: {
|
|
|
|
|
|
shell: {
|
|
|
|
|
|
maxBytes: customMaxBytes,
|
|
|
|
|
|
maxLines: customMaxLines,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
2,
|
|
|
|
|
|
);
|
|
|
|
|
|
memfs[testInstructionsPath] = "test instructions";
|
|
|
|
|
|
|
|
|
|
|
|
// Load config and verify custom shell settings
|
|
|
|
|
|
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Check shell settings were loaded correctly
|
|
|
|
|
|
expect(loadedConfig.tools?.shell?.maxBytes).toBe(customMaxBytes);
|
|
|
|
|
|
expect(loadedConfig.tools?.shell?.maxLines).toBe(customMaxLines);
|
|
|
|
|
|
|
|
|
|
|
|
// Modify shell settings and save
|
|
|
|
|
|
const updatedMaxBytes = 20_000;
|
|
|
|
|
|
const updatedMaxLines = 1_000;
|
|
|
|
|
|
|
|
|
|
|
|
const updatedConfig = {
|
|
|
|
|
|
...loadedConfig,
|
|
|
|
|
|
tools: {
|
|
|
|
|
|
shell: {
|
|
|
|
|
|
maxBytes: updatedMaxBytes,
|
|
|
|
|
|
maxLines: updatedMaxLines,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
saveConfig(updatedConfig, testConfigPath, testInstructionsPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Verify saved config contains updated shell settings
|
|
|
|
|
|
expect(memfs[testConfigPath]).toContain(`"maxBytes": ${updatedMaxBytes}`);
|
|
|
|
|
|
expect(memfs[testConfigPath]).toContain(`"maxLines": ${updatedMaxLines}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Load again and verify updated values
|
|
|
|
|
|
const reloadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
|
|
|
|
|
disableProjectDoc: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(reloadedConfig.tools?.shell?.maxBytes).toBe(updatedMaxBytes);
|
|
|
|
|
|
expect(reloadedConfig.tools?.shell?.maxLines).toBe(updatedMaxLines);
|
|
|
|
|
|
});
|