mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 17:32:00 +00:00
9d8b41430e
- 完善口径编辑、只读试运行、统一质量验证与执行语义指纹 - 增加可执行指标约束、有预算的主动调查和过程评测 - 提取发布检查、证据读取与起草服务,支持任务停止、重试和检查点恢复 - 增加版本传递影响分析,修复并发覆盖、敏感值遮罩、异常评分及页面状态问题 - 修复 SQLite 复合查询追溯和 microVM 可选依赖缺失处理 - 纳入当前设计文档及部署配置调整 验证:相关主回归 3822 项通过,4 项跳过、8 项预期失败;补充回归通过。 前端构建、文案检查及 git diff --check 通过。 全量检查仍有环境权限、PostgreSQL 可选依赖和既有部署配置、SQLite 文件基线差异导致的未通过项。
177 lines
7.8 KiB
Python
177 lines
7.8 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)
|
|
graph["nodes"][0]["position"] = {"x": 100, "y": 200}
|
|
saved = await client.patch(f"/api/workflows/{body['workflow_id']}", json={"graph": graph})
|
|
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})
|
|
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")
|