Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
26 KiB
JSON

{"repo": "agent-field/swe-af", "files": {"tests/test_model_config.py": {"ef508a8fb38155c683fa5d249ddbbcde6d89b3be": {"t": 1786068513.7224889, "n": 25511, "h": "19799757e4986a10", "c": "\"\"\"Tests for V2 runtime + flat model configuration.\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport os\nimport unittest\nfrom unittest import mock\n\nfrom swe_af.execution.schemas import (\n ALL_MODEL_FIELDS,\n BuildConfig,\n ExecutionConfig,\n ROLE_TO_MODEL_FIELD,\n _OPENROUTER_AUTO_DEFAULT_MODEL,\n _default_planning_model,\n _default_runtime,\n resolve_runtime_models,\n)\n\n# Env vars that steer provider/runtime/model selection. Tests clear them so the\n# result never depends on the developer's ambient shell.\n_PROVIDER_ENV_KEYS = (\n \"ANTHROPIC_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"SWE_DEFAULT_RUNTIME\",\n \"SWE_DEFAULT_MODEL\",\n \"AI_MODEL\",\n \"HARNESS_MODEL\",\n \"SWE_MODEL_LOW\",\n \"SWE_MODEL_MED\",\n \"SWE_MODEL_HIGH\",\n)\n\n\n@contextlib.contextmanager\ndef _provider_env(**overrides: str):\n \"\"\"Run with the provider-selection env vars cleared, then apply overrides.\"\"\"\n saved = {k: os.environ.get(k) for k in _PROVIDER_ENV_KEYS}\n for k in _PROVIDER_ENV_KEYS:\n os.environ.pop(k, None)\n os.environ.update(overrides)\n try:\n yield\n finally:\n for k in _PROVIDER_ENV_KEYS:\n os.environ.pop(k, None)\n if saved[k] is not None:\n os.environ[k] = saved[k]\n\n\nclass TestDefaultPlanningModel(unittest.TestCase):\n \"\"\"`_default_planning_model` picks the planning-reasoner model default.\"\"\"\n\n def test_claude_env_defaults_to_sonnet(self) -> None:\n with _provider_env(ANTHROPIC_API_KEY=\"sk-ant\"):\n self.assertEqual(_default_planning_model(), \"sonnet\")\n\n def test_no_provider_env_defaults_to_sonnet(self) -> None:\n with _provider_env():\n self.assertEqual(_default_planning_model(), \"sonnet\")\n\n def test_openrouter_only_defaults_to_openrouter_model(self) -> None:\n with _provider_env(OPENROUTER_API_KEY=\"sk-or\"):\n self.assertEqual(_default_planning_model(), _OPENROUTER_AUTO_DEFAULT_MODEL)\n\n def test_swe_default_model_wins_over_openrouter_auto(self) -> None:\n with _provider_env(OPENROUTER_API_KEY=\"sk-or\", SWE_DEFAULT_MODEL=\"openrouter/qwen/qwen3-max\"):\n self.assertEqual(_default_planning_model(), \"openrouter/qwen/qwen3-max\")\n\n def test_ai_model_cascade_applies(self) -> None:\n with _provider_env(ANTHROPIC_API_KEY=\"sk-ant\", AI_MODEL=\"opus\"):\n self.assertEqual(_default_planning_model(), \"opus\")\n\n\nclass TestResolveRuntimeModels(unittest.TestCase):\n def test_claude_code_defaults(self) -> None:\n resolved = resolve_runtime_models(runtime=\"claude_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n if field == \"qa_synthesizer_model\":\n continue\n self.assertEqual(resolved[field], \"sonnet\")\n self.assertEqual(resolved[\"qa_synthesizer_model\"], \"haiku\")\n\n def test_open_code_defaults(self) -> None:\n # No provider env set → the shared open_code base default applies.\n with _provider_env():\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(resolved[field], \"openrouter/deepseek/deepseek-v4-flash-0731\")\n\n def test_models_default_applies_to_all(self) -> None:\n resolved = resolve_runtime_models(\n runtime=\"claude_code\",\n models={\"default\": \"opus\"},\n )\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(resolved[field], \"opus\")\n\n def test_role_override_beats_default(self) -> None:\n resolved = resolve_runtime_models(\n runtime=\"claude_code\",\n models={\"default\": \"sonnet\", \"coder\": \"opus\"},\n )\n self.assertEqual(resolved[\"coder_model\"], \"opus\")\n self.assertEqual(resolved[\"qa_model\"], \"sonnet\")\n\n def test_invalid_runtime_raises(self) -> None:\n with self.assertRaises(ValueError):\n resolve_runtime_models(runtime=\"bad_runtime\", models=None)\n\n def test_invalid_model_key_raises(self) -> None:\n with self.assertRaises(ValueError):\n resolve_runtime_models(runtime=\"claude_code\", models={\"bad\": \"opus\"})\n\n\nclass TestBuildConfig(unittest.TestCase):\n def test_default_runtime_and_provider(self) -> None:\n cfg = BuildConfig()\n self.assertEqual(cfg.runtime, \"claude_code\")\n self.assertEqual(cfg.ai_provider, \"claude\")\n\n def test_open_code_runtime_provider(self) -> None:\n with _provider_env():\n cfg = BuildConfig(runtime=\"open_code\")\n self.assertEqual(cfg.ai_provider, \"opencode\")\n resolved = cfg.resolved_models()\n self.assertEqual(resolved[\"coder_model\"], \"openrouter/deepseek/deepseek-v4-flash-0731\")\n\n\nclass TestOpenRouterAutoSelection(unittest.TestCase):\n \"\"\"When only an OpenRouter key is present (no explicit runtime), SWE-AF\n auto-selects the open_code runtime and defaults to DeepSeek.\"\"\"\n\n def test_openrouter_only_auto_selects_open_code(self) -> None:\n with _provider_env(OPENROUTER_API_KEY=\"sk-or-x\"):\n self.assertEqual(_default_runtime(), \"open_code\")\n\n def test_anthropic_key_keeps_claude_code(self) -> None:\n with _provider_env(ANTHROPIC_API_KEY=\"sk-ant\"):\n self.assertEqual(_default_runtime(), \"claude_code\")\n\n def test_both_keys_keep_claude_code(self) -> None:\n # Anthropic present → claude_code even if OpenRouter is also set.\n with _provider_env(ANTHROPIC_API_KEY=\"sk-ant\", OPENROUTER_API_KEY=\"sk-or\"):\n self.assertEqual(_default_runtime(), \"claude_code\")\n\n def test_no_keys_default_claude_code(self) -> None:\n with _provider_env():\n self.assertEqual(_default_runtime(), \"claude_code\")\n\n def test_explicit_runtime_overrides_autoselect(self) -> None:\n # Explicit SWE_DEFAULT_RUNTIME wins over key-based auto-selection.\n with _provider_env(OPENROUTER_API_KEY=\"sk-or\", SWE_DEFAULT_RUNTIME=\"claude_code\"):\n self.assertEqual(_default_runtime(), \"claude_code\")\n\n def test_auto_openrouter_defaults_to_deepseek(self) -> None:\n with _provider_env(OPENROUTER_API_KEY=\"sk-or\"):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(resolved[field], \"openrouter/deepseek/deepseek-v4-flash-0731\")\n\n def test_explicit_open_code_same_default(self) -> None:\n # A deployer who explicitly sets open_code resolves to the SAME model\n # as the auto-selected OpenRouter path — opting in explicitly must\n # never silently swap the model.\n with _provider_env(OPENROUTER_API_KEY=\"sk-or\", SWE_DEFAULT_RUNTIME=\"open_code\"):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(resolved[field], \"openrouter/deepseek/deepseek-v4-flash-0731\")\n\n def test_swe_default_model_overrides_auto_deepseek(self) -> None:\n with _provider_env(OPENROUTER_API_KEY=\"sk-or\", SWE_DEFAULT_MODEL=\"openrouter/qwen/qwen-3\"):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(resolved[field], \"openrouter/qwen/qwen-3\")\n\n def test_build_config_auto_openrouter_end_to_end(self) -> None:\n with _provider_env(OPENROUTER_API_KEY=\"sk-or\"):\n cfg = BuildConfig()\n self.assertEqual(cfg.runtime, \"open_code\")\n resolved = cfg.resolved_models()\n self.assertEqual(resolved[\"coder_model\"], \"openrouter/deepseek/deepseek-v4-flash-0731\")\n\n def test_to_execution_config_dict_roundtrips(self) -> None:\n cfg = BuildConfig(runtime=\"open_code\", models={\"coder\": \"deepseek/deepseek-chat\"})\n d = cfg.to_execution_config_dict()\n self.assertEqual(d[\"runtime\"], \"open_code\")\n self.assertEqual(d[\"models\"][\"coder\"], \"deepseek/deepseek-chat\")\n exec_cfg = ExecutionConfig(**d)\n self.assertEqual(exec_cfg.coder_model, \"deepseek/deepseek-chat\")\n self.assertEqual(exec_cfg.qa_model, \"openrouter/deepseek/deepseek-v4-flash-0731\")\n\n def test_legacy_top_level_keys_rejected(self) -> None:\n with self.assertRaises(ValueError) as ctx:\n BuildConfig(**{\"ai_provider\": \"claude\"})\n self.assertIn(\"ai_provider\", str(ctx.exception))\n self.assertIn(\"runtime\", str(ctx.exception))\n\n with self.assertRaises(ValueError) as ctx:\n BuildConfig(**{\"coder_model\": \"opus\"})\n self.assertIn(\"coder_model\", str(ctx.exception))\n self.assertIn(\"models.coder\", str(ctx.exception))\n\n with self.assertRaises(ValueError) as ctx:\n BuildConfig(**{\"preset\": \"fast\"})\n self.assertIn(\"preset\", str(ctx.exception))\n self.assertIn(\"runtime + models\", str(ctx.exception))\n\n with self.assertRaises(ValueError) as ctx:\n BuildConfig(**{\"model\": \"opus\"})\n self.assertIn(\"model\", str(ctx.exception))\n self.assertIn(\"models.default\", str(ctx.exception))\n\n def test_legacy_model_group_rejected(self) -> None:\n with self.assertRaises(ValueError) as ctx:\n BuildConfig(models={\"planning\": \"opus\"})\n self.assertIn(\"planning\", str(ctx.exception))\n self.assertIn(\"models.pm\", str(ctx.exception))\n\n\nclass TestDefaultRuntimeFromEnv(unittest.TestCase):\n \"\"\"`SWE_DEFAULT_RUNTIME` lets the deployer pick runtime without callers\n threading a config through. Callers that DO pass `runtime=...` win.\"\"\"\n\n def test_env_open_code_overrides_default(self) -> None:\n with mock.patch.dict(os.environ, {\"SWE_DEFAULT_RUNTIME\": \"open_code\"}):\n self.assertEqual(BuildConfig().runtime, \"open_code\")\n self.assertEqual(ExecutionConfig().runtime, \"open_code\")\n\n def test_env_claude_code_overrides_default(self) -> None:\n with mock.patch.dict(os.environ, {\"SWE_DEFAULT_RUNTIME\": \"claude_code\"}):\n self.assertEqual(BuildConfig().runtime, \"claude_code\")\n self.assertEqual(ExecutionConfig().runtime, \"claude_code\")\n\n def test_explicit_runtime_beats_env(self) -> None:\n with mock.patch.dict(os.environ, {\"SWE_DEFAULT_RUNTIME\": \"open_code\"}):\n self.assertEqual(BuildConfig(runtime=\"claude_code\").runtime, \"claude_code\")\n self.assertEqual(ExecutionConfig(runtime=\"claude_code\").runtime, \"claude_code\")\n\n def test_invalid_env_falls_back_to_claude_code(self) -> None:\n with mock.patch.dict(os.environ, {\"SWE_DEFAULT_RUNTIME\": \"bogus_runtime\"}):\n self.assertEqual(BuildConfig().runtime, \"claude_code\")\n\n def test_unset_env_uses_claude_code(self) -> None:\n env = {k: v for k, v in os.environ.items() if k != \"SWE_DEFAULT_RUNTIME\"}\n with mock.patch.dict(os.environ, env, clear=True):\n self.assertEqual(BuildConfig().runtime, \"claude_code\")\n\n def test_empty_env_uses_claude_code(self) -> None:\n with mock.patch.dict(os.environ, {\"SWE_DEFAULT_RUNTIME\": \"\"}):\n self.assertEqual(BuildConfig().runtime, \"claude_code\")\n\n\nclass TestCodexRuntimeConfig(unittest.TestCase):\n def test_codex_runtime_provider_and_defaults(self) -> None:\n # API-key auth: the -codex models are available, so that's the default.\n with mock.patch.dict(os.environ, {\"SWE_CODEX_AUTH_MODE\": \"api_key\"}):\n cfg = BuildConfig(runtime=\"codex\")\n self.assertEqual(cfg.ai_provider, \"codex\")\n resolved = cfg.resolved_models()\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(resolved[field], \"gpt-5.3-codex\")\n\n def test_codex_execution_config_provider_and_defaults(self) -> None:\n with mock.patch.dict(os.environ, {\"SWE_CODEX_AUTH_MODE\": \"api_key\"}):\n cfg = ExecutionConfig(runtime=\"codex\")\n self.assertEqual(cfg.ai_provider, \"codex\")\n self.assertEqual(cfg.coder_model, \"gpt-5.3-codex\")\n self.assertEqual(cfg.verifier_model, \"gpt-5.3-codex\")\n\n def test_codex_chatgpt_auth_uses_non_codex_model(self) -> None:\n # ChatGPT-account auth: the -codex models 400, so the default must be a\n # ChatGPT-compatible model (#82 Gap 3 root cause).\n with mock.patch.dict(\n os.environ, {\"SWE_CODEX_AUTH_MODE\": \"chatgpt\", \"OPENAI_API_KEY\": \"\"}\n ):\n resolved = BuildConfig(runtime=\"codex\").resolved_models()\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(resolved[field], \"gpt-5.5\")\n self.assertEqual(ExecutionConfig(runtime=\"codex\").coder_model, \"gpt-5.5\")\n\n def test_codex_auto_auth_follows_openai_api_key_presence(self) -> None:\n # auto: API key present -> -codex; absent -> ChatGPT-compatible.\n with mock.patch.dict(\n os.environ, {\"SWE_CODEX_AUTH_MODE\": \"auto\", \"OPENAI_API_KEY\": \"sk-test\"}\n ):\n self.assertEqual(\n ExecutionConfig(runtime=\"codex\").coder_model, \"gpt-5.3-codex\"\n )\n with mock.patch.dict(\n os.environ, {\"SWE_CODEX_AUTH_MODE\": \"auto\", \"OPENAI_API_KEY\": \"\"}\n ):\n self.assertEqual(ExecutionConfig(runtime=\"codex\").coder_model, \"gpt-5.5\")\n\n def test_env_codex_runtime_overrides_default(self) -> None:\n with mock.patch.dict(os.environ, {\"SWE_DEFAULT_RUNTIME\": \"codex\"}):\n self.assertEqual(BuildConfig().runtime, \"codex\")\n self.assertEqual(ExecutionConfig().runtime, \"codex\")\n self.assertEqual(BuildConfig().ai_provider, \"codex\")\n\n def test_codex_models_default_and_role_override(self) -> None:\n cfg = ExecutionConfig(\n runtime=\"codex\",\n models={\"default\": \"gpt-5.3-codex\", \"coder\": \"gpt-5.3-codex-spark\"},\n )\n self.assertEqual(cfg.coder_model, \"gpt-5.3-codex-spark\")\n self.assertEqual(cfg.qa_model, \"gpt-5.3-codex\")\n\n\nclass TestDefaultModelFromEnv(unittest.TestCase):\n \"\"\"`SWE_DEFAULT_MODEL` lets the deployer pin a single model id without\n code changes or threading config through every caller. Caller-supplied\n models still win at higher precedence layers.\"\"\"\n\n def test_env_overrides_runtime_base_default(self) -> None:\n with mock.patch.dict(\n os.environ,\n {\"SWE_DEFAULT_MODEL\": \"openrouter/minimax/minimax-m2.6\"},\n ):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(\n resolved[field], \"openrouter/minimax/minimax-m2.6\"\n )\n\n def test_env_overrides_claude_code_runtime_too(self) -> None:\n with mock.patch.dict(os.environ, {\"SWE_DEFAULT_MODEL\": \"opus\"}):\n resolved = resolve_runtime_models(runtime=\"claude_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(resolved[field], \"opus\")\n\n def test_caller_models_default_beats_env(self) -> None:\n with mock.patch.dict(\n os.environ,\n {\"SWE_DEFAULT_MODEL\": \"openrouter/minimax/minimax-m2.6\"},\n ):\n resolved = resolve_runtime_models(\n runtime=\"open_code\",\n models={\"default\": \"openrouter/qwen/qwen-3-coder\"},\n )\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(\n resolved[field], \"openrouter/qwen/qwen-3-coder\"\n )\n\n def test_caller_per_role_beats_env(self) -> None:\n with mock.patch.dict(\n os.environ,\n {\"SWE_DEFAULT_MODEL\": \"openrouter/minimax/minimax-m2.6\"},\n ):\n resolved = resolve_runtime_models(\n runtime=\"open_code\",\n models={\"coder\": \"openrouter/deepseek/deepseek-v3\"},\n )\n self.assertEqual(\n resolved[\"coder_model\"], \"openrouter/deepseek/deepseek-v3\"\n )\n # Other roles still pick up the env default\n self.assertEqual(\n resolved[\"pm_model\"], \"openrouter/minimax/minimax-m2.6\"\n )\n\n def test_empty_env_value_treated_as_unset(self) -> None:\n # All three cascade vars empty/whitespace → falls through to runtime base\n with mock.patch.dict(\n os.environ,\n {\"SWE_DEFAULT_MODEL\": \" \", \"AI_MODEL\": \"\", \"HARNESS_MODEL\": \" \"},\n ):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(\n resolved[field], \"openrouter/deepseek/deepseek-v4-flash-0731\"\n )\n\n def test_unset_env_uses_runtime_base(self) -> None:\n cascade_vars = {\"SWE_DEFAULT_MODEL\", \"AI_MODEL\", \"HARNESS_MODEL\"}\n env = {k: v for k, v in os.environ.items() if k not in cascade_vars}\n with mock.patch.dict(os.environ, env, clear=True):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(\n resolved[field], \"openrouter/deepseek/deepseek-v4-flash-0731\"\n )\n\n def test_ai_model_env_used_when_swe_default_unset(self) -> None:\n # AI_MODEL is the env var the rest of the stack (pr-af, github-buddy)\n # uses — SWE-AF must respect it too without requiring a SWE-specific\n # var, otherwise the deployer has to set the same model in two places.\n cascade_vars = {\"SWE_DEFAULT_MODEL\", \"AI_MODEL\", \"HARNESS_MODEL\"}\n env = {k: v for k, v in os.environ.items() if k not in cascade_vars}\n env[\"AI_MODEL\"] = \"openrouter/moonshotai/kimi-k2.6\"\n with mock.patch.dict(os.environ, env, clear=True):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(\n resolved[field], \"openrouter/moonshotai/kimi-k2.6\"\n )\n\n def test_harness_model_env_used_when_others_unset(self) -> None:\n cascade_vars = {\"SWE_DEFAULT_MODEL\", \"AI_MODEL\", \"HARNESS_MODEL\"}\n env = {k: v for k, v in os.environ.items() if k not in cascade_vars}\n env[\"HARNESS_MODEL\"] = \"openrouter/moonshotai/kimi-k2.6\"\n with mock.patch.dict(os.environ, env, clear=True):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(\n resolved[field], \"openrouter/moonshotai/kimi-k2.6\"\n )\n\n def test_swe_default_model_beats_ai_model_in_cascade(self) -> None:\n # When both are set, the SWE-specific name wins so deployers can\n # override the global AI_MODEL just for this service.\n cascade_vars = {\"SWE_DEFAULT_MODEL\", \"AI_MODEL\", \"HARNESS_MODEL\"}\n env = {k: v for k, v in os.environ.items() if k not in cascade_vars}\n env[\"SWE_DEFAULT_MODEL\"] = \"openrouter/qwen/qwen-3-coder\"\n env[\"AI_MODEL\"] = \"openrouter/moonshotai/kimi-k2.6\"\n with mock.patch.dict(os.environ, env, clear=True):\n resolved = resolve_runtime_models(runtime=\"open_code\", models=None)\n for field in ALL_MODEL_FIELDS:\n self.assertEqual(\n resolved[field], \"openrouter/qwen/qwen-3-coder\"\n )\n\n def test_env_flows_through_build_config(self) -> None:\n with mock.patch.dict(\n os.environ,\n {\"SWE_DEFAULT_MODEL\": \"openrouter/minimax/minimax-m2.6\"},\n ):\n cfg = BuildConfig(runtime=\"open_code\")\n resolved = cfg.resolved_models()\n self.assertEqual(\n resolved[\"coder_model\"], \"openrouter/minimax/minimax-m2.6\"\n )\n\n def test_env_flows_through_execution_config(self) -> None:\n with mock.patch.dict(\n os.environ,\n {\"SWE_DEFAULT_MODEL\": \"openrouter/minimax/minimax-m2.6\"},\n ):\n cfg = ExecutionConfig(runtime=\"open_code\")\n self.assertEqual(\n cfg.coder_model, \"openrouter/minimax/minimax-m2.6\"\n )\n self.assertEqual(\n cfg.qa_synthesizer_model, \"openrouter/minimax/minimax-m2.6\"\n )\n\n\nclass TestExecutionConfig(unittest.TestCase):\n def test_default_resolution(self) -> None:\n cfg = ExecutionConfig()\n self.assertEqual(cfg.runtime, \"claude_code\")\n self.assertEqual(cfg.ai_provider, \"claude\")\n self.assertEqual(cfg.coder_model, \"sonnet\")\n self.assertEqual(cfg.qa_synthesizer_model, \"haiku\")\n\n def test_open_code_resolution(self) -> None:\n cfg = ExecutionConfig(runtime=\"open_code\")\n self.assertEqual(cfg.ai_provider, \"opencode\")\n self.assertEqual(cfg.coder_model, \"openrouter/deepseek/deepseek-v4-flash-0731\")\n self.assertEqual(cfg.qa_synthesizer_model, \"openrouter/deepseek/deepseek-v4-flash-0731\")\n\n def test_models_override(self) -> None:\n cfg = ExecutionConfig(runtime=\"claude_code\", models={\"default\": \"sonnet\", \"qa\": \"opus\"})\n self.assertEqual(cfg.qa_model, \"opus\")\n self.assertEqual(cfg.coder_model, \"sonnet\")\n\n def test_all_role_keys_resolve(self) -> None:\n models = {role: f\"model-{role}\" for role in ROLE_TO_MODEL_FIELD}\n cfg = ExecutionConfig(runtime=\"open_code\", models=models)\n for role, field in ROLE_TO_MODEL_FIELD.items():\n self.assertEqual(getattr(cfg, field), f\"model-{role}\")\n\n def test_legacy_keys_rejected(self) -> None:\n with self.assertRaises(ValueError) as ctx:\n ExecutionConfig(**{\"ai_provider\": \"claude\"})\n self.assertIn(\"ai_provider\", str(ctx.exception))\n self.assertIn(\"runtime\", str(ctx.exception))\n\n with self.assertRaises(ValueError) as ctx:\n ExecutionConfig(**{\"replan_model\": \"sonnet\"})\n self.assertIn(\"replan_model\", str(ctx.exception))\n self.assertIn(\"models.replan\", str(ctx.exception))\n\n with self.assertRaises(ValueError) as ctx:\n ExecutionConfig(models={\"coding\": \"opus\"})\n self.assertIn(\"coding\", str(ctx.exception))\n self.assertIn(\"models.coder\", str(ctx.exception))\n\n def test_ci_fixer_role_resolves(self) -> None:\n \"\"\"ci_fixer is a real role with its own model field, defaulting to the\n runtime base. It can be overridden per-role like any other.\"\"\"\n cfg = ExecutionConfig(runtime=\"claude_code\")\n self.assertEqual(cfg.ci_fixer_model, \"sonnet\")\n\n cfg = ExecutionConfig(runtime=\"open_code\")\n self.assertEqual(cfg.ci_fixer_model, \"openrouter/deepseek/deepseek-v4-flash-0731\")\n\n cfg = ExecutionConfig(\n runtime=\"claude_code\", models={\"ci_fixer\": \"opus\"}\n )\n self.assertEqual(cfg.ci_fixer_model, \"opus\")\n # other roles untouched\n self.assertEqual(cfg.coder_model, \"sonnet\")\n\n\nclass TestCIGateConfig(unittest.TestCase):\n \"\"\"check_ci defaults to True and its caps round-trip from BuildConfig\n into ExecutionConfig so the post-PR gate sees consistent settings.\"\"\"\n\n def test_check_ci_defaults_true(self) -> None:\n self.assertTrue(BuildConfig().check_ci)\n self.assertTrue(ExecutionConfig().check_ci)\n\n def test_ci_gate_caps_have_sensible_defaults(self) -> None:\n cfg = BuildConfig()\n self.assertEqual(cfg.max_ci_fix_cycles, 2)\n self.assertEqual(cfg.ci_wait_seconds, 1500)\n self.assertEqual(cfg.ci_poll_seconds, 30)\n\n def test_ci_gate_caps_round_trip(self) -> None:\n cfg = BuildConfig(\n check_ci=False,\n max_ci_fix_cycles=5,\n ci_wait_seconds=600,\n ci_poll_seconds=15,\n )\n d = cfg.to_execution_config_dict()\n self.assertEqual(d[\"check_ci\"], False)\n self.assertEqual(d[\"max_ci_fix_cycles\"], 5)\n self.assertEqual(d[\"ci_wait_seconds\"], 600)\n self.assertEqual(d[\"ci_poll_seconds\"], 15)\n\n exec_cfg = ExecutionConfig(**d)\n self.assertFalse(exec_cfg.check_ci)\n self.assertEqual(exec_cfg.max_ci_fix_cycles, 5)\n self.assertEqual(exec_cfg.ci_wait_seconds, 600)\n self.assertEqual(exec_cfg.ci_poll_seconds, 15)\n\n\nclass TestRuntimeProviderMapping(unittest.TestCase):\n def test_runtime_to_harness_adapter_maps_all_supported_runtimes(self) -> None:\n from swe_af.runtime.providers import runtime_to_harness_adapter\n\n self.assertEqual(runtime_to_harness_adapter(\"claude_code\"), \"claude-code\")\n self.assertEqual(runtime_to_harness_adapter(\"claude\"), \"claude-code\")\n self.assertEqual(runtime_to_harness_adapter(\"claude-code\"), \"claude-code\")\n self.assertEqual(runtime_to_harness_adapter(\"open_code\"), \"opencode\")\n self.assertEqual(runtime_to_harness_adapter(\"opencode\"), \"opencode\")\n self.assertEqual(runtime_to_harness_adapter(\"codex\"), \"codex\")\n\n def test_runtime_to_harness_provider_maps_all_supported_runtimes(self) -> None:\n from swe_af.runtime.providers import runtime_to_harness_provider\n\n self.assertEqual(runtime_to_harness_provider(\"claude_code\"), \"claude\")\n self.assertEqual(runtime_to_harness_provider(\"open_code\"), \"opencode\")\n self.assertEqual(runtime_to_harness_provider(\"codex\"), \"codex\")\n\n def test_unknown_runtime_provider_raises(self) -> None:\n from swe_af.runtime.providers import normalize_runtime_provider\n\n with self.assertRaises(ValueError):\n normalize_runtime_provider(\"bad_runtime\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n"}}}}