assistant/tests/test_action_contract_d43.py

111 lines
4.9 KiB
Python

"""Provider action shape matches the pinned framework, without response repair."""
import json
import pytest
from jsonschema import Draft202012Validator
from pydantic import BaseModel, ValidationError
from backend.browser import diagnostics, structured_output as contract
def envelope(actions):
return {'evaluation_previous_goal': 'fixture', 'memory': 'fixture', 'next_goal': 'fixture', 'action': actions}
@pytest.fixture(scope='module')
def actual_contract():
from browser_use.agent.views import AgentOutput
from backend.browser.tools import build_prospecting_tools
actions = build_prospecting_tools('d43_contract_fixture').registry.create_action_model()
output = AgentOutput.type_with_custom_actions(actions)
function, instruction = contract.request_contract(output)
return output, function['function']['parameters'], instruction
@pytest.mark.parametrize('actions,valid', [
([{'go_back': {}}], True),
([{'go_back': {}}, {'inspect_source_candidates': {'max_comments': 1}}], True),
([{'go_back': {}, 'inspect_source_candidates': {'max_comments': 1}}], False),
([{'go_back': {}, 'wait': None}], False),
([{'name': 'go_back', 'parameters': {}}], False),
([{}], False),
])
def test_real_action_union_and_sent_schema_agree_on_complete_single_branches(actual_contract, actions, valid):
output, schema, instruction = actual_contract
arguments = envelope(actions)
assert Draft202012Validator(schema).is_valid(arguments) is valid
if valid:
result = output.model_validate(arguments)
assert len(result.action) == len(actions)
else:
with pytest.raises(ValidationError):
output.model_validate(arguments)
assert '只能包含一个' in instruction and '多个数组元素' in instruction
def test_empty_action_array_is_rejected_by_standard_schema_as_well_as_transport(actual_contract):
from types import SimpleNamespace as Obj
output, schema, _ = actual_contract
arguments = envelope([])
assert not Draft202012Validator(schema).is_valid(arguments)
message = Obj(tool_calls=[Obj(type='function', function=Obj(
name=contract.FUNCTION_NAME, arguments=json.dumps(arguments)))])
with pytest.raises(contract.ResponseContractError, match='empty_actions'):
contract.validated_tool_result(message, output)
def test_generic_action_payload_is_not_narrowed_to_a_browser_action():
class GenericOutput(BaseModel):
action: list[dict]
original = GenericOutput.model_json_schema()
# A custom caller can cache its exported schema; request preparation must
# not mutate that caller-owned value either.
class CachedSchema:
@classmethod
def model_json_schema(cls):
return original
function, instruction = contract.request_contract(CachedSchema)
schema = function['function']['parameters']
assert Draft202012Validator(schema).is_valid({'action': [{'a': 1, 'b': 2}]})
assert 'maxProperties' not in schema['properties']['action']['items']
assert '只能包含一个' not in instruction
assert 'minItems' not in original['properties']['action']
def test_safe_union_feedback_explains_single_action_shape_without_guessing_bad_input(actual_contract):
output, _, _ = actual_contract
with pytest.raises(ValidationError) as caught:
output.model_validate(envelope([{'PRIVATE-ACTION': {'PRIVATE-PARAM': 'PRIVATE-VALUE'}}]))
errors = diagnostics.validation_fields(caught.value, output)
hint = diagnostics.correction_hint({'kind': 'schema_error', 'errors': errors})
assert 'extra_forbidden' in hint and 'missing' in hint
assert '每个数组元素只能包含一个' in hint and '多个数组元素' in hint
assert 'PRIVATE' not in json.dumps(errors) + hint
assert '不要执行或复用' in hint
async def test_single_action_feedback_reaches_only_the_next_normal_call(actual_contract, tmp_path, monkeypatch):
from types import SimpleNamespace as Obj
from browser_use.llm.messages import UserMessage
from backend.browser import runner
output, _, _ = actual_contract
monkeypatch.setattr(diagnostics, 'task_doc_dir', lambda _: tmp_path)
calls = []
class Base:
async def get_model_output(self, messages):
calls.append(messages)
return Obj(action=[])
agent = runner._prospecting_agent_class(Base)()
agent._goal_based = True
original = [UserMessage(content='fixture')]
with diagnostics.run_scope('d43_fixture', 'd43_run'):
with pytest.raises(ValidationError) as caught:
output.model_validate(envelope([{'go_back': {}, 'wait': None}]))
diagnostics.schema_error(None, caught.value, output)
await agent.get_model_output(original)
assert len(calls) == 1 and len(original) == 1
assert '每个数组元素只能包含一个' in calls[0][-1].text
diagnostics.validated_response('browser')
await agent.get_model_output(original)
assert len(calls) == 2 and calls[1] is original