"""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