Files
AgentLab/backend/tests/test_analysis_probe.py

130 lines
7.0 KiB
Python
Raw Permalink 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.
"""可比性检查的现查(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]))