105 lines
5.4 KiB
Python
105 lines
5.4 KiB
Python
"""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
|