CI only lints app/, so 11 findings had accumulated in tests/ and scripts/. Mechanical and behaviour-neutral, but two were not auto-fixable and needed a judgement call rather than `ruff --fix`: - E741 in run_fip_breadth_diagnostics: `l` is the OHLCV low and is genuinely used, so this was a naming fix (`l` -> `lo`), not a deletion. - F841 in the same file: `vol_ix`/`momr_ix` are assigned from a pure local `_index()` and never read, so removing them cannot change any output. Their upstream `vol_weeks`/`momr_weeks` maps *are* used further down and stay; the comment above `_index` was corrected to say so. The rest are unused imports and f-strings without placeholders (literal markdown table headers, so identical output). Verified beyond the linter, since py_compile does not catch a removed-but-used import: every removed symbol has zero remaining references, all scripts compile, and the full unit suite passes (852 passed, 1 skipped). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
4.4 KiB
Python
133 lines
4.4 KiB
Python
"""Completion-manifest guard for research.sqlite breadth runs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
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
|