"""Prompt-cache prefix discipline (架构文档 §7.2 缓存前缀纪律, TD-04). A volatile token in the fixed prefix cannot be caught by functional tests: the answer still looks right, the bill just quietly multiplies. So it gets its own mechanical check. """ from __future__ import annotations from pathlib import Path import pytest from packages.llm.errors import CachePrefixViolationError, PromptNotFoundError from packages.llm.prompts import ( PromptRegistry, find_volatile_tokens, parse_prompt_file, validate_cache_prefix, ) CLEAN_PREFIX = "你是「美搭」的穿搭顾问助手。只回答穿搭、色彩与风格相关的问题。" def test_clean_prefix_has_no_violations() -> None: assert find_volatile_tokens(CLEAN_PREFIX) == [] assert validate_cache_prefix(CLEAN_PREFIX, mode="strict") == [] @pytest.mark.parametrize( ("prefix", "expected_kind"), [ ("今天是 2026-08-13,请为用户搭配。", "iso-date"), ("当前时间 09:31:00,请回答。", "iso-time"), ("现在是 2026 年 8 月。", "cn-date"), ("时间戳 1755012345 供参考。", "epoch-seconds"), ("会话 550e8400-e29b-41d4-a716-446655440000 开始。", "uuid"), ("追踪号 9f8c1d2e3b4a5f6071829304 记录在案。", "long-hex-id"), ("你好 {nickname},我来帮你搭配。", "volatile-placeholder"), ("用户编号 {user_id} 的偏好如下。", "volatile-placeholder"), ("当前时刻 {now}。", "volatile-placeholder"), ], ) def test_volatile_tokens_are_detected(prefix: str, expected_kind: str) -> None: kinds = {v.kind for v in find_volatile_tokens(prefix)} assert expected_kind in kinds def test_strict_mode_raises_with_the_offending_samples() -> None: with pytest.raises(CachePrefixViolationError) as excinfo: validate_cache_prefix("生成于 2026-08-13 的提示词", mode="strict", ref="p@v1") assert "p@v1" in str(excinfo.value) assert excinfo.value.violations def test_warn_mode_reports_without_raising() -> None: violations = validate_cache_prefix("生成于 2026-08-13", mode="warn") assert [v.kind for v in violations] == ["iso-date"] def test_off_mode_skips_the_check() -> None: assert validate_cache_prefix("生成于 2026-08-13", mode="off") == [] def test_registry_rejects_a_bad_prompt_at_load_time(tmp_path: Path) -> None: bad = tmp_path / "bad" / "v1.txt" bad.parent.mkdir(parents=True) bad.write_text("[system]\n你好 {nickname}\n\n[user]\n{user_input}\n", encoding="utf-8") registry = PromptRegistry(tmp_path, cache_prefix_mode="strict") with pytest.raises(CachePrefixViolationError): registry.get("bad", "v1") def test_shipped_prompts_all_satisfy_the_discipline(repo_root: Path) -> None: """Regression guard: every prompt in the repo keeps a cacheable prefix.""" prompts_root = repo_root / "prompts" files = sorted(prompts_root.glob("*/*.txt")) assert files, "no prompt templates found" for path in files: template = parse_prompt_file( path.read_text(encoding="utf-8"), prompt_id=path.parent.name, version=path.stem ) assert find_volatile_tokens(template.system_prefix) == [], f"{path} has a volatile prefix" def test_prompt_file_without_system_section_is_rejected() -> None: with pytest.raises(PromptNotFoundError): parse_prompt_file("[user]\nhello\n", prompt_id="x", version="v1")