Merge branch 'research/fip-breadth-ic' — park Phase B fip breadth
Brings env-gated liquid-breadth harness hooks, research tooling, compact evidence, and the completion-manifest race guard. No production behavior change when liquid env vars are unset. Nothing to deploy.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Completion-manifest guard for research.sqlite breadth runs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPTS = ROOT / "scripts"
|
||||
if str(SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
from research_snapshot_manifest import ( # noqa: E402
|
||||
assert_research_snapshot_complete,
|
||||
clear_manifest,
|
||||
load_manifest,
|
||||
manifest_path_for,
|
||||
write_completion_manifest,
|
||||
)
|
||||
|
||||
|
||||
def _tiny_research_db(path: Path, *, tickers: int = 3, bars_each: int = 5) -> None:
|
||||
engine = create_engine(f"sqlite:///{path.resolve().as_posix()}", future=True)
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE tickers ("
|
||||
"id INTEGER PRIMARY KEY, symbol TEXT NOT NULL UNIQUE, "
|
||||
"name TEXT, created_at TEXT)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE ohlcv_records ("
|
||||
"id INTEGER PRIMARY KEY, ticker_id INTEGER, date TEXT, "
|
||||
"open REAL, high REAL, low REAL, close REAL, volume INTEGER, "
|
||||
"created_at TEXT)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE research_rank_only ("
|
||||
"ticker_id INTEGER PRIMARY KEY, symbol TEXT NOT NULL UNIQUE)"
|
||||
)
|
||||
)
|
||||
for i in range(tickers):
|
||||
sym = f"T{i}"
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO tickers (id, symbol, name, created_at) "
|
||||
"VALUES (:id, :sym, NULL, '2026-01-01')"
|
||||
),
|
||||
{"id": i + 1, "sym": sym},
|
||||
)
|
||||
if i > 0:
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO research_rank_only (ticker_id, symbol) "
|
||||
"VALUES (:id, :sym)"
|
||||
),
|
||||
{"id": i + 1, "sym": sym},
|
||||
)
|
||||
for d in range(bars_each):
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO ohlcv_records "
|
||||
"(ticker_id, date, open, high, low, close, volume, created_at) "
|
||||
"VALUES (:tid, :date, 1,1,1,1,100, '2026-01-01')"
|
||||
),
|
||||
{"tid": i + 1, "date": f"2026-01-{d+1:02d}"},
|
||||
)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_write_and_assert_complete(tmp_path: Path) -> None:
|
||||
snap = tmp_path / "research.sqlite"
|
||||
_tiny_research_db(snap)
|
||||
path = write_completion_manifest(snap, complete=True, sources={"t": "unit"})
|
||||
assert path == manifest_path_for(snap)
|
||||
assert path.exists()
|
||||
|
||||
m = assert_research_snapshot_complete(snap)
|
||||
assert m["complete"] is True
|
||||
assert m["ticker_count"] == 3
|
||||
assert m["ohlcv_row_count"] == 15
|
||||
assert m["rank_only_count"] == 2
|
||||
assert m["live_counts"]["ticker_count"] == 3
|
||||
|
||||
|
||||
def test_refuse_missing_manifest(tmp_path: Path) -> None:
|
||||
snap = tmp_path / "research.sqlite"
|
||||
_tiny_research_db(snap)
|
||||
with pytest.raises(SystemExit, match="manifest missing"):
|
||||
assert_research_snapshot_complete(snap)
|
||||
|
||||
|
||||
def test_refuse_incomplete_flag(tmp_path: Path) -> None:
|
||||
snap = tmp_path / "research.sqlite"
|
||||
_tiny_research_db(snap)
|
||||
write_completion_manifest(snap, complete=False, limit=50)
|
||||
with pytest.raises(SystemExit, match="marked incomplete"):
|
||||
assert_research_snapshot_complete(snap)
|
||||
|
||||
|
||||
def test_refuse_count_mismatch(tmp_path: Path) -> None:
|
||||
snap = tmp_path / "research.sqlite"
|
||||
_tiny_research_db(snap)
|
||||
write_completion_manifest(snap, complete=True)
|
||||
# Tamper: change live DB after manifest written
|
||||
engine = create_engine(f"sqlite:///{snap.resolve().as_posix()}", future=True)
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO tickers (id, symbol, name, created_at) "
|
||||
"VALUES (99, 'EXTRA', NULL, '2026-01-01')"
|
||||
)
|
||||
)
|
||||
engine.dispose()
|
||||
with pytest.raises(SystemExit, match="does not match"):
|
||||
assert_research_snapshot_complete(snap)
|
||||
|
||||
|
||||
def test_clear_manifest(tmp_path: Path) -> None:
|
||||
snap = tmp_path / "research.sqlite"
|
||||
_tiny_research_db(snap)
|
||||
write_completion_manifest(snap, complete=True)
|
||||
assert load_manifest(snap) is not None
|
||||
clear_manifest(snap)
|
||||
assert load_manifest(snap) is None
|
||||
Reference in New Issue
Block a user