muse-agent-example/tests/单元/test_调用结算与治理.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

271 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""终态费用事实与一次外发边界;使用内存事务替身,不访问数据库或模型。"""
import asyncio
import hashlib
from contextlib import contextmanager
from copy import deepcopy
from decimal import Decimal
from types import SimpleNamespace
import pytest
import muse.任务运行.调用结算 as 结算模块
from muse.任务运行.接口 import 模型结果, 模型请求
from muse.任务运行.模型 import 状态冲突
from muse.任务运行.调用治理 import 受控发送
from muse.任务运行.调用结算 import 结算调用, 调用结算请求
from muse.共享.调用身份 import 用途
class 游标:
def __init__(self, row):
self.row = row
def fetchone(self):
return self.row
class 账本:
用途 = 用途.评测
def __init__(self):
self.task = {
"author_id": "author",
"state": "cancelled",
"last_sequence": 8,
"accounting_revision": 2,
}
self.call = {
"call_id": "call",
"task_id": "task",
"attempt_id": "attempt",
"account_id": "account",
"state": "unknown",
"actual_amount": None,
}
self.receipts = {}
self.statements = []
@contextmanager
def 连接(self):
yield self
@contextmanager
def transaction(self):
原 = deepcopy((self.task, self.call, self.receipts))
try:
yield self
except Exception:
self.task, self.call, self.receipts = 原
raise
class 存储替身:
def __init__(self, db, purpose):
self.db = db
def 任务行(self, task, **kwargs):
assert task == "task"
return self.db.task
def 查询(self, sql, args=()):
self.db.statements.append(sql)
if sql.startswith("SELECT request_hash"):
return 游标(self.db.receipts.get(args[1]))
if "SELECT * FROM {s}.muse_budget_reservation" in sql:
return 游标(self.db.call if args[1] == "call" else None)
if sql.startswith("SELECT accounting_revision"):
return 游标(self.db.task)
if sql.startswith("INSERT INTO {s}.muse_call_settlement"):
self.db.receipts[args[1]] = {"request_hash": args[3], "receipt": args[4].obj}
return 游标(None)
raise AssertionError(sql)
class 预算替身:
def __init__(self, db, account):
self.db = db
def 结算于(self, conn, call, amount, *, 回执ID):
if self.db.call["state"] != "unknown":
raise 状态冲突("不能改写已结费用")
self.db.call.update(state="settled", actual_amount=amount, receipt_id=回执ID)
self.db.task["accounting_revision"] += 1
def 请求(**changes):
statement = "作者核对账单2026-09-17第12行,实际费用0.15美元。"
return 调用结算请求(
**{
"command_id": "settle-1",
"expected_revision": 2,
"amount": "0.15",
"currency": "USD",
"source_reference": "账单第12行",
"source_statement": statement,
"source_hash": hashlib.sha256(statement.encode()).hexdigest(),
**changes,
}
)
@pytest.mark.case_id("NC-O02-SETTLE-TERMINAL")
def test_取消任务结费幂等且不修改执行状态(monkeypatch):
db = 账本()
monkeypatch.setattr(结算模块, "任务存储", 存储替身)
monkeypatch.setattr(结算模块, "预算管理", 预算替身)
receipt = 结算调用(db, "task", "call", "author", 请求())
assert receipt["author_id"] == "author"
assert receipt["accounting_revision"] == 3 and receipt["execution_final_sequence"] == 8
assert db.task["state"] == "cancelled" and db.task["last_sequence"] == 8
assert db.call["actual_amount"] == Decimal("0.15")
assert 结算调用(db, "task", "call", "author", 请求()) == receipt
with pytest.raises(状态冲突, match="不能改变"):
结算调用(db, "task", "call", "author", 请求(amount="0.16"))
assert not any(
"muse_attempt" in s or "muse_step" in s or "muse_task_event" in s for s in db.statements
)
@pytest.mark.case_id("NC-O02-SETTLE-SCOPE")
def test_错作者错调用和旧账目版本不能结算(monkeypatch):
db = 账本()
monkeypatch.setattr(结算模块, "任务存储", 存储替身)
monkeypatch.setattr(结算模块, "预算管理", 预算替身)
for call, author, req in [
("call", "other", 请求()),
("other", "author", 请求()),
("call", "author", 请求(expected_revision=1)),
]:
with pytest.raises(状态冲突):
结算调用(db, "task", call, author, req)
assert db.call["state"] == "unknown" and not db.receipts
@pytest.mark.case_id("NC-O02-SETTLE-EVIDENCE")
def test_零费用缺明确确认和伪造来源哈希均拒绝():
with pytest.raises(状态冲突, match="零费用"):
请求(amount="0")
with pytest.raises(状态冲突, match="来源哈希"):
请求(source_hash="0" * 64)
with pytest.raises(状态冲突, match="提供方认证"):
请求(source_kind="provider_receipt")
assert 请求(amount="0", zero_cost_confirmed=True).amount == "0.000000"
class 发送预算:
def __init__(self, 允许=True):
self.允许 = 允许
self.events = []
def 预留(self, *args, **kwargs):
self.events.append("reserved")
def 标记已发送(self, *args, **kwargs):
kwargs["发送复检"](None)
self.events.append("sent")
return SimpleNamespace(允许外发=self.允许)
def 取消预留(self, *args):
self.events.append("release")
def 结算(self, call, amount, *, 回执ID):
self.events.append(("settle", amount, 回执ID))
return SimpleNamespace(状态="unknown" if amount is None else "settled")
class 宿主:
def __init__(self, failure=None):
self.calls = 0
self.failure = failure
def 准备(self, request):
return self
async def 调用(self):
self.calls += 1
if self.failure:
raise self.failure
return 模型结果("completed", '{"ok": true}', "model", None, "response")
def _模型请求():
return 模型请求("call", "provider", "model", "probe", "probe", {}, 64, 60, thinking="off")
@pytest.mark.case_id("NC-O02-GOVERNED-CANCEL")
def test_发送取消不推定免费():
budget, host = 发送预算(), 宿主(asyncio.CancelledError())
pricing = SimpleNamespace(金额=lambda _: Decimal("0.1"))
with pytest.raises(asyncio.CancelledError):
asyncio.run(
受控发送(budget, None, _模型请求(), "extractor", host, pricing, 发送复检=lambda _: None)
)
assert host.calls == 1 and budget.events[-1] == ("settle", None, "call")
@pytest.mark.case_id("NC-O02-GOVERNED-ONCE")
def test_已发送调用不会再次外发():
budget, host = 发送预算(False), 宿主()
with pytest.raises(状态冲突):
asyncio.run(
受控发送(budget, None, _模型请求(), "extractor", host, None, 发送复检=lambda _: None)
)
assert host.calls == 0
@pytest.mark.case_id("NC-O02-GOVERNED-PRICING")
def test_计价失败仍登记未知费用():
budget, host = 发送预算(), 宿主()
def broken(_):
raise ValueError("rate missing")
result, settlement = asyncio.run(
受控发送(
budget,
None,
_模型请求(),
"extractor",
host,
SimpleNamespace(金额=broken),
发送复检=lambda _: None,
)
)
assert result.响应ID == "response" and settlement.状态 == "unknown"
assert budget.events[-1] == ("settle", None, "response")
@pytest.mark.case_id("NC-O02-HTTP-CONTRACT")
def test_HTTP结算验证材料哈希并返回认证作者(monkeypatch):
from dataclasses import asdict
from fastapi import FastAPI
from fastapi.testclient import TestClient
from muse.任务运行.接口 import 任务服务
from muse.接入.http.作者会话 import 要求作者
from muse.接入.http.路由.任务运行 import 路由
db = 账本()
monkeypatch.setattr(结算模块, "任务存储", 存储替身)
monkeypatch.setattr(结算模块, "预算管理", 预算替身)
service = 任务服务(db, None)
app = FastAPI()
app.state.装配 = SimpleNamespace(任务运行=service)
app.dependency_overrides[要求作者] = lambda: "author"
app.include_router(路由)
with TestClient(app) as client:
body = asdict(请求())
response = client.post("/api/v1/tasks/task/calls/call/settlements", json=body)
assert response.status_code == 200, response.text
assert response.json()["author_id"] == "author"
assert response.json()["amount"] == "0.150000"
assert response.json()["state"] == "cancelled"
assert (
client.post(
"/api/v1/tasks/task/calls/call/settlements", json={**body, "author_id": "other"}
).status_code
== 422
)