fix: harden the observability fields against the verifier findings
This commit is contained in:
@@ -119,6 +119,105 @@ async def _fetch(dsn: str, sql: str, *args):
|
||||
await conn.close()
|
||||
|
||||
|
||||
_LEGACY_DDL = """
|
||||
CREATE TABLE {schema}.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 DOUBLE PRECISION,
|
||||
max_inter_token_ms DOUBLE PRECISION,
|
||||
cache_hit BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
error TEXT,
|
||||
cost DOUBLE PRECISION,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def legacy_schema(dsn):
|
||||
"""在**自建的临时 schema** 里造一张 18 列旧表,验证补列(issue #3)。
|
||||
|
||||
绝不碰共享的 public.llm_calls: 用 search_path 把 recorder 指向临时 schema,
|
||||
teardown 只 DROP 自己建的 schema。
|
||||
"""
|
||||
import asyncpg
|
||||
|
||||
name = f"pgwtest_{uuid4().hex[:8]}"
|
||||
conn = await asyncpg.connect(dsn, timeout=10)
|
||||
try:
|
||||
await conn.execute(f"CREATE SCHEMA {name}")
|
||||
await conn.execute(_LEGACY_DDL.format(schema=name))
|
||||
finally:
|
||||
await conn.close()
|
||||
sep = "&" if "?" in dsn else "?"
|
||||
yield f"{dsn}{sep}options=-csearch_path%3D{name}", name
|
||||
conn = await asyncpg.connect(dsn, timeout=10)
|
||||
try:
|
||||
await conn.execute(f"DROP SCHEMA {name} CASCADE")
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
class TestObservabilityColumns:
|
||||
"""issue #3: 两列写入可回读,且已存在的 18 列旧表会被自动补列。"""
|
||||
|
||||
async def test_values_round_trip(self, dsn):
|
||||
recorder = PostgresRecorder(dsn)
|
||||
try:
|
||||
await _record_minimal(recorder, call_id=_cid("hit"), cached_prompt_tokens=64)
|
||||
await _record_minimal(recorder, call_id=_cid("zero"), cached_prompt_tokens=0)
|
||||
await _record_minimal(recorder, call_id=_cid("model"), model_reported="MiniMax-01")
|
||||
rows = await _fetch(
|
||||
dsn,
|
||||
"SELECT call_id, cached_prompt_tokens, model_reported FROM llm_calls "
|
||||
"WHERE call_id LIKE $1",
|
||||
f"{_RUN_PREFIX}-%",
|
||||
)
|
||||
by_id = {r["call_id"]: r for r in rows}
|
||||
assert by_id[_cid("hit")]["cached_prompt_tokens"] == 64
|
||||
assert by_id[_cid("zero")]["cached_prompt_tokens"] == 0 # 真实零命中 ≠ NULL
|
||||
assert by_id[_cid("model")]["cached_prompt_tokens"] is None
|
||||
assert by_id[_cid("model")]["model_reported"] == "MiniMax-01"
|
||||
finally:
|
||||
await recorder.aclose()
|
||||
|
||||
async def test_legacy_table_is_upgraded_in_place(self, legacy_schema):
|
||||
"""18 列旧表不补列的话,每行写入都会被逐行 warning 丢弃(遥测静默全失)。"""
|
||||
schema_dsn, schema = legacy_schema
|
||||
recorder = PostgresRecorder(schema_dsn)
|
||||
try:
|
||||
await _record_minimal(
|
||||
recorder, call_id=_cid("legacy"), cached_prompt_tokens=7, model_reported="m-real"
|
||||
)
|
||||
cols = await _fetch(
|
||||
schema_dsn,
|
||||
"SELECT column_name FROM information_schema.columns "
|
||||
"WHERE table_schema = $1 AND table_name = 'llm_calls' ORDER BY ordinal_position",
|
||||
schema,
|
||||
)
|
||||
# ALTER 只能追加到末尾: 与新建库的列序一致才不会分叉
|
||||
assert [r["column_name"] for r in cols] == _EXPECTED_COLUMNS
|
||||
rows = await _fetch(
|
||||
schema_dsn,
|
||||
"SELECT cached_prompt_tokens, model_reported FROM llm_calls WHERE call_id = $1",
|
||||
_cid("legacy"),
|
||||
)
|
||||
assert (rows[0]["cached_prompt_tokens"], rows[0]["model_reported"]) == (7, "m-real")
|
||||
finally:
|
||||
await recorder.aclose()
|
||||
|
||||
|
||||
class TestSchema:
|
||||
async def test_schema_has_frozen_columns_in_order(self, dsn):
|
||||
recorder = PostgresRecorder(dsn)
|
||||
|
||||
@@ -360,6 +360,19 @@ class TestObservabilityFields:
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.model_reported == "MiniMax-Text-01-250321"
|
||||
|
||||
async def test_empty_first_model_does_not_block_a_later_real_one(self):
|
||||
"""首帧报空串不得锁死 sink: 守卫按"有效值"判断,否则真实版本会丢。"""
|
||||
|
||||
def handler(request):
|
||||
return _sse_stream(
|
||||
_chunk(content="a", model=""),
|
||||
_chunk(content="b", model="MiniMax-Text-01-250321"),
|
||||
_chunk(usage=_USAGE),
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.model_reported == "MiniMax-Text-01-250321"
|
||||
|
||||
@pytest.mark.parametrize("bad", [None, "", " ", 123, {}])
|
||||
async def test_missing_or_malformed_model_is_none(self, bad):
|
||||
def handler(request):
|
||||
|
||||
@@ -77,6 +77,10 @@ class TestCachedInputTier:
|
||||
def test_no_hit_is_billed_in_full(self, cached):
|
||||
assert self._CACHED.cost("m", 1_000_000, 0, cached) == pytest.approx(10.0)
|
||||
|
||||
def test_negative_cached_is_billed_in_full(self):
|
||||
"""负数命中数不得抬高成本: cost() 是公共方法,外部输入须校验后使用(P5)。"""
|
||||
assert self._CACHED.cost("m", 1_000_000, 0, -500_000) == pytest.approx(10.0)
|
||||
|
||||
def test_cached_over_prompt_is_clamped_and_never_negative(self):
|
||||
"""网关口径异常时按输入总数夹取: 全部按缓存价,不得算出负成本。"""
|
||||
clamped = self._CACHED.cost("m", 1_000_000, 0, 5_000_000)
|
||||
|
||||
@@ -202,8 +202,9 @@ class TestSQLiteColumnBackfill:
|
||||
async def test_backfill_failure_keeps_the_recorder_usable(self, tmp_path):
|
||||
"""补列失败只能逐行降级,绝不能把 recorder 整体变成 no-op(设计 D1 纪律)。
|
||||
|
||||
把 llm_calls 建成 view: 表不存在故 CREATE TABLE IF NOT EXISTS 会撞名失败,
|
||||
ALTER 也无从谈起——这是最坏路径。
|
||||
把 llm_calls 建成同名 view: `CREATE TABLE IF NOT EXISTS` 遇 view 静默
|
||||
no-op(不抛),随后的 ALTER 才抛 "Cannot add a column to a view"——正是
|
||||
补列失败这条分支。`_conn` 必须保持非 None,否则整个 recorder 永久失能。
|
||||
"""
|
||||
db = tmp_path / "view.db"
|
||||
conn = sqlite3.connect(db)
|
||||
@@ -213,6 +214,7 @@ class TestSQLiteColumnBackfill:
|
||||
conn.close()
|
||||
|
||||
recorder = SQLiteRecorder(db) # 不得抛
|
||||
assert recorder._conn is not None # 补列失败 ≠ recorder 失能(D1 纪律)
|
||||
await _record_minimal(recorder) # 不得抛
|
||||
recorder.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user