test: prove old telemetry tables gain the tenant column safely

This commit is contained in:
2026-08-17 11:49:07 -04:00
parent 6ad58a6553
commit d553d142c3
2 changed files with 360 additions and 0 deletions
+126
View File
@@ -2,6 +2,7 @@
import asyncio
import json
import os
import sqlite3
import subprocess
from pathlib import Path
@@ -285,6 +286,131 @@ class TestSQLiteColumnBackfill:
recorder.close()
# issue #11 之前的表形态: 22 个 recorder 字段 + created_at = 23 个物理列,没有任何租户维度
_PRE_TENANT_DDL = """
CREATE TABLE llm_calls (
call_id TEXT PRIMARY KEY,
parent_call_id TEXT,
session_id TEXT,
model TEXT NOT NULL,
provider TEXT NOT NULL,
source_name TEXT NOT NULL,
messages TEXT NOT NULL,
response TEXT NOT NULL,
thinking TEXT NOT NULL DEFAULT '',
prompt_tokens INTEGER NOT NULL,
completion_tokens INTEGER NOT NULL,
usage_source TEXT NOT NULL,
latency_ms INTEGER NOT NULL,
ttft_ms REAL,
max_inter_token_ms REAL,
cache_hit INTEGER NOT NULL DEFAULT 0,
error TEXT,
cost REAL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
cached_prompt_tokens INTEGER,
model_reported TEXT,
sampling TEXT,
reasoning_tokens INTEGER
);
"""
_PRE_TENANT_INSERT = (
"INSERT INTO llm_calls (call_id, model, provider, source_name, messages, response, "
"prompt_tokens, completion_tokens, usage_source, latency_ms) "
"VALUES ('old-row', 'm', 'p', 's1', '[]', 'contract text', 1, 2, 'measured', 10)"
)
def _make_pre_tenant_db(path: Path) -> None:
"""造一个 issue #11 之前的库: 22 字段旧表 + 一行没有租户归属的历史数据。"""
conn = sqlite3.connect(path)
conn.execute(_PRE_TENANT_DDL)
conn.execute(_PRE_TENANT_INSERT)
conn.commit()
conn.close()
class TestSQLiteCallerDimensionsAcceptance:
"""issue #11 的机械化验收(SQLite 侧,真实临时文件): 新建库 / 旧表补列 / 补列失败方向。"""
async def test_fresh_db_round_trips_the_dimensions(self, tmp_path):
"""新建库: 列齐全,且维度值原样读回——只验列存在会漏掉写错列位的错。"""
db = tmp_path / "fresh.db"
recorder = SQLiteRecorder(db)
await _record_minimal(
recorder, call_id="c-dim", tenant_id="tenant-a", meta='{"batch": "b7"}'
)
recorder.close()
conn = sqlite3.connect(db)
assert [r[1] for r in conn.execute("PRAGMA table_info(llm_calls)")] == _EXPECTED_COLUMNS
row = conn.execute(
"SELECT tenant_id, meta FROM llm_calls WHERE call_id = 'c-dim'"
).fetchone()
assert row[0] == "tenant-a"
assert json.loads(row[1]) == {"batch": "b7"}
async def test_pre_tenant_table_gains_columns_and_old_rows_stay_auditable(self, tmp_path):
"""22 字段旧表补列后,新行带维度,而**老行的 tenant_id 是空串而非 NULL**。
这条直接验收 issue #11 的核心论点(先启用落库、后加列,补列之前的行没有
租户归属)。断言方向必须是空串: PG 的 RLS `USING` 表达式对返回 false **或
NULL** 的行一律隐藏且不报错,故 NULL 的 `tenant_id` 不是"未归属",而是对
所有人永久不可见的黑洞;哨兵空串则能被一条 `COUNT(*) WHERE tenant_id = ''`
审计出来,历史欠账是可见、可量化、可补录的。
"""
db = tmp_path / "pre_tenant.db"
_make_pre_tenant_db(db)
recorder = SQLiteRecorder(db)
await _record_minimal(recorder, call_id="new-row", tenant_id="tenant-a", meta='{"k": 1}')
recorder.close()
conn = sqlite3.connect(db)
cols = [r[1] for r in conn.execute("PRAGMA table_info(llm_calls)")]
assert cols == _EXPECTED_COLUMNS # 22 → 24 个 recorder 字段(+ created_at 共 25 物理列)
rows = dict(conn.execute("SELECT call_id, tenant_id FROM llm_calls").fetchall())
assert rows["new-row"] == "tenant-a"
assert rows["old-row"] == "" # 不是 None: NULL 会被 RLS 静默吞掉
assert (
conn.execute("SELECT meta FROM llm_calls WHERE call_id = 'old-row'").fetchone()[0]
== "{}"
)
async def test_readonly_file_backfill_failure_keeps_the_recorder_alive(self, tmp_path):
"""补列失败的降级方向(SQLite 等价构造: 文件只读)。
SQLite 没有角色权限模型,与 PG「只有 SELECT/INSERT 权限的角色」等价的构造
是文件本身只读。库文件必须**预先置为 WAL 且干净关闭**,否则 `__init__` 的
`PRAGMA journal_mode=WAL` 会先撞上只读而让失败点跑到补列之前,测不到本用例
要测的那条分支(实测: 非 WAL 库 chmod 444 后该 PRAGMA 报 readonly database)。
只读库连 INSERT 都做不了,故这里**只断言**补列失败不清空 `_conn`、不抛出
`__init__`(sqlite.py `_backfill_columns` 那条纪律),不断言"写入仍成功"
"""
if os.geteuid() == 0:
pytest.skip("root 无视文件权限位,只读构造不成立")
db = tmp_path / "readonly.db"
conn = sqlite3.connect(db)
conn.execute("PRAGMA journal_mode=WAL") # 预置 WAL: 让只读连接不必改日志模式
conn.execute(_PRE_TENANT_DDL)
conn.execute(_PRE_TENANT_INSERT)
conn.commit()
conn.close()
db.chmod(0o444)
recorder = SQLiteRecorder(db) # 不得抛
assert recorder._conn is not None # 补列失败 ≠ recorder 失能
await _record_minimal(recorder, call_id="doomed") # 只读库写不进,但不得抛
recorder.close()
db.chmod(0o644) # 还原,让 tmp_path 清理不受阻
stale = sqlite3.connect(db)
assert [r[1] for r in stale.execute("PRAGMA table_info(llm_calls)")] == (
_EXPECTED_COLUMNS[:-2]
) # 补列确实没成功,用例不是在只读库上空转
class _FakePgConn:
"""记录执行过的语句;可让 ALTER/CREATE/探测抛错以模拟权限不足与抖动。