feat(server): add workbench API, DSH planner worker and RAG pipeline
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""Hermes 移植模块单元测试:轮次预算 / 抖动退避 / 工具停滞护栏。"""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.hermes_core import (IterationBudget, StallGuard, jittered_backoff,
|
||||
parse_retry_after_seconds, retry_sleep,
|
||||
tool_call_fingerprint)
|
||||
|
||||
|
||||
def test_budget_consumes_and_exhausts():
|
||||
budget = IterationBudget(3)
|
||||
assert budget.consume() is True
|
||||
assert budget.consume() is True
|
||||
assert budget.consume() is True
|
||||
assert budget.consume() is False
|
||||
assert budget.used == 3
|
||||
assert budget.remaining == 0
|
||||
|
||||
|
||||
def test_budget_refund_returns_capacity():
|
||||
budget = IterationBudget(1)
|
||||
assert budget.consume() is True
|
||||
assert budget.consume() is False
|
||||
budget.refund()
|
||||
assert budget.consume() is True
|
||||
|
||||
|
||||
def test_budget_thread_safe_under_concurrency():
|
||||
budget = IterationBudget(10)
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
results = list(pool.map(lambda _: budget.consume(), range(50)))
|
||||
assert sum(1 for ok in results if ok) == 10
|
||||
assert budget.used == 10
|
||||
|
||||
|
||||
def test_retry_after_numeric_and_headers():
|
||||
assert parse_retry_after_seconds("2.5") == 2.5
|
||||
assert parse_retry_after_seconds({"Retry-After": "3"}) == 3.0
|
||||
assert parse_retry_after_seconds({"retry-after": 1}) == 1.0
|
||||
assert parse_retry_after_seconds(None) is None
|
||||
assert parse_retry_after_seconds("not-a-number") is None
|
||||
|
||||
|
||||
def test_retry_after_http_date():
|
||||
from datetime import datetime, timedelta, timezone
|
||||
future = datetime.now(timezone.utc) + timedelta(seconds=7)
|
||||
header = future.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||
value = parse_retry_after_seconds(header)
|
||||
assert value is not None and 5 <= value <= 8
|
||||
|
||||
|
||||
def test_jittered_backoff_stays_in_range():
|
||||
for attempt in range(8):
|
||||
for _ in range(30):
|
||||
delay = jittered_backoff(attempt)
|
||||
assert 0 <= delay <= min(24.0, 1.6 * (2 ** attempt)) + 1e-9
|
||||
|
||||
|
||||
def test_retry_sleep_prefers_retry_after():
|
||||
assert retry_sleep(3, retry_after="9") == 9.0
|
||||
assert 0 <= retry_sleep(2, retry_after=None) <= 1.6 * 4
|
||||
|
||||
|
||||
def test_fingerprint_distinguishes_arguments():
|
||||
a = tool_call_fingerprint("knowledge_search", {"q": "催情"})
|
||||
b = tool_call_fingerprint("knowledge_search", {"q": "催情"})
|
||||
c = tool_call_fingerprint("knowledge_search", {"q": "缩阴"})
|
||||
d = tool_call_fingerprint("web_search", {"q": "催情"})
|
||||
assert a == b and a != c and a != d
|
||||
|
||||
|
||||
def test_stall_guard_notice_then_halt():
|
||||
guard = StallGuard()
|
||||
fp = tool_call_fingerprint("read_agent_memory", {})
|
||||
assert guard.observe(fp) == "ok"
|
||||
assert guard.observe(fp) == "ok"
|
||||
assert guard.observe(fp) == "notice"
|
||||
assert guard.observe(fp) == "notice"
|
||||
assert guard.observe(fp) == "halt"
|
||||
|
||||
|
||||
def test_stall_guard_different_calls_do_not_accumulate():
|
||||
guard = StallGuard()
|
||||
assert guard.observe(tool_call_fingerprint("knowledge_search", {"q": "1"})) == "ok"
|
||||
assert guard.observe(tool_call_fingerprint("knowledge_search", {"q": "2"})) == "ok"
|
||||
assert guard.observe(tool_call_fingerprint("knowledge_search", {"q": "1"})) == "ok"
|
||||
assert guard.observe(tool_call_fingerprint("knowledge_search", {"q": "2"})) == "ok"
|
||||
assert all(count < 3 for count in guard._counts.values())
|
||||
Reference in New Issue
Block a user