mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:11:58 +00:00
b3dcddb52e
- W4R-01:召回只查新版图里会不会写外部系统(apps/backfill 抽出 graph_external_write,graph_side_effect 复用它)。召回图只有两版
的取数和口径卡,图里别处的人工节点、按流程使用的问题碰不到它;口径卡上游的人工节点 derive 本来就认作数据部分不能复用。
「按新版重新出具」走完整的正式运行,仍按 graph_side_effect 全套拒绝。原来带审批的口径升版后一组都不召回,L6 对它们失效。
- W4R-09:收尾时召回卡读不出、或缺了某个比较的旧值 / 新值,照实记重算失败(读不出召回卡),原答案一个不动。原来两边都是空,
按渲染比被当成「没变」,给每个原答案永久盖上「已核对,不影响」,收过尾之后再也不核对。
- W4R-12:这一版已经不是当前发布版(之后又发布了更新的版本)时不再发起召回、不给接着算,批次里写 continue_reason,接着算的
接口 409 带同一句。同启动时接着做只认当前发布版:新版本的召回覆盖更早的所有版本,再按旧版开「口径升到 vN」的待办只是添乱。
- W4R-13:开登录时一组答案几个主人就几条待办,全局渠道却按这一批(recall.batch_of_key:同一次召回、同一个来由)只在最早开的
那条上推一次,答案、待办按这一批汇总,几条的链到待办箱;别的几条连 task.opened 也不再推全局渠道。个人渠道照旧每人一条。
- W4R-11:目录审阅之后的核对按数据源排队、批量审阅的几张表合成一遍(catalog_pass,同一数据源一把锁);查询快照按数据源在库里
筛、按运行结束时间分页、每张表凑够 BASIS_MAX_RUNS 次运行就停;冻结的表结构快照这一遍里按工件 id 只读一次。原来批量审阅
几十张表就同时起几十个后台任务,每个都全表扫查询快照、反复读同一份几 MB 的表结构快照。
- W4R-03(本包那一半):GET /api/runs/{id}/ledger 加 recall:checked(已按 vN 核对,不影响)、basis(依据有变:哪一种、几项、
新出现几条错误级问题)、tasks(看的这个人手上说的是这个答案、还没处理的 recall 待办)。不放数、不放表名。几种状态都带。
- W4R-10(本包那一半):新事件 ledger.feedback(对账运行上的封存后事件),POST /api/ledger/answers/{run}/reconcile/{rid}/feedback
{verdict: useful | unclear} 记反馈,同一个人改主意只算最后一次(app/ledger/feedback.py);账本接口的 reconciled 每一项带
feedback {mine, useful, unclear};统计 E2 的 feedback 数到它,去掉「第 4 波接上」的占位说法。离线核对页、封存后事件摘要
都认得这个事件。
测试:test_ledger_recall 新增七个用例、改了人工节点那一个(召回照算、重新出具照实拒绝);新增 test_ledger_feedback;
test_ledger_api 的 E2 断言跟上。每个修复都先写失败的测试、去掉修复回跑确认抓得住。后端全量 14237 通过。
120 lines
5.9 KiB
Python
120 lines
5.9 KiB
Python
"""对账结果的「说明有用 / 看不懂」(next-round 02 做法 10 的 E2;app/ledger/feedback.py)。
|
||
|
||
真的跑:虚构店铺库里的问数据答案(算上了退款)和受管口径「周销售」(只算已支付)对一次账,再在结果上给反馈:
|
||
- 反馈记成对账运行上的封存后事件 ledger.feedback(批注不是证据,不进封存范围);同一个人改主意只算最后一次;
|
||
- 账本接口 reconciled 每一项带上反馈(看的这个人给过的、各几个),界面照着画选中的那一个;
|
||
- 统计 E2 数到它(不再写「第 4 波接上」);
|
||
- 不认的反馈、不是这个答案的对账、还没跑完的对账照实拒绝。
|
||
数据都是编的。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
from httpx import ASGITransport, AsyncClient
|
||
from sqlalchemy import select
|
||
|
||
from app.core.events import EventType
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import RunEvent
|
||
from app.engine.runner import run_manager
|
||
from app.ledger import match as ledger_match
|
||
from app.main import app
|
||
from tests import ledger_samples as S
|
||
|
||
CHECKED = {"start": "2026-09-01", "end": "2026-09-08"}
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
async def engine_up(tmp_path):
|
||
await run_manager.setup()
|
||
path = tmp_path / "shop.db"
|
||
await S.fresh_source(path)
|
||
yield path
|
||
await ledger_match.drain()
|
||
await run_manager.shutdown()
|
||
|
||
|
||
@pytest.fixture
|
||
async def client():
|
||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
||
yield c
|
||
|
||
|
||
async def reconciled(client, monkeypatch) -> tuple[str, str]:
|
||
"""问一次(算上了退款)、按核对过的参数和「周销售」对一次账,等它跑完。返回 (答案, 对账运行)。"""
|
||
wf = await S.publish()
|
||
row = await S.explore(monkeypatch, S.WITH_REFUNDS)
|
||
body = (await client.get(f"/api/runs/{row.id}/ledger")).json()
|
||
[action] = [a for a in body["actions"] if a["kind"] == "reconcile"]
|
||
res = await client.post(f"/api/ledger/answers/{row.id}/reconcile",
|
||
json={"query": action["query"], "workflow_id": wf, "version": 1, "inputs": CHECKED,
|
||
"confirm": True})
|
||
assert res.status_code == 201, res.text
|
||
done = await S.wait(res.json()["run_id"])
|
||
assert done.status == "succeeded", done.error
|
||
return row.id, done.id
|
||
|
||
|
||
async def test_feedback_is_a_post_seal_note_counted_by_e2(client, monkeypatch):
|
||
answer, rec = await reconciled(client, monkeypatch)
|
||
before = (await client.get("/api/ledger/stats")).json()["e2"]["feedback"]
|
||
assert before["available"] is True and "第 4 波" not in str(before)
|
||
|
||
res = await client.post(f"/api/ledger/answers/{answer}/reconcile/{rec}/feedback", json={"verdict": "useful"})
|
||
assert res.status_code == 200, res.text
|
||
assert res.json() == {"run_id": rec, "feedback": {"mine": "useful", "useful": 1, "unclear": 0}}
|
||
async with SessionLocal() as session:
|
||
[data] = (await session.execute(select(RunEvent.data).where(
|
||
RunEvent.run_id == rec, RunEvent.type == EventType.LEDGER_FEEDBACK))).scalars()
|
||
assert data["verdict"] == "useful" and data["post_seal"] is True and data["source_run"] == answer
|
||
|
||
# 改主意:同一个人只算最后一次
|
||
res = await client.post(f"/api/ledger/answers/{answer}/reconcile/{rec}/feedback", json={"verdict": "unclear"})
|
||
assert res.json()["feedback"] == {"mine": "unclear", "useful": 0, "unclear": 1}
|
||
[item] = (await client.get(f"/api/runs/{answer}/ledger")).json()["reconciled"]
|
||
assert item["run_id"] == rec and item["feedback"] == {"mine": "unclear", "useful": 0, "unclear": 1}
|
||
|
||
after = (await client.get("/api/ledger/stats")).json()["e2"]["feedback"]
|
||
assert after["useful"] - before["useful"] == 0 and after["unclear"] - before["unclear"] == 1
|
||
assert after["runs"] - before["runs"] == 1 and after["available"] is True
|
||
|
||
|
||
async def test_feedback_refusals(client, monkeypatch):
|
||
answer, rec = await reconciled(client, monkeypatch)
|
||
bad = await client.post(f"/api/ledger/answers/{answer}/reconcile/{rec}/feedback", json={"verdict": "great"})
|
||
assert bad.status_code == 422
|
||
other = await S.explore(monkeypatch, S.ELSEWHERE, text="门店 [[v:Q1.r0.n]] 家。")
|
||
res = await client.post(f"/api/ledger/answers/{other.id}/reconcile/{rec}/feedback", json={"verdict": "useful"})
|
||
assert res.status_code == 404 and res.json()["code"] == "ledger_feedback_refused", "不是这个答案的对账"
|
||
res = await client.post(f"/api/ledger/answers/{answer}/reconcile/{answer}/feedback", json={"verdict": "useful"})
|
||
assert res.status_code == 404, "答案自己不是对账运行"
|
||
# 还没封存的对账(正在跑、刚发起):没有结果可评
|
||
from app.db.models import Run
|
||
|
||
async with SessionLocal() as session:
|
||
row = await session.get(Run, rec)
|
||
sealed = row.manifest_hash
|
||
row.manifest_hash = None
|
||
await session.commit()
|
||
try:
|
||
res = await client.post(f"/api/ledger/answers/{answer}/reconcile/{rec}/feedback", json={"verdict": "useful"})
|
||
assert res.status_code == 409 and "还没运行完" in res.json()["detail"], res.text
|
||
finally:
|
||
async with SessionLocal() as session:
|
||
(await session.get(Run, rec)).manifest_hash = sealed
|
||
await session.commit()
|
||
|
||
|
||
def test_offline_page_and_summary_know_the_feedback_event():
|
||
import re
|
||
from pathlib import Path
|
||
|
||
from app.bundle.build import _summary
|
||
|
||
script = (Path(__file__).resolve().parents[1] / "app" / "bundle" / "verifier.js").read_text(encoding="utf-8")
|
||
block = re.search(r"const EVENT_LABEL = \{(.*?)\n \}", script, re.S)
|
||
labels = dict(re.findall(r"'([a-z_.]+)': '([^']+)'", block.group(1)))
|
||
assert labels["ledger.feedback"] == "对账说明的反馈"
|
||
assert _summary("ledger.feedback", {"verdict": "useful"}) == "反馈:说明有用"
|
||
assert _summary("ledger.feedback", {"verdict": "unclear"}) == "反馈:看不懂"
|