Files
AgentLab/backend/tests/test_ledger_certify.py
T
ThearchyHelios 1b31c49a00 问题聚类的类 id 由成员定,「不是口径」按成员记
类的 id 以前按种子问题的原文算:种子是一类里最新的那一句,时间相同的几句读出来先后不定、来了一句新问法种子就换了,
两次读取 id 就变,认证队列里点过的「不是口径」随之失效。

- app/chat/questions:Question.key 是稳定的 id(问数据的那一轮 turn:…、别的探索运行 run:…);QuestionCluster.id 是成员的
  id 排好之后取的哈希,同一批问题两次读取一样;recent_questions 同一时刻问的几句按 id 排出先后,不靠库里读出来的先后。
- ledger/certify:问题聚类那一项的 id 用类的 id;「不是口径」时服务端按同一批问题重新聚一次,记下这一类现在的成员(不收
  客户端传的成员,找不到、取向量出错就只记 id);之后这一类过半的成员是某一次记下的,就照样不进队——来了几句新问法 id
  会变,按成员认不失效。以前按种子原文算的 id 记的照样认;记的成员读不懂的照实忽略。
- api/ledger:dismiss 把当前身份交给 certify(重新聚类要按这个人读得到的问题)。

测试:tests/test_ledger_certify.py 新增三条——同一批问题换个先后读,种子不同、类的 id 一样;点过「不是口径」之后换个说法
再问一句,成员变了照样不进队,读不懂的老记录不报错也不藏;按旧的种子原文 id 记的照样生效。把 id 改回按种子原文、把按
成员认去掉再跑,各自的用例失败。
2026-10-17 12:15:07 +08:00

425 lines
24 KiB
Python
Raw 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.
"""沉淀:可沉淀的问题(认证队列)和「固化成口径草稿」(next-round 02 做法 8,app/ledger/certify.py)。
真的跑三次问数据(两段对话、同一个 SQL 形状、日期不同,虚构店铺库),等后台比对做完:
- 队列:确定性重复进队(3 次运行、2 段对话),变了的字面量是参数候选、不变的是常量,形状里不带取值;同一个问题问了 3 次
进问题聚类;导出过两次的、做成过应用的答案进「被反复用的答案」;点「不是口径」写审计后不再进队;
- 固化:报告引用的查询变成固定 SQL 的取数节点,跨轮变化的日期成为入口参数(缺省值取代表运行的),常量 'paid' 照写;单行
结果的数值列成口径卡指标;出口契约 report_from + metrics_from;过 validate_graph 和受管级别的发布门禁;origin.sources 记
三个来源问题;
- 按受管级别发布之后:来源运行在后台按它补比(trigger=certified),查询变成等价已证明、出了队;再问一次同样的形状,标
「等价已证明」;
- 统计 E3:认证队列的转化(固化、发布、不是口径)有数了。
数据是虚构店铺库。
"""
from __future__ import annotations
import pytest
from httpx import ASGITransport, AsyncClient
from app.db.base import SessionLocal
from app.db.models import Conversation, ConversationTurn, Run, Workflow
from app.engine.runner import run_manager
from app.engine.schema import GraphSpec, validate_graph
from app.ledger import certify
from app.ledger import match as ledger_match
from app.ledger import store
from app.main import app
from tests import ledger_samples as S
QUESTION = "九月第一周卖了多少"
WEEKS = [("2026-09-01", "2026-09-08"), ("2026-09-02", "2026-09-09"), ("2026-09-03", "2026-09-10")]
def weekly(start: str, end: str) -> str:
return (f"SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND paid_at >= '{start}' "
f"AND paid_at < '{end}'")
@pytest.fixture(autouse=True)
async def engine_up(tmp_path):
await run_manager.setup()
await S.fresh_source(tmp_path / "shop.db")
yield
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 conversation() -> str:
async with SessionLocal() as session:
conv = Conversation(title="问销售", kind="chat")
session.add(conv)
await session.commit()
return conv.id
async def ask(monkeypatch, conversation_id: str, sql: str, question: str = QUESTION) -> Run:
"""问数据的一轮:建轮次、跑一次问数据运行(来源记着对话和轮次)、挂回轮次、等比对做完。"""
async with SessionLocal() as session:
turn = ConversationTurn(conversation_id=conversation_id, question=question)
session.add(turn)
await session.commit()
turn_id = turn.id
S.script(monkeypatch, "这一周销售额 [[v:Q1.r0.gmv]]。")
run = await run_manager.start(graph=S.explore_graph(sql), input_payload={"question": question}, origin_kind="chat",
origin={"conversation_id": conversation_id, "turn_id": turn_id})
row = await S.wait(run.id)
assert row.status == "succeeded", row.error
async with SessionLocal() as session:
(await session.get(ConversationTurn, turn_id)).run_id = row.id
await session.commit()
await ledger_match.drain()
return row
async def three_asks(monkeypatch) -> list[Run]:
a, b = await conversation(), await conversation()
return [await ask(monkeypatch, conv, weekly(*week)) for conv, week in zip((a, a, b), WEEKS)]
def shape_items(body: dict) -> list[dict]:
return [i for i in body["items"] if i["kind"] == "shape" and i["source"] == S.CURRENT["source"]]
# --------------------------------------------------------------------------
# 队列
# --------------------------------------------------------------------------
def test_varying_literals_are_parameters_and_the_template_puts_them_back():
shape = certify.Shape("shop", "SELECT SUM(amount) AS gmv FROM orders WHERE status = ? AND paid_at >= ? AND x = ?",
("'paid'", "'2026-09-01'", "3"))
assert certify.varying([shape.literals, ("'paid'", "'2026-09-02'", "4")]) == [1, 2]
template, fields = certify.template_of(shape, [1, 2], "sqlite", start=3)
assert template == ("SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND paid_at >= '{{ input.p3 }}' "
"AND x = {{ input.p4 }}")
assert fields == [{"name": "p3", "type": "string", "required": True, "default": "2026-09-01"},
{"name": "p4", "type": "number", "required": True, "default": 3}]
async def test_repeated_shapes_questions_and_reused_answers_queue_up(client, monkeypatch):
runs = await three_asks(monkeypatch)
body = (await client.get("/api/ledger/certify-queue")).json()
[item] = shape_items(body)
assert item["runs"] == 3 and item["conversations"] == 2 and item["question"] == QUESTION
assert item["params"] == [{"index": 1, "distinct": 3}, {"index": 2, "distinct": 3}] and item["constants"] == 1
assert "2026-09" not in item["shape"] and "'paid'" not in item["shape"] and "?" in item["shape"]
assert {t["run_id"] for t in item["turns"]} == {r.id for r in runs}
assert all(t["turn_id"] and t["conversation_id"] for t in item["turns"])
assert item["representative"]["run_id"] in {r.id for r in runs} and len(item["peers"]) == 2
assert item["related"] == [] and item["frozen_into"] == []
clusters = [i for i in body["items"] if i["kind"] == "cluster" and i["question"] == QUESTION]
assert clusters and clusters[0]["count"] >= 3
# 被反复用的答案:导出过两次的、做成过应用的
from app.db.models_blueprints import Blueprint
async with SessionLocal() as session:
from app.auth.audit import record
for _ in range(2):
await record(session, "report.exported", "run", runs[0].id, data={"format": "html"})
session.add(Blueprint(name="周销售应用", source={"kind": "run", "id": runs[1].id}, spec={}))
await session.commit()
body = (await client.get("/api/ledger/certify-queue")).json()
reused = {i["representative"]["run_id"]: i for i in body["items"] if i["kind"] == "reused"}
assert reused[runs[0].id]["uses"]["exports"] == 2 and reused[runs[1].id]["uses"]["app"] is True
assert runs[2].id not in reused
# 「不是口径」:记审计,之后不再进队
res = await client.post("/api/ledger/certify-queue/dismiss", json={"item_id": item["id"], "kind": "shape"})
assert res.status_code == 200
assert shape_items((await client.get("/api/ledger/certify-queue")).json()) == []
stats = (await client.get("/api/ledger/stats")).json()["e3"]["certify"]
assert stats["available"] is True and stats["dismissed"] >= 1
async def test_two_runs_or_one_conversation_are_not_enough(client, monkeypatch):
conv = await conversation()
for week in WEEKS:
await ask(monkeypatch, conv, weekly(*week))
assert shape_items((await client.get("/api/ledger/certify-queue")).json()) == [], "只来自一段对话"
# --------------------------------------------------------------------------
# 固化、发布、再问
# --------------------------------------------------------------------------
async def test_freezing_makes_a_fixed_sql_draft_that_passes_the_governed_gate(client, monkeypatch):
from app.engine.governance import publish_issues
runs = await three_asks(monkeypatch)
[item] = shape_items((await client.get("/api/ledger/certify-queue")).json())
rep = item["representative"]["run_id"]
res = await client.post("/api/copilot/from-run", json={"run_id": rep, "freeze": True, "peers": item["peers"]})
assert res.status_code == 200, res.text
made = res.json()
assert made["frozen"] is True and made["params"] == ["p1", "p2"] and made["metrics"] == ["m1"]
async with SessionLocal() as session:
wf = await session.get(Workflow, made["workflow_id"])
assert wf.status == "draft" and "certified" in wf.tags
nodes = {n["id"]: n for n in wf.graph["nodes"]}
assert set(nodes) == {"start", "fetch", "card", "write", "out"}
rep_run = next(r for r in runs if r.id == rep)
start, end = WEEKS[[r.id for r in runs].index(rep)]
assert nodes["start"]["data"]["config"]["fields"] == [
{"name": "p1", "type": "string", "required": True, "default": start},
{"name": "p2", "type": "string", "required": True, "default": end}]
sql = nodes["fetch"]["data"]["config"]["args"]["sql"]
assert "'{{ input.p1 }}'" in sql and "'{{ input.p2 }}'" in sql and "'paid'" in sql
assert nodes["fetch"]["data"]["config"]["tool"] == S.tool()
assert nodes["card"]["data"]["config"]["metrics"] == [
{"id": "m1", "name": "gmv", "expression": "cell(nodes.fetch, 0, 'gmv')"}]
assert nodes["out"]["data"]["config"]["contract"] == {"report_from": "write", "metrics_from": "card",
"required": ["m1"]}
assert {k: nodes["write"]["data"]["config"].get(k) for k in ("numbers", "on_violation", "claims")} == {
"numbers": "strict", "on_violation": "fail", "claims": "require_citation"}
# 有入口参数:不写代表那一轮的问题原文(「九月第一周」参数化之后就不对了),写运行时渲染的参数
instructions = nodes["write"]["data"]["config"]["instructions"]
assert QUESTION not in instructions and "2026-09" not in instructions, instructions
assert "{{ input.p1 }}" in instructions and "{{ input.p2 }}" in instructions
assert nodes["card"]["data"]["config"]["caliber"] == QUESTION, "没传名字:口径卡照旧用代表问题"
spec = GraphSpec.model_validate(wf.graph)
assert not [i for i in validate_graph(spec).issues if i.level == "error"]
assert not [i for i in publish_issues(spec, level="governed") if i.level == "error"], \
[i.message for i in publish_issues(spec, level="governed") if i.level == "error"]
assert [s["run_id"] for s in wf.origin["sources"]][0] == rep and len(wf.origin["sources"]) == 3
assert all(s["question"] == QUESTION for s in wf.origin["sources"])
assert wf.origin["run_id"] == rep_run.id and wf.origin["freeze"] is True
# 队列上标着已经固化过:同一个工作流只列一次(三个来源运行都指着它)
body = (await client.get("/api/ledger/certify-queue")).json()
[again] = shape_items(body)
assert [w["workflow_id"] for w in again["frozen_into"]] == [wf.id]
[cluster] = [i for i in body["items"] if i["kind"] == "cluster" and i["question"] == QUESTION]
assert [w["workflow_id"] for w in cluster["frozen_into"]] == [wf.id], cluster["frozen_into"]
# 按受管级别发布:来源运行在后台补比,变成等价已证明、出了队
res = await client.post(f"/api/workflows/{wf.id}/publish", json={"level": "governed"})
assert res.status_code == 200, res.text
await ledger_match.drain()
for run in runs:
recorded = await store.latest(run.id)
assert recorded.trigger == "certified" and recorded.retro is True
[query] = recorded.content["queries"]
assert query["status"] == "proven" and query["candidates"][0]["caliber"] == f"{wf.id}@1"
body = (await client.get(f"/api/runs/{runs[0].id}/ledger")).json()
assert body["retro_note"] == "按之后发布的口径回看"
assert shape_items((await client.get("/api/ledger/certify-queue")).json()) == []
# 再问一次同样的形状:标等价已证明
later = await ask(monkeypatch, await conversation(), weekly("2026-09-05", "2026-09-12"))
[label] = (await store.latest(later.id)).content["reports"]["write"]["labels"].values()
assert label["kind"] == "caliber" and label["how"] == "independent" and label["caliber"] == f"{wf.id}@1"
assert label["binding"] == {"p1": "2026-09-05", "p2": "2026-09-12"}
stats = (await client.get("/api/ledger/stats")).json()["e3"]["certify"]
assert stats["frozen"] >= 1 and stats["published"] >= 1
async def test_freezing_uses_the_given_name_and_catalog_labels_and_no_dates_without_parameters(client, monkeypatch):
"""传了名字:口径卡用它(下一轮验收:以前用代表问题的原文)。指标名能从数据目录取到中文名的用中文名,取不到照旧用
列名。没有入口参数(字面量都没变过)时报告指令照旧写问题原文:那时问题里的日期就是口径的常量。"""
from app.data import catalog
from app.db.models import DataSource
from sqlalchemy import select
async with SessionLocal() as session:
source = (await session.execute(select(DataSource).where(DataSource.name == S.CURRENT["source"]))).scalar_one()
await catalog.write_entry(session, source.id, "orders", {"columns": {
"amount": {"label": catalog.make_item("金额", "human")}}}, if_version=0, actor="测试员")
sql = "SELECT SUM(amount) AS amount, COUNT(*) AS n FROM orders WHERE status = 'paid' AND paid_at >= '2026-09-01'"
S.script(monkeypatch, "金额 [[v:Q1.r0.amount]],[[v:Q1.r0.n]] 单。")
conv = await conversation()
async with SessionLocal() as session:
turn = ConversationTurn(conversation_id=conv, question=QUESTION)
session.add(turn)
await session.commit()
turn_id = turn.id
run = await run_manager.start(graph=S.explore_graph(sql), input_payload={"question": QUESTION}, origin_kind="chat",
origin={"conversation_id": conv, "turn_id": turn_id})
row = await S.wait(run.id)
assert row.status == "succeeded", row.error
async with SessionLocal() as session:
(await session.get(ConversationTurn, turn_id)).run_id = row.id
await session.commit()
await ledger_match.drain()
res = await client.post("/api/copilot/from-run", json={"run_id": row.id, "freeze": True, "name": "九月销售额口径"})
assert res.status_code == 200, res.text
made = res.json()
assert made["params"] == [] and made["name"] == "九月销售额口径"
async with SessionLocal() as session:
wf = await session.get(Workflow, made["workflow_id"])
nodes = {n["id"]: n for n in wf.graph["nodes"]}
card = nodes["card"]["data"]["config"]
assert card["caliber"] == "九月销售额口径"
assert [(m["name"], m["expression"]) for m in card["metrics"]] == [
("金额", "cell(nodes.fetch, 0, 'amount')"), ("n", "cell(nodes.fetch, 0, 'n')")]
assert nodes["write"]["data"]["config"]["instructions"] == f"回答:{QUESTION}"
async def test_freezing_needs_a_matched_successful_run(client, monkeypatch):
res = await client.post("/api/copilot/from-run", json={"run_id": "nope", "freeze": True})
assert res.status_code == 404
row = await S.explore(monkeypatch, S.ELSEWHERE, text="一共几家店,没有引用。")
res = await client.post("/api/copilot/from-run", json={"run_id": row.id, "freeze": True})
assert res.status_code == 409 and res.json()["detail"] == certify.NO_QUERIES
# --------------------------------------------------------------------------
# 确定性重复按规范形分组:条件的先后不算,条件真不同的不算一组
# --------------------------------------------------------------------------
#: 同一个形状的三种写法:条件换序、AND 两边换位、比较两边换位
REORDERED = [
"SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND paid_at >= '{s}' AND paid_at < '{e}'",
"SELECT SUM(amount) AS gmv FROM orders WHERE paid_at < '{e}' AND paid_at >= '{s}' AND status = 'paid'",
"SELECT SUM(amount) AS gmv FROM orders WHERE '{s}' <= paid_at AND ('paid' = status AND paid_at < '{e}')",
]
def test_conditions_in_another_order_are_the_same_shape_and_their_literals_line_up():
from app.data.sql_semantics import sql_shape
shapes = [sql_shape(sql.format(s=s, e=e), "sqlite") for sql, (s, e) in zip(REORDERED, WEEKS)]
assert all(sh is not None for sh in shapes)
assert len({sh.key for sh in shapes}) == 1
# 按规范形里的先后对齐:同一个位置是同一个条件里的那个字面量——变了的是两个日期,'paid' 在每一条里都落在同一个位置
assert len(certify.varying([sh.literals for sh in shapes])) == 2
assert len({sh.literals.index("'paid'") for sh in shapes}) == 1
starts = {sh.literals.index(f"'{s}'") for sh, (s, _) in zip(shapes, WEEKS)}
assert len(starts) == 1, "起始日期在每一条里都落在同一个位置"
@pytest.mark.parametrize("other", [
"SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND created_at >= '2026-09-01' AND paid_at < '2026-09-08'",
"SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' OR paid_at >= '2026-09-01' AND paid_at < '2026-09-08'",
"SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND paid_at >= '2026-09-01'",
"SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND paid_at > '2026-09-01' AND paid_at < '2026-09-08'",
"SELECT SUM(amount) AS gmv FROM orders WHERE status = 1 AND paid_at >= '2026-09-01' AND paid_at < '2026-09-08'",
"SELECT SUM(amount) AS gmv FROM orders WHERE status IN ('paid', 'open') AND paid_at >= '2026-09-01' "
"AND paid_at < '2026-09-08'",
"SELECT COUNT(*) AS gmv FROM orders WHERE status = 'paid' AND paid_at >= '2026-09-01' AND paid_at < '2026-09-08'",
], ids=["other-column", "or", "missing-condition", "other-operator", "number-not-string", "in-list", "other-aggregate"])
def test_really_different_conditions_are_different_shapes(other):
from app.data.sql_semantics import sql_shape
base = sql_shape(REORDERED[0].format(s="2026-09-01", e="2026-09-08"), "sqlite")
found = sql_shape(other, "sqlite")
assert base is not None and found is not None and found.key != base.key
async def test_queue_groups_reordered_conditions_and_freezing_aligns_their_parameters(client, monkeypatch):
a, b = await conversation(), await conversation()
runs = [await ask(monkeypatch, conv, sql.format(s=s, e=e))
for conv, sql, (s, e) in zip((a, a, b), REORDERED, WEEKS)]
# 条件真不同的(多筛了一个地区)不进这一组
other = await ask(monkeypatch, b, "SELECT SUM(amount) AS gmv FROM orders WHERE status = 'paid' AND region = 'east' "
"AND paid_at >= '2026-09-01' AND paid_at < '2026-09-08'")
[item] = shape_items((await client.get("/api/ledger/certify-queue")).json())
assert item["runs"] == 3 and {t["run_id"] for t in item["turns"]} == {r.id for r in runs}
assert other.id not in {t["run_id"] for t in item["turns"]}
assert len(item["params"]) == 2 and all(p["distinct"] == 3 for p in item["params"]) and item["constants"] == 1
rep = item["representative"]["run_id"]
res = await client.post("/api/copilot/from-run", json={"run_id": rep, "freeze": True, "peers": item["peers"]})
assert res.status_code == 200, res.text
made = res.json()
assert made["params"] == ["p1", "p2"], "条件换了序的几轮照样认得出两个日期是参数"
async with SessionLocal() as session:
wf = await session.get(Workflow, made["workflow_id"])
sql = {n["id"]: n for n in wf.graph["nodes"]}["fetch"]["data"]["config"]["args"]["sql"]
assert "'paid'" in sql and "'{{ input.p1 }}'" in sql and "'{{ input.p2 }}'" in sql
async def test_a_not_a_caliber_recorded_by_the_old_text_id_still_counts(client, monkeypatch):
"""以前「不是口径」按规范文本算的 id 记:分组改按规范形之后,组里任何一条的规范文本对得上就照样不进队。"""
await three_asks(monkeypatch)
[item] = shape_items((await client.get("/api/ledger/certify-queue")).json())
old = certify.item_id("shape", f"{S.CURRENT['source']}\n{item['shape']}")
assert old != item["id"]
res = await client.post("/api/ledger/certify-queue/dismiss", json={"item_id": old, "kind": "shape"})
assert res.status_code == 200
assert shape_items((await client.get("/api/ledger/certify-queue")).json()) == []
# --------------------------------------------------------------------------
# 问题聚类的 id 由成员定;「不是口径」按成员记
# --------------------------------------------------------------------------
async def test_cluster_ids_come_from_the_members_and_do_not_change_between_reads():
from datetime import datetime, timezone
from app.chat.questions import Question, QuestionCluster, cluster_questions
at = datetime(2026, 10, 1, tzinfo=timezone.utc)
# 同一时刻问的三句(时间相同,库里读出来的先后不定):两次读出来先后不同,种子就不同,类的 id 仍然一样
texts = ["仓库乙本月出库多少件", "仓库乙本月出库多少件?", "仓库乙本月出库了多少件"]
questions = [Question(text=t, at=at, turn_id=f"t-{i}") for i, t in enumerate(texts)]
first = await cluster_questions(questions)
again = await cluster_questions(list(reversed(questions)))
assert len(first) == 1 and len(again) == 1 and first[0].members[0].text != again[0].members[0].text
assert [c.id for c in first] == [c.id for c in again]
assert first[0].keys == ["turn:t-0", "turn:t-1", "turn:t-2"]
assert QuestionCluster(members=list(reversed(questions))).id == first[0].id
# 成员不同,id 就不同
assert QuestionCluster(members=questions[:2]).id != first[0].id
async def test_not_a_caliber_on_a_cluster_survives_a_new_member_and_old_records_still_count(client, monkeypatch):
import uuid
from app.auth.audit import record
text = f"仓库甲本周入库了多少件({uuid.uuid4().hex[:6]})"
a, b = await conversation(), await conversation()
asked = {(await ask(monkeypatch, conv, weekly(*WEEKS[0]), question=text)).id for conv in (a, a, b)}
async def mine() -> list[dict]:
body = (await client.get("/api/ledger/certify-queue")).json()
return [i for i in body["items"] if i["kind"] == "cluster" and asked & {t["run_id"] for t in i["turns"]}]
[item] = await mine()
[again] = await mine()
assert item["id"] == again["id"], "同一批问题两次读取,类的 id 一样"
# 读不懂的老记录照实忽略:不报错、不把这一类藏起来
async with SessionLocal() as session:
await record(session, certify.DISMISS_ACTION, certify.SUBJECT_KIND, "cluster:0000000000000000",
data={"kind": "cluster", "members": "看不懂"})
await session.commit()
assert [i["id"] for i in await mine()] == [item["id"]]
res = await client.post("/api/ledger/certify-queue/dismiss", json={"item_id": item["id"], "kind": "cluster"})
assert res.status_code == 200
assert await mine() == []
# 再换个说法问一句:成员多了一个、种子换成了新的这句,类的 id 变了;过半的成员是点「不是口径」时记下的,照样不进队
newer = await ask(monkeypatch, b, weekly(*WEEKS[1]), question=text + "?")
asked.add(newer.id)
assert await mine() == []
async def test_a_not_a_caliber_recorded_by_the_old_seed_text_id_still_counts(client, monkeypatch):
import uuid
from app.auth.audit import record
text = f"仓库丙上周退货多少件({uuid.uuid4().hex[:6]})"
a, b = await conversation(), await conversation()
for conv in (a, a, b):
await ask(monkeypatch, conv, weekly(*WEEKS[0]), question=text)
body = (await client.get("/api/ledger/certify-queue")).json()
assert [i for i in body["items"] if i["kind"] == "cluster" and i["question"] == text]
async with SessionLocal() as session:
await record(session, certify.DISMISS_ACTION, certify.SUBJECT_KIND, certify.item_id("cluster", text),
data={"kind": "cluster"})
await session.commit()
body = (await client.get("/api/ledger/certify-queue")).json()
assert not [i for i in body["items"] if i["kind"] == "cluster" and i["question"] == text]