feat(harness): persist CTE preview rows, add tht cte info, --doc for cte plan
WS1 of review-gates-v2: gives the JS gate (WS2) deterministic data to build the cte_result v2 payload. - CteTestRecord gains optional preview_rows (JSON-coerced, truncated cells); test_cmd populates it from the bounded result rows. - New read-only `tht cte info <name> --session <id> [--json]`: plan index/total, persisted .sql, cte_plan_doc.json entry (if any), last CteTestRecord. Exits 1 with a clean stderr message on missing session/plan/name/sql. - `tht cte plan --doc -` validates a chain-doc JSON (ctes[].name must match --name, same order) and writes it to cte_plan_doc.json; cte_plan.json stays a plain list[str] (load-bearing for tht.phase.next_cte). --doc is optional.
This commit is contained in:
@@ -7,7 +7,8 @@ and the ledger I/O (load/append, tolerant of JSON-array and JSONL formats).
|
||||
Pure logic, no DB.
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -15,6 +16,7 @@ import pytest
|
||||
from tht.ctetest import (
|
||||
CteError,
|
||||
CteTestRecord,
|
||||
_jsonable,
|
||||
append_cte_test,
|
||||
build_test_sql,
|
||||
has_trailing_select,
|
||||
@@ -131,3 +133,60 @@ def test_corrupt_ledger_raises(tmp_path):
|
||||
(tmp_path / "cte_tests.json").write_text("{not valid json")
|
||||
with pytest.raises(CteError, match="corrotto"):
|
||||
load_cte_tests(tmp_path)
|
||||
|
||||
|
||||
# --- _jsonable ----------------------------------------------------------
|
||||
|
||||
def test_jsonable_passes_through_native_types():
|
||||
assert _jsonable(None) is None
|
||||
assert _jsonable(True) is True
|
||||
assert _jsonable(1) == 1
|
||||
assert _jsonable(1.5) == 1.5
|
||||
assert _jsonable("abc") == "abc"
|
||||
|
||||
|
||||
def test_jsonable_coerces_decimal_and_date_to_str():
|
||||
assert _jsonable(Decimal("12.34")) == "12.34"
|
||||
assert _jsonable(date(2025, 1, 1)) == "2025-01-01"
|
||||
assert _jsonable(datetime(2025, 1, 1, 12, 0, 0)) == "2025-01-01 12:00:00"
|
||||
|
||||
|
||||
def test_jsonable_truncates_long_strings():
|
||||
long_str = "x" * 500
|
||||
out = _jsonable(long_str)
|
||||
assert isinstance(out, str)
|
||||
assert len(out) <= 200
|
||||
|
||||
|
||||
def test_jsonable_truncates_long_coerced_values():
|
||||
class Weird:
|
||||
def __str__(self):
|
||||
return "y" * 500
|
||||
|
||||
out = _jsonable(Weird())
|
||||
assert len(out) <= 200
|
||||
|
||||
|
||||
# --- preview_rows on CteTestRecord ---------------------------------------
|
||||
|
||||
def test_preview_rows_optional_default_none():
|
||||
r = _record()
|
||||
assert r.preview_rows is None
|
||||
|
||||
|
||||
def test_preview_rows_roundtrip_through_ledger(tmp_path):
|
||||
rec = _record(name="a", preview_rows=[["x", 1], [None, 2.5]])
|
||||
append_cte_test(tmp_path, rec)
|
||||
loaded = load_cte_tests(tmp_path)
|
||||
assert loaded[0].preview_rows == [["x", 1], [None, 2.5]]
|
||||
|
||||
|
||||
def test_old_record_without_preview_rows_still_validates(tmp_path):
|
||||
# back-compat: an old cte_tests.json row has no "preview_rows" key at all
|
||||
path = tmp_path / "cte_tests.json"
|
||||
path.write_text(json.dumps({
|
||||
"name": "x", "ts": "2025-01-01T00:00:00", "sql_hash": "h", "status": "ok",
|
||||
}))
|
||||
loaded = load_cte_tests(tmp_path)
|
||||
assert len(loaded) == 1
|
||||
assert loaded[0].preview_rows is None
|
||||
|
||||
Reference in New Issue
Block a user