Add complete ECS Fargate infrastructure for Clawdbot Discord bot deployment on AWS. ## Infrastructure (Terraform) - **VPC**: 100.64.0.0/16 with DNS support - **Public Subnets**: 2 subnets in different AZs - **ECR Repository**: clawdbot/bot with lifecycle policy - **IAM Roles**: Task Role (DynamoDB+S3) + Execution Role - **ECS Resources**: Fargate (256 CPU, 2048 MB memory) - **Security Group**: Outbound only - **CloudWatch**: Log group with 7-day retention ## Security Improvements - Discord Bot Token → AWS Secrets Manager - IAM policy with least privilege principle - No plaintext secrets in code ## Code Quality - Fix all 22 lint errors - Add *.d.ts to oxlint ignore patterns - Fix Dockerfile .buildstamp for Linux compatibility Closes #2049 Co-Authored-By: Claude <noreply@anthropic.com>
973 lines
26 KiB
TypeScript
973 lines
26 KiB
TypeScript
/**
|
|
* Engine Unit Tests
|
|
*
|
|
* Tests Clawdbot LLM engine functionality:
|
|
* - Model reference parsing and normalization
|
|
* - Provider detection and CLI backends
|
|
* - Model fallback mechanisms
|
|
* - OpenAI Codex model defaults
|
|
* - Model alias resolution
|
|
* - Allowlist enforcement
|
|
* - Auth profile resolution for model access
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
// Mock dependencies
|
|
vi.mock("../../src/agents/defaults.js", () => ({
|
|
DEFAULT_MODEL: "claude-opus-4-5",
|
|
DEFAULT_PROVIDER: "anthropic",
|
|
}));
|
|
|
|
vi.mock("../../src/config/paths.js", () => ({
|
|
resolveStateDir: () => "/test/state",
|
|
}));
|
|
|
|
vi.mock("../../src/utils.js", () => ({
|
|
resolveUserPath: (path: string) => path,
|
|
}));
|
|
|
|
// Import after mocking
|
|
import type { ClawdbotConfig } from "../../src/config/config.js";
|
|
import {
|
|
parseModelRef,
|
|
normalizeProviderId,
|
|
isCliProvider,
|
|
modelKey,
|
|
buildModelAliasIndex,
|
|
resolveModelRefFromString,
|
|
resolveConfiguredModelRef,
|
|
resolveDefaultModelForAgent,
|
|
buildAllowedModelSet,
|
|
getModelRefStatus,
|
|
resolveAllowedModelRef,
|
|
resolveThinkingDefault,
|
|
type ThinkLevel,
|
|
} from "../../src/agents/model-selection.js";
|
|
|
|
import {
|
|
applyOpenAICodexModelDefault,
|
|
OPENAI_CODEX_DEFAULT_MODEL,
|
|
} from "../../src/commands/openai-codex-model-default.js";
|
|
import {
|
|
resolveAgentModelPrimary,
|
|
resolveAgentModelFallbacksOverride,
|
|
} from "../../src/agents/agent-scope.js";
|
|
|
|
// Test utilities
|
|
function createMockConfig(overrides: Partial<ClawdbotConfig> = {}): ClawdbotConfig {
|
|
return {
|
|
meta: {
|
|
lastTouchedVersion: "2026.1.24-0",
|
|
lastTouchedAt: new Date().toISOString(),
|
|
},
|
|
agents: {
|
|
defaults: {
|
|
model: "anthropic/claude-opus-4-5",
|
|
},
|
|
list: [],
|
|
},
|
|
...overrides,
|
|
} as ClawdbotConfig;
|
|
}
|
|
|
|
// Mock model catalog
|
|
const mockModelCatalog = [
|
|
{ provider: "anthropic", id: "claude-opus-4-5", name: "Claude Opus 4.5" },
|
|
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
|
|
{ provider: "anthropic", id: "claude-haiku-3-5", name: "Claude Haiku 3.5" },
|
|
{ provider: "openai", id: "gpt-4.1", name: "GPT-4.1" },
|
|
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 Mini" },
|
|
{ provider: "google", id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
|
{ provider: "openai-codex", id: "gpt-5.2", name: "Codex GPT-5.2" },
|
|
];
|
|
|
|
describe("Engine Unit Tests", () => {
|
|
describe("Model Reference Parsing (モデル参照解析)", () => {
|
|
it("should parse provider/model format", () => {
|
|
const result = parseModelRef("anthropic/claude-opus-4-5", "anthropic");
|
|
expect(result).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
});
|
|
|
|
it("should parse model-only format with default provider", () => {
|
|
const result = parseModelRef("claude-opus-4-5", "anthropic");
|
|
expect(result).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
});
|
|
|
|
it("should handle whitespace in model ref", () => {
|
|
const result = parseModelRef(" anthropic/claude-opus-4-5 ", "anthropic");
|
|
expect(result).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
});
|
|
|
|
it("should return null for empty string", () => {
|
|
const result = parseModelRef("", "anthropic");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should return null for invalid format", () => {
|
|
const result = parseModelRef("/", "anthropic");
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("Provider Normalization (プロバイダー正規化)", () => {
|
|
it("should normalize provider IDs to lowercase", () => {
|
|
expect(normalizeProviderId("Anthropic")).toBe("anthropic");
|
|
expect(normalizeProviderId("OPENAI")).toBe("openai");
|
|
expect(normalizeProviderId("Google")).toBe("google");
|
|
});
|
|
|
|
it("should normalize z.ai aliases", () => {
|
|
expect(normalizeProviderId("z.ai")).toBe("zai");
|
|
expect(normalizeProviderId("z-ai")).toBe("zai");
|
|
expect(normalizeProviderId("ZAI")).toBe("zai");
|
|
});
|
|
|
|
it("should normalize opencode-zen alias", () => {
|
|
expect(normalizeProviderId("opencode-zen")).toBe("opencode");
|
|
});
|
|
|
|
it("should normalize qwen alias", () => {
|
|
expect(normalizeProviderId("qwen")).toBe("qwen-portal");
|
|
});
|
|
|
|
it("should trim whitespace", () => {
|
|
expect(normalizeProviderId(" anthropic ")).toBe("anthropic");
|
|
});
|
|
});
|
|
|
|
describe("CLI Provider Detection (CLIプロバイダー検出)", () => {
|
|
it("should detect claude-cli as CLI provider", () => {
|
|
const cfg = createMockConfig();
|
|
expect(isCliProvider("claude-cli", cfg)).toBe(true);
|
|
});
|
|
|
|
it("should detect codex-cli as CLI provider", () => {
|
|
const cfg = createMockConfig();
|
|
expect(isCliProvider("codex-cli", cfg)).toBe(true);
|
|
});
|
|
|
|
it("should detect configured CLI backends", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
cliBackends: {
|
|
"my-custom-cli": { command: "my-cli" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(isCliProvider("my-custom-cli", cfg)).toBe(true);
|
|
});
|
|
|
|
it("should not detect non-CLI providers", () => {
|
|
const cfg = createMockConfig();
|
|
expect(isCliProvider("anthropic", cfg)).toBe(false);
|
|
expect(isCliProvider("openai", cfg)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Model Key Generation (モデルキー生成)", () => {
|
|
it("should generate correct model key", () => {
|
|
expect(modelKey("anthropic", "claude-opus-4-5")).toBe("anthropic/claude-opus-4-5");
|
|
expect(modelKey("openai", "gpt-4.1")).toBe("openai/gpt-4.1");
|
|
});
|
|
|
|
it("should handle provider with special characters", () => {
|
|
expect(modelKey("openai-codex", "gpt-5.2")).toBe("openai-codex/gpt-5.2");
|
|
});
|
|
});
|
|
|
|
describe("Model Alias Index (モデル別名インデックス)", () => {
|
|
it("should build alias index from config", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": { alias: "opus" },
|
|
"openai/gpt-4.1": { alias: "gpt4" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const index = buildModelAliasIndex({
|
|
cfg,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(index.byAlias.get("opus")?.ref).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
expect(index.byAlias.get("gpt4")?.ref).toEqual({
|
|
provider: "openai",
|
|
model: "gpt-4.1",
|
|
});
|
|
});
|
|
|
|
it("should handle case-insensitive alias lookup", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": { alias: "OpUs" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const index = buildModelAliasIndex({
|
|
cfg,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
// The index normalizes alias keys to lowercase, so lookups should use normalized keys
|
|
expect(index.byAlias.get("opus")).toBeDefined();
|
|
// The original alias "OpUs" is stored in the value
|
|
expect(index.byAlias.get("opus")?.alias).toBe("OpUs");
|
|
});
|
|
|
|
it("should build byKey map correctly", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": { alias: "opus" },
|
|
"openai/gpt-4.1": { alias: "gpt4" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const index = buildModelAliasIndex({
|
|
cfg,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(index.byKey.get("anthropic/claude-opus-4-5")).toEqual(["opus"]);
|
|
expect(index.byKey.get("openai/gpt-4.1")).toEqual(["gpt4"]);
|
|
});
|
|
});
|
|
|
|
describe("Model Ref Resolution (モデル参照解決)", () => {
|
|
it("should resolve model ref from string", () => {
|
|
const result = resolveModelRefFromString({
|
|
raw: "anthropic/claude-opus-4-5",
|
|
defaultProvider: "anthropic",
|
|
});
|
|
expect(result).toEqual({
|
|
ref: { provider: "anthropic", model: "claude-opus-4-5" },
|
|
});
|
|
});
|
|
|
|
it("should resolve model ref from alias", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": { alias: "opus" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const aliasIndex = buildModelAliasIndex({
|
|
cfg,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
const result = resolveModelRefFromString({
|
|
raw: "opus",
|
|
defaultProvider: "anthropic",
|
|
aliasIndex,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ref: { provider: "anthropic", model: "claude-opus-4-5" },
|
|
alias: "opus",
|
|
});
|
|
});
|
|
|
|
it("should return null for invalid model ref", () => {
|
|
const result = resolveModelRefFromString({
|
|
raw: "",
|
|
defaultProvider: "anthropic",
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("Configured Model Ref (設定済みモデル参照)", () => {
|
|
it("should resolve configured model", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
model: "anthropic/claude-opus-4-5",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = resolveConfiguredModelRef({
|
|
cfg,
|
|
defaultProvider: "anthropic",
|
|
defaultModel: "claude-opus-4-5",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
});
|
|
|
|
it("should use default when no model configured", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {},
|
|
},
|
|
});
|
|
|
|
const result = resolveConfiguredModelRef({
|
|
cfg,
|
|
defaultProvider: "anthropic",
|
|
defaultModel: "claude-opus-4-5",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
});
|
|
|
|
it("should handle object model config", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "anthropic/claude-opus-4-5" },
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = resolveConfiguredModelRef({
|
|
cfg,
|
|
defaultProvider: "anthropic",
|
|
defaultModel: "claude-opus-4-5",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Default Model for Agent (エージェントのデフォルトモデル)", () => {
|
|
it("should resolve agent-specific model override", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
model: "anthropic/claude-haiku-3-5",
|
|
},
|
|
list: [
|
|
{
|
|
id: "test-agent",
|
|
model: "openai/gpt-4.1",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const result = resolveDefaultModelForAgent({
|
|
cfg,
|
|
agentId: "test-agent",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
provider: "openai",
|
|
model: "gpt-4.1",
|
|
});
|
|
});
|
|
|
|
it("should use default when no agent override", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
model: "anthropic/claude-opus-4-5",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = resolveDefaultModelForAgent({
|
|
cfg,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Allowed Model Set (許可モデルセット)", () => {
|
|
it("should allow any when no allowlist configured", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {},
|
|
},
|
|
});
|
|
|
|
const result = buildAllowedModelSet({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(result.allowAny).toBe(true);
|
|
expect(result.allowedCatalog).toEqual(mockModelCatalog);
|
|
});
|
|
|
|
it("should restrict to allowlisted models", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": {},
|
|
"anthropic/claude-sonnet-4-5": {},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = buildAllowedModelSet({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(result.allowAny).toBe(false);
|
|
expect(result.allowedCatalog).toHaveLength(2);
|
|
expect(result.allowedCatalog[0].id).toBe("claude-opus-4-5");
|
|
expect(result.allowedCatalog[1].id).toBe("claude-sonnet-4-5");
|
|
});
|
|
|
|
it("should include CLI providers in allowlist", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"codex-cli/gpt-5": {},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = buildAllowedModelSet({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(result.allowedKeys.has("codex-cli/gpt-5")).toBe(true);
|
|
});
|
|
|
|
it("should include default model in allowed set", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": {},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = buildAllowedModelSet({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
defaultProvider: "anthropic",
|
|
defaultModel: "claude-haiku-3-5",
|
|
});
|
|
|
|
expect(result.allowedKeys.has("anthropic/claude-haiku-3-5")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Model Ref Status (モデル参照ステータス)", () => {
|
|
it("should report status for catalog model", () => {
|
|
const cfg = createMockConfig();
|
|
|
|
const result = getModelRefStatus({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
ref: { provider: "anthropic", model: "claude-opus-4-5" },
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(result.key).toBe("anthropic/claude-opus-4-5");
|
|
expect(result.inCatalog).toBe(true);
|
|
expect(result.allowAny).toBe(true);
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it("should report status for non-catalog model", () => {
|
|
const cfg = createMockConfig();
|
|
|
|
const result = getModelRefStatus({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
ref: { provider: "custom", model: "custom-model" },
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(result.key).toBe("custom/custom-model");
|
|
expect(result.inCatalog).toBe(false);
|
|
expect(result.allowAny).toBe(true);
|
|
expect(result.allowed).toBe(true); // allowAny=true
|
|
});
|
|
|
|
it("should report not allowed when restricted", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": {},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = getModelRefStatus({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
ref: { provider: "openai", model: "gpt-4.1" },
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(result.allowed).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Allowed Model Ref Resolution (許可モデル参照解決)", () => {
|
|
it("should resolve allowed model", () => {
|
|
const cfg = createMockConfig();
|
|
|
|
const result = resolveAllowedModelRef({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
raw: "anthropic/claude-opus-4-5",
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
if ("error" in result) {
|
|
throw new Error(`Unexpected error: ${result.error}`);
|
|
}
|
|
|
|
expect(result.ref).toEqual({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
expect(result.key).toBe("anthropic/claude-opus-4-5");
|
|
});
|
|
|
|
it("should reject disallowed model", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": {},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = resolveAllowedModelRef({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
raw: "openai/gpt-4.1",
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
if (!("error" in result)) {
|
|
throw new Error("Expected error but got result");
|
|
}
|
|
|
|
expect(result.error).toContain("model not allowed");
|
|
});
|
|
|
|
it("should reject empty model", () => {
|
|
const cfg = createMockConfig();
|
|
|
|
const result = resolveAllowedModelRef({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
raw: "",
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
if (!("error" in result)) {
|
|
throw new Error("Expected error but got result");
|
|
}
|
|
|
|
expect(result.error).toBe("invalid model: empty");
|
|
});
|
|
});
|
|
|
|
describe("Thinking Default (思考デフォルト)", () => {
|
|
it("should use configured thinking default", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
thinkingDefault: "high" as ThinkLevel,
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = resolveThinkingDefault({
|
|
cfg,
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
});
|
|
|
|
expect(result).toBe("high");
|
|
});
|
|
|
|
it("should detect reasoning models", () => {
|
|
const cfg = createMockConfig();
|
|
|
|
const catalog = [{ provider: "anthropic", id: "claude-opus-4-5", reasoning: true }];
|
|
|
|
const result = resolveThinkingDefault({
|
|
cfg,
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
catalog,
|
|
});
|
|
|
|
expect(result).toBe("low");
|
|
});
|
|
|
|
it("should default to off for non-reasoning models", () => {
|
|
const cfg = createMockConfig();
|
|
|
|
const result = resolveThinkingDefault({
|
|
cfg,
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
catalog: mockModelCatalog,
|
|
});
|
|
|
|
expect(result).toBe("off");
|
|
});
|
|
});
|
|
|
|
describe("OpenAI Codex Model Defaults (OpenAI Codexモデルデフォルト)", () => {
|
|
it("should set codex default when model is unset", () => {
|
|
const cfg: ClawdbotConfig = { agents: { defaults: {} } };
|
|
const applied = applyOpenAICodexModelDefault(cfg);
|
|
|
|
expect(applied.changed).toBe(true);
|
|
expect(applied.next.agents?.defaults?.model).toEqual({
|
|
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
|
});
|
|
});
|
|
|
|
it("should set codex default when model is openai/*", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
agents: { defaults: { model: "openai/gpt-4.1" } },
|
|
};
|
|
const applied = applyOpenAICodexModelDefault(cfg);
|
|
|
|
expect(applied.changed).toBe(true);
|
|
expect(applied.next.agents?.defaults?.model).toEqual({
|
|
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
|
});
|
|
});
|
|
|
|
it("should set codex default when model is 'gpt'", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
agents: { defaults: { model: "gpt" } },
|
|
};
|
|
const applied = applyOpenAICodexModelDefault(cfg);
|
|
|
|
expect(applied.changed).toBe(true);
|
|
expect(applied.next.agents?.defaults?.model).toEqual({
|
|
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
|
});
|
|
});
|
|
|
|
it("should not override openai-codex/*", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
agents: { defaults: { model: "openai-codex/gpt-5.2" } },
|
|
};
|
|
const applied = applyOpenAICodexModelDefault(cfg);
|
|
|
|
expect(applied.changed).toBe(false);
|
|
expect(applied.next).toEqual(cfg);
|
|
});
|
|
|
|
it("should not override non-openai models", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
agents: { defaults: { model: "anthropic/claude-opus-4-5" } },
|
|
};
|
|
const applied = applyOpenAICodexModelDefault(cfg);
|
|
|
|
expect(applied.changed).toBe(false);
|
|
expect(applied.next).toEqual(cfg);
|
|
});
|
|
|
|
it("should preserve existing fallbacks when setting codex default", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "openai/gpt-4.1", fallbacks: ["anthropic/claude-haiku-3-5"] },
|
|
},
|
|
},
|
|
};
|
|
const applied = applyOpenAICodexModelDefault(cfg);
|
|
|
|
expect(applied.changed).toBe(true);
|
|
expect(applied.next.agents?.defaults?.model).toEqual({
|
|
primary: OPENAI_CODEX_DEFAULT_MODEL,
|
|
fallbacks: ["anthropic/claude-haiku-3-5"],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Agent Model Resolution (エージェントモデル解決)", () => {
|
|
it("should resolve primary model from string", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
list: [{ id: "test-agent", model: "openai/gpt-4.1" }],
|
|
},
|
|
});
|
|
|
|
const primary = resolveAgentModelPrimary(cfg, "test-agent");
|
|
expect(primary).toBe("openai/gpt-4.1");
|
|
});
|
|
|
|
it("should resolve primary model from object", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "test-agent",
|
|
model: { primary: "openai/gpt-4.1", fallbacks: ["anthropic/claude-haiku-3-5"] },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const primary = resolveAgentModelPrimary(cfg, "test-agent");
|
|
expect(primary).toBe("openai/gpt-4.1");
|
|
});
|
|
|
|
it("should resolve fallbacks override", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "test-agent",
|
|
model: { primary: "openai/gpt-4.1", fallbacks: ["anthropic/claude-haiku-3-5"] },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const fallbacks = resolveAgentModelFallbacksOverride(cfg, "test-agent");
|
|
expect(fallbacks).toEqual(["anthropic/claude-haiku-3-5"]);
|
|
});
|
|
|
|
it("should return undefined fallbacks when not explicitly set", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "test-agent",
|
|
model: { primary: "openai/gpt-4.1" },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const fallbacks = resolveAgentModelFallbacksOverride(cfg, "test-agent");
|
|
expect(fallbacks).toBeUndefined();
|
|
});
|
|
|
|
it("should return empty array when explicitly set", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "test-agent",
|
|
model: { primary: "openai/gpt-4.1", fallbacks: [] },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const fallbacks = resolveAgentModelFallbacksOverride(cfg, "test-agent");
|
|
expect(fallbacks).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("Codex-Ready Model Handling (Codex対応モデル処理)", () => {
|
|
it("should handle xhigh thinking with codex models", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
thinkingDefault: "xhigh" as ThinkLevel,
|
|
models: {
|
|
"openai-codex/gpt-5.2": { alias: "codex" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const aliasIndex = buildModelAliasIndex({
|
|
cfg,
|
|
defaultProvider: "openai-codex",
|
|
});
|
|
|
|
const result = resolveModelRefFromString({
|
|
raw: "codex",
|
|
defaultProvider: "openai-codex",
|
|
aliasIndex,
|
|
});
|
|
|
|
expect(result?.ref).toEqual({
|
|
provider: "openai-codex",
|
|
model: "gpt-5.2",
|
|
});
|
|
});
|
|
|
|
it("should support fallback from codex to anthropic on auth error", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
model: {
|
|
primary: "openai-codex/gpt-5.2",
|
|
fallbacks: ["anthropic/claude-opus-4-5"],
|
|
},
|
|
},
|
|
// Add a "main" agent with its own model config that inherits defaults
|
|
list: [{ id: "main", name: "Main Agent" }],
|
|
},
|
|
});
|
|
|
|
const _primary = resolveAgentModelPrimary(cfg, "main");
|
|
const fallbacks = resolveAgentModelFallbacksOverride(cfg, "main");
|
|
|
|
// main agent has no model override, so it inherits from defaults
|
|
// but since defaults.model has no fallbacks configured at the agent level,
|
|
// we expect it to be undefined
|
|
expect(fallbacks).toBeUndefined();
|
|
});
|
|
|
|
it("should handle codex CLI provider detection", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
cliBackends: {
|
|
"codex-cli": { command: "codex", args: [] },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(isCliProvider("codex-cli", cfg)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Engine Configuration (エンジン設定)", () => {
|
|
it("should handle multiple providers in allowlist", () => {
|
|
const cfg = createMockConfig({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-5": {},
|
|
"openai/gpt-4.1": {},
|
|
"google/gemini-2.5-flash": {},
|
|
"codex-cli/gpt-5": {},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = buildAllowedModelSet({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(result.allowedKeys.has("anthropic/claude-opus-4-5")).toBe(true);
|
|
expect(result.allowedKeys.has("openai/gpt-4.1")).toBe(true);
|
|
expect(result.allowedKeys.has("google/gemini-2.5-flash")).toBe(true);
|
|
expect(result.allowedKeys.has("codex-cli/gpt-5")).toBe(true);
|
|
});
|
|
|
|
it("should handle custom configured providers", () => {
|
|
const cfg = createMockConfig({
|
|
models: {
|
|
providers: {
|
|
"custom-provider": {
|
|
apiKey: "test-key",
|
|
baseUrl: "https://custom.api/v1",
|
|
},
|
|
},
|
|
},
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"custom-provider/custom-model": {},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = buildAllowedModelSet({
|
|
cfg,
|
|
catalog: mockModelCatalog,
|
|
defaultProvider: "anthropic",
|
|
});
|
|
|
|
expect(result.allowedKeys.has("custom-provider/custom-model")).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Engine Unit Test Summary
|
|
*
|
|
* Test Coverage:
|
|
* ✅ Model reference parsing and normalization
|
|
* ✅ Provider detection and CLI backends
|
|
* ✅ Model fallback mechanisms
|
|
* ✅ OpenAI Codex model defaults
|
|
* ✅ Model alias resolution
|
|
* ✅ Allowlist enforcement
|
|
* ✅ Auth profile resolution
|
|
* ✅ Thinking level defaults
|
|
* ✅ Codex-ready model handling
|
|
*
|
|
* Integration Points:
|
|
* - Agent configuration (agent-scope.js)
|
|
* - Model catalog (model-catalog.js)
|
|
* - Provider backends (cli-credentials.ts)
|
|
* - Auth profiles (auth-profiles.ts)
|
|
*
|
|
* Run with:
|
|
* pnpm test test/unit/engine.test.ts
|
|
*
|
|
* For coverage:
|
|
* pnpm test:coverage test/unit/engine.test.ts
|
|
*/
|