import copy import unittest from src.step3_question_answer.next_actions import build_next_actions, append_to_summary from src.step3_question_answer.recommendation import Recommender from src.step3_question_answer.report import render from test_recommendation import candidate, good_analysis, handoff, retrieval class NextActionTests(unittest.TestCase): def result(self): def model(prompt, data): if 'cards' in data: return {'text': '可了解合成支持,登记地区仍待确认。', 'source_ids': [card['source_id'] for card in data['cards']]} return good_analysis() return Recommender(model).answer(handoff(), retrieval()) def test_actions_surface_pending_and_cite_exact_original(self): result = self.result() actions = result['next_actions'] self.assertEqual(len(actions), 3) self.assertEqual(actions[0]['description'], '在哪个区登记?') self.assertEqual(actions[0]['citations'][0]['path'], '/data/conditions') self.assertEqual(actions[0]['citations'][0]['quote'], '在本区登记') self.assertEqual(result['summary']['status'], 'generated') html = render(result) self.assertEqual(html.count('

下一步建议

'), 1) self.assertIn('1. **确认相关地区**', result['summary']['text']) self.assertIn('3. ', result['summary']['text']) self.assertNotIn('相关政策要求:', result['summary']['text']) self.assertIn('相关政策要求:', actions[0]['reason']) self.assertIn('在哪个区登记?', html) self.assertIn('href="#source-' + result['sources'][0]['id'], html) self.assertLess(html.index('下一步建议'), html.index('

完整参考资料')) def test_failed_policy_summary_does_not_request_application_details(self): result = Recommender(lambda *args: good_analysis()).answer(handoff(), retrieval()) self.assertEqual(result['summary']['status'], 'fallback') self.assertEqual(result['next_actions'], []) self.assertEqual(result['follow_up_questions'], []) self.assertNotIn('下一步建议', result['summary']['text']) def test_duplicate_conditions_merge_sources_and_limit_three(self): result = self.result() first = result['items'][0] # Action generation uses full internal conditions before card projection. first['conditions'] = copy.deepcopy(result['candidate_diagnostics'][0]['analyses'][0]['conditions']) second = copy.deepcopy(first) second['source_id'] = 'another' source = copy.deepcopy(result['sources'][0]) source['id'] = 'another' sources = result['sources'] + [source] actions = build_next_actions([first, second], sources) self.assertEqual(actions[0]['source_ids'], [first['source_id'], 'another']) self.assertEqual(sum(a['kind'] == 'confirm_information' for a in actions), 1) for index in range(5): condition = dict(first['conditions'][0], requirement=f'合成不同条件{index}', question=f'问题{index}') first['conditions'].append(condition) actions = build_next_actions([first], sources) self.assertEqual(len(actions), 3) self.assertTrue(all(a['kind'] == 'confirm_information' for a in actions)) def test_no_evidence_not_confirmed_or_overview_has_no_forced_questions(self): result = self.result() for status, quote in [('满足', '在本区登记'), ('待确认', ''), ('待确认', '不存在的条件')]: result['items'][0]['conditions'] = [dict(good_analysis()['conditions'][0], status=status, quote=quote)] actions = build_next_actions(result['items'], result['sources']) self.assertEqual(len(actions), 3) self.assertFalse(any(a['kind'] == 'confirm_information' for a in actions)) self.assertEqual(build_next_actions([], result['sources']), []) self.assertEqual(build_next_actions(result['items'], result['sources'], False), []) for field in ('support_methods_standards', 'application_conditions'): items = copy.deepcopy(result['items']) items[0]['card'][field] = None self.assertEqual(len(build_next_actions(items, result['sources'])), 3) def test_excluded_or_no_results_and_overview_do_not_emit_actions(self): for mode in ('overview', 'empty', 'failure', 'ineligible'): context, records = handoff(), retrieval() analysis = good_analysis() if mode == 'overview': context['conversation_kind'] = 'policy_overview' elif mode in ('empty', 'failure'): records = retrieval([], complete=mode != 'failure') else: analysis['conditions'][0].update(status='不满足', user_evidence=context['original_question']) result = Recommender(lambda *args: copy.deepcopy(analysis)).answer(context, records) self.assertEqual(result['next_actions'], [], mode) self.assertNotIn('### 下一步建议', result['summary']['text']) def test_action_text_is_html_escaped(self): result = self.result() result['next_actions'][0]['description'] = '' result['summary'] = append_to_summary({'text': '原总结', 'source_ids': []}, result['next_actions']) self.assertIn('<script>bad()', render(result)) self.assertNotIn('