mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 16:52:00 +00:00
2f90f0eb3a
- QueryCore 固定执行(K09):按已发布的不可变口径跑一次,交回类型化标量、发布/manifest 引用与逐查询证据 - 依赖闭包(K08):按选中的指标算出要拷的取数节点、要的入口字段,接进 derive / compose / metrics - SQL 模板参数完整性(K01):试跑、组合问数、派生图、运行时取数共用一套语义,缺参数在渲染前就拒 - 受限字段变更服务(K03)与口径领域边界(K01):改动打在整图深拷贝上,身份与证据归属不可改 - 版本化受控模板绑定(K04):业务口径按显式绑定落到 SQL,对不上就报 unsupported / needs_update - 验证记录(K05)与最小审阅:样例匹配不等于正式批准,审批人只从服务端身份取 - 发布依据清单(K07):发布时按不可变图物化内容寻址清单,正式用途按它判 - 增量重导入(K11)与 B/L/R 三方合并、受限自然语言修改提案(K12):只产候选,落图仍走既有写路径 - 接口与前端:口径草稿页接上字段变更、绑定状态、验证与重导入,新增 caliber-drafts / studio 检查脚本 验证:前端 tsc -b 通过。
180 lines
8.0 KiB
Python
180 lines
8.0 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import copy
|
|
import json
|
|
|
|
import pytest
|
|
from sqlalchemy import select
|
|
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import CaliberDraftJob, Workflow
|
|
from app.ledger import draftplan, jobs
|
|
from tests.test_ledger_docdraft_api import (client, datasource, documents, extract, fake_model, fake_trial, plan_reply)
|
|
from tests.test_caliber_regressions import draft
|
|
|
|
|
|
@pytest.mark.sqlite_only
|
|
async def test_concurrent_draft_edit_is_not_overwritten_by_trial_completion(
|
|
client, documents, datasource, monkeypatch):
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.ledger import _save_trial
|
|
from app.db.models import CaliberDraft
|
|
|
|
body = await draft(client, documents, datasource, monkeypatch)
|
|
execute = AsyncSession.execute
|
|
raced = False
|
|
|
|
async def concurrent(session, statement, *args, **kwargs):
|
|
nonlocal raced
|
|
if (not raced and getattr(statement, "is_update", False)
|
|
and statement.table.name == CaliberDraft.__tablename__):
|
|
raced = True
|
|
async with SessionLocal() as other:
|
|
row = await other.get(CaliberDraft, body["id"])
|
|
row.payload = {**row.payload, "guidance": "刚提交的审阅意见"}
|
|
await other.commit()
|
|
return await execute(session, statement, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(AsyncSession, "execute", concurrent)
|
|
assert not await _save_trial(body["id"], {**body["trial"], "error": "较早试跑的结果"})
|
|
assert raced
|
|
async with SessionLocal() as session:
|
|
current = await session.get(CaliberDraft, body["id"])
|
|
assert current.payload["guidance"] == "刚提交的审阅意见"
|
|
assert current.payload["trial"] == body["trial"]
|
|
|
|
|
|
async def test_layout_save_keeps_trial_evidence_but_sql_change_invalidates(client, documents, datasource, monkeypatch):
|
|
body = await draft(client, documents, datasource, monkeypatch)
|
|
published = await client.post(f"/api/workflows/{body['workflow_id']}/publish", json={"level": "published"})
|
|
assert published.status_code == 200 and published.json()["ok"]
|
|
async with SessionLocal() as session:
|
|
workflow = await session.get(Workflow, body["workflow_id"])
|
|
graph = copy.deepcopy(workflow.graph)
|
|
revision = workflow.version
|
|
graph["nodes"][0]["position"] = {"x": 100, "y": 200}
|
|
saved = await client.patch(f"/api/workflows/{body['workflow_id']}",
|
|
json={"graph": graph, "expected_revision": revision})
|
|
assert saved.status_code == 200
|
|
assert saved.json()["same_as_published"] is True
|
|
result = (await client.get(f"/api/ledger/drafts/{body['id']}/trial")).json()
|
|
assert result["status"] == "succeeded" and result["version"] == 1
|
|
fetch = next(n for n in graph["nodes"] if n["id"] == "fetch")
|
|
fetch["data"]["config"]["args"]["sql"] = "SELECT 9 AS refund_rate FROM orders"
|
|
changed = await client.patch(f"/api/workflows/{body['workflow_id']}",
|
|
json={"graph": graph, "expected_revision": saved.json()["version"]})
|
|
assert changed.json()["same_as_published"] is False
|
|
result = (await client.get(f"/api/ledger/drafts/{body['id']}/trial")).json()
|
|
assert result["status"] == "stale" and result["metrics"] == []
|
|
|
|
|
|
async def test_interrupted_authoring_resumes_completed_groups_only_for_same_material(
|
|
client, documents, datasource, monkeypatch):
|
|
doc = await documents("architecture-resume")
|
|
source = await datasource()
|
|
group = json.loads(plan_reply(f"{doc}-2"))
|
|
calls = []
|
|
|
|
async def interrupted(_session, **kwargs):
|
|
calls.append(list(kwargs["covered"]))
|
|
if len(calls) == 1:
|
|
return {"groups": [group], "uncovered": ["第二个指标"]}
|
|
raise asyncio.CancelledError
|
|
|
|
monkeypatch.setattr(draftplan, "plan_groups", interrupted)
|
|
made = await extract(client, doc, source)
|
|
draft_id = made["draft_id"]
|
|
body = (await client.get(f"/api/ledger/drafts/{draft_id}")).json()
|
|
assert body["status"] == "failed" and body["job"]["status"] == "interrupted"
|
|
assert body["groups"] and body["material_hash"]
|
|
second = copy.deepcopy(group)
|
|
second["metrics"][0].update(id="m2", name="第二个指标")
|
|
|
|
async def resume(_session, **kwargs):
|
|
assert kwargs["covered"] == [group["metrics"][0]["name"]]
|
|
return {"groups": [second], "uncovered": []}
|
|
|
|
monkeypatch.setattr(draftplan, "plan_groups", resume)
|
|
reply = await client.post(f"/api/ledger/drafts/{draft_id}/retry", json={})
|
|
assert reply.status_code == 200, reply.text
|
|
body = (await client.get(f"/api/ledger/drafts/{draft_id}")).json()
|
|
assert body["status"] == "ready" and len(body["metrics"]) == 2
|
|
assert body["job"]["attempt"] == 2 and body["job"]["status"] == "succeeded"
|
|
assert (await client.post(f"/api/ledger/drafts/{draft_id}/retry", json={})).status_code == 409
|
|
|
|
|
|
async def test_parallel_group_repairs_have_independent_sessions(client, documents, datasource, monkeypatch):
|
|
doc = await documents("architecture-sessions")
|
|
source = await datasource()
|
|
group = json.loads(plan_reply(f"{doc}-2"))
|
|
broken = [{**copy.deepcopy(group), "sql": "SELECT * FROM unknown_table"} for _ in range(2)]
|
|
broken[1]["metrics"][0].update(id="m2", name="另一个指标")
|
|
sessions = []
|
|
|
|
async def plan(_session, **kwargs):
|
|
return {"groups": broken, "uncovered": []}
|
|
|
|
async def repair(session, **kwargs):
|
|
sessions.append(session)
|
|
await session.execute(select(Workflow.id).limit(1))
|
|
await asyncio.sleep(.01)
|
|
return {**kwargs["group"], "sql": group["sql"]}
|
|
|
|
monkeypatch.setattr(draftplan, "plan_groups", plan)
|
|
monkeypatch.setattr(draftplan, "repair_group", repair)
|
|
made = await extract(client, doc, source)
|
|
body = (await client.get(f"/api/ledger/drafts/{made['draft_id']}")).json()
|
|
assert len(sessions) == 2 and sessions[0] is not sessions[1]
|
|
assert body["status"] == "ready" and len(body["metrics"]) == 2
|
|
|
|
|
|
async def test_startup_marks_orphaned_job_interrupted_and_preserves_progress(client, documents, datasource, monkeypatch):
|
|
from app.db.models import CaliberDraft
|
|
|
|
async with SessionLocal() as session:
|
|
row = CaliberDraft(title="中断任务", payload={"progress": {"round": 1}}, status="drafting")
|
|
session.add(row)
|
|
await session.flush()
|
|
session.add(CaliberDraftJob(id=row.id, status="running"))
|
|
await session.commit()
|
|
draft_id = row.id
|
|
await jobs.startup()
|
|
body = (await client.get(f"/api/ledger/drafts/{draft_id}")).json()
|
|
assert body["status"] == "failed" and body["job"]["status"] == "interrupted"
|
|
assert body["progress"]["round"] == 1 and body["job"]["retryable"]
|
|
|
|
|
|
async def test_stop_api_cancels_the_worker_and_preserves_checkpoint(client, monkeypatch):
|
|
from app.db.models import CaliberDraft
|
|
|
|
checkpoint = {"groups": [{"metrics": [{"id": "m1", "name": "已完成指标"}]}]}
|
|
async with SessionLocal() as session:
|
|
row = CaliberDraft(title="停止任务", payload={"checkpoint": checkpoint}, status="drafting")
|
|
session.add(row)
|
|
await session.flush()
|
|
session.add(CaliberDraftJob(id=row.id))
|
|
await session.commit()
|
|
draft_id = row.id
|
|
entered = asyncio.Event()
|
|
|
|
async def generating(_draft_id):
|
|
entered.set()
|
|
await asyncio.Event().wait()
|
|
|
|
monkeypatch.setattr(jobs, "executor", generating)
|
|
jobs.manager.start(draft_id)
|
|
try:
|
|
await asyncio.wait_for(entered.wait(), 3)
|
|
edit = await client.patch(f"/api/ledger/drafts/{draft_id}", json={"metrics": []})
|
|
assert edit.status_code == 409, edit.text
|
|
result = await client.post(f"/api/ledger/drafts/{draft_id}/stop", json={})
|
|
assert result.status_code == 200, result.text
|
|
body = result.json()
|
|
assert body["status"] == "failed" and body["job"]["status"] == "cancelled"
|
|
assert body["checkpoint"] == checkpoint and body["job"]["retryable"]
|
|
assert not jobs.manager.running(draft_id)
|
|
finally:
|
|
await jobs.manager.stop(draft_id, "user")
|