"""Unit test suite for Task A52: Local Models, Supervisor, and Dual Coder Pipeline.""" import json from pathlib import Path from unittest.mock import MagicMock, patch import pytest from antigravity_provider.router.dual_coder_pipeline import ( DualCoderPipeline, DualCoderResult, JudgeVerdict, ) from antigravity_provider.router.local_supervisor import ( ContextExhaustedError, IndivisibleTaskError, LocalSupervisor, ModelMemoryRecord, ServerPropsResult, SupervisorOutcome, TokenCountResult, ) from antigravity_provider.router.role_registry import CANONICAL_ROLES, RoleRegistry # ===================================================================== # 1. Role Registry Tests (P0-4) # ===================================================================== def test_role_registry_contains_local_supervisor(): """Verify local-supervisor canonical role is properly defined in RoleRegistry.""" assert "local-supervisor" in CANONICAL_ROLES role = RoleRegistry.get_role("local-supervisor") assert role is not None assert role.role_id == "local-supervisor" assert role.display_name_ru == "Надзиратель локальных моделей" assert role.tier == "governance" assert "task-splitter" in role.capabilities assert "token-counter" in role.capabilities assert "memory-tracker" in role.capabilities assert role.is_implemented is True def test_role_registry_local_supervisor_aliases(): """Verify all aliases resolve to local-supervisor.""" for alias in ["local-supervisor", "local_supervisor", "надзиратель локальных моделей", "supervisor", "локальный надзиратель"]: assert RoleRegistry.resolve_canonical_role(alias) == "local-supervisor" # ===================================================================== # 2. Local Supervisor Props & Token Counting Tests (P0-5) # ===================================================================== def test_supervisor_query_props_success(): supervisor = LocalSupervisor(base_url="http://mock-server:8081") fake_props_response = json.dumps({ "default_generation_settings": {"n_ctx": 65536}, "total_slots": 1, "model_path": "/srv/ai/models/qwen3-coder-30b-a3b/Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf", }).encode("utf-8") mock_resp = MagicMock() mock_resp.read.return_value = fake_props_response mock_resp.__enter__.return_value = mock_resp with patch("urllib.request.urlopen", return_value=mock_resp): res = supervisor.query_server_props() assert res.n_ctx == 65536 assert res.total_slots == 1 assert "Qwen3-Coder-30B" in res.model_name assert res.is_measured is True def test_supervisor_query_props_offline_fallback(): supervisor = LocalSupervisor(base_url="http://unreachable-host:8081") with patch("urllib.request.urlopen", side_effect=Exception("Connection refused")): res = supervisor.query_server_props() assert res.n_ctx == 65536 assert res.is_measured is False def test_supervisor_count_tokens_exact_via_api(): supervisor = LocalSupervisor(base_url="http://mock-server:8081") fake_tok_response = json.dumps({ "tokens": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }).encode("utf-8") mock_resp = MagicMock() mock_resp.read.return_value = fake_tok_response mock_resp.__enter__.return_value = mock_resp with patch("urllib.request.urlopen", return_value=mock_resp): res = supervisor.count_tokens("def add(a, b): return a + b") assert res.tokens_count == 10 assert res.is_estimated is False assert res.method == "tokenize_api" def test_supervisor_count_tokens_fallback_heuristic(): supervisor = LocalSupervisor(base_url="http://unreachable-host:8081") with patch("urllib.request.urlopen", side_effect=Exception("Timeout")): sample_text = "A" * 70 res = supervisor.count_tokens(sample_text) assert res.is_estimated is True assert res.tokens_count == int(70 / 3.5) assert res.method == "char_heuristic" # ===================================================================== # 3. Semantic Task Splitting Tests (P0-6) # ===================================================================== def test_supervisor_split_task_fits_in_single_chunk(): supervisor = LocalSupervisor() with patch.object(supervisor, "count_tokens", return_value=TokenCountResult(tokens_count=500, is_estimated=False, method="mock")): chunks = supervisor.split_task_semantically("def small_task(): pass", max_chunk_tokens=2000) assert len(chunks) == 1 assert chunks[0] == "def small_task(): pass" def test_supervisor_split_task_across_files(): supervisor = LocalSupervisor() task = """--- file: a.py def func_a(): pass --- file: b.py def func_b(): pass """ def mock_count(text): if "a.py" in text and "b.py" in text: return TokenCountResult(tokens_count=250, is_estimated=False, method="mock") return TokenCountResult(tokens_count=80, is_estimated=False, method="mock") with patch.object(supervisor, "count_tokens", side_effect=mock_count): chunks = supervisor.split_task_semantically(task, max_chunk_tokens=150) assert len(chunks) == 2 assert "a.py" in chunks[0] assert "b.py" in chunks[1] def test_supervisor_split_task_across_functions(): supervisor = LocalSupervisor() task = """class MyService: def method_one(self): print("1") def method_two(self): print("2") """ def mock_count(text): if "method_one" in text and "method_two" in text: return TokenCountResult(tokens_count=200, is_estimated=False, method="mock") return TokenCountResult(tokens_count=40, is_estimated=False, method="mock") with patch.object(supervisor, "count_tokens", side_effect=mock_count): chunks = supervisor.split_task_semantically(task, max_chunk_tokens=100) assert len(chunks) >= 2 def test_supervisor_indivisible_task_raises_error(): supervisor = LocalSupervisor() indivisible_giant_line = "x = " + ("1+" * 5000) with patch.object(supervisor, "count_tokens", return_value=TokenCountResult(tokens_count=10000, is_estimated=False, method="mock")): with pytest.raises(IndivisibleTaskError): supervisor.split_task_semantically(indivisible_giant_line, max_chunk_tokens=500) # ===================================================================== # 4. Outcome & Reasoning Exhaustion (A39) Tests (P0-7) # ===================================================================== def test_supervisor_detect_outcome_success(): supervisor = LocalSupervisor() resp = { "choices": [{"message": {"content": "def add(a, b): return a + b"}}], "timings": {"predicted_n": 25}, } outcome, desc = supervisor.detect_outcome(resp, elapsed_sec=2.5) assert outcome == SupervisorOutcome.SUCCESS assert "Успешно" in desc def test_supervisor_detect_outcome_timeout(): supervisor = LocalSupervisor() outcome, desc = supervisor.detect_outcome(None, error=TimeoutError("Request timed out"), elapsed_sec=185.0) assert outcome == SupervisorOutcome.TIMEOUT assert "Превышен таймаут" in desc def test_supervisor_detect_outcome_a39_reasoning_exhaustion(): supervisor = LocalSupervisor() # Case A39: content is empty or whitespace, but predicted_n > 50 or reasoning_content exists resp = { "choices": [{ "message": { "content": "", "reasoning_content": "Let me think about how to write the adder function... wait..." }, "finish_reason": "length" }], "timings": {"predicted_n": 1500}, } outcome, desc = supervisor.detect_outcome(resp, elapsed_sec=45.0) assert outcome == SupervisorOutcome.REASONING_EXHAUSTED assert "A39" in desc assert "enable_thinking: false" in desc # ===================================================================== # 5. AI-Memory Integration Tests (P0-8) # ===================================================================== def test_supervisor_memory_tracking_roundtrip(tmp_path: Path): mem_file = tmp_path / "local_models_memory.json" supervisor = LocalSupervisor(memory_path=mem_file) rec1 = supervisor.record_working_volume( gguf_name="Qwen3-Coder-30B-A3B", prompt_tokens=16000, output_tokens=250, outcome=SupervisorOutcome.SUCCESS, speed_tps=110.5, task_id="T01", ) assert rec1.successful_dispatches == 1 assert rec1.safe_chunk_tokens == 16000 assert rec1.avg_generation_tps > 100.0 # Fetch from memory fetched = supervisor.get_model_memory("Qwen3-Coder-30B-A3B") assert fetched is not None assert fetched.last_working_context == 16000 # Record failure -> safe chunk reduced rec2 = supervisor.record_working_volume( gguf_name="Qwen3-Coder-30B-A3B", prompt_tokens=48000, output_tokens=0, outcome=SupervisorOutcome.TIMEOUT, speed_tps=0.0, task_id="T02", ) assert rec2.failed_dispatches == 1 assert rec2.safe_chunk_tokens < 48000 # ===================================================================== # 6. Dual Coder Pipeline Tests (P0-9) # ===================================================================== def test_dual_coder_pipeline_success_winner_a(): coder_a = MagicMock(return_value={"content": "def solution_a(): return True", "tokens_generated": 40}) coder_b = MagicMock(return_value={"content": "def solution_b(): return False", "tokens_generated": 45}) judge = MagicMock(return_value={ "verdict": "ACCEPTED_A", "commentary": "Solution A is correct and handles edge cases.", "tokens_consumed": 120, }) pipeline = DualCoderPipeline(coder_a_fn=coder_a, coder_b_fn=coder_b, judge_fn=judge, max_rounds=3, enabled=True) result = pipeline.run_pipeline("Write boolean check", judge_model_name="claude-3-5-sonnet") assert result.success is True assert result.final_verdict == JudgeVerdict.ACCEPTED_A assert result.winning_coder == "coder-a" assert result.total_rounds == 1 assert result.total_judge_calls == 1 assert result.total_judge_tokens == 120 assert "solution_a" in result.final_code def test_dual_coder_pipeline_rework_then_winner_b(): call_count = {"judge": 0} def mock_coder_a(prompt, feedback): return {"content": f"code_a_round_{call_count['judge']}", "tokens_generated": 30} def mock_coder_b(prompt, feedback): return {"content": f"code_b_round_{call_count['judge']}", "tokens_generated": 35} def mock_judge(prompt, code_a, code_b, model): call_count["judge"] += 1 if call_count["judge"] == 1: return { "verdict": "REWORK_BOTH", "commentary": "Both missing docstrings.", "feedback_for_a": "Add docstring to A", "feedback_for_b": "Add docstring to B", "tokens_consumed": 100, } return { "verdict": "ACCEPTED_B", "commentary": "B added perfect docstrings.", "tokens_consumed": 110, } pipeline = DualCoderPipeline(coder_a_fn=mock_coder_a, coder_b_fn=mock_coder_b, judge_fn=mock_judge, max_rounds=3, enabled=True) result = pipeline.run_pipeline("Write function with docs") assert result.success is True assert result.final_verdict == JudgeVerdict.ACCEPTED_B assert result.winning_coder == "coder-b" assert result.total_rounds == 2 assert result.total_judge_calls == 2 assert result.total_judge_tokens == 210 def test_dual_coder_pipeline_stagnation_detection(): # Both coders return identical unchanged code on round 2 coder_a = MagicMock(return_value={"content": "unchanged_code_a", "tokens_generated": 20}) coder_b = MagicMock(return_value={"content": "unchanged_code_b", "tokens_generated": 25}) judge = MagicMock(return_value={"verdict": "REWORK_BOTH", "commentary": "Still broken", "tokens_consumed": 50}) pipeline = DualCoderPipeline(coder_a_fn=coder_a, coder_b_fn=coder_b, judge_fn=judge, max_rounds=3, enabled=True) result = pipeline.run_pipeline("Fix algorithm") assert result.success is False assert result.final_verdict == JudgeVerdict.STAGNATION assert "Застревание" in result.failure_reason assert result.total_rounds == 2 def test_dual_coder_pipeline_rounds_exhausted(): round_cnt = [0] def mock_a(prompt, fb): round_cnt[0] += 1 return {"content": f"varying_code_a_step_{round_cnt[0]}", "tokens_generated": 20} def mock_b(prompt, fb): return {"content": f"varying_code_b_step_{round_cnt[0]}", "tokens_generated": 25} judge = lambda prompt, ca, cb, m: {"verdict": "REWORK_BOTH", "commentary": "Needs more work", "tokens_consumed": 50} pipeline = DualCoderPipeline(coder_a_fn=mock_a, coder_b_fn=mock_b, judge_fn=judge, max_rounds=3, enabled=True) result = pipeline.run_pipeline("Tough problem") assert result.success is False assert result.final_verdict == JudgeVerdict.ROUNDS_EXHAUSTED assert result.total_rounds == 3 assert result.total_judge_calls == 3 assert result.total_judge_tokens == 150