muse-agent-example/tests/集成/test_预算预留与结算.py
zizi 713cc45c63 重构(数据库): 迁移链压扁为单基线并精简测试至保留集
- 51 个增量迁移文件压扁为 V0001__基线.sql 完整快照(结构+种子+授权),
  等价门禁:旧链全量执行库与基线库 pg_dump 逐字节一致
- 剔除 pg_dump 固化的 public schema 超级用户归属断言(muse_maint 无权执行)
- 测试删减至保留集:备份往返 2 + 预算 2 + 租约 2 + 基线建库 1
- 租约/预算夹具改共享库,消除按例克隆建库
- 修复共享库三类既有污染:账本注入残留(系统管理)、失败触发器残留(评测)、
  建表残留(正式变更事务),发布包迁移文件名硬编码改动态核对
- 全量数据库验收 722 passed / 0 failed / 0 errors(main 基线为 24F+291E)
- 运行手册登记基线模式改表流程与账本校验和同步
2026-09-22 10:18:51 +08:00

142 lines
5.3 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 uuid
from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from pathlib import Path
import pytest
from muse.任务运行.接口 import (
任务服务,
任务请求,
步骤处理器,
步骤结果,
步骤计划,
)
from muse.任务运行.预算管理 import (
任务预算计划,
角色预算,
预算不足,
预算状态冲突,
预算管理,
额度策略,
)
from muse.共享.调用身份 import 内容用途, 用途
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
pytestmark = pytest.mark.数据库
@pytest.fixture
def 预算环境(数据库底座, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
with 数据库底座.共享库(tmp_path / "连接引用") as 工厂:
for 声明, 当前 in 工厂.items():
monkeypatch.setenv(f"MUSE_BUDGET_{声明.name}_URL", Path(当前.引用.位置).read_text())
yield 工厂
def 新任务(工厂: 数据库工厂):
登记 = 流程登记()
登记.登记处理器(步骤处理器("call", "1", lambda _: 步骤结果({}), "v1", "v1"))
登记.登记类型("budget-test", 必需保护=())
运行 = 任务服务(工厂, 登记)
编排 = 流程服务(运行, 登记)
编排.发布(流程定义("budget-test", "1", (步骤计划("call", "call", "1"),)))
请求 = 任务请求(
"budget-test",
uuid.uuid4().hex,
"author",
工厂.用途,
内容用途.生成,
{},
"policy-1",
"release-1",
{
"source_scope": {},
"schema_versions": {},
"authorization": "grant",
"budget": {"approved": True},
"stop_conditions": ["cancelled"],
},
)
身份 = 运行.创建任务(请求, "budget-test", "1")
领取 = 运行.领取步骤("worker", ["call"])
assert 领取 is not None and 领取.任务ID == 身份
return 身份, 领取
def 建预算(工厂: 数据库工厂, 任务ID: str, *, 窗口金额="24", 单次="6", 次数=3, 窗口次数=6000):
管理 = 预算管理(工厂, "quota")
管理.登记策略(额度策略("quota", "1", Decimal(窗口金额), 窗口次数))
管理.登记任务预算(
任务ID,
任务预算计划(
Decimal(单次) * 次数,
(角色预算("writer", 次数, 次数, Decimal(单次)),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
return 管理
@pytest.mark.case_id(
"NC-budget-concurrent-reserve",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="两个真实任务共用10额度账户",
when="两个独立连接同时预留6",
then=["只有一笔成功且余额为4"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
def test_并发预留共享窗口且不超额__a61001(预算环境) -> None:
from threading import Barrier
A, 领取A = 新任务(预算环境[用途.生产])
B, 领取B = 新任务(预算环境[用途.生产])
预算A = 建预算(预算环境[用途.生产], A, 窗口金额="10")
预算B = 建预算(预算环境[用途.生产], B, 窗口金额="10")
栅栏 = Barrier(2)
def 预留(输入):
管理, 领取 = 输入
栅栏.wait()
try:
return 管理.预留(领取, 领取.任务ID, "writer")
except 预算不足:
return None
with ThreadPoolExecutor(max_workers=2) as 线程:
结果 = list(线程.map(预留, [(预算A, 领取A), (预算B, 领取B)]))
assert sum(r is not None for r in 结果) == 1
assert 预算A.窗口余额()["在途预留"] == Decimal("6")
assert 预算A.窗口余额()["可用金额"] == Decimal("4")
@pytest.mark.case_id(
"NC-budget-idempotent-settlement",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="同调用身份和失败调用可信成本回执",
when="重复预留、外发申请与结算",
then=["只授权外发一次、只计费一次,冲突成本拒绝"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
def test_重复预留发送和结算不重复计费__a61002(预算环境) -> None:
身份, 领取 = 新任务(预算环境[用途.生产])
预算 = 建预算(预算环境[用途.生产], 身份)
初次 = 预算.预留(领取, "call-1", "writer")
assert 预算.预留(领取, "call-1", "writer") == 初次
assert 预算.标记已发送(领取, "call-1", 最长秒=30).允许外发
assert not 预算.标记已发送(领取, "call-1", 最长秒=30).允许外发
结果 = 预算.结算("call-1", Decimal("0.125"), 回执ID="failed-call-receipt")
assert 预算.结算("call-1", Decimal("0.125"), 回执ID="failed-call-receipt") == 结果
with pytest.raises(预算状态冲突):
预算.结算("call-1", Decimal("0.2"), 回执ID="changed")
assert 预算.窗口余额()["已知成本"] == Decimal("0.125")
assert 预算.窗口余额()["已占次数"] == 1