mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 06:51:59 +00:00
78e9b0c6da
验收时同一张表「使用 4 次」、「共 3 次运行」对不上,接口说明(GET /catalog/{table}/usage、catalog_review.table_runs)
却写「取法同使用次数」,看着该是同一个数。两者数的确实是同一批查询快照(catalog.query_snapshot_tables),但使用次数按
查询计(同一次运行里查了两次算两次),用过这张表的运行按运行去重,前者可能比后者大——数没错,说明错了。
改了接口说明、table_runs 的说明和 catalog_review 模块说明里那一条,写清两种计法和为什么会不一样;表清单上「使用 N 次」的
悬停说明写明「按查询算」,并点出下面「用过这张表的运行」按运行算(运行列表那边的悬停说明本来就写清了,没动)。
测试:test_catalog_review_queue.py::test_usage_counts_queries_and_the_runs_list_counts_runs(同一次运行查两次:清单 4、
运行 3;接口说明不再写「取法同使用次数」、写明按运行和按查询)。去掉修复回跑失败。
419 lines
24 KiB
Python
419 lines
24 KiB
Python
"""数据目录的审阅队列、批量审阅、表被哪些运行用过、审阅进度(data/catalog_review.py)。
|
||
|
||
- GET /catalog/review-queue:只列推断(proposed)的项,按「使用次数权重 × 风险权重 × 来源权重」从高到低,同分按表名、
|
||
路径;facts(有效记录条件、去重规则、业务日期、关联关系、业务主键、度量类型)/ names(中文名、说明、含义、单位、码值、
|
||
粒度、表类型)两组;按表筛选;分页;field_label 是给人看的位置(「列 status 的码值」);reasons 写为什么排在这里
|
||
(被几次运行用到,和 /usage 的 total 一致);table_usage 是使用次数(同目录清单);表结构里已经没有的表、列不列
|
||
- POST /catalog/review/batch:逐表按 if_version 乐观锁,一张表冲突不影响别的表;同一张表的几条一次写入;路径不对、表不存在
|
||
逐条报
|
||
- GET /catalog/{table}/usage:用过这张表的运行,新的在前,取法同 catalog.table_usage
|
||
- GET /catalog/review-progress:常用 20 张表(使用次数前 20 张,同起草的默认范围:用到的不够 20 张时按表结构的顺序
|
||
补齐)、全部、影响数字的项各自的确认情况
|
||
|
||
表名、列名、业务数字都是虚构的。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import uuid
|
||
from datetime import datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
from urllib.parse import quote
|
||
|
||
import pytest
|
||
from httpx import ASGITransport, AsyncClient
|
||
|
||
from app.data import catalog, catalog_review
|
||
from app.data.catalog import make_item
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import Artifact, DataSource, Run
|
||
from app.main import app as _fastapi_app
|
||
from tests.fixtures.sources import drop_source
|
||
|
||
ACTOR = {"X-Actor": quote("林晓")}
|
||
TERMS = Path(__file__).resolve().parents[2] / "frontend" / "src" / "lib" / "terms.ts"
|
||
|
||
|
||
def _col(name: str, type_: str = "INTEGER") -> dict:
|
||
return {"name": name, "type": type_, "nullable": True, "comment": None}
|
||
|
||
|
||
def _tables() -> dict:
|
||
return {
|
||
"orders": {"qualified": "orders", "primary_key": ["id"], "comment": None, "is_view": False,
|
||
"foreign_keys": [], "unique": [],
|
||
"columns": [_col("id"), _col("status"), _col("amount", "REAL"), _col("member_id"),
|
||
_col("paid_at", "TEXT")]},
|
||
"members": {"qualified": "members", "primary_key": ["id"], "comment": None, "is_view": False,
|
||
"foreign_keys": [], "unique": [],
|
||
"columns": [_col("id"), _col("nick", "TEXT"), _col("tier")]},
|
||
"coupons": {"qualified": "coupons", "primary_key": ["id"], "comment": None, "is_view": False,
|
||
"foreign_keys": [], "unique": [], "columns": [_col("id"), _col("face_value", "REAL")]},
|
||
}
|
||
|
||
|
||
def _member_rel(status: str = "proposed", source: str = "name") -> dict:
|
||
return {"id": catalog.relation_id("orders", ["member_id"], "members", ["id"]), "columns": ["member_id"],
|
||
"to_table": "members", "to_columns": ["id"], "cardinality": "many_to_one", "coverage": 0.98,
|
||
"source": source, "status": status}
|
||
|
||
|
||
ORDERS = {
|
||
"label": make_item("订单", "llm"),
|
||
"grain": make_item("每笔订单一行", "llm", "confirmed"),
|
||
"valid_filter": make_item("status <> 9", "llm", note="剖析核对:排除 3.2% 的行(共 12,345 行)"),
|
||
"kind": make_item("fact", "llm"),
|
||
"columns": {
|
||
"status": {"label": make_item("状态", "comment"), "codes": make_item({"1": "已支付", "9": ""}, "profile")},
|
||
"amount": {"measure": make_item("flow", "llm"), "unit": make_item("元", "llm", "rejected")},
|
||
# 表结构里已经没有这一列:不进队列
|
||
"legacy_flag": {"label": make_item("旧标记", "llm")},
|
||
},
|
||
"relations": [_member_rel()],
|
||
}
|
||
MEMBERS = {"label": make_item("会员", "llm"), "keys": make_item(["id"], "name"),
|
||
"columns": {"nick": {"label": make_item("昵称", "comment")}}}
|
||
COUPONS = {"label": make_item("优惠券", "comment")}
|
||
#: 表结构里已经没有这张表:不进队列
|
||
GONE = {"label": make_item("旧表", "llm")}
|
||
|
||
_CREATED: list[str] = []
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
async def _cleanup():
|
||
yield
|
||
while _CREATED:
|
||
await drop_source(_CREATED.pop())
|
||
|
||
|
||
@pytest.fixture
|
||
async def client():
|
||
async with AsyncClient(transport=ASGITransport(app=_fastapi_app), base_url="http://test") as c:
|
||
yield c
|
||
|
||
|
||
async def _write(sid: str, table: str, notes: dict) -> None:
|
||
async with SessionLocal() as session:
|
||
entry = await catalog.read_entry(session, sid, table)
|
||
await catalog.write_entry(session, sid, table, notes, if_version=entry.version if entry else 0, actor=None)
|
||
|
||
|
||
async def _use(sid: str, name: str, table_runs: dict[str, int]) -> list[str]:
|
||
"""给每张表造几次运行查询它(每次运行一份查询快照),返回运行 id(先建的在前、开始得早)。"""
|
||
runs: list[str] = []
|
||
base = datetime(2026, 9, 1, tzinfo=timezone.utc)
|
||
async with SessionLocal() as session:
|
||
for table, n in table_runs.items():
|
||
for _ in range(n):
|
||
rid = uuid.uuid4().hex
|
||
session.add(Run(id=rid, graph={}, workflow_name=f"周报-{table}", run_class="formal",
|
||
status="succeeded", started_at=base + timedelta(hours=len(runs))))
|
||
session.add(Artifact(id=uuid.uuid4().hex, run_id=rid, kind=catalog.QUERY_SNAPSHOT_KIND,
|
||
meta={"source_id": sid, "source": name, "tables": [table]}))
|
||
runs.append(rid)
|
||
await session.commit()
|
||
return runs
|
||
|
||
|
||
async def _source(*, usage: dict[str, int] | None = None) -> str:
|
||
name = f"rq_{uuid.uuid4().hex[:10]}"
|
||
async with SessionLocal() as session:
|
||
row = DataSource(name=name, kind="sqlite", database=":memory:", schema_cache={"tables": _tables()})
|
||
session.add(row)
|
||
await session.commit()
|
||
sid = row.id
|
||
_CREATED.append(sid)
|
||
for table, notes in (("orders", ORDERS), ("members", MEMBERS), ("coupons", COUPONS), ("gone_table", GONE)):
|
||
await _write(sid, table, notes)
|
||
if usage:
|
||
await _use(sid, name, usage)
|
||
return sid
|
||
|
||
|
||
def _base(sid: str) -> str:
|
||
return f"/api/datasources/{sid}/catalog"
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 排序规则(纯函数)
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def test_priority_multiplies_usage_risk_and_source_weights():
|
||
p = catalog_review.priority
|
||
# 风险:有效记录条件 > 业务主键 > 码值 > 中文名
|
||
assert p(0, ("t", "valid_filter"), "llm") > p(0, ("t", "keys"), "llm") > p(0, ("c", "x", "codes"), "llm") \
|
||
> p(0, ("c", "x", "label"), "llm")
|
||
assert p(0, ("r", "r1"), "name") == p(0, ("t", "dedup"), "name") == p(0, ("t", "business_date"), "name")
|
||
assert p(0, ("c", "x", "measure"), "llm") == p(0, ("t", "keys"), "llm")
|
||
assert p(0, ("t", "grain"), "llm") == p(0, ("t", "kind"), "llm") == p(0, ("c", "x", "codes"), "llm")
|
||
assert p(0, ("c", "x", "meaning"), "llm") == p(0, ("c", "x", "unit"), "llm") == p(0, ("t", "label"), "llm")
|
||
# 来源:模型起草、命名推断比数据库注释优先
|
||
assert p(0, ("t", "label"), "llm") == p(0, ("t", "label"), "name") > p(0, ("t", "label"), "comment")
|
||
# 使用次数:用得多的在前,但按对数增长,不压过风险
|
||
assert p(37, ("t", "label"), "llm") > p(1, ("t", "label"), "llm") > p(0, ("t", "label"), "llm")
|
||
assert p(37, ("t", "label"), "llm") == pytest.approx(
|
||
p(0, ("t", "label"), "llm") * p(37, ("t", "label"), "comment") / p(0, ("t", "label"), "comment"))
|
||
|
||
|
||
def test_source_labels_follow_the_ui():
|
||
text = TERMS.read_text(encoding="utf-8")
|
||
block = re.search(r"export const CATALOG_SOURCE_LABEL\b[^=]*=\s*\{(.*?)\n\}", text, re.S)
|
||
assert block
|
||
ui = dict(re.findall(r"(\w+):\s*'([^']+)'", block.group(1)))
|
||
assert ui == catalog_review.SOURCE_LABEL
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 队列
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
async def test_queue_lists_only_proposed_items_by_priority(client):
|
||
sid = await _source(usage={"orders": 37, "members": 2})
|
||
r = await client.get(f"{_base(sid)}/review-queue")
|
||
assert r.status_code == 200, r.text
|
||
body = r.json()
|
||
items = body["items"]
|
||
paths = [(i["table_name"], i["path"]) for i in items]
|
||
# 已确认、已驳回的不列;表结构里没有的表、列不列
|
||
assert ("orders", "grain") not in paths and ("orders", "columns.amount.unit") not in paths
|
||
assert not any(t == "gone_table" for t, _ in paths)
|
||
assert ("orders", "columns.legacy_flag.label") not in paths
|
||
assert body["total"] == len(items) == 11
|
||
assert all(i["status"] == "proposed" for i in items)
|
||
# 从高到低,同分按表名、路径
|
||
keys = [(-i["priority"], i["table_name"], i["path"]) for i in items]
|
||
assert keys == sorted(keys)
|
||
first = items[0]
|
||
rel_id = _member_rel()["id"]
|
||
assert (first["table_name"], first["path"]) in {("orders", "valid_filter"), ("orders", f"relations.{rel_id}")}
|
||
vf = next(i for i in items if i["path"] == "valid_filter")
|
||
assert vf["field"] == "valid_filter" and vf["field_label"] == "表的有效记录条件"
|
||
assert vf["value"] == "status <> 9" and vf["display"] == "status <> 9"
|
||
assert vf["source"] == "llm" and vf["note"] == "剖析核对:排除 3.2% 的行(共 12,345 行)"
|
||
assert vf["table_usage"] == 37 and vf["version"] == 1
|
||
assert vf["reasons"] == ["常用表(被 37 次运行用到)", "影响数字的项(有效记录条件)", "模型起草,未确认"]
|
||
# 常用表上的中文名排在没人用的表上的中文名前面;同一张表上影响数字的项排在中文名前面
|
||
order = [(i["table_name"], i["path"]) for i in items]
|
||
assert order.index(("orders", "label")) < order.index(("coupons", "label"))
|
||
assert order.index(("members", "keys")) < order.index(("members", "label"))
|
||
|
||
|
||
async def test_queue_values_are_displayed_in_ui_words(client):
|
||
sid = await _source()
|
||
items = {(i["table_name"], i["path"]): i for i in (await client.get(f"{_base(sid)}/review-queue")).json()["items"]}
|
||
rel = items[("orders", f"relations.{_member_rel()['id']}")]
|
||
assert rel["field"] == "relation" and rel["field_label"] == "指向 members 的关联关系"
|
||
assert rel["value"] == {"columns": ["member_id"], "to_table": "members", "to_columns": ["id"],
|
||
"cardinality": "many_to_one", "coverage": 0.98}
|
||
assert rel["display"] == "member_id → members.id(多对一,覆盖率 98%)"
|
||
assert rel["reasons"][-1] == "命名推断,未确认"
|
||
kind = items[("orders", "kind")]
|
||
assert kind["display"] == "明细表" and kind["value"] == "fact"
|
||
codes = items[("orders", "columns.status.codes")]
|
||
assert codes["field"] == "codes" and codes["field_label"] == "列 status 的码值" and codes["column"] == "status"
|
||
assert codes["display"] == "1=已支付、9=含义待填写"
|
||
assert "1 个码值的含义待填写" in codes["reasons"] and "数据剖析,未确认" in codes["reasons"]
|
||
assert items[("orders", "columns.amount.measure")]["display"] == "可累加"
|
||
assert items[("members", "keys")]["display"] == "id" and items[("members", "keys")]["field_label"] == "表的业务主键"
|
||
assert items[("orders", "columns.amount.measure")]["field_label"] == "列 amount 的度量类型"
|
||
assert items[("members", "label")]["reasons"] == ["名称和说明(中文名)", "模型起草,未确认"]
|
||
assert items[("coupons", "label")]["reasons"] == ["名称和说明(中文名)", "数据库注释,未确认"]
|
||
|
||
|
||
async def test_queue_reasons_count_runs_like_the_usage_list(client):
|
||
"""同一次运行里查了几次这张表:原因写「被几次运行用到」(和 /usage 的 total 一样),table_usage 是使用次数(和目录清单的
|
||
「使用 N 次」一样),两个数不一样时原因里两个都写;优先级按使用次数(同清单的排序)。"""
|
||
sid = await _source()
|
||
async with SessionLocal() as session:
|
||
name = (await session.get(DataSource, sid)).name
|
||
runs = await _use(sid, name, {"orders": 2, "members": 1})
|
||
async with SessionLocal() as session:
|
||
for _ in range(3):
|
||
session.add(Artifact(id=uuid.uuid4().hex, run_id=runs[0], kind=catalog.QUERY_SNAPSHOT_KIND,
|
||
meta={"source_id": sid, "source": name, "tables": ["orders"]}))
|
||
await session.commit()
|
||
usage = await catalog.table_usage(session, await session.get(DataSource, sid))
|
||
assert usage["orders"] == 5
|
||
items = {(i["table_name"], i["path"]): i for i in (await client.get(f"{_base(sid)}/review-queue")).json()["items"]}
|
||
vf = items[("orders", "valid_filter")]
|
||
assert vf["table_usage"] == 5
|
||
assert vf["reasons"][0] == "常用表(被 2 次运行用到,使用 5 次)"
|
||
assert items[("members", "label")]["reasons"][0] == "常用表(被 1 次运行用到)"
|
||
assert vf["priority"] == round(catalog_review.priority(5, ("t", "valid_filter"), "llm"), 4)
|
||
assert (await client.get(f"{_base(sid)}/orders/usage")).json()["total"] == 2
|
||
|
||
|
||
async def test_queue_filters_by_kind_and_table_and_pages(client):
|
||
sid = await _source(usage={"orders": 3})
|
||
facts = (await client.get(f"{_base(sid)}/review-queue", params={"kind": "facts"})).json()
|
||
assert {i["field"] for i in facts["items"]} == {"valid_filter", "relation", "measure", "keys"}
|
||
names = (await client.get(f"{_base(sid)}/review-queue", params={"kind": "names"})).json()
|
||
assert {i["field"] for i in names["items"]} == {"label", "codes", "kind"}
|
||
assert facts["total"] + names["total"] == 11
|
||
only = (await client.get(f"{_base(sid)}/review-queue", params={"table": "MEMBERS"})).json()
|
||
assert {i["table_name"] for i in only["items"]} == {"members"} and only["total"] == 3
|
||
everything = (await client.get(f"{_base(sid)}/review-queue")).json()["items"]
|
||
page = (await client.get(f"{_base(sid)}/review-queue", params={"limit": 4, "offset": 4})).json()
|
||
assert page["total"] == 11 and page["items"] == everything[4:8]
|
||
bad = await client.get(f"{_base(sid)}/review-queue", params={"kind": "other"})
|
||
assert bad.status_code == 422
|
||
assert (await client.get(f"{_base(sid)}/review-queue", params={"limit": 0})).status_code == 422
|
||
missing = await client.get("/api/datasources/nope/catalog/review-queue")
|
||
assert missing.status_code == 404
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 批量审阅
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
async def test_batch_review_writes_each_table_once_and_isolates_conflicts(client):
|
||
sid = await _source()
|
||
rel_path = f"relations.{_member_rel()['id']}"
|
||
body = {"items": [
|
||
{"table_name": "orders", "path": "valid_filter", "action": "confirm", "if_version": 1},
|
||
{"table_name": "orders", "path": rel_path, "action": "reject", "if_version": 1},
|
||
{"table_name": "orders", "path": "columns.nothing.label", "action": "confirm", "if_version": 1},
|
||
{"table_name": "members", "path": "label", "action": "confirm", "if_version": 7}, # 版本不对
|
||
{"table_name": "coupons", "path": "label", "action": "reject", "if_version": 1},
|
||
{"table_name": "ghost", "path": "label", "action": "confirm", "if_version": 0},
|
||
]}
|
||
r = await client.post(f"{_base(sid)}/review/batch", json=body, headers=ACTOR)
|
||
assert r.status_code == 200, r.text
|
||
results = r.json()["results"]
|
||
assert [(x["table_name"], x["path"], x["ok"]) for x in results] == [
|
||
("orders", "valid_filter", True), ("orders", rel_path, True), ("orders", "columns.nothing.label", False),
|
||
("members", "label", False), ("coupons", "label", True), ("ghost", "label", False)]
|
||
# 同一张表的两条一次写进去:版本只加 1
|
||
assert results[0]["version"] == results[1]["version"] == 2
|
||
assert results[2]["code"] == "catalog_path_invalid" and "找不到" in results[2]["error"]
|
||
assert results[2]["version"] is None
|
||
assert results[3]["code"] == "catalog_conflict" and "重新载入" in results[3]["error"]
|
||
assert results[4]["version"] == 2 and results[4]["error"] is None and results[4]["code"] is None
|
||
assert results[5]["code"] == "table_missing"
|
||
async with SessionLocal() as session:
|
||
orders = await catalog.read_entry(session, sid, "orders")
|
||
members = await catalog.read_entry(session, sid, "members")
|
||
coupons = await catalog.read_entry(session, sid, "coupons")
|
||
assert orders.version == 2 and orders.updated_by == "林晓"
|
||
assert orders.notes["valid_filter"]["status"] == "confirmed"
|
||
assert orders.notes["relations"][0]["status"] == "rejected"
|
||
assert members.version == 1 and members.notes["label"]["status"] == "proposed"
|
||
assert coupons.notes["label"]["status"] == "rejected"
|
||
# 撤销审阅:回到来源的初始状态
|
||
again = await client.post(f"{_base(sid)}/review/batch", json={"items": [
|
||
{"table_name": "orders", "path": "valid_filter", "action": "reset", "if_version": 2}]})
|
||
assert again.json()["results"][0]["ok"] is True
|
||
async with SessionLocal() as session:
|
||
assert (await catalog.read_entry(session, sid, "orders")).notes["valid_filter"]["status"] == "proposed"
|
||
|
||
|
||
async def test_batch_review_limits(client):
|
||
sid = await _source()
|
||
many = [{"table_name": "orders", "path": "label", "action": "confirm", "if_version": 1}] * 201
|
||
assert (await client.post(f"{_base(sid)}/review/batch", json={"items": many})).status_code == 422
|
||
assert (await client.post(f"{_base(sid)}/review/batch", json={"items": []})).status_code == 422
|
||
wrong = [{"table_name": "orders", "path": "label", "action": "approve", "if_version": 1}]
|
||
assert (await client.post(f"{_base(sid)}/review/batch", json={"items": wrong})).status_code == 422
|
||
ok = [{"table_name": "orders", "path": "label", "action": "confirm", "if_version": 1}] * 200
|
||
r = await client.post(f"{_base(sid)}/review/batch", json={"items": ok})
|
||
assert r.status_code == 200 and all(x["ok"] for x in r.json()["results"])
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 被哪些运行用过
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
async def test_table_usage_lists_runs_newest_first(client):
|
||
sid = await _source()
|
||
async with SessionLocal() as session:
|
||
name = (await session.get(DataSource, sid)).name
|
||
runs = await _use(sid, name, {"orders": 3, "members": 1})
|
||
# 同一次运行查了两次 orders:只算一次运行
|
||
async with SessionLocal() as session:
|
||
session.add(Artifact(id=uuid.uuid4().hex, run_id=runs[0], kind=catalog.QUERY_SNAPSHOT_KIND,
|
||
meta={"source_id": sid, "source": name, "tables": ["orders", "members"]}))
|
||
# 别的数据源的同名表不算
|
||
other = uuid.uuid4().hex
|
||
session.add(Run(id=other, graph={}, workflow_name="别处", status="succeeded"))
|
||
session.add(Artifact(id=uuid.uuid4().hex, run_id=other, kind=catalog.QUERY_SNAPSHOT_KIND,
|
||
meta={"source_id": "elsewhere", "source": "elsewhere", "tables": ["orders"]}))
|
||
await session.commit()
|
||
r = await client.get(f"{_base(sid)}/orders/usage")
|
||
assert r.status_code == 200, r.text
|
||
body = r.json()
|
||
assert body["total"] == 3
|
||
assert [x["run_id"] for x in body["runs"]] == [runs[2], runs[1], runs[0]]
|
||
first = body["runs"][0]
|
||
assert first["workflow_name"] == "周报-orders" and first["run_class"] == "formal"
|
||
assert first["status"] == "succeeded" and first["started_at"].startswith("2026-09-01T02:00")
|
||
assert set(first) == {"run_id", "workflow_id", "workflow_name", "run_class", "status", "started_at"}
|
||
limited = (await client.get(f"{_base(sid)}/orders/usage", params={"limit": 1})).json()
|
||
assert limited["total"] == 3 and len(limited["runs"]) == 1
|
||
members = (await client.get(f"{_base(sid)}/members/usage")).json()
|
||
assert members["total"] == 2
|
||
assert (await client.get(f"{_base(sid)}/ghost/usage")).status_code == 404
|
||
|
||
|
||
async def test_usage_counts_queries_and_the_runs_list_counts_runs(client):
|
||
"""两个数口径不同、说明照实:表的「使用次数」按查询计(同一次运行里查了两次算两次),「用过这张表的运行」按运行计。
|
||
接口说明以前写「取法同使用次数」,看着该是同一个数,验收时 4 对 3 对不上。"""
|
||
sid = await _source()
|
||
async with SessionLocal() as session:
|
||
name = (await session.get(DataSource, sid)).name
|
||
runs = await _use(sid, name, {"orders": 3})
|
||
async with SessionLocal() as session:
|
||
session.add(Artifact(id=uuid.uuid4().hex, run_id=runs[0], kind=catalog.QUERY_SNAPSHOT_KIND,
|
||
meta={"source_id": sid, "source": name, "tables": ["orders"]}))
|
||
await session.commit()
|
||
listed = (await client.get(f"/api/datasources/{sid}/catalog")).json()
|
||
assert next(t for t in listed["tables"] if t["table_name"] == "orders")["usage"] == 4
|
||
assert (await client.get(f"{_base(sid)}/orders/usage")).json()["total"] == 3
|
||
doc = _fastapi_app.openapi()["paths"]["/api/datasources/{source_id}/catalog/{table}/usage"]["get"]["description"]
|
||
assert "取法同使用次数" not in doc and "按运行" in doc and "按查询" in doc, doc
|
||
from app.data import catalog_review
|
||
|
||
assert "取法同使用次数" not in (catalog_review.table_runs.__doc__ or "")
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 进度
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
async def test_review_progress_counts_top_tables_all_and_facts(client, monkeypatch):
|
||
sid = await _source(usage={"orders": 5})
|
||
r = await client.get(f"{_base(sid)}/review-progress")
|
||
assert r.status_code == 200, r.text
|
||
body = r.json()
|
||
assert body["top_n"] == 20
|
||
# 表结构里只有 3 张表:都算常用表(使用次数前 20 张);表结构里没有的表、列不算
|
||
assert body["top"] == {"tables": 3, "items_total": 13, "items_confirmed": 1}
|
||
assert body["all"] == {"items_total": 13, "items_confirmed": 1, "items_proposed": 11, "items_rejected": 1}
|
||
# 影响数字的项:有效记录条件、关联关系、度量类型、业务主键
|
||
assert body["facts"] == {"proposed": 4, "confirmed": 0}
|
||
await client.post(f"{_base(sid)}/review/batch", json={"items": [
|
||
{"table_name": "orders", "path": "valid_filter", "action": "confirm", "if_version": 1}]})
|
||
after = (await client.get(f"{_base(sid)}/review-progress")).json()
|
||
assert after["top"]["items_confirmed"] == 2 and after["facts"] == {"proposed": 3, "confirmed": 1}
|
||
# 只取前 2 张:用得多的在前,没人用的按表结构的顺序补(同起草的默认范围)
|
||
monkeypatch.setattr(catalog_review, "TOP_N", 2)
|
||
used = await _source(usage={"coupons": 1})
|
||
top = (await client.get(f"{_base(used)}/review-progress")).json()["top"]
|
||
assert top == {"tables": 2, "items_total": 1 + 9, "items_confirmed": 1}
|
||
|
||
|
||
def test_top_tables_are_the_draft_default_scope():
|
||
"""常用表和起草对话框「使用次数最多的 20 张表」是同一批:使用次数从多到少,一样多(包括都没用过)的按表结构里的顺序。"""
|
||
tables = {f"t{i:02d}": {} for i in range(30)}
|
||
usage = {"t29": 4, "t05": 4, "t17": 9}
|
||
top = catalog_review.top_tables(tables, usage)
|
||
assert catalog_review.TOP_N == catalog.DRAFT_DEFAULT_TABLES == 20
|
||
assert top[:3] == ["t17", "t05", "t29"] and len(top) == 20
|
||
assert top[3:] == [n for n in tables if n not in usage][:17]
|
||
assert catalog_review.top_tables({"a": {}, "b": {}}, {}) == ["a", "b"]
|