"""分析入口接真算子跑一遍(不用替身):agent 的五个分析工具(变化拆解含下钻,波动基线、趋势、差异检验、可比性检查各一次)、 分析卡(波动基线 + 变化拆解;输入是合并查询的一次)、口径卡的可比性挂钩、审批恢复时的重放。断言工件、台账、交回模型的那一段、 节点产出,并用 analysis.recompute 按封存的工件复算、逐字段相同。 虚构零售库 shop:orders(day, region, channel, amount),2026-03-02 起 12 个整周,每周每个地区、渠道两笔(周三、周日), 最后一周华东掉三成。模型换成脚本(MockChatModel._decide),不调用任何模型。 """ from __future__ import annotations import asyncio import json import sqlite3 from datetime import date, timedelta import pytest from langchain_core.messages import AIMessage, ToolMessage from sqlalchemy import select import app.analysis as analysis_ops from app.analysis import contract from app.core import artifact_store from app.core.artifact_store import canonical_json, content_hash from app.db.base import SessionLocal from app.db.models import Run, RunEvent from app.engine.runner import run_manager TOOL = "db_query__shop" DECOMPOSE = contract.TOOL_NAMES["decompose"] BASELINE, TREND, DIFFER, COMPARABLE = (contract.TOOL_NAMES[op] for op in ("baseline", "trend", "differ", "comparable")) WEEK = "date(day, '-6 days', 'weekday 1')" BY_REGION = (f"SELECT region, {WEEK} AS week, SUM(amount) AS gmv FROM orders WHERE day >= '2026-05-11' " "GROUP BY region, week ORDER BY region, week") TOTALS = f"SELECT {WEEK} AS week, SUM(amount) AS gmv FROM orders WHERE day >= '2026-05-11' GROUP BY week ORDER BY week" EAST = (f"SELECT channel, {WEEK} AS week, SUM(amount) AS gmv FROM orders WHERE region = '华东' AND day >= '2026-05-11' " "GROUP BY channel, week ORDER BY channel, week") WEEKLY = f"SELECT {WEEK} AS week, SUM(amount) AS gmv FROM orders GROUP BY week ORDER BY week" TWO_WEEKS = {"period": "week", "base": "2026-05-11", "target": "2026-05-18", "measure": "gmv"} A1_ARGS = {"mode": "additive", "query": "Q1", "by": ["region"], **TWO_WEEKS, "label": "销售额", "headline": {"base": "Q2.r0.gmv", "target": "Q2.r1.gmv"}, "hypothesis": "下降是不是集中在某个地区"} A2_ARGS = {"mode": "additive", "query": "Q3", "by": ["channel"], **TWO_WEEKS, "parent": {"of": "A1", "key": ["华东"]}, "hypothesis": "华东是哪个渠道掉的"} def _orders(): out, i = [], 0 base = {"华东": 400, "华南": 300, "华北": 250, "西南": 50} for w in range(12): monday = date(2026, 3, 2) + timedelta(weeks=w) for region, amount in base.items(): for channel, share in (("线上", 3), ("线下", 2)): value = amount * share // 5 + (w % 3) * 2 if w == 11 and region == "华东": value = value * 7 // 10 for offset in (2, 6): i += 1 out.append((i, (monday + timedelta(days=offset)).isoformat(), region, channel, value)) return out @pytest.fixture(autouse=True) async def engine_up(): await run_manager.setup() yield await run_manager.shutdown() @pytest.fixture(autouse=True) async def shop(tmp_path, scratch_source): """虚构零售库 shop(tests/conftest.py 的 scratch_source:同名的旧源连同数据目录、现查缓存先删掉,用完再删)。""" path = tmp_path / "shop.db" db = sqlite3.connect(path) db.executescript("CREATE TABLE orders (id INTEGER PRIMARY KEY, day TEXT, region TEXT, channel TEXT, amount INTEGER);") db.executemany("INSERT INTO orders VALUES (?, ?, ?, ?, ?)", _orders()) db.commit() db.close() return await scratch_source("shop", path) def node(nid, ntype, **config): return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0}, "data": {"label": nid, "config": config}} def chain(*nodes): return {"nodes": list(nodes), "edges": [{"source": a["id"], "target": b["id"]} for a, b in zip(nodes, nodes[1:])]} class Script: """Agent 依次发 calls 里的工具调用,再给结论;seen 记下每次带工具的调用看到的消息。""" def __init__(self, monkeypatch, calls): from app.providers import mock_model self.seen: list[list] = [] def decide(model, messages): if not model.tools: return AIMessage(content="收尾") self.seen.append(list(messages)) done = sum(isinstance(m, ToolMessage) for m in messages) if done < len(calls): name, args = calls[done] return AIMessage(content="", tool_calls=[{"name": name, "args": args, "id": f"call_{done}"}]) return AIMessage(content="销售额下降主要来自华东。") monkeypatch.setattr(mock_model.MockChatModel, "_decide", decide) def replies(self): return [m.content for m in self.seen[-1] if isinstance(m, ToolMessage)] class Counting: """真算子外面包一层只数次数(重放时不该再算)。""" def __init__(self, monkeypatch): self.calls = 0 real = analysis_ops.run async def run(*args, **kwargs): self.calls += 1 return await real(*args, **kwargs) monkeypatch.setattr(analysis_ops, "run", run) async def wait(run_id, statuses=("succeeded", "failed")): for _ in range(600): await asyncio.sleep(0.05) async with SessionLocal() as session: row = await session.get(Run, run_id) if row.status in statuses and (row.status == "interrupted" or row.manifest_seq is not None): return row raise AssertionError(f"run {run_id} 没有结束:{row.status}") async def events(run_id, etype=None, node_id=None): async with SessionLocal() as session: q = select(RunEvent).where(RunEvent.run_id == run_id) if etype: q = q.where(RunEvent.type == etype) if node_id: q = q.where(RunEvent.node_id == node_id) return [e.data for e in (await session.execute(q.order_by(RunEvent.seq))).scalars()] def agent_graph(approval="never", tools=(TOOL, DECOMPOSE)): return chain(node("start", "input"), node("bot", "agent", prompt="销售额为什么下降", tools=list(tools), approval=approval), node("out", "output", fields=[{"name": "answer", "value": "{{ nodes.bot.text }}"}])) def recomputes(stored): """按封存的工件复算(查询快照、上一层结论都按 sha256 取回),和封存的逐字段相同。""" again = analysis_ops.recompute(stored, artifact_store.load, load_result=artifact_store.load) assert contract.same(stored, again), {k: (stored.get(k), again.get(k)) for k in stored if stored.get(k) != again.get(k)} AGENT_CALLS = [(TOOL, {"sql": BY_REGION}), (TOOL, {"sql": TOTALS}), (TOOL, {"sql": EAST}), (DECOMPOSE, A1_ARGS), (DECOMPOSE, A2_ARGS)] # -------------------------------------------------------------------------- # agent 分析工具:拆解、下钻 # -------------------------------------------------------------------------- async def test_an_agent_decomposes_and_drills_down_with_the_real_operator(monkeypatch): script = Script(monkeypatch, AGENT_CALLS) run = await run_manager.start(graph=agent_graph(), input_payload={}) row = await wait(run.id) assert row.status == "succeeded", row.error ends = await events(row.id, "tool.end", "bot") q1, q2, q3 = (ends[i]["query_artifact"] for i in range(3)) a1_artifact, a2_artifact = ends[3]["analysis_artifact"], ends[4]["analysis_artifact"] a1, a2 = artifact_store.load(a1_artifact), artifact_store.load(a2_artifact) # 结论:华东是主要来源,下钻到渠道是线上;总数闭合(headline 是 Q2 的两格) assert a1["status"] == "ok" and a1["verdict"] == {"code": "driver", "subjects": [["华东"]], "direction": "down"} assert "主要来自华东" in a1["phrase"] and a1["hypothesis"] == "下降是不是集中在某个地区" assert {i["ref"] for i in a1["inputs"]} >= {"Q1", "Q2.r0.gmv", "Q2.r1.gmv"} assert a2["verdict"]["code"] == "driver" and a2["verdict"]["subjects"] == [["线上"]] # 下钻:算子按上一层结论的内容哈希认 parent,就是存下的那份工件 [parent] = [i for i in a2["inputs"] if i.get("role") == "parent"] assert parent == {"ref": "A1", "artifact": a1_artifact, "role": "parent"} assert content_hash(canonical_json(a1)) == a1_artifact # 台账:两条 kind analysis,带 call_id,输入是那几份查询快照(下钻的另有上一层结论) [finished] = await events(row.id, "node.finished", "bot") analyses = [e for e in finished["evidence"] if e["kind"] == "analysis"] assert [(e["call_id"], e["artifact"]) for e in analyses] == [("call_3", a1_artifact), ("call_4", a2_artifact)] assert set(analyses[0]["inputs"]) == {q1, q2} and set(analyses[1]["inputs"]) == {q3, a1_artifact} assert analyses[0] == contract.ledger_entry(a1, node_id="bot", exec_no=1, artifact=a1_artifact, call_id="call_3") # 交回模型的:编号、判定语、关键数 replies = script.replies() assert [r.split("\n", 1)[0] for r in replies] == ["【证据 Q1】", "【证据 Q2】", "【证据 Q3】", "【分析 A1】", "【分析 A2】"] body = json.loads(replies[3].split("\n", 1)[1]) assert body["phrase"] == a1["phrase"] and body["verdict"] == "driver" and body["values"] == a1["values"] assert body["table"]["rows"][0][0] == "华东" # 按封存的工件复算,逐字段相同 recomputes(a1) recomputes(a2) async def agent_analysis(monkeypatch, tool, calls): """Agent 依次查库、最后调一次分析工具(真算子):交回 (那份结论, 交回模型的那一段的 JSON)。顺带断言台账记了这一条(带 call_id)、交回模型的编号是「【分析 A1】」、判定语和结论一字不差,并按封存的工件复算、逐字段相同。""" script = Script(monkeypatch, calls) run = await run_manager.start(graph=agent_graph(tools=(TOOL, tool)), input_payload={}) row = await wait(run.id) assert row.status == "succeeded", row.error ends = await events(row.id, "tool.end", "bot") artifact = ends[-1]["analysis_artifact"] stored = artifact_store.load(artifact) [finished] = await events(row.id, "node.finished", "bot") [entry] = [e for e in finished["evidence"] if e["kind"] == "analysis"] assert entry == contract.ledger_entry(stored, node_id="bot", exec_no=1, artifact=artifact, call_id=f"call_{len(calls) - 1}") assert set(entry["inputs"]) == {e["query_artifact"] for e in ends[:-1]} head, body = script.replies()[-1].split("\n", 1) assert head == "【分析 A1】" and json.loads(body)["phrase"] == stored["phrase"] recomputes(stored) return stored, json.loads(body) def _live(stored): """结论里现查的表范围:按这次运行的数据源探 orders.day。""" return [(p["table"], p["column"], p["basis"], p["max"]) for p in stored["probes"]] async def test_an_agent_puts_the_week_into_its_history_with_the_real_operator(monkeypatch): stored, body = await agent_analysis(monkeypatch, BASELINE, [ (TOOL, {"sql": WEEKLY}), (BASELINE, {"query": "Q1", "period": "week", "measure": "gmv", "label": "销售额", "hypothesis": "这周是不是异常"})]) assert stored["op"] == "baseline" and stored["status"] == "ok" and stored["hypothesis"] == "这周是不是异常" assert stored["verdict"] == {"code": "far_outside_band", "direction": "down", "subjects": []} assert stored["phrase"].startswith("销售额本期变化 -10.6%,明显超出过去 10 周的常规波动范围") assert "record_low" in {c["code"] for c in stored["caveats"]} and body["verdict"] == "far_outside_band" assert _live(stored) == [("orders", "day", "live", "2026-05-24")] # 本期完不完整按现查的表范围判 async def test_an_agent_asks_for_a_trend_with_the_real_operator(monkeypatch): stored, body = await agent_analysis(monkeypatch, TREND, [ (TOOL, {"sql": WEEKLY}), (TREND, {"query": "Q1", "period": "week", "measure": "gmv", "label": "销售额"})]) assert stored["op"] == "trend" and stored["status"] == "ok" assert stored["verdict"] == {"code": "no_trend", "direction": None, "subjects": []} assert stored["phrase"] == "过去 12 周销售额没有明显的上升或下降趋势" and stored["values"]["n"] == "12" assert _live(stored) == [("orders", "day", "live", "2026-05-24")] and body["values"] == stored["values"] async def test_an_agent_tests_a_difference_with_the_real_operator(monkeypatch): rows = "SELECT region, amount FROM orders WHERE region IN ('华东', '华南') ORDER BY region, day" stored, body = await agent_analysis(monkeypatch, DIFFER, [ (TOOL, {"sql": rows}), (DIFFER, {"mode": "rows", "query": "Q1", "by": ["region"], "value": "amount", "label": "单笔金额"})]) assert stored["op"] == "differ" and stored["mode"] == "rows" and stored["status"] == "ok" assert stored["verdict"] == {"code": "differs", "direction": "up", "subjects": [["华东"], ["华南"]]} assert stored["phrase"] == "华东的单笔金额(196.83)高于华南(152),差异站得住" assert stored["values"]["n_a"] == stored["values"]["n_b"] == "48" and stored["values"]["test"] == "welch" assert body["subjects"] == [["华东"], ["华南"]] async def test_an_agent_checks_two_weeks_are_comparable_with_the_real_operator(monkeypatch): stored, body = await agent_analysis(monkeypatch, COMPARABLE, [ (TOOL, {"sql": _week_sql("2026-05-18", "2026-05-24")}), (TOOL, {"sql": _week_sql("2026-05-11", "2026-05-17")}), (COMPARABLE, {"base_query": "Q2", "target_query": "Q1", "measure": "gmv"})]) assert stored["op"] == "comparable" and stored["status"] == "ok" and stored["phrase"] == "两期可比" assert stored["params"]["base_query"] == "Q2" and stored["params"]["target_query"] == "Q1" values = stored["values"] assert (values["base_start"], values["base_end"], values["target_start"], values["target_end"]) == ( "2026-05-11", "2026-05-17", "2026-05-18", "2026-05-24") assert {values[k] for k in ("same_query", "window", "complete", "weekdays", "data")} == {"pass"} assert _live(stored) == [("orders", "day", "live", "2026-05-24")] and body["verdict"] == "comparable" async def test_replaying_after_approvals_does_not_recompute(monkeypatch): counted = Counting(monkeypatch) script = Script(monkeypatch, AGENT_CALLS) run = await run_manager.start(graph=agent_graph(approval="always"), input_payload={}) for _ in AGENT_CALLS: row = await wait(run.id, ("interrupted", "failed", "succeeded")) assert row.status == "interrupted", row.error await run_manager.resume(run.id, {"approved": True}) row = await wait(run.id, ("succeeded", "failed", "interrupted")) assert row.status == "succeeded", row.error assert counted.calls == 2, "重放时分析又算了一次" ends = await events(row.id, "tool.end", "bot") assert len(ends) == len(AGENT_CALLS) a1_artifact, a2_artifact = ends[3]["analysis_artifact"], ends[4]["analysis_artifact"] assert [r.split("\n", 1)[0] for r in script.replies()][-2:] == ["【分析 A1】", "【分析 A2】"] [parent] = [i for i in artifact_store.load(a2_artifact)["inputs"] if i.get("role") == "parent"] assert parent["artifact"] == a1_artifact [finished] = await events(row.id, "node.finished", "bot") assert [e["artifact"] for e in finished["evidence"] if e["kind"] == "analysis"] == [a1_artifact, a2_artifact] # -------------------------------------------------------------------------- # 分析卡:波动基线 + 变化拆解 # -------------------------------------------------------------------------- async def test_an_analysis_card_runs_the_real_operators(): items = [ {"id": "wow", "op": "baseline", "from": "weekly", "params": {"period": "week", "measure": "gmv", "label": "销售额"}}, {"id": "where", "op": "decompose", "mode": "additive", "from": "by_region", "params": {"by": ["region"], **TWO_WEEKS, "label": "销售额", "headline": {"base": "weekly.r10.gmv", "target": "weekly.r11.gmv"}}}, ] graph = chain(node("start", "input"), node("weekly", "tool", tool=TOOL, args={"sql": WEEKLY}), node("by_region", "tool", tool=TOOL, args={"sql": BY_REGION}), node("card", "analysis", items=items), node("out", "output", fields=[{"name": "判定", "value": "{{ nodes.card.results.wow.phrase }}"}])) run = await run_manager.start(graph=graph, input_payload={}) row = await wait(run.id) assert row.status == "succeeded", row.error [finished] = await events(row.id, "node.finished", "card") results = artifact_store.load(finished["artifact"])["results"] wow, where = results["wow"], results["where"] assert wow["status"] == "ok" and wow["verdict"] in ("outside_band", "far_outside_band") and wow["direction"] == "down" assert where["status"] == "ok" and where["verdict"] == "driver" and "主要来自华东" in where["phrase"] assert row.output["判定"] == wow["phrase"] baseline = artifact_store.load(wow["artifact"]) # 本期完不完整按现查的表范围判(analysis.make_prober 按这次运行的数据源探 orders.day):探到了,不再说核对不了 [probe] = baseline["probes"] assert (probe["source"], probe["table"], probe["column"], probe["basis"]) == ("shop", "orders", "day", "live") assert probe["max"] == "2026-05-24" assert not [c for c in baseline["caveats"] if c["code"] == "comparability"] decomposed = artifact_store.load(where["artifact"]) assert decomposed["verdict"]["subjects"] == [["华东"]] entries = finished["evidence"] assert [(e["kind"], e["item"], e["artifact"]) for e in entries] == [ ("analysis", "wow", wow["artifact"]), ("analysis", "where", where["artifact"])] recomputes(baseline) recomputes(decomposed) async def test_an_analysis_card_reads_a_merged_query(): """分析卡的输入是合并查询(merge 节点):按合并查询节点的做法取它的快照,输入写明 sqlite 方言(合并查询在 SQLite 里 执行),真算子照常拆、复算逐字段相同。""" where = {"id": "where", "op": "decompose", "mode": "additive", "from": "merged", "params": {"by": ["region"], **TWO_WEEKS, "label": "销售额"}} graph = chain(node("start", "input"), node("by_region", "tool", tool=TOOL, args={"sql": BY_REGION}), node("merged", "merge", inputs={"a": "by_region"}, sql="SELECT region, week, gmv FROM a"), node("card", "analysis", items=[where]), node("out", "output", fields=[{"name": "r", "value": "{{ nodes.card.results.where.phrase }}"}])) run = await run_manager.start(graph=graph, input_payload={}) row = await wait(run.id) assert row.status == "succeeded", row.error [finished] = await events(row.id, "node.finished", "card") results = artifact_store.load(finished["artifact"])["results"] stored = artifact_store.load(results["where"]["artifact"]) assert stored["verdict"]["code"] == "driver" and stored["verdict"]["subjects"] == [["华东"]] assert row.output["r"] == stored["phrase"] and "主要来自华东" in stored["phrase"] [given] = stored["inputs"] [merged] = await events(row.id, "node.finished", "merged") assert (given["ref"], given["role"], given["dialect"]) == ("merged", "data", "sqlite") assert given["artifact"] == merged["evidence"][0]["artifact"] assert [(e["kind"], e["item"]) for e in finished["evidence"]] == [("analysis", "where")] recomputes(stored) # -------------------------------------------------------------------------- # 口径卡的可比性挂钩:真 comparable # -------------------------------------------------------------------------- def _week_sql(start: str, end: str) -> str: return f"SELECT SUM(amount) AS gmv FROM orders WHERE day BETWEEN '{start}' AND '{end}'" @pytest.mark.parametrize("this_week, verdict, has_reasons", [ (("2026-05-18", "2026-05-24"), "comparable", False), (("2026-05-18", "2026-05-23"), "conditional", True), # 本期 6 天对上期 7 天:窗口长度、周末天数都不一样 ]) async def test_a_caliber_card_checks_two_periods_with_the_real_operator(this_week, verdict, has_reasons): metrics = [{"id": "wow", "name": "销售额环比", "decimals": 4, "expression": "(cell(nodes.this_week, 0, 'gmv') - cell(nodes.last_week, 0, 'gmv'))" " / cell(nodes.last_week, 0, 'gmv')"}] graph = chain(node("start", "input"), node("this_week", "tool", tool=TOOL, args={"sql": _week_sql(*this_week)}), node("last_week", "tool", tool=TOOL, args={"sql": _week_sql("2026-05-11", "2026-05-17")}), node("card", "metrics", caliber="周报口径", metrics=metrics), node("out", "output", fields=[{"name": "r", "value": "{{ nodes.card.text }}"}])) run = await run_manager.start(graph=graph, input_payload={}) row = await wait(run.id) assert row.status == "succeeded", row.error [finished] = await events(row.id, "node.finished", "card") [metric] = artifact_store.load(finished["artifact"])["metrics"] found = metric["comparability"] stored = artifact_store.load(found["artifact"]) assert stored["op"] == "comparable" and stored["params"]["base_query"] == "last_week" assert stored["params"]["target_query"] == "this_week" assert found == {"artifact": found["artifact"], "verdict": verdict, "phrase": stored["phrase"], "reasons": analysis_ops.comparable_reasons(stored)} assert bool(found["reasons"]) is has_reasons # 整期按现查的表范围判 assert any(p.get("table") == "orders" and p.get("basis") == "live" for p in stored["probes"]) recomputes(stored) assert not [e for e in await events(row.id, "log", "card") if e.get("code") == "metric_comparability_skipped"] def _region_sql(region: str, start: str, end: str) -> str: return f"SELECT SUM(amount) AS gmv FROM orders WHERE region = '{region}' AND day BETWEEN '{start}' AND '{end}'" @pytest.mark.parametrize("south_week, verdict", [ (("2026-05-18", "2026-05-24"), None), # 同一周、只差地区:是两段不是两期,不查 (("2026-05-11", "2026-05-17"), "incomparable"), # 日期和地区都不同:两边除了日期,筛选的还不一样 ]) async def test_a_ratio_between_two_regions_is_not_a_period_comparison(south_week, verdict): """华东对华南的倍数:两份同形 SQL 只差地区字面量,不做可比性检查,带契约的出口照常正式出具(以前判成两期不可比、降档)。 日期和地区都不同的照查,真算子判不可比,出口记缺口降档。""" metrics = [{"id": "times", "name": "华东对华南的倍数", "decimals": 4, "expression": "cell(nodes.east, 0, 'gmv') / cell(nodes.south, 0, 'gmv')"}] graph = chain(node("start", "input"), node("east", "tool", tool=TOOL, args={"sql": _region_sql("华东", "2026-05-18", "2026-05-24")}), node("south", "tool", tool=TOOL, args={"sql": _region_sql("华南", *south_week)}), node("card", "metrics", caliber="周报口径", metrics=metrics), node("out", "output", fields=[{"name": "r", "value": "{{ nodes.card.text }}"}], contract={"metrics_from": ["card"], "required": ["times"]})) run = await run_manager.start(graph=graph, input_payload={}) row = await wait(run.id) assert row.status == "succeeded", row.error [finished] = await events(row.id, "node.finished", "card") [metric] = artifact_store.load(finished["artifact"])["metrics"] issuance = row.output["_issuance"] assert not [e for e in await events(row.id, "log", "card") if e.get("code") == "metric_comparability_skipped"] if verdict is None: assert "comparability" not in metric assert issuance["tier"] == "formal" and not issuance.get("gaps") and not issuance.get("comparability") return found = metric["comparability"] assert found["verdict"] == verdict and found["reasons"] == ["两边除了日期,筛选的还不一样"] assert issuance["tier"] == "degraded" assert issuance["gaps"] == ["指标「华东对华南的倍数」两期不可比:两边除了日期,筛选的还不一样"] recomputes(artifact_store.load(found["artifact"])) # -------------------------------------------------------------------------- # 测试夹具:复用名叫 shop 的源,前后都删干净 # -------------------------------------------------------------------------- async def test_a_scratch_source_starts_clean_and_forgets_its_probes(tmp_path): """分析相关的用例都建名叫 shop 的源(tests/conftest.py 的 scratch_source):别的用例留下的数据目录、coverage 的现查缓存里 按同一个源 id 记下的表范围(5 分钟有效)会改掉这次的判定。sqlite_source 建的源是新的 id,同名的旧源连同数据目录、现查 缓存先删掉;forget_source 删源(数据目录随外键级联删除)、清掉现查缓存里它的表范围。""" from app.analysis.probe import make_prober from app.data import catalog as data_catalog from app.data import coverage from app.data.catalog import make_item from app.db.models import CatalogNote, DataSource from tests.fixtures.sources import forget_source, sqlite_source path = tmp_path / "other.db" db = sqlite3.connect(path) db.executescript("CREATE TABLE orders (day TEXT, amount INTEGER); INSERT INTO orders VALUES ('2026-01-05', 1);") db.close() async def leave_traces(source_id): async with SessionLocal() as session: await data_catalog.write_entry(session, source_id, "orders", {"columns": {"amount": { "measure": make_item("flow", "human")}}}, if_version=0, actor=None) row = await session.get(DataSource, source_id) probe = await make_prober({"scratch": row})("scratch", "orders", "day") assert probe["max"] == "2026-01-05" and probe["basis"] == "live" assert any(k[0] == source_id for k in coverage._LIVE) async def traces(source_id): async with SessionLocal() as session: notes = (await session.execute(select(CatalogNote).where(CatalogNote.source_id == source_id))).scalars().all() row = await session.get(DataSource, source_id) return {"row": row is not None, "catalog": len(notes), "probes": sum(k[0] == source_id for k in coverage._LIVE)} old = await sqlite_source("scratch", path) await leave_traces(old) new = await sqlite_source("scratch", path) assert new != old and await traces(old) == {"row": False, "catalog": 0, "probes": 0} assert await traces(new) == {"row": True, "catalog": 0, "probes": 0} await leave_traces(new) await forget_source(new) assert await traces(new) == {"row": False, "catalog": 0, "probes": 0}