from __future__ import annotations import json import pytest from src.services.ai_provider_route_matrix import load_latest_ai_provider_route_matrix def test_load_latest_ai_provider_route_matrix_reads_newest_file(tmp_path): older = _snapshot(generated_at="2026-06-04T00:00:00+08:00", completion=40) newer = _snapshot(generated_at="2026-06-05T00:00:00+08:00", completion=100) (tmp_path / "ai_provider_route_matrix_2026-06-04.json").write_text( json.dumps(older), encoding="utf-8", ) (tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text( json.dumps(newer), encoding="utf-8", ) loaded = load_latest_ai_provider_route_matrix(tmp_path) assert loaded["generated_at"] == "2026-06-05T00:00:00+08:00" assert loaded["program_status"]["overall_completion_percent"] == 100 assert loaded["rollups"]["total_routes"] == 6 assert loaded["operation_boundaries"]["provider_switch_allowed"] is False def test_ai_provider_route_matrix_requires_read_only_mode(tmp_path): snapshot = _snapshot() snapshot["program_status"]["read_only_mode"] = False (tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text( json.dumps(snapshot), encoding="utf-8", ) with pytest.raises(ValueError, match="read_only_mode"): load_latest_ai_provider_route_matrix(tmp_path) def test_ai_provider_route_matrix_blocks_provider_and_paid_call_changes(tmp_path): snapshot = _snapshot() snapshot["operation_boundaries"]["provider_switch_allowed"] = True snapshot["operation_boundaries"]["paid_api_call_allowed"] = True (tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text( json.dumps(snapshot), encoding="utf-8", ) with pytest.raises(ValueError, match="operation boundaries"): load_latest_ai_provider_route_matrix(tmp_path) def test_ai_provider_route_matrix_requires_rollup_consistency(tmp_path): snapshot = _snapshot() snapshot["rollups"]["route_ids_requiring_action"] = [] (tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text( json.dumps(snapshot), encoding="utf-8", ) with pytest.raises(ValueError, match="route_ids_requiring_action"): load_latest_ai_provider_route_matrix(tmp_path) def test_ai_provider_route_matrix_requires_nemotron_to_stay_blocked(tmp_path): snapshot = _snapshot() snapshot["provider_routes"][4]["status"] = "verified" _refresh_rollups(snapshot) (tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text( json.dumps(snapshot), encoding="utf-8", ) with pytest.raises(ValueError, match="Nemotron candidates"): load_latest_ai_provider_route_matrix(tmp_path) def test_ai_provider_route_matrix_requires_candidate_gates_to_require_approval(tmp_path): snapshot = _snapshot() snapshot["candidate_gates"][0]["approval_required"] = False snapshot["rollups"]["candidate_gate_ids_requiring_approval"] = [ "nemotron_replay_gate", "paid_provider_call_gate", ] (tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text( json.dumps(snapshot), encoding="utf-8", ) with pytest.raises(ValueError, match="candidate gates"): load_latest_ai_provider_route_matrix(tmp_path) def test_ai_provider_route_matrix_requires_operator_denials(tmp_path): snapshot = _snapshot() snapshot["operator_contract"]["must_not_interpret_as"].remove( "provider 切換批准" ) (tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text( json.dumps(snapshot), encoding="utf-8", ) with pytest.raises(ValueError, match="operator_contract"): load_latest_ai_provider_route_matrix(tmp_path) def test_ai_provider_route_matrix_rejects_secret_payload_keys(tmp_path): snapshot = _snapshot() snapshot["latest_observations"][0]["authorization_header"] = "redacted" (tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text( json.dumps(snapshot), encoding="utf-8", ) with pytest.raises(ValueError, match="forbidden secret payload key"): load_latest_ai_provider_route_matrix(tmp_path) def test_ai_provider_route_matrix_fails_when_missing(tmp_path): with pytest.raises(FileNotFoundError): load_latest_ai_provider_route_matrix(tmp_path) def _snapshot( *, generated_at: str = "2026-06-05T00:00:00+08:00", completion: int = 100, ) -> dict: routes = [ _route( "ai_router_execution_policy", "AI Router 執行決策核心", "ai_router_core", "action_required", "route_preserved", ), _route( "ollama_global_endpoint_order", "Ollama GCP-A → GCP-B → 111", "ollama_failover", "verified", "route_preserved", ), _route( "alert_ai_ollama_first_lane", "告警 AI Ollama-first Lane", "alert_governance_lane", "verified", "route_preserved", ), _route( "openclaw_nemo_rca_lane", "OpenClaw Nemo RCA Lane", "openclaw_nemo", "action_required", "review_required", ), _route( "nemotron_tool_calling_candidate", "Nemotron Tool Calling 候選", "nemotron_candidate", "blocked", "candidate_blocked", ), _route( "gemini_final_fallback_policy", "Gemini Final Fallback", "paid_cloud_fallback", "verified", "route_preserved", ), ] gates = [ _gate("provider_switch_gate", "production_change_blocked"), _gate("nemotron_replay_gate", "blocked_by_evidence"), _gate("paid_provider_call_gate", "cost_approval_required"), ] gaps = [ { "gap_id": "ai_router_comment_drift", "display_name": "AI Router 註解與現況 drift", "status": "action_required", "severity": "medium", "summary": "舊註解需整理,不能誤讀成現行路由。", "evidence_refs": ["apps/api/src/services/ai_router.py"], "next_action": "只整理 source truth,不改 provider logic。", } ] return { "schema_version": "ai_provider_route_matrix_v1", "generated_at": generated_at, "program_status": { "overall_completion_percent": completion, "current_priority": "P1", "current_task_id": "P1-004", "next_task_id": "P1-005", "read_only_mode": True, }, "source_refs": ["docs/schemas/ai_provider_route_matrix_v1.schema.json"], "rollups": { "total_routes": len(routes), "by_kind": _count_by(routes, "kind"), "by_status": _count_by(routes, "status"), "by_route_gate": _count_by(routes, "route_gate"), "route_ids_requiring_action": [ "ai_router_execution_policy", "openclaw_nemo_rca_lane", ], "candidate_gate_ids_requiring_approval": [ "nemotron_replay_gate", "paid_provider_call_gate", "provider_switch_gate", ], "source_gap_ids": ["ai_router_comment_drift"], "read_only_denials_total": 12, "provider_switch_allowed_count": 0, "paid_api_call_allowed_count": 0, "shadow_or_canary_allowed_count": 0, "runtime_route_change_allowed_count": 0, }, "provider_routes": routes, "candidate_gates": gates, "source_gaps": gaps, "latest_observations": [ { "observation_id": "provider_matrix_seed", "status": "verified", "summary": "只讀 route matrix seed。", "evidence_refs": ["apps/api/src/services/ai_router.py"], } ], "operator_contract": { "display_mode": "read_only_ai_provider_route_matrix", "must_not_interpret_as": [ "provider 切換批准", "production routing change 批准", "Gemini / Claude / NVIDIA 付費呼叫批准", "OpenClaw 取代或降級批准", "Nemotron 進 shadow / canary 批准", "fallback order 修改批准", "Ollama endpoint / ConfigMap 修改批准", "Secret payload 已讀取或可輸出", "外部 live probe 或 benchmark 批准", "workflow / deploy / reload 觸發批准", "runtime execution 授權", ], "secret_display_policy": "只顯示 env var 名稱。", "provider_switch_policy": "P1-004 只能盤點與顯示;切換 provider 需另行批准。", "cost_policy": "付費呼叫需批准。", "runtime_policy": "active runtime gate 維持 0。", }, "operation_boundaries": { "read_only_api_allowed": True, "provider_switch_allowed": False, "production_routing_change_allowed": False, "use_ai_router_toggle_allowed": False, "fallback_order_change_allowed": False, "ollama_endpoint_change_allowed": False, "paid_api_call_allowed": False, "paid_api_frequency_increase_allowed": False, "external_provider_probe_allowed": False, "live_benchmark_allowed": False, "shadow_or_canary_allowed": False, "openclaw_replacement_allowed": False, "nemotron_shadow_allowed": False, "gemini_direct_call_allowed": False, "secret_read_allowed": False, "secret_plaintext_allowed": False, "notification_send_allowed": False, "workflow_trigger_allowed": False, "deploy_trigger_allowed": False, "reload_trigger_allowed": False, "runtime_execution_allowed": False, }, "approval_boundaries": { "provider_switch_approved": False, "production_routing_change_approved": False, "cost_change_approved": False, "shadow_or_canary_approved": False, "external_provider_call_approved": False, "openclaw_replacement_approved": False, "nemotron_replay_approved": False, "secret_access_approved": False, "runtime_execution_approved": False, }, } def _route( route_id: str, display_name: str, kind: str, status: str, route_gate: str, ) -> dict: return { "route_id": route_id, "display_name": display_name, "kind": kind, "status": status, "risk_level": "critical", "route_gate": route_gate, "evidence_status": "committed_source", "current_policy": "只讀盤點 provider route。", "provider_order": ["ollama_gcp_a", "ollama_gcp_b", "ollama_local"], "fallback_policy": "不切 provider。", "evidence_refs": ["apps/api/src/services/ai_router.py"], "next_action": "準備批准包,不改 runtime。", } def _gate(gate_id: str, status: str) -> dict: return { "gate_id": gate_id, "display_name": gate_id, "status": status, "approval_required": True, "summary": "需人工批准。", "evidence_refs": ["docs/HARD_RULES.md"], "next_action": "維持阻擋。", } def _count_by(items: list[dict], key: str) -> dict[str, int]: counts: dict[str, int] = {} for item in items: value = item[key] counts[value] = counts.get(value, 0) + 1 return counts def _refresh_rollups(snapshot: dict) -> None: routes = snapshot["provider_routes"] gates = snapshot["candidate_gates"] snapshot["rollups"]["by_status"] = _count_by(routes, "status") snapshot["rollups"]["by_route_gate"] = _count_by(routes, "route_gate") snapshot["rollups"]["route_ids_requiring_action"] = sorted( route["route_id"] for route in routes if route["status"] == "action_required" ) snapshot["rollups"]["candidate_gate_ids_requiring_approval"] = sorted( gate["gate_id"] for gate in gates if gate["approval_required"] is True )