import test from "node:test"; import assert from "node:assert/strict"; import { buildClaudeAuthEnv, loadConfig, resolveAnthropicToken, resolveOpenAiApiKey, } from "../src/config.js"; test("loads defaults and freezes config", () => { const config = loadConfig({}); assert.equal(config.agentManager.maxConcurrentAgents, 4); assert.equal(config.orchestration.maxDepth, 4); assert.equal(config.orchestration.mergeConflictMaxAttempts, 2); assert.equal(config.provisioning.portRange.basePort, 36000); assert.equal(config.discovery.fileRelativePath, ".agent-context/resources.json"); assert.equal(config.security.violationHandling, "hard_abort"); assert.equal(config.security.commandTimeoutMs, 120000); assert.equal(config.runtimeEvents.logPath, ".ai_ops/events/runtime-events.ndjson"); assert.equal(config.runtimeEvents.discordMinSeverity, "critical"); assert.deepEqual(config.runtimeEvents.discordAlwaysNotifyTypes, [ "session.started", "session.completed", "session.failed", ]); assert.equal(config.provider.openAiAuthMode, "auto"); assert.equal(config.provider.claudeMaxTurns, 2); assert.equal(config.provider.claudeObservability.mode, "off"); assert.equal(config.provider.claudeObservability.verbosity, "summary"); assert.equal(config.provider.claudeObservability.logPath, ".ai_ops/events/claude-trace.ndjson"); assert.equal(config.provider.claudeObservability.includePartialMessages, false); assert.equal(config.provider.claudeObservability.debug, false); assert.equal(Object.isFrozen(config), true); assert.equal(Object.isFrozen(config.orchestration), true); }); test("validates OPENAI_AUTH_MODE values", () => { assert.throws( () => loadConfig({ OPENAI_AUTH_MODE: "oauth" }), /OPENAI_AUTH_MODE must be one of/, ); }); test("validates boolean env values", () => { assert.throws( () => loadConfig({ CODEX_SKIP_GIT_CHECK: "maybe" }), /must be "true" or "false"/, ); }); test("validates security violation mode", () => { assert.throws( () => loadConfig({ AGENT_SECURITY_VIOLATION_MODE: "retry_forever" }), /invalid_union|Invalid input/i, ); }); test("loads dangerous_warn_only security violation mode", () => { const config = loadConfig({ AGENT_SECURITY_VIOLATION_MODE: "dangerous_warn_only" }); assert.equal(config.security.violationHandling, "dangerous_warn_only"); }); test("validates runtime discord severity mode", () => { assert.throws( () => loadConfig({ AGENT_RUNTIME_DISCORD_MIN_SEVERITY: "verbose" }), /Runtime event severity/, ); }); test("validates claude observability mode", () => { assert.throws( () => loadConfig({ CLAUDE_OBSERVABILITY_MODE: "stream" }), /CLAUDE_OBSERVABILITY_MODE must be one of/, ); }); test("validates CLAUDE_MAX_TURNS bounds", () => { assert.throws( () => loadConfig({ CLAUDE_MAX_TURNS: "0" }), /CLAUDE_MAX_TURNS must be an integer >= 1/, ); }); test("validates claude observability verbosity", () => { assert.throws( () => loadConfig({ CLAUDE_OBSERVABILITY_VERBOSITY: "verbose" }), /CLAUDE_OBSERVABILITY_VERBOSITY must be one of/, ); }); test("loads claude observability settings", () => { const config = loadConfig({ CLAUDE_OBSERVABILITY_MODE: "both", CLAUDE_OBSERVABILITY_VERBOSITY: "full", CLAUDE_OBSERVABILITY_LOG_PATH: ".ai_ops/debug/claude.ndjson", CLAUDE_OBSERVABILITY_INCLUDE_PARTIAL: "true", CLAUDE_OBSERVABILITY_DEBUG: "true", CLAUDE_OBSERVABILITY_DEBUG_LOG_PATH: ".ai_ops/debug/claude-sdk.log", }); assert.equal(config.provider.claudeObservability.mode, "both"); assert.equal(config.provider.claudeObservability.verbosity, "full"); assert.equal(config.provider.claudeObservability.logPath, ".ai_ops/debug/claude.ndjson"); assert.equal(config.provider.claudeObservability.includePartialMessages, true); assert.equal(config.provider.claudeObservability.debug, true); assert.equal(config.provider.claudeObservability.debugLogPath, ".ai_ops/debug/claude-sdk.log"); }); test("prefers CLAUDE_CODE_OAUTH_TOKEN over ANTHROPIC_API_KEY", () => { const config = loadConfig({ CLAUDE_CODE_OAUTH_TOKEN: "oauth-token", ANTHROPIC_API_KEY: "api-key", }); assert.equal(config.provider.anthropicOauthToken, "oauth-token"); assert.equal(config.provider.anthropicApiKey, "api-key"); assert.equal(resolveAnthropicToken(config.provider), "oauth-token"); const authEnv = buildClaudeAuthEnv(config.provider); assert.equal(authEnv.CLAUDE_CODE_OAUTH_TOKEN, "oauth-token"); assert.equal(authEnv.ANTHROPIC_API_KEY, undefined); }); test("falls back to ANTHROPIC_API_KEY when oauth token is absent", () => { const config = loadConfig({ ANTHROPIC_API_KEY: "api-key", }); assert.equal(config.provider.anthropicOauthToken, undefined); assert.equal(config.provider.anthropicApiKey, "api-key"); assert.equal(resolveAnthropicToken(config.provider), "api-key"); const authEnv = buildClaudeAuthEnv(config.provider); assert.equal(authEnv.CLAUDE_CODE_OAUTH_TOKEN, undefined); assert.equal(authEnv.ANTHROPIC_API_KEY, "api-key"); }); test("resolveOpenAiApiKey respects chatgpt auth mode", () => { const config = loadConfig({ OPENAI_AUTH_MODE: "chatgpt", CODEX_API_KEY: "codex-key", OPENAI_API_KEY: "openai-key", }); assert.equal(resolveOpenAiApiKey(config.provider), undefined); }); test("resolveOpenAiApiKey prefers CODEX_API_KEY in auto mode", () => { const config = loadConfig({ OPENAI_AUTH_MODE: "auto", CODEX_API_KEY: "codex-key", OPENAI_API_KEY: "openai-key", }); assert.equal(resolveOpenAiApiKey(config.provider), "codex-key"); }); test("normalizes anthropic-prefixed CLAUDE_MODEL values", () => { const config = loadConfig({ CLAUDE_MODEL: "anthropic/claude-sonnet-4-6", }); assert.equal(config.provider.claudeModel, "claude-sonnet-4-6"); }); test("normalizes AGENT_WORKTREE_TARGET_PATH", () => { const config = loadConfig({ AGENT_WORKTREE_TARGET_PATH: "./src/agents/", }); assert.equal(config.provisioning.gitWorktree.targetPath, "src/agents"); }); test("validates AGENT_WORKTREE_TARGET_PATH against parent traversal", () => { assert.throws( () => loadConfig({ AGENT_WORKTREE_TARGET_PATH: "../secrets" }), /must not contain "\.\." path segments/, ); }); test("validates AGENT_MERGE_CONFLICT_MAX_ATTEMPTS bounds", () => { assert.throws( () => loadConfig({ AGENT_MERGE_CONFLICT_MAX_ATTEMPTS: "0" }), /AGENT_MERGE_CONFLICT_MAX_ATTEMPTS must be an integer >= 1/, ); });