Files
AgentLab/backend/tests/test_version_level.py

160 lines
7.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.
"""发布级别记在版本上(WorkflowVersion.level)。
以前「这次正式运行按不按受管出具」看的是工作流当前的 status。可 status 说的是当前画布:
受管版本发布之后在画布上改一笔,status 就退回 draft,再从那个受管版本发起的正式运行
就被当成已发布级别出具(单元格不必在契约里声明 cells 就放行)。级别要跟着版本走:
发布时记在那一版上,正式运行看它钉的版本;没有记过级别的老版本照旧看 status。
"""
from __future__ import annotations
import asyncio
import pytest
from httpx import ASGITransport, AsyncClient
from langchain_core.messages import AIMessage
from sqlalchemy import create_engine, select, text
from app.db.base import SessionLocal
from app.db.models import Run, RunEvent, Workflow, WorkflowVersion
from app.engine.runner import run_manager
from app.main import app
@pytest.fixture(autouse=True)
async def engine_up():
await run_manager.setup()
yield
await run_manager.shutdown()
@pytest.fixture
async def client():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
yield c
@pytest.fixture(autouse=True)
def writer(monkeypatch):
from app.providers import mock_model
monkeypatch.setattr(mock_model.MockChatModel, "_decide",
lambda model, messages: AIMessage(content="本周销售额 [[m:gmv]]。"))
def node(nid, ntype, **config):
return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0}, "data": {"label": nid, "config": config}}
def weekly() -> dict:
nodes = [
node("start", "input"),
node("fetch", "transform", mode="expression", expression="{'gmv': 300.5}", assign_to="kpi"),
node("card", "metrics", caliber="周报口径", metrics=[
{"id": "gmv", "name": "销售额", "unit": "元", "expression": "vars.kpi.gmv"}]),
node("write", "report", instructions="写周报",
numbers="strict", on_violation="fail", claims="require_citation", analysis_claims="strict"),
node("out", "output", fields=[{"name": "周报", "value": "{{ nodes.write.text }}"}],
contract={"report_from": "write", "metrics_from": ["card"], "required": ["gmv"], "strict": True}),
]
return {"nodes": nodes, "edges": [{"source": a["id"], "target": b["id"]} for a, b in zip(nodes, nodes[1:])]}
async def version_row(wf: str, version: int) -> WorkflowVersion:
async with SessionLocal() as session:
return (await session.execute(select(WorkflowVersion).where(
WorkflowVersion.workflow_id == wf, WorkflowVersion.version == version))).scalar_one()
async def notes(run_id: str) -> list[dict]:
async with SessionLocal() as session:
rows = (await session.execute(select(RunEvent).where(
RunEvent.run_id == run_id, RunEvent.type == "log").order_by(RunEvent.seq))).scalars()
return [e.data for e in rows if e.data.get("code") == "run_governance"]
async def settle(run_id: str) -> Run:
for _ in range(400):
await asyncio.sleep(0.05)
async with SessionLocal() as session:
row = await session.get(Run, run_id)
if row.status in ("succeeded", "failed") and row.manifest_seq is not None:
return row
raise AssertionError(f"run {run_id} 没跑完:{row.status}")
@pytest.mark.parametrize("level", ["published", "governed"])
async def test_publishing_records_the_level_on_the_version(client, level):
wf = (await client.post("/api/workflows", json={"name": f"周报-{level}", "graph": weekly()})).json()["id"]
body = (await client.post(f"/api/workflows/{wf}/publish", json={"level": level})).json()
assert body["ok"] is True, body["issues"]
assert (await version_row(wf, 1)).level == level
versions = (await client.get(f"/api/workflows/{wf}/versions")).json()
assert versions[0]["level"] == level
detail = (await client.get(f"/api/workflows/{wf}/versions/1")).json()
assert detail["level"] == level and detail["published"] is True
async def test_a_blocked_publish_records_nothing(client):
graph = weekly()
graph["nodes"][-1]["data"]["config"]["contract"].pop("required")
wf = (await client.post("/api/workflows", json={"name": "周报-被拦", "graph": graph})).json()["id"]
assert (await client.post(f"/api/workflows/{wf}/publish", json={"level": "governed"})).json()["ok"] is False
assert (await version_row(wf, 1)).level is None
async def test_republishing_the_same_version_updates_its_level(client):
wf = (await client.post("/api/workflows", json={"name": "周报-升档", "graph": weekly()})).json()["id"]
await client.post(f"/api/workflows/{wf}/publish", json={"level": "published"})
await client.post(f"/api/workflows/{wf}/publish", json={"level": "governed"})
assert (await version_row(wf, 1)).level == "governed"
@pytest.mark.parametrize("level, governed", [("governed", True), ("published", False)])
async def test_editing_the_canvas_after_publishing_does_not_change_how_the_version_issues(client, level, governed):
from app.engine.governance import governed_formal
wf = (await client.post("/api/workflows", json={"name": f"周报-改画布-{level}", "graph": weekly()})).json()["id"]
assert (await client.post(f"/api/workflows/{wf}/publish", json={"level": level})).json()["ok"] is True
edited = weekly()
edited["nodes"][3]["data"]["label"] = "改过的报告"
after = (await client.patch(f"/api/workflows/{wf}", json={"graph": edited})).json()
assert after["status"] == "draft" and after["published_version"] == 1
r = await client.post("/api/runs", json={"workflow_id": wf, "run_class": "formal"})
assert r.status_code == 201, r.text
run_id = r.json()["id"]
row = await settle(run_id)
[note] = await notes(run_id)
assert note["governed"] is governed and "v1" in note["message"], note
assert await governed_formal(run_id) is governed
assert row.status == "succeeded", row.error
async def test_a_version_published_before_the_column_existed_follows_the_status(client):
"""老版本没有记级别:照旧看工作流现在的 status(test_contract_cells 那套旧规则)。"""
async with SessionLocal() as session:
w = Workflow(name="周报-老版本", graph=weekly(), status="governed", published_version=1)
session.add(w)
await session.flush()
session.add(WorkflowVersion(workflow_id=w.id, version=1, graph=weekly()))
await session.commit()
wf = w.id
r = await client.post("/api/runs", json={"workflow_id": wf, "run_class": "formal"})
run_id = r.json()["id"]
await settle(run_id)
[note] = await notes(run_id)
assert note["governed"] is True
def test_the_column_is_added_to_old_databases():
from app.db.base import _migrate
engine = create_engine("sqlite://")
with engine.begin() as conn:
conn.execute(text("CREATE TABLE workflow_versions (id VARCHAR(32) PRIMARY KEY, workflow_id VARCHAR(32), "
"version INTEGER, graph JSON, note TEXT, graph_hash VARCHAR(64))"))
conn.execute(text("INSERT INTO workflow_versions (id, workflow_id, version) VALUES ('a', 'w', 1)"))
_migrate(conn)
cols = {row[1] for row in conn.execute(text("PRAGMA table_info(workflow_versions)"))}
assert "level" in cols
assert conn.execute(text("SELECT level FROM workflow_versions")).scalar() is None