← octto
// tests/config/loader-integration.test.ts
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { AgentConfig } from "@opencode-ai/sdk";
import { AGENTS, type AgentName } from "../../src/agents";
import { loadCustomConfig } from "../../src/config/loader";
const stubAgents: Record<AgentName, AgentConfig> = {
[AGENTS.octto]: { description: "octto", model: "test-model", prompt: "octto prompt" },
[AGENTS.bootstrapper]: { description: "bootstrapper", model: "test-model", prompt: "bootstrapper prompt" },
[AGENTS.probe]: { description: "probe", model: "test-model", prompt: "probe prompt" },
};
describe("loadCustomConfig", () => {
let configDir: string;
let warnSpy: ReturnType<typeof spyOn>;
beforeEach(() => {
configDir = mkdtempSync(join(tmpdir(), "octto-config-test-"));
warnSpy = spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
warnSpy.mockRestore();
rmSync(configDir, { recursive: true, force: true });
});
it("should return default agents when no config file exists", async () => {
const result = await loadCustomConfig(stubAgents, configDir);
expect(result.agents).toEqual(stubAgents);
expect(result.port).toBe(0);
});
it("should merge valid config overrides into agents", async () => {
writeFileSync(
join(configDir, "octto.json"),
JSON.stringify({
agents: {
octto: { model: "custom-model", temperature: 0.5 },
},
port: 3000,
}),
);
const result = await loadCustomConfig(stubAgents, configDir);
expect(result.agents.octto.model).toBe("custom-model");
expect(result.agents.octto.prompt).toBe("octto prompt");
expect(result.agents.bootstrapper.model).toBe("test-model");
expect(result.port).toBe(3000);
});
it("should handle invalid JSON gracefully and return defaults", async () => {
writeFileSync(join(configDir, "octto.json"), "{ not valid json }");
const result = await loadCustomConfig(stubAgents, configDir);
expect(result.agents).toEqual(stubAgents);
expect(result.port).toBe(0);
});
it("should salvage valid agents from partially invalid config", async () => {
writeFileSync(
join(configDir, "octto.json"),
JSON.stringify({
agents: {
octto: { model: "good-model" },
bootstrapper: { temperature: 999 },
},
}),
);
const result = await loadCustomConfig(stubAgents, configDir);
expect(result.agents.octto.model).toBe("good-model");
expect(warnSpy).toHaveBeenCalled();
});
it("should include fragments from config", async () => {
writeFileSync(
join(configDir, "octto.json"),
JSON.stringify({
fragments: {
octto: ["Be concise"],
},
}),
);
const result = await loadCustomConfig(stubAgents, configDir);
expect(result.fragments).toEqual({ octto: ["Be concise"] });
});
it("should apply top-level model to all agents", async () => {
writeFileSync(
join(configDir, "octto.json"),
JSON.stringify({
model: "anthropic/claude-sonnet-4",
}),
);
const result = await loadCustomConfig(stubAgents, configDir);
expect(result.agents.octto.model).toBe("anthropic/claude-sonnet-4");
expect(result.agents.bootstrapper.model).toBe("anthropic/claude-sonnet-4");
expect(result.agents.probe.model).toBe("anthropic/claude-sonnet-4");
// Non-model fields should remain unchanged
expect(result.agents.octto.prompt).toBe("octto prompt");
});
it("should let per-agent model override top-level model", async () => {
writeFileSync(
join(configDir, "octto.json"),
JSON.stringify({
model: "anthropic/claude-sonnet-4",
agents: {
probe: { model: "openai/gpt-4o" },
},
}),
);
const result = await loadCustomConfig(stubAgents, configDir);
expect(result.agents.octto.model).toBe("anthropic/claude-sonnet-4");
expect(result.agents.bootstrapper.model).toBe("anthropic/claude-sonnet-4");
expect(result.agents.probe.model).toBe("openai/gpt-4o");
});
it("should use defaults when top-level model is not set", async () => {
writeFileSync(join(configDir, "octto.json"), JSON.stringify({}));
const result = await loadCustomConfig(stubAgents, configDir);
expect(result.agents.octto.model).toBe("test-model");
expect(result.agents.bootstrapper.model).toBe("test-model");
expect(result.agents.probe.model).toBe("test-model");
});
it("should salvage top-level model from partially invalid config", async () => {
writeFileSync(
join(configDir, "octto.json"),
JSON.stringify({
model: "anthropic/claude-sonnet-4",
agents: {
octto: { temperature: 999 },
},
}),
);
const result = await loadCustomConfig(stubAgents, configDir);
// Top-level model should still apply even when agent config is invalid
expect(result.agents.octto.model).toBe("anthropic/claude-sonnet-4");
expect(result.agents.bootstrapper.model).toBe("anthropic/claude-sonnet-4");
expect(result.agents.probe.model).toBe("anthropic/claude-sonnet-4");
expect(warnSpy).toHaveBeenCalled();
});
});