From 3cabbb9bca24f3d54b631353356f8025147afb65 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Date: Wed, 23 Sep 2026 00:14:30 +0100 Subject: [PATCH] test(agent): restore exact turn-policy regressions --- routes/chat_routes.py | 7 +- tests/test_agent_evidence_loop.py | 2 +- tests/test_chat_route_tool_policy.py | 99 ++++++++++++++++++++--- tests/test_foreground_model_routing.py | 3 +- tests/test_pr6020_rebase_regressions.py | 19 ++++- tests/test_product_turn_contract_route.py | 4 +- 6 files changed, 113 insertions(+), 21 deletions(-) diff --git a/routes/chat_routes.py b/routes/chat_routes.py index a7666021f..116cd10df 100644 --- a/routes/chat_routes.py +++ b/routes/chat_routes.py @@ -3499,11 +3499,12 @@ def setup_chat_routes( # has an exact routed contract. Adding the whole native # workspace inventory here reintroduced overlapping PDF # readers and caused the model to abandon the selected - # OCR operation. Exact operations therefore stay exact; - # ordinary native turns retain warm and workspace tools. + # OCR operation. A task-only turn likewise has an exact + # personal manager; core shell/Web tools are not task + # fallbacks. Other turns retain warm and workspace tools. extra_tools=( frozenset() - if _exact_selected_native_chain or ( + if _exact_selected_native_chain or _active_turn_capabilities == frozenset({"tasks"}) or ( _active_turn_capabilities in ( frozenset({"transcription"}), frozenset({"ocr"}), ) diff --git a/tests/test_agent_evidence_loop.py b/tests/test_agent_evidence_loop.py index 485efa78e..5b7acfde8 100644 --- a/tests/test_agent_evidence_loop.py +++ b/tests/test_agent_evidence_loop.py @@ -617,7 +617,7 @@ def test_finish_nudge_does_not_accept_unfinished_correction_promise(monkeypatch) monkeypatch, [ '```write_file\n/workspace/output.html\ndraft\n```', - "The preview revealed a defect. I should complete output.html by adding labels.", + "The draft has a defect. I should complete output.html by adding labels.", '```write_file\n/workspace/output.html\ncorrected\n```', "Done. Corrected and checked output.html.", ], diff --git a/tests/test_chat_route_tool_policy.py b/tests/test_chat_route_tool_policy.py index 4f796d143..efb955ef2 100644 --- a/tests/test_chat_route_tool_policy.py +++ b/tests/test_chat_route_tool_policy.py @@ -9,6 +9,7 @@ Fix: (1) Read from JSON body as fallback. """ import ast +import json from pathlib import Path import pytest @@ -35,6 +36,7 @@ from src.tool_policy import ( web_intent_may_enable_for_turn, web_search_enabled_for_turn, ) +from tests.test_foreground_model_routing import _RouteRequest, _chat_stream_endpoint _CHAT_ROUTES = Path(__file__).resolve().parent.parent / "routes" / "chat_routes.py" @@ -283,23 +285,94 @@ def test_contextual_browser_followup_recognizes_current_page_inspection(): assert not _is_contextual_browser_followup("Show my notes.", session) -def test_clean_browser_filter_preserves_native_pdf_extraction_contract(): - source = _CHAT_ROUTES.read_text(encoding="utf-8") - assert "INTERACTIVE_CORE_TOOLS" in source - assert "NATIVE_WORKSPACE_TOOLS" in source - assert "scope_preview_contract(" in source +async def _clean_route_contract(monkeypatch, message, *, history=(), native=False): + from routes import chat_routes + from src import tool_security + + with monkeypatch.context() as route_patch: + endpoint = _chat_stream_endpoint( + route_patch, "agent", {}, + session_model="odysseus-qwen3.5-tools-pre-heretic", + session_history=history, + ) + route_patch.setattr( + chat_routes, "coerce_message_and_session", + lambda *args, **kwargs: (message, "session-1"), + ) + route_patch.setattr( + tool_security, "owner_is_admin_or_single_user", lambda owner: True, + ) + observed = [] + + async def capture_agent(*args, **kwargs): + observed.append(kwargs["turn_contract"]) + yield 'data: {"delta":"Contract constructed."}\n\n' + yield "data: [DONE]\n\n" + + route_patch.setattr(chat_routes, "stream_agent_loop", capture_agent) + request = _RouteRequest("agent") + request._form.update({"message": message, "compare_mode": "false"}) + if native: + request._form.update({ + "cwd": "/tmp/native-workspace", + "workspace": "/tmp/native-workspace", + "client_runtime_context": json.dumps({ + "surface": "odysseus-native", "terminal_agent": True, + "unattended_mode": True, + "input_files": ["/workspace/paper.pdf"], + }), + }) + response = await endpoint(request) + async for _ in response.body_iterator: + pass + assert len(observed) == 1 + return observed[0] -def test_clean_preview_only_offers_browser_for_explicit_or_typed_warm_turns(): - source = _CHAT_ROUTES.read_text(encoding="utf-8") - assert "INTERACTIVE_CORE_TOOLS" in source - assert '{"private_browser"} if _local_browser_render_intent else frozenset()' in source +@pytest.mark.asyncio +async def test_clean_browser_filter_preserves_native_pdf_extraction_contract(monkeypatch): + contract = await _clean_route_contract( + monkeypatch, + "Extract Table 2 from /workspace/paper.pdf using pdf_extract.", + native=True, + ) + assert contract.capabilities == {"shell_files"} + assert contract.permits("pdf_extract") + assert "private_browser" not in contract.offered + assert not {"manage_tasks", "search_emails", "send_email"} & contract.offered -def test_explicit_web_fetch_is_not_erased_by_generic_browser_intent(): - source = _CHAT_ROUTES.read_text(encoding="utf-8") - assert "INTERACTIVE_CORE_TOOLS" in source - assert "_exact_selected_native_chain" in source +@pytest.mark.asyncio +async def test_clean_preview_only_offers_browser_for_explicit_or_typed_warm_turns(monkeypatch): + message = "Summarize the status." + no_history = await _clean_route_contract(monkeypatch, message) + failed_history = [{"role": "assistant", "metadata": {"tool_events": [{ + "tool": "private_browser", "exit_code": 1, "error": True, + }]}}] + failed = await _clean_route_contract(monkeypatch, message, history=failed_history) + successful_history = [{"role": "assistant", "metadata": {"tool_events": [{ + "tool": "private_browser", "exit_code": 0, "error": False, + }]}}] + warm = await _clean_route_contract(monkeypatch, message, history=successful_history) + explicit = await _clean_route_contract( + monkeypatch, "Open https://example.com with the private browser", + ) + assert "private_browser" not in no_history.offered + assert "private_browser" not in failed.offered + assert warm.permits("private_browser") + assert explicit.permits("private_browser") + + +@pytest.mark.asyncio +async def test_explicit_web_fetch_is_not_erased_by_generic_browser_intent(monkeypatch): + contract = await _clean_route_contract( + monkeypatch, + "Use web_fetch to read https://example.com/report in the private browser.", + ) + assert contract.capabilities == {"search_browser"} + assert contract.required == {"web_fetch"} + assert contract.permits("web_fetch") + assert not {"manage_tasks", "search_emails", "send_email"} & contract.offered def test_web_followup_grammar_covers_article_detail_questions(): diff --git a/tests/test_foreground_model_routing.py b/tests/test_foreground_model_routing.py index 5def63b48..659c6761b 100644 --- a/tests/test_foreground_model_routing.py +++ b/tests/test_foreground_model_routing.py @@ -147,6 +147,7 @@ def _chat_stream_endpoint( capture_context=False, endpoint_url="https://selected.example/v1", session_model="selected-model", + session_history=(), ): def add_message(message): captured.setdefault("added_messages", []).append(message) @@ -156,7 +157,7 @@ def _chat_stream_endpoint( model=session_model, headers={"Authorization": "Bearer selected"}, name="test", - history=[], + history=list(session_history), add_message=add_message, ) session_manager = SimpleNamespace( diff --git a/tests/test_pr6020_rebase_regressions.py b/tests/test_pr6020_rebase_regressions.py index c46669a27..d92c6bf8c 100644 --- a/tests/test_pr6020_rebase_regressions.py +++ b/tests/test_pr6020_rebase_regressions.py @@ -107,7 +107,7 @@ def test_odysseus_notes_mode_clamps_without_overriding_caller_denials(monkeypatc assert {"manage_notes", "manage_calendar", "manage_tasks"} <= route["disabled_tools"] -def test_odysseus_general_mode_uses_compact_core(monkeypatch): +def test_odysseus_router_uses_compact_core(monkeypatch): prompt_calls, _ = _install_route_probe(monkeypatch) _run_probe( @@ -119,6 +119,23 @@ def test_odysseus_general_mode_uses_compact_core(monkeypatch): assert route["relevant_tools"] == GENERAL_COMPACT_TOOLS +def test_odysseus_general_no_tool_mode_has_no_executable_surface(monkeypatch): + from src.tool_policy import known_tool_names + + # The current merged profile takes the compact-router branch. Exercise + # the legacy general mode itself so its execution denial stays covered. + monkeypatch.setattr(agent_loop, "_is_qwen38_tool_router", lambda model: False) + prompt_calls, stream_calls = _install_route_probe(monkeypatch) + + _run_probe( + [{"role": "user", "content": "Explain the CAP theorem with a concrete distributed database example."}], + relevant_tools={"bash", "manage_notes", "ask_user"}, + ) + + assert stream_calls[0]["tools"] is None + assert known_tool_names() <= prompt_calls[0]["disabled_tools"] + + def test_odysseus_calendar_intent_uses_compact_calendar_route(monkeypatch): prompt_calls, _ = _install_route_probe(monkeypatch) diff --git a/tests/test_product_turn_contract_route.py b/tests/test_product_turn_contract_route.py index 998d2ea83..8055c6516 100644 --- a/tests/test_product_turn_contract_route.py +++ b/tests/test_product_turn_contract_route.py @@ -368,13 +368,13 @@ async def test_exact_odysseus_clean_route_offers_only_requested_compact_family( async for _ in response.body_iterator: pass - from src.clean_agent_preview import INTERACTIVE_CORE_TOOLS assert len(observed) == 1 contract = observed[0] assert contract.selection_mode == "clean_compact_v3_preview" assert contract.capabilities == {"tasks"} - assert contract.offered == {"manage_tasks"} | set(INTERACTIVE_CORE_TOOLS) + assert contract.offered == {"manage_tasks", "ask_user"} assert contract.required == {"manage_tasks"} + assert contract.permits("manage_tasks") @pytest.mark.asyncio