Covers routing, failover, the circuit breaker, structured-output retries, the cache-prefix validator, cost computation including cached-token pricing, the daily budget breaker, the quota protocol, the task state machine and log redaction. HTTP is mocked -- no unit test spends money. The integration test runs the real slice against PostgreSQL and Redis with a faked provider.
90 lines
3.4 KiB
Python
Executable File
90 lines
3.4 KiB
Python
Executable File
"""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")
|