test(agent): restore exact turn-policy regressions

This commit is contained in:
Alexandre Teixeira
2026-09-23 00:14:30 +01:00
parent c0b71a5cef
commit 3cabbb9bca
6 changed files with 113 additions and 21 deletions
+4 -3
View File
@@ -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"}),
)
+1 -1
View File
@@ -617,7 +617,7 @@ def test_finish_nudge_does_not_accept_unfinished_correction_promise(monkeypatch)
monkeypatch,
[
'```write_file\n/workspace/output.html\n<body>draft</body>\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\n<body>corrected</body>\n```',
"Done. Corrected and checked output.html.",
],
+86 -13
View File
@@ -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():
+2 -1
View File
@@ -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(
+18 -1
View File
@@ -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)
+2 -2
View File
@@ -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