mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
126 lines
6.2 KiB
Python
126 lines
6.2 KiB
Python
import pytest
|
|
|
|
from src.clean_agent_preview import browser_transport_recovery
|
|
|
|
|
|
URL = 'https://example.com/catalog/'
|
|
|
|
|
|
@pytest.mark.parametrize('error', ['HTTP2_PROTOCOL_ERROR', 'NAME_NOT_RESOLVED', 'CONNECTION_RESET'])
|
|
def test_failed_navigation_preserves_task_and_uses_permitted_fetch(error):
|
|
message = browser_transport_recovery(
|
|
{'action': 'open', 'url': URL}, 'net::ERR_' + error, {'web_fetch'}, set())
|
|
assert URL in message
|
|
assert 'original objective' in message
|
|
assert 'Use web_fetch once' in message
|
|
|
|
|
|
def test_exhausted_fetch_uses_search_instead_of_bouncing():
|
|
message = browser_transport_recovery(
|
|
{'action': 'open', 'url': URL}, 'net::ERR_HTTP2_PROTOCOL_ERROR',
|
|
{'web_fetch', 'web_search'}, {URL.rstrip('/')})
|
|
assert 'Use web_search' in message
|
|
assert 'Use web_fetch' not in message
|
|
|
|
|
|
@pytest.mark.parametrize('action', ['click', 'fill', 'press', 'evaluate'])
|
|
def test_mutating_batch_never_replayed(action):
|
|
assert not browser_transport_recovery(
|
|
{'action': 'batch', 'commands': [['open', URL], [action, 'target']]},
|
|
'net::ERR_HTTP2_PROTOCOL_ERROR', {'web_fetch'}, set())
|
|
|
|
|
|
def test_read_only_batch_and_no_available_tools():
|
|
message = browser_transport_recovery(
|
|
{'action': 'batch', 'commands': [['open', URL], ['snapshot']]},
|
|
'net::ERR_HTTP2_PROTOCOL_ERROR', set(), set())
|
|
assert 'No permitted retrieval fallback' in message
|
|
|
|
|
|
@pytest.mark.parametrize('output', ['net::ERR_CERT_AUTHORITY_INVALID', 'CAPTCHA', 'Access denied', 'OK'])
|
|
def test_no_transport_recovery_for_security_or_success(output):
|
|
assert not browser_transport_recovery({'action': 'open', 'url': URL}, output, {'web_fetch'}, set())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('fetch_succeeds', [False, True])
|
|
async def test_stream_recovers_navigation_then_fetch_without_email_classifier(monkeypatch, fetch_succeeds):
|
|
import json
|
|
from types import SimpleNamespace
|
|
from dataclasses import replace
|
|
import src.clean_agent_preview as module
|
|
import src.email_task_intent as email_intent
|
|
from src.tool_policy import ToolPolicy
|
|
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
|
|
from src.turn_contract import resolve_full_inventory_contract
|
|
|
|
requests, calls = [], []
|
|
def call(name, args):
|
|
return {'tool_calls': [{'index': 0, 'id': name, 'type': 'function',
|
|
'function': {'name': name, 'arguments': json.dumps(args)}}]}
|
|
responses = iter([
|
|
call('private_browser', {'action': 'open', 'url': URL}),
|
|
call('web_fetch', {'url': URL}),
|
|
call('web_search', {'query': 'wardrobe'}),
|
|
{'content': 'The site could not be read and no usable product evidence was found.'},
|
|
])
|
|
class Response:
|
|
def __init__(self, delta): self.delta = delta
|
|
async def __aenter__(self): return self
|
|
async def __aexit__(self, *args): pass
|
|
def raise_for_status(self): pass
|
|
async def aiter_lines(self):
|
|
yield 'data: ' + json.dumps({'choices': [{'delta': self.delta}]})
|
|
yield 'data: [DONE]'
|
|
class Client:
|
|
def __init__(self, **kwargs): pass
|
|
async def __aenter__(self): return self
|
|
async def __aexit__(self, *args): pass
|
|
def stream(self, *args, **kwargs):
|
|
requests.append(kwargs['json'])
|
|
return Response(next(responses))
|
|
async def execute(block, **kwargs):
|
|
calls.append(block.tool_type)
|
|
if block.tool_type == 'private_browser':
|
|
return block.tool_type, {'output': 'net::ERR_HTTP2_PROTOCOL_ERROR', 'exit_code': 1}
|
|
if block.tool_type == 'web_fetch':
|
|
return block.tool_type, {'output': 'Homepage navigation' if fetch_succeeds else 'Connection reset',
|
|
'exit_code': 0 if fetch_succeeds else 1}
|
|
assert 'site:example.com' in block.content
|
|
return block.tool_type, {'output': 'No usable product results.', 'exit_code': 0}
|
|
async def no_email_classifier(*args, **kwargs):
|
|
raise AssertionError('Web-only request must not use email interpretation')
|
|
monkeypatch.setattr(module.httpx, 'AsyncClient', Client)
|
|
monkeypatch.setattr(module, 'execute_tool_block', execute)
|
|
monkeypatch.setattr(email_intent, 'classify_email_task', no_email_classifier)
|
|
policy = ToolPolicy()
|
|
schemas = [s for s in FUNCTION_TOOL_SCHEMAS if s['function']['name'] in {'private_browser', 'web_fetch', 'web_search'}]
|
|
contract = replace(resolve_full_inventory_contract(schemas=schemas, policy=policy),
|
|
required=frozenset({'private_browser'}))
|
|
chunks = [chunk async for chunk in module.stream_preview(
|
|
endpoint_url='http://fixture', model='Ajax', headers={}, turn_contract=contract,
|
|
history_session=SimpleNamespace(history=[
|
|
{'role': 'user', 'content': 'Browse https://wrong.example/catalog/ and find a wardrobe'},
|
|
{'role': 'assistant', 'content': 'Navigation failed.'}]),
|
|
messages=[{'role': 'user', 'content': 'Browse https://wrong.example/catalog/ and find a wardrobe'},
|
|
{'role': 'assistant', 'content': 'Navigation failed.'},
|
|
{'role': 'user', 'content': URL}],
|
|
session_id='fixture-browser', owner='test', disabled_tools=set(), tool_policy=policy)]
|
|
assert calls == ['private_browser', 'web_fetch', 'web_search'], '\n'.join(chunks)
|
|
assert requests[1]['tool_choice'] == 'auto'
|
|
assert [s['function']['name'] for s in requests[1]['tools']] == ['web_fetch']
|
|
assert requests[2]['tool_choice'] == 'auto'
|
|
assert [s['function']['name'] for s in requests[2]['tools']] == ['web_search']
|
|
assert any('browser_transport_fallback' in chunk for chunk in chunks)
|
|
assert any('[DONE]' in chunk for chunk in chunks)
|
|
assert all(m['role'] != 'system' for m in requests[0]['messages'][1:])
|
|
assert 'Corrected target: ' + URL in requests[0]['messages'][0]['content']
|
|
|
|
|
|
@pytest.mark.parametrize('arguments', ['"url"', '[]', 'null', '42'])
|
|
def test_provider_history_requires_object_tool_arguments(arguments):
|
|
from src.clean_agent_preview import protocol_safe_tool_calls
|
|
calls = [{'function': {'name': 'web_fetch', 'arguments': arguments}}]
|
|
assert protocol_safe_tool_calls(calls)[0]['function']['arguments'] == '{}'
|
|
assert calls[0]['function']['arguments'] == arguments
|