mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 20:51:59 +00:00
194 lines
11 KiB
Python
194 lines
11 KiB
Python
"""能力字段语义的审阅(D-i):写进来的规矩、待审阅的队列、批量审阅、权限。
|
|
|
|
- 字段语义沿用数据目录「项」:人工新填、改过的记为人工填写、已确认;值没动的保持原状态;客户端冒充不了有确证的来源;
|
|
明确标成推断(模型起草、命名推断)的新项按推断收下;取值不合规的报错用「返回字段」的说法;
|
|
- 队列只列推断的项,按用到它的运行次数、是不是写入能力、项的风险、来源排;来源删掉了的能力不列;
|
|
- 批量审阅逐个能力按 version 乐观锁、一次写入、version 加一、写审计;冲突、位置不对、能力不存在只记在那几条上;
|
|
- 搭建者能看队列,审阅只给管理员。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
from sqlalchemy import delete, select
|
|
|
|
from app.data import capability_review as review
|
|
from app.data.capability_review import BatchItem, FieldsInvalid, normalize_fields
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import AuditEvent, Capability, Run
|
|
from tests.fakes import auth_env
|
|
|
|
|
|
# ---------------------------------------------------------------- 写进来
|
|
|
|
|
|
def test_human_edits_are_confirmed_and_shorthand_is_accepted():
|
|
out = normalize_fields({}, {"/amount": {"label": "金额", "unit": {"value": "元"}}}, at="t")
|
|
assert out["/amount"]["label"] == {"value": "金额", "source": "human", "status": "confirmed", "updated_at": "t"}
|
|
assert out["/amount"]["unit"]["status"] == "confirmed"
|
|
|
|
|
|
def test_unchanged_items_keep_their_state_and_sources_cannot_be_faked():
|
|
existing = {"/amount": {"label": {"value": "金额", "source": "llm", "status": "proposed", "updated_at": "a"}}}
|
|
kept = normalize_fields(existing, existing, at="t")
|
|
assert kept == existing, "值没动、状态没改:原样留着"
|
|
faked = normalize_fields({}, {"/id": {"label": {"value": "单号", "source": "fk", "status": "verified"}}}, at="t")
|
|
assert faked["/id"]["label"]["source"] == "human" and faked["/id"]["label"]["status"] == "confirmed", \
|
|
"客户端冒充不了有确证的来源"
|
|
proposed = normalize_fields({}, {"/id": {"label": {"value": "单号", "source": "llm", "status": "proposed"}}},
|
|
at="t")
|
|
assert proposed["/id"]["label"] == {"value": "单号", "source": "llm", "status": "proposed", "updated_at": "t"}
|
|
confirmed = normalize_fields(existing, {"/amount": {"label": {**existing["/amount"]["label"],
|
|
"status": "confirmed"}}}, at="t")
|
|
assert confirmed["/amount"]["label"]["status"] == "confirmed", "等同单项审阅"
|
|
assert normalize_fields(existing, {}, at="t") == {}, "提交里没有的项删掉"
|
|
|
|
|
|
@pytest.mark.parametrize("submitted, said", [
|
|
({"amount": {"label": "金额"}}, "以 / 开头"),
|
|
({"/amount": "金额"}, "按项分开的对象"),
|
|
({"/amount": {"measure": "很多"}}, "返回字段 /amount 的度量类型"),
|
|
({"/amount": {"colour": "红"}}, "返回字段 /amount 含有不认识的字段「colour」"),
|
|
({"/avg": {"meaning": "客单价=金额/人数"}}, "返回字段 /avg 的含义看起来是计算公式"),
|
|
([], "按 JSON 指针分项"),
|
|
])
|
|
def test_bad_fields_are_refused_in_capability_words(submitted, said):
|
|
with pytest.raises(FieldsInvalid) as e:
|
|
normalize_fields({}, submitted)
|
|
assert said in str(e.value) and "列 " not in str(e.value) and "数据目录" not in str(e.value), str(e.value)
|
|
|
|
|
|
# ---------------------------------------------------------------- 队列与批量审阅
|
|
|
|
|
|
def _proposed(value, source="llm"):
|
|
return {"value": value, "source": source, "status": "proposed"}
|
|
|
|
|
|
@pytest.fixture
|
|
async def caps():
|
|
"""一项写入能力(被 2 次运行用到)、一项只读能力(没用过)、一项来源已删掉的能力,各带几项推断。"""
|
|
tag = uuid.uuid4().hex[:6]
|
|
names = {"pay": f"erp_pay_{tag}", "orders": f"erp_orders_{tag}", "gone": f"gone_{tag}"}
|
|
async with SessionLocal() as s:
|
|
s.add_all([
|
|
Capability(name=names["pay"], system="erp", title="付款", effect_system="write", fields={
|
|
"/amount": {"label": _proposed("金额"), "measure": _proposed("flow", "name")},
|
|
"/card": {"label": {"value": "卡号", "source": "human", "status": "confirmed"}}}),
|
|
Capability(name=names["orders"], system="erp", title="订单", effect_system="read", fields={
|
|
"/status": {"codes": _proposed({"1": "已付"}, "comment"), "label": _proposed("状态")}}),
|
|
Capability(name=names["gone"], system="erp", effect_system="read", missing=True,
|
|
fields={"/x": {"label": _proposed("某字段")}}),
|
|
])
|
|
runs = [Run(workflow_name="用过", options={"capabilities": {names["pay"]: {"version": 1}}}) for _ in range(2)]
|
|
s.add_all(runs)
|
|
await s.commit()
|
|
run_ids = [r.id for r in runs]
|
|
try:
|
|
yield names
|
|
finally:
|
|
async with SessionLocal() as s:
|
|
await s.execute(delete(Capability).where(Capability.name.in_(list(names.values()))))
|
|
await s.execute(delete(Run).where(Run.id.in_(run_ids)))
|
|
await s.execute(delete(AuditEvent).where(AuditEvent.subject_id.in_(list(names.values()))))
|
|
await s.commit()
|
|
|
|
|
|
async def test_the_queue_ranks_by_runs_effect_risk_and_source(caps):
|
|
async with SessionLocal() as s:
|
|
counts = await review.run_counts(s)
|
|
queue = await review.review_queue(s, limit=200)
|
|
assert counts[caps["pay"]] == 2
|
|
mine = [i for i in queue["items"] if i["capability"] in caps.values()]
|
|
assert [(i["capability"], i["pointer"], i["field"]) for i in mine] == [
|
|
(caps["pay"], "/amount", "measure"), # 用过 2 次 × 写入 × 度量类型(影响能不能加总)
|
|
(caps["pay"], "/amount", "label"),
|
|
(caps["orders"], "/status", "codes"),
|
|
(caps["orders"], "/status", "label"),
|
|
], "已确认的、来源已删掉的能力不列"
|
|
top = mine[0]
|
|
assert top["field_label"] == "返回字段 /amount 的度量类型" and top["display"] == "可累加"
|
|
assert top["runs"] == 2 and top["effect"] == "write" and top["version"] == 1
|
|
assert top["reasons"][0] == "被 2 次运行用到" and "写入能力" in top["reasons"][1] and top["reasons"][-1] == \
|
|
"命名推断,未确认"
|
|
async with SessionLocal() as s:
|
|
facts = await review.review_queue(s, kind="facts", capability=caps["pay"])
|
|
names = await review.review_queue(s, kind="names", capability=caps["orders"])
|
|
assert [i["field"] for i in facts["items"]] == ["measure"] and facts["total"] == 1
|
|
assert {i["field"] for i in names["items"]} == {"codes", "label"}
|
|
|
|
|
|
async def test_batch_review_locks_per_capability_and_audits(caps):
|
|
pay, orders = caps["pay"], caps["orders"]
|
|
items = [
|
|
BatchItem(pay, "/amount", "label", "confirm", 1),
|
|
BatchItem(pay, "/amount", "measure", "reject", 1),
|
|
BatchItem(pay, "/nope", "label", "confirm", 1),
|
|
BatchItem(orders, "/status", "label", "confirm", 7), # 版本对不上
|
|
BatchItem(caps["gone"], "/x", "label", "confirm", 1),
|
|
BatchItem("no_such_capability", "/x", "label", "confirm", 1),
|
|
]
|
|
async with SessionLocal() as s:
|
|
results = await review.review_batch(s, items, actor="root", verified=True)
|
|
assert [(r["ok"], r["code"]) for r in results] == [
|
|
(True, None), (True, None), (False, review.CODE_PATH), (False, review.CODE_CONFLICT),
|
|
(False, review.CODE_MISSING), (False, review.CODE_MISSING)]
|
|
assert results[0]["version"] == 2 and "返回字段 /nope 的中文名" in results[2]["error"]
|
|
async with SessionLocal() as s:
|
|
row = await s.get(Capability, pay)
|
|
audits = list((await s.execute(select(AuditEvent).where(AuditEvent.subject_id == pay))).scalars())
|
|
untouched = await s.get(Capability, orders)
|
|
assert row.version == 2 and row.updated_by == "root"
|
|
assert row.fields["/amount"]["label"]["status"] == "confirmed"
|
|
assert row.fields["/amount"]["measure"]["status"] == "rejected"
|
|
assert untouched.version == 1 and untouched.fields["/status"]["label"]["status"] == "proposed"
|
|
assert [a.action for a in audits] == ["capability.review"] and audits[0].data["version"] == 2
|
|
assert {(c["field"], c["before"], c["after"]) for c in audits[0].data["items"]} == {
|
|
("label", "proposed", "confirmed"), ("measure", "proposed", "rejected")}
|
|
|
|
# 撤销审阅回到来源的初始状态;人工填写的撤销就是删掉(同数据目录)
|
|
async with SessionLocal() as s:
|
|
again = await review.review_batch(s, [BatchItem(pay, "/amount", "label", "reset", 2),
|
|
BatchItem(pay, "/card", "label", "reset", 2)], actor="root",
|
|
verified=True)
|
|
assert all(r["ok"] for r in again)
|
|
row = await s.get(Capability, pay)
|
|
await s.refresh(row)
|
|
assert row.fields["/amount"]["label"]["status"] == "proposed" and "/card" not in row.fields
|
|
|
|
|
|
# ---------------------------------------------------------------- 接口与权限
|
|
|
|
|
|
async def test_review_api_and_permissions(caps):
|
|
await auth_env.reset()
|
|
try:
|
|
async with auth_env.client() as c:
|
|
queue = (await c.get("/api/capabilities/review-queue", params={"capability": caps["pay"]})).json()
|
|
assert [i["field"] for i in queue["items"]] == ["measure", "label"]
|
|
r = await c.put(f"/api/capabilities/{caps['orders']}/governance",
|
|
json={"version": 1, "fields": {"/status": {"label": "订单状态"}}})
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["fields"]["/status"]["label"]["status"] == "confirmed", "治理写进来的人工填写记为已确认"
|
|
assert "codes" not in r.json()["fields"]["/status"], "提交里没有的项删掉"
|
|
bad = await c.put(f"/api/capabilities/{caps['orders']}/governance",
|
|
json={"version": 2, "fields": {"/status": {"measure": "很多"}}})
|
|
assert bad.status_code == 422 and "返回字段 /status 的度量类型" in bad.json()["detail"]
|
|
|
|
admin = await auth_env.turn_on_local()
|
|
await auth_env.make_user("bob", "builder")
|
|
bob = await auth_env.login("bob")
|
|
assert (await bob.get("/api/capabilities/review-queue")).status_code == 200, "搭建者能看"
|
|
body = {"items": [{"capability": caps["pay"], "pointer": "/amount", "field": "label", "action": "confirm",
|
|
"if_version": 1}]}
|
|
assert (await bob.post("/api/capabilities/review/batch", json=body)).status_code == 403, "审阅只给管理员"
|
|
r = await admin.post("/api/capabilities/review/batch", json=body)
|
|
assert r.status_code == 200 and r.json()["results"][0]["ok"], r.text
|
|
async with SessionLocal() as s:
|
|
audit = (await s.execute(select(AuditEvent).where(
|
|
AuditEvent.subject_id == caps["pay"], AuditEvent.action == "capability.review"))).scalar_one()
|
|
assert audit.actor == "root" and audit.verified is True
|
|
finally:
|
|
await auth_env.reset()
|