"""Disabled reply experiments cannot choose execution or delivery paths.""" from copy import deepcopy from unittest.mock import AsyncMock, Mock import pytest from backend.config import settings from backend.prospecting import orchestrator as subject, preflight, prompts, request_routing from backend.prospecting.contracts import IntentPlan, KNOWLEDGE_ID, ProspectingBrief from backend.prospecting.delivery import project REQUEST = '独立做一轮只读预检,不保存来源,最多查看2篇新笔记,不分析名单。' @pytest.fixture(autouse=True) def disable_reply_relation_preflight(monkeypatch): monkeypatch.setattr(settings, 'reply_relation_preflight_enabled', False) def forbidden(*_args, **_kwargs): raise AssertionError('Disabled preflight must not parse the request or project results') for module in (subject, prompts, request_routing): monkeypatch.setattr(module, 'compile_preflight_policy', forbidden) monkeypatch.setattr(preflight, 'delivery_projection', forbidden) @pytest.fixture def memory(monkeypatch, tmp_path): brief = ProspectingBrief(intent='discover', target_candidates=10, max_notes=9, keywords=['用户已给关键词'], user_notes=REQUEST) row = {'id': 'preflight-disabled-p0', 'knowledge_id': KNOWLEDGE_ID, 'status': 'running', 'current_step': 'search_read', 'browser_profile_path': '/fixture', 'inputs': {**brief.model_dump(), 'goal': REQUEST, '_goal_version': 1, '_execution_policy': subject.EXECUTION_POLICY, '_needs_interpretation': False}, 'artifacts': {'sources': [], 'messages': [], 'analysis_pending': False, 'plan': {'missing': []}}} monkeypatch.setattr(subject.db, 'get_task', lambda _tid: deepcopy(row)) def update(_tid, **fields): row.update(deepcopy(fields)) return deepcopy(row) monkeypatch.setattr(subject.db, 'update_task', update) monkeypatch.setattr(subject, 'append_progress', lambda *_args, **_kwargs: None) monkeypatch.setattr(subject, 'write_summary', lambda *_args, **_kwargs: None) monkeypatch.setattr(subject, 'task_doc_dir', lambda _tid: tmp_path) monkeypatch.setattr(subject, 'get_llm_config', lambda: {'api_key': 'fixture-only'}) monkeypatch.setattr(subject.runner, 'has_live_session', lambda _tid: False) engine = subject.ProspectingOrchestrator() review = Mock(side_effect=lambda _tid, artifacts: artifacts) monkeypatch.setattr(engine, '_review', review) monkeypatch.setattr(engine, '_learn', Mock()) return row, engine, review def test_disabled_preflight_does_not_change_intent_or_note_limit(): plan = IntentPlan(brief=ProspectingBrief(intent='discover', target_candidates=10, max_notes=9), summary='模型抽取的任务', stages=['搜索', '读取'], reuse_existing_sources=True) assert request_routing.ground_execution_plan(plan, REQUEST) == plan # This phrasing previously enabled a current-page recheck solely through # preflight regex, without an independent browser verification instruction. assert request_routing.browser_recheck_requested('只读预检当前页面,不保存来源。') is False @pytest.mark.parametrize('message', [REQUEST, '请在当前网页核对现有原话,不新增来源。']) def test_default_prompt_has_no_disabled_tool_or_parent_repair_instructions(message): prompt = prompts.browser_prompt(ProspectingBrief(intent='discover', target_candidates=10, user_notes=message), []) assert 'inspect_reply_relations' not in prompt and 'resolve_reply_target' not in prompt assert '先按实际给出的父/根定位保存缺失来源' not in prompt assert '不为补关系重存回复' in prompt async def test_prepare_does_not_rewrite_a_model_discover_intent_from_preflight_regex(memory, monkeypatch): row, engine, _review = memory execute = AsyncMock() monkeypatch.setattr(engine, '_run', execute) result = await engine._prepare(deepcopy(row)) await engine.jobs[row['id']] assert result['inputs']['intent'] == 'discover' and result['inputs']['max_notes'] == 9 brief = execute.await_args.args[1] assert brief.intent == 'discover' and brief.max_notes == 9 async def test_scoped_execution_uses_normal_tools_and_delivery_when_flag_is_off(memory, monkeypatch): row, engine, review = memory ranking = Mock(return_value=[]) monkeypatch.setattr(subject, 'ranked_experience', ranking) browser = AsyncMock(return_value={'verdict': 'success', 'summary': 'fixture', 'history': {'is_successful': True}, 'artifacts': {}}) monkeypatch.setattr(subject.runner, 'run_browser_step', browser) await engine._run_scoped(row['id'], engine._brief(row), '') args = browser.await_args.kwargs assert args['preflight_policy'] is None and args['keep_session'] is False assert args['prospecting_limits']['max_notes'] == 9 assert '本轮交付目标是10位去重候选' in args['agent_task'] assert row['current_step'] == 'stage_results' assert row['artifacts']['experience_injected'] is True ranking.assert_called_once() review.assert_called_once() def test_default_project_does_not_activate_preflight_even_with_old_artifacts(memory): row, _engine, _review = memory row['artifacts']['preflight_run'] = {'run_id': 'a'*32, 'goal_version': 1} before = deepcopy(row) result = project(row) assert 'preflight' not in result assert row == before