assistant/tests/test_preflight_policy_d44.py

219 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""User-authorized preflight scope, independent of model/runtime state."""
import pytest
from backend.browser.preflight_policy import PreflightPolicy, compile_preflight_policy, instruction_text, note_id_from_url
from backend.config import settings
@pytest.fixture(autouse=True)
def enable_reply_relation_preflight(monkeypatch):
monkeypatch.setattr(settings, 'reply_relation_preflight_enabled', True)
A = '6adaf8a6000000001901d400'
B = '6a7f04b8000000003302f624'
C = '6ad9c5e1000000001400c800'
URL = 'https://www.xiaohongshu.com/explore/'+A+'?xsec_token=fixture-private'
ACTUAL_REQUEST = (
'在小红书搜索“杭州装修”,本轮只做二级回复关系的只读预检,不找客户人数。最多查看2篇不同的新笔记,'
f'排除ID为{A}和{B}的已测笔记。每篇只预检第一组当前可见讨论,最多5条回复;'
'评论未进视口时沿真实评论入口推进。使用 inspect_reply_relations 检查是否有稳定且唯一的直接父为另一条回复的DOM依据。'
'只报告每篇checked_count、eligible_count和不合格原因,不保存正文或任何来源,不保存知识,不展开折叠回复,'
'不点击回复对象名字,不调用resolve_reply_target,不分析名单。发现eligible=true就保留页面并报告该样本可进入后续取证,'
'本轮仍不取证;两篇都没有合格样本就说明未展开范围并结束,不重复尝试,不把缺少依据当成没有评论。'
'不关注、不点赞、不收藏、不发评论、不私信。'
)
ACTUAL_SECOND_REQUEST = (
'独立做一轮只读预检,只访问下面这个此前已实际观察到的笔记入口,不搜索、不换笔记。'
'最多1篇,只检查第一组当前可见讨论,每次最多5条回复。'
f'严格排除ID为{A}和{B}的旧笔记。\n[实际入口省略]\n'
'先沿真实评论入口进入评论区,再使用 inspect_reply_relations。'
'若首组只有根评论或回复仍折叠,就按实际可见范围报告;不展开折叠回复,不点击回复对象名字,'
'不调用resolve_reply_target。不保存正文或来源,不保存知识,不分析客户名单,'
'不关注、不点赞、不收藏、不发评论、不私信。'
'只报告程序预检的篇数、checked_count、eligible_count和未读原因;'
'没有合格样本也正常结束,不能凭昵称或顺序补关系。有eligible=true也仅保留页面,本轮不自动进入取证。'
)
@pytest.mark.parametrize('message', [
'这轮只预检,不保存任何来源。最多浏览2篇新笔记。',
'先仅预检,不保存原话,至多阅读两篇笔记。',
'只做关系预检,不调用save_source。上限为2篇。',
'仅进行回复关系预检,禁止保存来源,最多2篇候选笔记。',
'只读预检,不保存正文或来源,最多2篇。',
'独立做一轮只读预检,不保存来源,最多2篇。',
])
def test_explicit_read_only_preflight_has_only_the_actual_user_note_limit(message):
policy = compile_preflight_policy(message, max_notes=99)
assert policy.active is True and policy.max_notes == 2
assert policy.excluded_note_ids == ()
@pytest.mark.parametrize('message', [
'预检一下关系,再保存来源。最多2篇。',
'只预检关系,稍后保存来源。',
'不保存来源,最多2篇。',
'只预检,正常保存来源。',
'不要只做关系预检,直接按正常流程保存来源。',
'只做关系预检,不保存;取消预检,改成正常取证并保存。',
'只预检,不保存;最后要保存来源。',
'“只预检,不保存,最多2篇”是引用,实际正常取证。',
'「只做关系预检,不保存」只是示例,正常整理。',
'```text\n只预检,不保存,最多2篇\n```\n正常整理。',
'> 只预检,不保存,最多2篇\n正常取证。',
'比如只预检、不保存;实际先讨论方案。',
'先解释只预检、不保存是什么意思,暂不操作。',
'只预检关系,不保存经验。',
'只预检关系,不保存截图。',
'只预检关系,不保存知识。',
'正常找客户,每篇只预检第一组,不保存评论原话。',
'只读讨论,不保存来源,最多1篇。',
'独立做一轮只读预检,不保存知识。',
'独立做一轮只读预检,之后保存来源。',
'不保存来源,独立做一轮只读预检;先预检后保存来源。',
'不保存来源,独立做一轮只读预检;先预检再保存来源。',
'不保存来源,独立做一轮只读预检;先预检,稍后保存来源。',
'独立做一轮只读预检,不保存来源;取消只读预检。',
'不做只读预检,不保存来源。',
'不进行只读预检,不保存来源。',
'不要独立做一轮只读预检,不保存来源。',
'“独立做一轮只读预检,不保存来源”是引用,先讨论方案。',
'比如独立做一轮只读预检、不保存来源;实际先讨论方案。',
'正常找客户,每篇做一轮只读预检,不保存来源。',
])
def test_ordinary_evidence_negation_quotes_examples_and_later_cancellation_are_inactive(message):
assert compile_preflight_policy(message, max_notes=2) == PreflightPolicy()
def test_no_default_or_model_invented_note_count_is_added():
assert compile_preflight_policy('只预检,不保存来源。', max_notes=2).max_notes is None
assert compile_preflight_policy('只预检,不保存来源,最多检查5条可见回复。', max_notes=2).max_notes is None
assert compile_preflight_policy('只预检,不保存,最多2到3篇笔记。').max_notes is None
assert compile_preflight_policy('只预检,不保存,最多2篇左右。').max_notes is None
assert compile_preflight_policy('只预检,不保存,最多2篇,篇数不限。').max_notes is None
def test_actual_d44_user_scope_is_grounded_without_relying_on_model_brief():
policy = compile_preflight_policy(ACTUAL_REQUEST, max_notes=9)
assert policy == PreflightPolicy(active=True, excluded_note_ids=(A, B), max_notes=2)
def test_actual_second_d44_read_only_preflight_keeps_user_limit_and_exclusions():
policy = compile_preflight_policy(ACTUAL_SECOND_REQUEST, max_notes=9)
assert policy == PreflightPolicy(active=True, excluded_note_ids=(A, B), max_notes=1)
def test_full_second_goal_with_fictional_entry_url_has_exactly_two_exclusions():
entry = 'https://www.xiaohongshu.com/explore/'+C+'?xsec_token=fictional&xsec_source=fixture'
full = ACTUAL_SECOND_REQUEST.replace('[实际入口省略]', entry)
policy = compile_preflight_policy(full, max_notes=1)
assert policy == PreflightPolicy(active=True, excluded_note_ids=(A, B), max_notes=1)
assert C not in policy.excluded_note_ids
@pytest.mark.parametrize('boundary', ['。\n', ';\n', ';\n', '!\n', '?\n', '\n'])
def test_inline_exclusion_never_continues_across_a_hard_sentence_or_bare_newline(boundary):
entry = 'https://www.xiaohongshu.com/explore/'+C+'?xsec_token=fictional'
policy = compile_preflight_policy(f'只读预检,不保存来源,排除旧笔记{A}和{B}{boundary}{entry}')
assert policy.excluded_note_ids == (A, B)
@pytest.mark.parametrize('ending', ['\n\n', '\n实际入口:', '\n当前页面:', '\n请访问', '\n先沿评论入口进入评论区\n'])
def test_explicit_multiline_list_ends_before_a_new_entry_or_action(ending):
entry = 'https://www.xiaohongshu.com/explore/'+C
goal = f'只读预检,不保存来源。排除以下笔记:\n"{A}"\n`{B}`{ending}{entry}'
assert compile_preflight_policy(goal).excluded_note_ids == (A, B)
def test_declared_multiline_url_list_keeps_queries_atomic_and_does_not_collect_query_ids():
first = 'https://www.xiaohongshu.com/explore/'+A+'?xsec_token='+C+'&label=用户&rule=最多9篇'
second = 'https://www.xiaohongshu.com/explore/'+B+'?query=排除'+C
goal = f'只读预检,不保存来源,最多1篇。排除以下笔记:\n{first}\n{second}'
policy = compile_preflight_policy(goal)
assert policy.excluded_note_ids == (A, B) and policy.max_notes == 1
@pytest.mark.parametrize('header', ['不要排除以下笔记:', '无需排除以下笔记:', '“排除以下笔记:”', '> 排除以下笔记:'])
def test_negated_or_quoted_header_cannot_open_an_exclusion_list(header):
assert compile_preflight_policy(f'只读预检,不保存来源。\n{header}\n{A}\n{B}').excluded_note_ids == ()
def test_list_cancellation_and_soft_comma_continuation_stay_explicit():
assert compile_preflight_policy(f'只读预检,不保存来源。排除{A},{B},实际入口:{C}').excluded_note_ids == (A, B)
assert compile_preflight_policy(f'只读预检,不保存来源。排除以下笔记:\n{A}\n{B}\n不排除{B}\n{C}').excluded_note_ids == (A,)
assert compile_preflight_policy(f'只读预检,不保存来源。排除{A};{B}').excluded_note_ids == (A,)
def test_url_directive_words_cannot_enable_preflight_save_ban_exclusion_or_note_cap():
url = 'https://www.xiaohongshu.com/explore/'+C+'?mode=只读预检&save=不保存来源&scope=排除'+A+'&cap=最多9篇'
assert compile_preflight_policy(url, max_notes=9) == PreflightPolicy()
policy = compile_preflight_policy('只读预检,不保存来源。实际入口:'+url)
assert policy == PreflightPolicy(active=True)
bounded = compile_preflight_policy(f'只读预检,不保存来源,最多1篇,排除{A}。实际入口:'+url)
assert bounded == PreflightPolicy(active=True, excluded_note_ids=(A,), max_notes=1)
punctuation_query = 'https://www.xiaohongshu.com/explore/'+C+'?data=sample,只读预检,不保存来源;排除'+A+',最多9篇'
assert compile_preflight_policy(punctuation_query) == PreflightPolicy()
assert compile_preflight_policy('只读预检,不保存来源。'+punctuation_query) == PreflightPolicy(active=True)
def test_url_terminal_sentence_mark_ends_exclusion_even_with_a_query():
excluded = 'https://www.xiaohongshu.com/explore/'+A+'?xsec_token=fictional'
entry = 'https://www.xiaohongshu.com/explore/'+C+'?xsec_token=entry'
assert compile_preflight_policy(f'只读预检,不保存来源。排除{excluded}。\n{entry}').excluded_note_ids == (A,)
def test_note_id_in_url_path_or_query_is_data_only_until_explicitly_bound():
url = 'https://www.xiaohongshu.com/explore/'+C+'?note='+B+'&hint=排除'+A
assert compile_preflight_policy('只读预检,不保存来源。'+url).excluded_note_ids == ()
assert compile_preflight_policy('只读预检,不保存来源。排除笔记 '+url).excluded_note_ids == (C,)
quoted = '“'+url+',只读预检,不保存来源”是引用'
assert compile_preflight_policy(quoted) == PreflightPolicy()
def test_real_explicit_exclusions_bind_only_note_identities_and_can_be_cancelled():
policy = compile_preflight_policy(f'只做只读关系预检,不保存来源,排除D42旧笔记 {A}、D43旧笔记 {B}。最多2篇新笔记。')
assert policy.excluded_note_ids == (A, B)
assert policy.max_notes == 2
changed = compile_preflight_policy(f'只预检,不保存,排除笔记 {A} 和 {B},不要排除笔记 {B}。')
assert changed.excluded_note_ids == (A,)
def test_excluded_note_urls_and_formatted_constants_do_not_turn_whole_quotes_into_instructions():
policy = compile_preflight_policy(f'只预检,不保存,排除笔记 `{A}` 和 `{URL}`。')
assert policy.excluded_note_ids == (A,)
ignored = compile_preflight_policy(f'只预检,不保存。“排除笔记 {A}”是引用,只检查真实新页面。')
assert ignored.excluded_note_ids == ()
assert URL in instruction_text(f'排除笔记 `{URL}`')
def test_raw_profile_ids_and_unsupported_links_are_not_note_exclusions():
policy = compile_preflight_policy(f'只预检,不保存,排除用户 {A};跳过 https://www.xiaohongshu.com/user/profile/{B};排除 https://evil.example/explore/{C}。')
assert policy.excluded_note_ids == ()
assert compile_preflight_policy(f'只预检,不保存,排除 {A}f。').excluded_note_ids == ()
def test_later_explicit_include_or_reset_removes_excluded_notes():
assert compile_preflight_policy(f'只预检,不保存,排除笔记 {A};允许打开笔记 {A}。').excluded_note_ids == ()
assert compile_preflight_policy(f'只预检,不保存,排除笔记 {A};排除列表改为 {B}。').excluded_note_ids == (B,)
assert compile_preflight_policy(f'只预检,不保存,排除笔记 {A};不排除任何笔记。').excluded_note_ids == ()
assert compile_preflight_policy(f'只预检,不保存,不需要排除笔记 {A}。').excluded_note_ids == ()
assert compile_preflight_policy(f'只预检,不保存,排除笔记 {A};不排除笔记 {A}。').excluded_note_ids == ()
assert compile_preflight_policy(f'只预检,不保存,不需要打开笔记 {A}。').excluded_note_ids == (A,)
@pytest.mark.parametrize('url,expected', [
(URL, A),
('https://xiaohongshu.com/discovery/item/'+A+'/', A),
('https://www.xiaohongshu.com:443/explore/'+A, A),
('http://www.xiaohongshu.com/explore/'+A, None),
('https://www.xiaohongshu.com:9443/explore/'+A, None),
('https://user@www.xiaohongshu.com/explore/'+A, None),
('https://www.xiaohongshu.com.evil.example/explore/'+A, None),
('https://www.xiaohongshu.com/user/profile/'+A, None),
('app://ui/index.html', None),
('https://www.xiaohongshu.com/explore/'+A+'f', None),
])
def test_note_identity_is_read_only_from_an_actual_valid_note_url(url, expected):
assert note_id_from_url(url) == expected