Audit findings 6.1-6.4 + the audit's remediation plan itself (docs/superpowers/plans/2026-07-20-full-audit-remediation-plan.md). - ruff: 34 → 0 (unused imports/f-strings auto-fixed; E702 semicolon lines split in test files; one unused local dropped). Suite still 819 green. - CLAUDE.md + PROJECT_STATE.md no longer claim "no database / settings in settings.json": the harness selects filesystem OR PostgreSQL session storage (repository.py, server mode), and settings flow through harness preferences with the JSON file as fallback only. - tools/replay: stub /me (SPA boot was parsing the SPA's own HTML as JSON) and /runtime/prewarm. - failSession best-effort persistence now logs its failure server-side instead of vanishing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
192 lines
6.0 KiB
Python
192 lines
6.0 KiB
Python
"""L1: tht.ctetest — CTE file structure + test-record append-only ledger.
|
|
|
|
The CTE phase (F6) writes one .sql file per CTE block (WITH ... AS (...) only,
|
|
NO trailing SELECT) and a cte_tests.json ledger of execution outcomes. These tests
|
|
pin the structural helpers (has_trailing_select / last_cte_name / build_test_sql)
|
|
and the ledger I/O (load/append, tolerant of JSON-array and JSONL formats).
|
|
Pure logic, no DB.
|
|
"""
|
|
import json
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from tht.ctetest import (
|
|
CteError,
|
|
CteTestRecord,
|
|
_jsonable,
|
|
append_cte_test,
|
|
build_test_sql,
|
|
has_trailing_select,
|
|
last_cte_name,
|
|
load_cte_tests,
|
|
)
|
|
|
|
# CTE file contents
|
|
_CTE_OK = "WITH a AS (SELECT 1), b AS (SELECT * FROM a)"
|
|
_CTE_WITH_SELECT = "WITH a AS (SELECT 1) SELECT * FROM a"
|
|
_CTE_SYNTAX_GARBAGE = "WITH a AS (SELECT FROM WHERE"
|
|
|
|
|
|
# --- has_trailing_select (semantics: True = file VIOLATES the rule) ----------
|
|
|
|
def test_correct_cte_file_has_no_trailing_select():
|
|
# a correct CTE file has only WITH ... AS (...), no SELECT -> False
|
|
assert has_trailing_select(_CTE_OK) is False
|
|
|
|
|
|
def test_file_with_trailing_select_flagged():
|
|
# a file that already has a SELECT final VIOLATES the rule -> True
|
|
assert has_trailing_select(_CTE_WITH_SELECT) is True
|
|
|
|
|
|
def test_unparsable_file_returns_false_no_hint():
|
|
# garbage: no hint (can't tell), returns False
|
|
assert has_trailing_select(_CTE_SYNTAX_GARBAGE) is False
|
|
|
|
|
|
# --- last_cte_name ----------------------------------------------------------
|
|
|
|
def test_last_cte_name_of_two_cte_block():
|
|
assert last_cte_name(_CTE_OK) == "b"
|
|
|
|
|
|
def test_last_cte_name_single_cte():
|
|
assert last_cte_name("WITH solo AS (SELECT 1)") == "solo"
|
|
|
|
|
|
def test_last_cte_name_rejects_non_with():
|
|
with pytest.raises(CteError, match="WITH"):
|
|
last_cte_name("SELECT 1")
|
|
|
|
|
|
def test_last_cte_name_rejects_unparsable():
|
|
with pytest.raises(CteError, match="non parsabile"):
|
|
last_cte_name("WITH a AS (SELECT FROM WHERE")
|
|
|
|
|
|
# --- build_test_sql ---------------------------------------------------------
|
|
|
|
def test_build_test_sql_appends_select_from_last_cte():
|
|
sql = build_test_sql(_CTE_OK)
|
|
assert sql.endswith("SELECT * FROM b")
|
|
assert sql.startswith("WITH a AS") # the WITH block preserved
|
|
|
|
|
|
def test_build_test_sql_single_cte():
|
|
sql = build_test_sql("WITH solo AS (SELECT 1)")
|
|
assert sql.rstrip().endswith("SELECT * FROM solo")
|
|
|
|
|
|
# --- ledger I/O: load_cte_tests / append_cte_test ---------------------------
|
|
|
|
def _record(**kw) -> CteTestRecord:
|
|
base = dict(
|
|
name="ablazione_q", ts=datetime(2025, 1, 1, 12, 0), sql_hash="abc123",
|
|
status="ok", columns=["x"], row_sample=5, execution_ms=42,
|
|
)
|
|
base.update(kw)
|
|
return CteTestRecord(**base)
|
|
|
|
|
|
def test_load_empty_when_no_file(tmp_path):
|
|
assert load_cte_tests(tmp_path) == []
|
|
|
|
|
|
def test_append_then_load_roundtrip(tmp_path):
|
|
append_cte_test(tmp_path, _record(name="a", status="ok"))
|
|
append_cte_test(tmp_path, _record(name="b", status="error", error="boom"))
|
|
loaded = load_cte_tests(tmp_path)
|
|
assert len(loaded) == 2
|
|
assert loaded[0].name == "a" and loaded[0].status == "ok"
|
|
assert loaded[1].name == "b" and loaded[1].status == "error"
|
|
assert loaded[1].error == "boom"
|
|
|
|
|
|
def test_load_tolerates_json_array_format(tmp_path):
|
|
# the ledger may be either JSONL (append-style) or a JSON array
|
|
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].name == "x"
|
|
|
|
|
|
def test_load_tolerates_mixed_array_then_jsonl(tmp_path):
|
|
# defensive: array + appended JSONL rows (the tolerant parser must handle both)
|
|
path = tmp_path / "cte_tests.json"
|
|
path.write_text(
|
|
json.dumps([{"name": "x", "ts": "2025-01-01T00:00:00", "sql_hash": "h", "status": "ok"}])
|
|
+ "\n"
|
|
+ json.dumps({"name": "y", "ts": "2025-01-01T00:00:00", "sql_hash": "h2", "status": "ok"})
|
|
+ "\n"
|
|
)
|
|
loaded = load_cte_tests(tmp_path)
|
|
assert {r.name for r in loaded} == {"x", "y"}
|
|
|
|
|
|
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
|