Files
learningcircuit--local-deep…/tests/database/test_research_strategy_fk_regression.py
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

151 lines
5.4 KiB
Python

"""Regression tests for the ResearchStrategy FK retarget (migration 0008).
Pre-fix: ``ResearchStrategy.research_id`` was ``Integer FK research.id``,
pointing at a dormant table that no production path uses. The live writer
``save_research_strategy`` passes ``research_history`` UUID strings, so
every commit raised ``FOREIGN KEY constraint failed`` once v1.6.0 enabled
``PRAGMA foreign_keys``.
Post-fix: the FK targets ``research_history.id`` (String(36)) with cascade
delete. This module verifies both the schema-level repair and the live
behavior the production code depends on.
"""
from __future__ import annotations
import pytest
from sqlalchemy import create_engine, event, inspect, text
from sqlalchemy.orm import Session
from local_deep_research.database.alembic_runner import run_migrations
from local_deep_research.database.models.research import ResearchStrategy
@pytest.fixture
def fk_enforced_engine(tmp_path):
"""Migrated engine with PRAGMA foreign_keys=ON for every connection.
Mirrors the production hook ``apply_performance_pragmas`` so the test
actually exercises FK enforcement (off by default in a bare SQLite
connection).
"""
db_path = tmp_path / "research_strategy_fk_regression.db"
engine = create_engine(f"sqlite:///{db_path}")
@event.listens_for(engine, "connect")
def _enable_fk(dbapi_connection, _):
dbapi_connection.execute("PRAGMA foreign_keys = ON")
run_migrations(engine)
yield engine
engine.dispose()
def _seed_research_history(engine, research_id: str) -> None:
with engine.begin() as conn:
conn.execute(
text(
"INSERT INTO research_history (id, query, mode, status, created_at) "
"VALUES (:id, 'q', 'quick_summary', 'completed', '2026-01-01')"
),
{"id": research_id},
)
class TestResearchStrategyFKTarget:
"""Schema-level: column type and FK target match the live writer."""
def test_research_id_is_varchar_36_fk_research_history(
self, fk_enforced_engine
):
inspector = inspect(fk_enforced_engine)
cols = {
c["name"]: c for c in inspector.get_columns("research_strategies")
}
assert "VARCHAR" in str(cols["research_id"]["type"]) or "CHAR" in str(
cols["research_id"]["type"]
)
fks = inspector.get_foreign_keys("research_strategies")
research_id_fks = [
fk for fk in fks if "research_id" in fk["constrained_columns"]
]
assert len(research_id_fks) == 1
assert research_id_fks[0]["referred_table"] == "research_history"
assert research_id_fks[0]["referred_columns"] == ["id"]
def test_cascade_on_delete(self, fk_enforced_engine):
# Read the on_delete action from PRAGMA directly. SQLAlchemy's SQLite
# reflection has historically been inconsistent about populating
# ``options.ondelete``; PRAGMA always returns it.
with fk_enforced_engine.connect() as conn:
rows = conn.execute(
text("PRAGMA foreign_key_list(research_strategies)")
).fetchall()
research_id_fk = next(row for row in rows if row[3] == "research_id")
# PRAGMA columns: id, seq, table, from, to, on_update, on_delete, match
assert research_id_fk[6] == "CASCADE"
class TestResearchStrategyLiveWrite:
"""Behavior: the live save path no longer raises FK errors."""
def test_save_strategy_with_research_history_uuid_succeeds(
self, fk_enforced_engine
):
"""The exact pattern from save_research_strategy: insert by UUID."""
rid = "11111111-1111-1111-1111-111111111111"
_seed_research_history(fk_enforced_engine, rid)
with Session(fk_enforced_engine) as session:
session.add(
ResearchStrategy(
research_id=rid, strategy_name="langgraph-agent"
)
)
session.commit()
with Session(fk_enforced_engine) as session:
stored = (
session.query(ResearchStrategy).filter_by(research_id=rid).one()
)
assert stored.strategy_name == "langgraph-agent"
def test_orphan_strategy_insert_is_rejected(self, fk_enforced_engine):
"""FK enforcement is real: insert without parent row must fail."""
from sqlalchemy.exc import IntegrityError
with Session(fk_enforced_engine) as session:
session.add(
ResearchStrategy(
research_id="22222222-2222-2222-2222-222222222222",
strategy_name="x",
)
)
with pytest.raises(IntegrityError):
session.commit()
def test_cascade_delete_removes_strategy(self, fk_enforced_engine):
rid = "33333333-3333-3333-3333-333333333333"
_seed_research_history(fk_enforced_engine, rid)
with Session(fk_enforced_engine) as session:
session.add(
ResearchStrategy(research_id=rid, strategy_name="standard")
)
session.commit()
with fk_enforced_engine.begin() as conn:
conn.execute(
text("DELETE FROM research_history WHERE id = :id"),
{"id": rid},
)
with Session(fk_enforced_engine) as session:
assert (
session.query(ResearchStrategy)
.filter_by(research_id=rid)
.first()
is None
)