2026-02-05 15:57:52 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-02-10 17:53:04 +08:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
2026-02-16 11:00:48 +08:00
|
|
|
|
from src.core.api_format.conversion.thinking_cache import ThinkingSignatureCache
|
2026-02-06 16:37:06 +08:00
|
|
|
|
from src.services.provider.adapters.antigravity.constants import DUMMY_THOUGHT_SIGNATURE
|
|
|
|
|
|
|
|
|
|
|
|
# 测试用签名(需 >= MIN_SIGNATURE_LENGTH=50)
|
|
|
|
|
|
_SIG_A = "a" * 60
|
|
|
|
|
|
_SIG_B = "b" * 60
|
|
|
|
|
|
_SIG_C = "c" * 60
|
|
|
|
|
|
_SIG_D = "d" * 60
|
|
|
|
|
|
_SIG_E = "e" * 60
|
2026-02-05 15:57:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_or_dummy_returns_dummy_for_gemini_models() -> None:
|
2026-02-06 16:37:06 +08:00
|
|
|
|
cache = ThinkingSignatureCache()
|
2026-02-05 15:57:52 +08:00
|
|
|
|
assert cache.get_or_dummy("gemini-3-pro", "thinking...") == DUMMY_THOUGHT_SIGNATURE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_or_dummy_returns_none_for_non_gemini_models() -> None:
|
2026-02-06 16:37:06 +08:00
|
|
|
|
cache = ThinkingSignatureCache()
|
2026-02-05 15:57:52 +08:00
|
|
|
|
assert cache.get_or_dummy("claude-sonnet", "thinking...") is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cached_signature_preferred() -> None:
|
2026-02-06 16:37:06 +08:00
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache("gemini-3-pro", "thinking-text", _SIG_A)
|
|
|
|
|
|
assert cache.get_or_dummy("gemini-3-pro", "thinking-text") == _SIG_A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_short_signature_ignored() -> None:
|
|
|
|
|
|
"""短于 MIN_SIGNATURE_LENGTH 的签名不会被缓存。"""
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache("gemini-3-pro", "text", "short")
|
|
|
|
|
|
# 未命中缓存,回退到 DUMMY
|
|
|
|
|
|
assert cache.get_or_dummy("gemini-3-pro", "text") == DUMMY_THOUGHT_SIGNATURE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ===== Layer 1: Tool Signatures =====
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_tool_signature_cache() -> None:
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache_tool_signature("toolu_123", _SIG_A)
|
|
|
|
|
|
assert cache.get_tool_signature("toolu_123") == _SIG_A
|
|
|
|
|
|
assert cache.get_tool_signature("toolu_999") is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_tool_signature_short_ignored() -> None:
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache_tool_signature("toolu_123", "short")
|
|
|
|
|
|
assert cache.get_tool_signature("toolu_123") is None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-10 17:53:04 +08:00
|
|
|
|
def test_tool_signature_cache_enforces_limit(monkeypatch: Any) -> None:
|
2026-02-16 11:00:48 +08:00
|
|
|
|
import src.core.api_format.conversion.thinking_cache as tc_mod
|
2026-02-10 17:53:04 +08:00
|
|
|
|
|
|
|
|
|
|
# Use a small limit to make eviction deterministic in tests.
|
2026-02-16 11:00:48 +08:00
|
|
|
|
monkeypatch.setattr(tc_mod, "_TOOL_CACHE_LIMIT", 3)
|
|
|
|
|
|
cache = tc_mod.ThinkingSignatureCache()
|
2026-02-10 17:53:04 +08:00
|
|
|
|
cache.cache_tool_signature("toolu_1", _SIG_A)
|
|
|
|
|
|
cache.cache_tool_signature("toolu_2", _SIG_B)
|
|
|
|
|
|
cache.cache_tool_signature("toolu_3", _SIG_C)
|
|
|
|
|
|
cache.cache_tool_signature("toolu_4", _SIG_D)
|
|
|
|
|
|
|
|
|
|
|
|
# Oldest entry should be evicted once the limit is exceeded.
|
|
|
|
|
|
assert cache.get_tool_signature("toolu_1") is None
|
|
|
|
|
|
assert cache.get_tool_signature("toolu_4") == _SIG_D
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
# ===== Layer 2: Thinking Families =====
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_thinking_family_cache() -> None:
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache_thinking_family(_SIG_A, "claude-3-5-sonnet")
|
|
|
|
|
|
assert cache.get_signature_family(_SIG_A) == "claude-3-5-sonnet"
|
|
|
|
|
|
assert cache.get_signature_family(_SIG_B) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ===== Layer 3: Session Signatures =====
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_signature_basic() -> None:
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
assert cache.get_session_signature("sid-test") is None
|
|
|
|
|
|
|
|
|
|
|
|
cache.cache_session_signature("sid-test", _SIG_A, 5)
|
|
|
|
|
|
assert cache.get_session_signature("sid-test") == _SIG_A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_signature_longer_replaces_same_count() -> None:
|
|
|
|
|
|
"""同一 message_count 下,更长的签名替换更短的。"""
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
sig_short = "x" * 60
|
|
|
|
|
|
sig_long = "y" * 80
|
|
|
|
|
|
|
|
|
|
|
|
cache.cache_session_signature("sid-1", sig_short, 5)
|
|
|
|
|
|
cache.cache_session_signature("sid-1", sig_long, 5)
|
|
|
|
|
|
assert cache.get_session_signature("sid-1") == sig_long
|
|
|
|
|
|
|
|
|
|
|
|
# 更短的不会替换
|
|
|
|
|
|
cache.cache_session_signature("sid-1", sig_short, 5)
|
|
|
|
|
|
assert cache.get_session_signature("sid-1") == sig_long
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_signature_rewind_detection() -> None:
|
|
|
|
|
|
"""Rewind: message_count 减少时强制更新签名。"""
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache_session_signature("sid-1", _SIG_A, 10)
|
|
|
|
|
|
assert cache.get_session_signature("sid-1") == _SIG_A
|
|
|
|
|
|
|
|
|
|
|
|
# message_count 3 < 10 → rewind detected, force update
|
|
|
|
|
|
cache.cache_session_signature("sid-1", _SIG_B, 3)
|
|
|
|
|
|
assert cache.get_session_signature("sid-1") == _SIG_B
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_signature_short_ignored() -> None:
|
|
|
|
|
|
"""短签名即使 rewind 也不会被缓存。"""
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache_session_signature("sid-1", _SIG_A, 5)
|
|
|
|
|
|
cache.cache_session_signature("sid-1", "short", 1)
|
|
|
|
|
|
assert cache.get_session_signature("sid-1") == _SIG_A
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_isolation() -> None:
|
|
|
|
|
|
"""不同 session 之间互相隔离。"""
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache_session_signature("sid-1", _SIG_A, 1)
|
|
|
|
|
|
assert cache.get_session_signature("sid-1") == _SIG_A
|
|
|
|
|
|
assert cache.get_session_signature("sid-2") is None
|
|
|
|
|
|
|
2026-02-05 15:57:52 +08:00
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
# ===== Clear =====
|
2026-02-05 15:57:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
def test_clear_all_layers() -> None:
|
|
|
|
|
|
cache = ThinkingSignatureCache()
|
|
|
|
|
|
cache.cache_tool_signature("tool-1", _SIG_A)
|
|
|
|
|
|
cache.cache_thinking_family(_SIG_A, "claude")
|
|
|
|
|
|
cache.cache_session_signature("sid-1", _SIG_B, 1)
|
|
|
|
|
|
cache.cache("gemini-3-pro", "text", _SIG_C)
|
2026-02-05 15:57:52 +08:00
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
cache.clear()
|
2026-02-05 15:57:52 +08:00
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
assert cache.get_tool_signature("tool-1") is None
|
|
|
|
|
|
assert cache.get_signature_family(_SIG_A) is None
|
|
|
|
|
|
assert cache.get_session_signature("sid-1") is None
|
|
|
|
|
|
# Legacy layer: 回退到 DUMMY
|
|
|
|
|
|
assert cache.get_or_dummy("gemini-3-pro", "text") == DUMMY_THOUGHT_SIGNATURE
|