mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 05:51:57 +00:00
130 lines
7.0 KiB
Python
130 lines
7.0 KiB
Python
"""可比性检查的现查(app/analysis/probe.py):make_prober 在真的数据源上探表的日期范围、在库里算按运行当天现算的界,遮罩列
|
||
不探、超时限不探、现查不成退回剖析事实;Book 先登记、查完再算,结论只记最后真用到的。库是现造的 SQLite 文件,表名、列名、
|
||
数字都是虚构的。"""
|
||
from __future__ import annotations
|
||
|
||
import sqlite3
|
||
import uuid
|
||
from datetime import date, datetime, timedelta, timezone
|
||
|
||
import pytest
|
||
|
||
from app import analysis
|
||
from app.analysis.contract import effective_settings, same
|
||
from app.analysis.probe import Book, make_prober
|
||
from app.data import introspect
|
||
from app.data.engine import engines
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import DataSource
|
||
from tests.analysis_samples import CATALOGS, recomputed, snap
|
||
|
||
CLOCK = "2026-10-21T09:30:00"
|
||
#: SQLite 的 date('now') 按 UTC 算:表里的数据造到 UTC 的昨天
|
||
UTC_TODAY = datetime.now(timezone.utc).date()
|
||
LAST_DAY = UTC_TODAY - timedelta(days=1)
|
||
|
||
|
||
@pytest.fixture
|
||
async def shop(tmp_path):
|
||
"""orders:order_date 从 2026-01-01 到昨天(UTC),每天一单;customers 的 signup_date、phone 是遮罩列。"""
|
||
path = tmp_path / f"{uuid.uuid4().hex[:8]}.db"
|
||
db = sqlite3.connect(path)
|
||
db.executescript("CREATE TABLE orders (order_date TEXT, region TEXT, amount INTEGER);"
|
||
"CREATE TABLE customers (signup_date TEXT, phone TEXT);"
|
||
"INSERT INTO customers VALUES ('2026-01-05', '13900000000');")
|
||
day = date(2026, 1, 1)
|
||
while day <= LAST_DAY:
|
||
db.execute("INSERT INTO orders VALUES (?, '华东', 100)", (day.isoformat(),))
|
||
day += timedelta(days=1)
|
||
db.commit()
|
||
db.close()
|
||
name = f"shop_{uuid.uuid4().hex[:8]}"
|
||
async with SessionLocal() as session:
|
||
row = DataSource(name=name, kind="sqlite", database=str(path), readonly=True, enabled=True,
|
||
options={"mask_columns": ["phone", "signup_date"]})
|
||
session.add(row)
|
||
await session.flush()
|
||
row.schema_cache = await introspect.introspect(row)
|
||
await session.commit()
|
||
await session.refresh(row)
|
||
yield row
|
||
await engines.invalidate(row.id)
|
||
|
||
|
||
async def test_the_live_range_of_a_column(shop):
|
||
prober = make_prober({shop.name: shop}, clock=lambda: CLOCK)
|
||
probe = await prober(shop.name, "orders", "order_date")
|
||
assert probe == {"source": shop.name, "table": "orders", "column": "order_date", "min": "2026-01-01",
|
||
"max": LAST_DAY.isoformat(), "basis": "live", "at": CLOCK}
|
||
assert prober.dialect(shop.name) == "sqlite" and prober.dialect("nope") is None
|
||
assert await prober("nope", "orders", "order_date") is None
|
||
|
||
|
||
async def test_masked_columns_are_never_probed(shop):
|
||
prober = make_prober({shop.name: shop})
|
||
assert await prober(shop.name, "customers", "signup_date") is None # 数据源设的遮罩列
|
||
explicit = make_prober({shop.name: shop}, masked={shop.name: ["ORDER_DATE"]})
|
||
assert await explicit(shop.name, "orders", "order_date") is None
|
||
|
||
|
||
async def test_a_failed_probe_falls_back_to_profile_facts(shop):
|
||
async def facts(table):
|
||
return {"at": "2026-10-20T02:00:00", "date_ranges": [{"column": "paid_on", "min": "2026-02-01",
|
||
"max": "2026-10-19"}]}
|
||
|
||
prober = make_prober({shop.name: shop}, facts={shop.name: facts}, clock=lambda: CLOCK)
|
||
probe = await prober(shop.name, "payments", "paid_on") # 没有这张表:现查不成
|
||
assert probe == {"source": shop.name, "table": "payments", "column": "paid_on", "min": "2026-02-01",
|
||
"max": "2026-10-19", "basis": "profile", "at": "2026-10-20"}
|
||
|
||
|
||
async def test_the_budget_stops_further_probes(shop):
|
||
prober = make_prober({shop.name: shop}, budget=0)
|
||
await prober(shop.name, "orders", "order_date")
|
||
assert await prober(shop.name, "orders", "order_date") is None
|
||
|
||
|
||
async def test_bounds_are_computed_in_the_database(shop):
|
||
prober = make_prober({shop.name: shop}, clock=lambda: CLOCK)
|
||
probe = await prober.bounds(shop.name, "DATE('now', '-7 days')")
|
||
assert probe["expr"] == "DATE('now', '-7 days')" and probe["basis"] == "live" and probe["at"] == CLOCK
|
||
assert date.fromisoformat(probe["value"]) == UTC_TODAY - timedelta(days=7)
|
||
assert await prober.bounds(shop.name, "no_such_function(1)") is None
|
||
|
||
|
||
def test_the_book_registers_first_and_records_only_what_was_used():
|
||
book = Book({})
|
||
assert book.range("shop", "orders", "order_date") is None and book.bound("shop", "DATE('now')") is None
|
||
assert book.missing == [("range", "shop", "orders", "order_date"), ("bound", "shop", "DATE('now')")]
|
||
found = {"source": "shop", "table": "orders", "column": "order_date", "min": "2026-01-01", "max": "2026-10-18",
|
||
"basis": "live", "at": CLOCK}
|
||
filled = Book({("range", "shop", "orders", "order_date"): found, ("bound", "shop", "DATE('now')"): None})
|
||
assert filled.range("shop", "ORDERS", "Order_Date") is found # 表名、列名不分大小写
|
||
filled.bound("shop", "DATE('now')")
|
||
assert filled.missing == [] and filled.used == [found]
|
||
replay = Book([found]) # 复算:记下的就是全部
|
||
assert replay.bound("shop", "DATE('now', '-1 day')") is None and replay.missing == []
|
||
|
||
|
||
async def test_a_full_comparability_check_against_the_database_records_its_probes(shop):
|
||
sql = ("SELECT SUM(amount) AS gmv FROM orders WHERE order_date >= date('now', '{a}') "
|
||
"AND order_date < date('now', '{b}')")
|
||
base = snap("Q1", sql.format(a="-14 days", b="-7 days"), ["gmv"], [[700]], source=shop.name)
|
||
target = snap("Q2", sql.format(a="-7 days", b="0 days"), ["gmv"], [[300]], source=shop.name)
|
||
prober = make_prober({shop.name: shop}, clock=lambda: CLOCK)
|
||
result = await analysis.run({"op": "comparable", "params": {"base_query": "Q1", "target_query": "Q2",
|
||
"measure": "gmv"}},
|
||
{"Q1": base, "Q2": target}.get, settings=effective_settings(None),
|
||
context={"today": UTC_TODAY, "probe": prober,
|
||
"catalogs": {shop.name: CATALOGS["shop"]}})
|
||
exprs = [p["expr"] for p in result["probes"] if "expr" in p]
|
||
assert exprs == ["DATE('now', '-14 days')", "DATE('now', '-7 days')", "DATE('now', '0 days')"]
|
||
ranges = [p for p in result["probes"] if "table" in p]
|
||
assert [(p["table"], p["column"], p["max"], p["basis"]) for p in ranges] == \
|
||
[("orders", "order_date", LAST_DAY.isoformat(), "live")]
|
||
assert result["values"]["base_days"] == "7" and result["values"]["target_days"] == "7"
|
||
assert result["values"]["target_end"] == LAST_DAY.isoformat()
|
||
assert all(i["dialect"] == "sqlite" for i in result["inputs"])
|
||
assert result["verdict"]["code"] == "comparable" and result["phrase"] == "两期可比"
|
||
assert same(result, recomputed(result, [base, target]))
|