aps-agent/tests/golden/test_confirm_batch.py

195 lines
8.4 KiB
Python
Raw Permalink Normal View History

# 批量审批端点黄金测试(plan.md §3.3 / §6.10.2)
# ============================================================
# 覆盖:批量批准多条 P2、批量含 P3 首次批准(secondConfirmRequired)、
# 批量驳回、单条失败隔离、note 落库、仅当前用户作用域。
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from server.agent_core import harness
from server.auth.context import bind_identity, reset_identity
class _AuditStore:
def __init__(self, world_key: str = "personal-1001", tenant_uuid: str = "tenant-batch-test"):
self.world_key = world_key
self.tenant_uuid = tenant_uuid
self.data: dict = {"auditEvents": [], "scheduleVersions": [], "flexScheduleVersions": []}
def next_id(self, _kind: str) -> int:
return len(self.data["auditEvents"]) + 1
def save(self) -> None:
pass
class _CheckpointStore:
def __init__(self) -> None:
self.pairs: list[dict] = []
def create(self, *_args, **_kwargs) -> dict:
pair = {"pairId": f"pair-{len(self.pairs) + 1}"}
self.pairs.append(pair)
return pair
def get(self, pair_id: str) -> dict | None:
return next((p for p in self.pairs if p["pairId"] == pair_id), None)
class _ProjectStore:
def active_world_key(self) -> str:
return "default"
def require_active_write(self) -> None:
pass
@pytest.fixture(autouse=True)
def _clear_pending():
harness._approval_store.clear()
yield
harness._approval_store.clear()
@pytest.fixture
def client_factory(monkeypatch):
"""返回 (client, store, provider);测试认证替换 JMS,登录身份用于 stage/审批。"""
import server.gateway.app as gateway_module
import server.state.projects as projects_module
from tests.auth_provider import install_test_auth
provider = install_test_auth(monkeypatch, "tenant-batch-test")
def factory():
store = _AuditStore()
monkeypatch.setattr(gateway_module, "get_store", lambda: store)
monkeypatch.setattr(gateway_module, "get_checkpoints", lambda: _CheckpointStore())
monkeypatch.setattr(projects_module, "get_project_store", lambda: _ProjectStore())
client = TestClient(gateway_module.create_app())
login = client.post("/api/auth/login", json={"username": "planner", "password": "test"})
assert login.status_code == 200, login.text
return client, store, provider
return factory
def _stage(provider, action: str) -> str:
"""用与登录一致的测试身份出卡。"""
identity = provider._identity("planner")
params = {"versionId": 1, "track": "flex"} if action == "mes.dispatch" else {"versionId": 1}
token = bind_identity(identity)
try:
block = harness.stage_confirmation(
"session-batch", action, params, title="批量测试", summary_lines=["批量审批"],
evidence_refs=["schedule-version:1"] if action == "mes.dispatch" else None,
)
finally:
reset_identity(token)
return str(block.props["confirmId"])
def _as(identity, callback):
token = bind_identity(identity)
try:
return callback()
finally:
reset_identity(token)
def test_confirm_batch_approves_multiple_p2(client_factory):
"""批量批准多条 P2:全部执行成功,审计携带 beforeSnapshot/evidenceRefs。"""
client, store, provider = client_factory()
store.data["scheduleVersions"].append({"id": 1, "versionNo": "V1", "status": "DRAFT",
"poCount": 0, "woCount": 0, "conflictCount": 0,
"totalTardiness": 0})
store.data["productionOrders"] = []
ids = [_stage(provider, "schedule.publish"), _stage(provider, "schedule.publish")]
resp = client.post("/api/actions/confirm-batch",
json={"sessionId": "batch", "confirmIds": ids, "approve": True,
"note": "批量同意"})
assert resp.status_code == 200, resp.text
data = resp.json()
assert len(data["results"]) == 2
assert all(r["ok"] for r in data["results"])
assert all(r["secondConfirmRequired"] is False for r in data["results"])
events = [e for e in store.data["auditEvents"] if e["action"] == "schedule.publish"]
assert len(events) == 2
assert all(e["beforeSnapshot"] is not None for e in events)
assert all("schedule-version:1" in (e.get("evidenceRefs") or []) for e in events)
identity = provider._identity("planner")
history = _as(identity, harness.list_approval_history)
assert len(history) >= 2
assert all(h["note"] == "批量同意" and h["status"] == "APPROVED" for h in history[-2:])
def test_confirm_batch_p3_first_approval_marks_second_required(client_factory):
"""批量含 P3:首次批准标记 secondConfirmRequired,不视为失败。"""
client, store, provider = client_factory()
ids = [_stage(provider, "mes.dispatch"), _stage(provider, "mes.dispatch")]
resp = client.post("/api/actions/confirm-batch",
json={"sessionId": "batch", "confirmIds": ids, "approve": True})
assert resp.status_code == 200, resp.text
results = resp.json()["results"]
assert len(results) == 2
assert all(r["secondConfirmRequired"] is True for r in results)
assert all(r["ok"] for r in results)
def test_confirm_batch_rejects_all(client_factory):
"""批量驳回:全部 DENIED 且无世界写入。"""
client, store, provider = client_factory()
store.data["scheduleVersions"].append({"id": 1, "versionNo": "V1", "status": "DRAFT",
"poCount": 0, "woCount": 0, "conflictCount": 0,
"totalTardiness": 0})
store.data["productionOrders"] = []
ids = [_stage(provider, "schedule.publish"), _stage(provider, "schedule.publish")]
resp = client.post("/api/actions/confirm-batch",
json={"sessionId": "batch", "confirmIds": ids, "approve": False,
"note": "数据不齐"})
assert resp.status_code == 200, resp.text
results = resp.json()["results"]
assert len(results) == 2 and all(r["ok"] for r in results)
assert store.data["scheduleVersions"][0]["status"] == "DRAFT" # 未发布
identity = provider._identity("planner")
history = _as(identity, harness.list_approval_history)
assert len(history) >= 2
assert all(h["note"] == "数据不齐" and h["status"] == "REJECTED" for h in history[-2:])
def test_single_confirm_api_carries_note(client_factory):
"""单条 confirm API 透传 note:审批历史记录意见。"""
client, store, provider = client_factory()
store.data["scheduleVersions"].append({"id": 1, "versionNo": "V1", "status": "DRAFT",
"poCount": 0, "woCount": 0, "conflictCount": 0,
"totalTardiness": 0})
store.data["productionOrders"] = []
cid = _stage(provider, "schedule.publish")
resp = client.post("/api/actions/confirm",
json={"sessionId": "single", "confirmId": cid, "approve": True,
"note": "单条同意"})
assert resp.status_code == 200, resp.text
assert resp.json()["secondConfirmRequired"] is False
identity = provider._identity("planner")
history = _as(identity, harness.list_approval_history)
assert history[-1]["note"] == "单条同意"
assert history[-1]["status"] == "APPROVED"
def test_confirm_batch_isolates_failures(client_factory):
"""单条失败隔离:过期令牌失败不阻断其余成功。"""
client, store, provider = client_factory()
store.data["scheduleVersions"].append({"id": 1, "versionNo": "V1", "status": "DRAFT",
"poCount": 0, "woCount": 0, "conflictCount": 0,
"totalTardiness": 0})
store.data["productionOrders"] = []
good = _stage(provider, "schedule.publish")
bad = "nonexistent-confirm"
resp = client.post("/api/actions/confirm-batch",
json={"sessionId": "batch", "confirmIds": [good, bad], "approve": True})
assert resp.status_code == 200, resp.text
results = {r["confirmId"]: r for r in resp.json()["results"]}
assert results[good]["ok"] is True
assert results[bad]["ok"] is False
assert "不存在" in results[bad]["message"] or "失效" in results[bad]["message"]