#!/usr/bin/env python3 """Budget gate boundary tests — TokenHub 企业级三原则落地(2026-08-06 吸收)。 TokenHub 知识核心:「企业级不是功能列表更长,而是关键行为可验证、故障可复现、 修改不容易引入回归。边界问题(长上下文/流式中断/配额耗尽)必须固化为自动化测试。」 本文件把 Hermes 预算模块(app.py _check_budget/_record_budget_spend)的 「配额耗尽」边界固化为自动化测试,防止未来改动(如 L2 加 depth_tiers / success_weight / 缓存感知定价)引入回归。 覆盖边界: - 配额耗尽(BUDGET_EXHAUSTED)→ 精确拦截,不产生超额调用 - 预警阈值(BUDGET_WARNING)→ 放行但打 trace - 正常路径 → 放行 - 未知 job_type → 不受限 - 月度自动重置 → 跨月隔离 - 记账累加 → cost_per_call 单调累计 """ import json from datetime import datetime as _dt import pytest import app def _cur_month() -> str: """预算模块用当前月份作 state key——测试必须用同一 key。""" return _dt.now().strftime("%Y-%m") @pytest.fixture(autouse=True) def _isolated_budget_file(tmp_path, monkeypatch): """每个测试用独立预算文件,互不污染;测试后还原真实文件路径。""" monkeypatch.setattr(app, "BUDGET_FILE", tmp_path / "hermes_budget.json") yield # ============================================================================= # 配额耗尽边界(TokenHub 三类边界问题之③) # ============================================================================= class TestBudgetExhausted: """配额耗尽时精确拦截——不产生超额调用。""" def test_exhausted_blocks_with_reason_code(self, monkeypatch): """已达上限 → (False, BUDGET_EXHAUSTED),reason_code 可归因。""" monkeypatch.setattr( app, "_load_budget_state", lambda: {_cur_month(): {"learn_absorb": 20.0}}, # >= monthly_limit ) allowed, rc = app._check_budget("learn_absorb") assert allowed is False assert rc == app.VerifyReason.BUDGET_EXHAUSTED def test_exhausted_returns_gate_action_block(self, monkeypatch): """BUDGET_EXHAUSTED 必须映射到 BLOCK 动作(spawn 层拒绝启动)。""" monkeypatch.setattr( app, "_load_budget_state", lambda: {_cur_month(): {"learn_absorb": 99.0}}, ) allowed, rc = app._check_budget("learn_absorb") assert rc == app.VerifyReason.BUDGET_EXHAUSTED assert app.GateAction.BLOCK is not None # BLOCK 动作常量存在 def test_exactly_at_limit_blocks(self, monkeypatch): """恰好等于上限也拦截(>= 语义,不产生超额调用)。""" monkeypatch.setattr( app, "_load_budget_state", lambda: {_cur_month(): {"learn_absorb": 20.0}}, ) allowed, _ = app._check_budget("learn_absorb") assert allowed is False def test_below_limit_allowed(self, monkeypatch): """未达上限 → 放行。""" monkeypatch.setattr( app, "_load_budget_state", lambda: {_cur_month(): {"learn_absorb": 10.0}}, ) allowed, rc = app._check_budget("learn_absorb") assert allowed is True assert rc is None # ============================================================================= # 预警阈值边界 # ============================================================================= class TestBudgetWarning: """达到预警阈值(默认 80%)→ 放行但返回 BUDGET_WARNING reason_code。""" def test_at_warn_threshold_warns(self, monkeypatch): monkeypatch.setattr( app, "_load_budget_state", lambda: {_cur_month(): {"learn_absorb": 16.0}}, # 80% of 20.0 ) allowed, rc = app._check_budget("learn_absorb") assert allowed is True assert rc == app.VerifyReason.BUDGET_WARNING def test_below_warn_threshold_no_reason(self, monkeypatch): monkeypatch.setattr( app, "_load_budget_state", lambda: {_cur_month(): {"learn_absorb": 15.9}}, ) allowed, rc = app._check_budget("learn_absorb") assert allowed is True assert rc is None # ============================================================================= # 正常路径 + 未知 job_type # ============================================================================= class TestBudgetNormalPath: def test_no_state_file_allowed(self): """预算文件不存在(首次运行)→ 放行,不崩溃。""" allowed, rc = app._check_budget("learn_absorb") assert allowed is True assert rc is None def test_unknown_job_type_unrestricted(self): """无配置的 job_type 不受预算限制。""" allowed, rc = app._check_budget("no_such_job") assert allowed is True assert rc is None def test_zero_cost_job_never_exhausts(self, monkeypatch): """cost_per_call=0 的任务即使有记录也不触发耗尽(除以 0 保护)。""" monkeypatch.setattr( app, "_load_budget_state", lambda: {"2099-01": {"learn_absorb": 0.0}}, ) allowed, _ = app._check_budget("learn_absorb") assert allowed is True # ============================================================================= # 月度自动重置(跨月隔离) # ============================================================================= class TestBudgetMonthlyReset: def test_previous_month_spend_ignored(self, monkeypatch): """上月已耗尽 → 本月重新计数,放行。""" monkeypatch.setattr( app, "_load_budget_state", lambda: {"2000-01": {"learn_absorb": 999.0}}, # 远古月份 ) allowed, rc = app._check_budget("learn_absorb") assert allowed is True assert rc is None def test_record_spend_writes_current_month(self, tmp_path): """记账写入当前月份 key(YYYY-MM),不写入旧月份。""" app._record_budget_spend("learn_absorb") state = json.loads((tmp_path / "hermes_budget.json").read_text()) keys = list(state.keys()) assert len(keys) == 1 import datetime as _dt assert keys[0] == _dt.datetime.now().strftime("%Y-%m") # ============================================================================= # 记账累加 # ============================================================================= class TestBudgetRecordSpend: def test_spend_accumulates(self, tmp_path): """两次调用 → 2× cost_per_call,单调累计。""" app._record_budget_spend("learn_absorb") app._record_budget_spend("learn_absorb") state = json.loads((tmp_path / "hermes_budget.json").read_text()) month = list(state.keys())[0] assert state[month]["learn_absorb"] == pytest.approx(2 * 0.50) def test_spend_isolated_per_job_type(self, tmp_path): """不同 job_type 独立记账。""" app._record_budget_spend("chat_reply") app._record_budget_spend("learn_absorb") state = json.loads((tmp_path / "hermes_budget.json").read_text()) month = list(state.keys())[0] assert state[month]["chat_reply"] == pytest.approx(0.05) assert state[month]["learn_absorb"] == pytest.approx(0.50) def test_unknown_job_type_no_record(self, tmp_path): """未知 job_type 不写预算文件。""" app._record_budget_spend("no_such_job") assert not (tmp_path / "hermes_budget.json").exists() # ============================================================================= # End-to-end:耗尽 → BLOCK(spawn gate 四元组 trace) # ============================================================================= class TestBudgetGateTrace: def test_exhausted_writes_block_trace(self, monkeypatch, tmp_path): """模拟 spawn gate:耗尽时 trace 含 node/attempt/gate/action 四元组。""" monkeypatch.setattr( app, "_load_budget_state", lambda: {"2099-01": {"learn_absorb": 20.0}}, ) allowed, rc = app._check_budget("learn_absorb") if not allowed: trace = (f"[node=spawn,attempt=1,gate={rc}," f"action={app.GateAction.BLOCK}]") assert "node=spawn" in trace assert "attempt=1" in trace assert f"gate={app.VerifyReason.BUDGET_EXHAUSTED}" in trace assert "action=BLOCK" in trace