|
20 | 20 | build_canonical_request, |
21 | 21 | ) |
22 | 22 | from coding.proxy.routing.executor import ( |
| 23 | + _SESSION_TITLE_MAX_LEN, |
23 | 24 | _VENDOR_PROTOCOL_LABEL_MAP, |
| 25 | + _extract_session_title, |
24 | 26 | _has_tool_results, |
25 | 27 | _is_likely_request_format_error, |
26 | 28 | _log_vendor_response_error, |
27 | 29 | _RouteExecutor, |
| 30 | + _sanitize_user_text, |
28 | 31 | ) |
29 | 32 | from coding.proxy.routing.session_manager import RouteSessionManager |
30 | 33 | from coding.proxy.routing.tier import VendorTier |
@@ -1949,3 +1952,162 @@ def test_returns_body_for_unknown_tier(self): |
1949 | 1952 | result = exec_inst._prepare_body_for_tier(body, tier, source_vendor="zhipu") |
1950 | 1953 |
|
1951 | 1954 | assert result is body |
| 1955 | + |
| 1956 | + |
| 1957 | +# ── Session 标题清洗与抽取测试 ───────────────────────────────── |
| 1958 | + |
| 1959 | + |
| 1960 | +class TestSanitizeUserText: |
| 1961 | + """``_sanitize_user_text`` — 剥离 CC 注入的系统级 XML 块. |
| 1962 | +
|
| 1963 | + 覆盖典型 system-reminder/user-preferences 噪声、slash command |
| 1964 | + 短路、空白折叠与边界场景。 |
| 1965 | + """ |
| 1966 | + |
| 1967 | + def test_strips_system_reminder(self): |
| 1968 | + raw = "<system-reminder>MCP 指令</system-reminder>这是用户真实输入" |
| 1969 | + assert _sanitize_user_text(raw) == "这是用户真实输入" |
| 1970 | + |
| 1971 | + def test_strips_user_preferences(self): |
| 1972 | + raw = "用户问题<user-preferences>遵循 AGENTS.md</user-preferences>" |
| 1973 | + assert _sanitize_user_text(raw) == "用户问题" |
| 1974 | + |
| 1975 | + def test_strips_multiple_noise_blocks(self): |
| 1976 | + raw = ( |
| 1977 | + "<system-reminder>A</system-reminder>" |
| 1978 | + "<system-reminder>B</system-reminder>" |
| 1979 | + "<system-reminder>C</system-reminder>" |
| 1980 | + "<system-reminder>D</system-reminder>" |
| 1981 | + "真实输入文本" |
| 1982 | + "<user-preferences>P</user-preferences>" |
| 1983 | + ) |
| 1984 | + assert _sanitize_user_text(raw) == "真实输入文本" |
| 1985 | + |
| 1986 | + def test_strips_multiline_system_reminder(self): |
| 1987 | + """多行 system-reminder 块需被 DOTALL 完整匹配剥离.""" |
| 1988 | + raw = ( |
| 1989 | + "<system-reminder>\n" |
| 1990 | + "# MCP Server Instructions\n" |
| 1991 | + "Use this server to fetch ...\n" |
| 1992 | + "</system-reminder>\n" |
| 1993 | + "TITLE 中的 Session 标题应当取自用户输入" |
| 1994 | + ) |
| 1995 | + assert _sanitize_user_text(raw) == "TITLE 中的 Session 标题应当取自用户输入" |
| 1996 | + |
| 1997 | + def test_strips_tag_with_attributes(self): |
| 1998 | + """容忍标签携带属性(如 <system-reminder type="x">).""" |
| 1999 | + raw = '<system-reminder type="x">noise</system-reminder>真实' |
| 2000 | + assert _sanitize_user_text(raw) == "真实" |
| 2001 | + |
| 2002 | + def test_slash_command_with_args(self): |
| 2003 | + raw = ( |
| 2004 | + "<command-message>commit (user)</command-message>" |
| 2005 | + "<command-name>/commit</command-name>" |
| 2006 | + "<command-args>修复标题</command-args>" |
| 2007 | + ) |
| 2008 | + assert _sanitize_user_text(raw) == "/commit 修复标题" |
| 2009 | + |
| 2010 | + def test_slash_command_no_args(self): |
| 2011 | + raw = "<command-name>/review</command-name>" |
| 2012 | + assert _sanitize_user_text(raw) == "/review" |
| 2013 | + |
| 2014 | + def test_collapses_whitespace(self): |
| 2015 | + raw = "<system-reminder>X</system-reminder>\n\n 多余 空白\t\t折叠 " |
| 2016 | + assert _sanitize_user_text(raw) == "多余 空白 折叠" |
| 2017 | + |
| 2018 | + def test_empty_after_strip(self): |
| 2019 | + raw = "<system-reminder>仅噪声</system-reminder>" |
| 2020 | + assert _sanitize_user_text(raw) == "" |
| 2021 | + |
| 2022 | + def test_empty_input(self): |
| 2023 | + assert _sanitize_user_text("") == "" |
| 2024 | + |
| 2025 | + def test_preserves_user_xml_like_content(self): |
| 2026 | + """用户输入中合法的 XML/HTML 片段(非白名单标签)需完整保留.""" |
| 2027 | + raw = "请帮我审查这段代码:<div>hello</div> 是否符合规范?" |
| 2028 | + assert _sanitize_user_text(raw) == raw |
| 2029 | + |
| 2030 | + def test_strips_local_command_output(self): |
| 2031 | + raw = "<local-command-stdout>build ok</local-command-stdout>构建后的下一步问题" |
| 2032 | + assert _sanitize_user_text(raw) == "构建后的下一步问题" |
| 2033 | + |
| 2034 | + |
| 2035 | +class TestExtractSessionTitle: |
| 2036 | + """``_extract_session_title`` — 端到端从 CanonicalRequest 抽取标题.""" |
| 2037 | + |
| 2038 | + @staticmethod |
| 2039 | + def _build_request(messages: list[dict]): |
| 2040 | + return build_canonical_request({"model": "test", "messages": messages}, {}) |
| 2041 | + |
| 2042 | + def test_truncates_to_max_len(self): |
| 2043 | + long_text = "用户输入文本" * 20 |
| 2044 | + req = self._build_request([{"role": "user", "content": long_text}]) |
| 2045 | + title = _extract_session_title(req) |
| 2046 | + assert len(title) == _SESSION_TITLE_MAX_LEN |
| 2047 | + assert title == long_text[:_SESSION_TITLE_MAX_LEN] |
| 2048 | + |
| 2049 | + def test_strips_noise_from_first_user_message(self): |
| 2050 | + raw = ( |
| 2051 | + "<system-reminder>MCP 指令</system-reminder>" |
| 2052 | + "<user-preferences>偏好</user-preferences>" |
| 2053 | + "测试标题 ABC" |
| 2054 | + ) |
| 2055 | + req = self._build_request([{"role": "user", "content": raw}]) |
| 2056 | + assert _extract_session_title(req) == "测试标题 ABC" |
| 2057 | + |
| 2058 | + def test_handles_real_cc_first_message_shape(self): |
| 2059 | + """模拟 CC 真实首条消息(多个连续 system-reminder + 用户文本).""" |
| 2060 | + raw = ( |
| 2061 | + "<system-reminder>\n# MCP Server Instructions\n...</system-reminder>" |
| 2062 | + "<system-reminder>\nThe following skills...\n</system-reminder>" |
| 2063 | + "<system-reminder>\nPlan mode is active...\n</system-reminder>" |
| 2064 | + "\n\nTITLE 中的 Session 标题应当取自用户输入的信息前 30 个字\n\n" |
| 2065 | + "<user-preferences>始终遵循 AGENTS.md</user-preferences>" |
| 2066 | + ) |
| 2067 | + req = self._build_request([{"role": "user", "content": raw}]) |
| 2068 | + title = _extract_session_title(req) |
| 2069 | + assert title.startswith("TITLE 中的 Session") |
| 2070 | + assert len(title) <= _SESSION_TITLE_MAX_LEN |
| 2071 | + |
| 2072 | + def test_extracts_slash_command(self): |
| 2073 | + raw = ( |
| 2074 | + "<command-name>/commit</command-name>" |
| 2075 | + "<command-args>feat: 新增标题清洗</command-args>" |
| 2076 | + ) |
| 2077 | + req = self._build_request([{"role": "user", "content": raw}]) |
| 2078 | + assert _extract_session_title(req) == "/commit feat: 新增标题清洗" |
| 2079 | + |
| 2080 | + def test_returns_empty_when_only_noise(self): |
| 2081 | + raw = "<system-reminder>纯噪声</system-reminder>" |
| 2082 | + req = self._build_request([{"role": "user", "content": raw}]) |
| 2083 | + assert _extract_session_title(req) == "" |
| 2084 | + |
| 2085 | + def test_returns_empty_for_no_user_messages(self): |
| 2086 | + req = self._build_request([{"role": "assistant", "content": "你好"}]) |
| 2087 | + assert _extract_session_title(req) == "" |
| 2088 | + |
| 2089 | + def test_skips_noise_only_part_to_find_real_input(self): |
| 2090 | + """首个 user text part 全噪声时,fallback 到下一个非空 user part.""" |
| 2091 | + messages = [ |
| 2092 | + { |
| 2093 | + "role": "user", |
| 2094 | + "content": [ |
| 2095 | + { |
| 2096 | + "type": "text", |
| 2097 | + "text": "<system-reminder>noise</system-reminder>", |
| 2098 | + }, |
| 2099 | + {"type": "text", "text": "真实问题"}, |
| 2100 | + ], |
| 2101 | + } |
| 2102 | + ] |
| 2103 | + req = self._build_request(messages) |
| 2104 | + assert _extract_session_title(req) == "真实问题" |
| 2105 | + |
| 2106 | + def test_skips_assistant_role(self): |
| 2107 | + """assistant 角色的文本不应被作为标题候选.""" |
| 2108 | + messages = [ |
| 2109 | + {"role": "assistant", "content": "上一轮回答"}, |
| 2110 | + {"role": "user", "content": "新的用户问题"}, |
| 2111 | + ] |
| 2112 | + req = self._build_request(messages) |
| 2113 | + assert _extract_session_title(req) == "新的用户问题" |
0 commit comments