188 lines
6.4 KiB
TypeScript
188 lines
6.4 KiB
TypeScript
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/,
|
|
);
|
|
});
|