Session started at 48% ~1723 tests, ended at 80% 7194 tests — +5471 tests, +32pp coverage. ROOT CAUSE FIXED: test_maintenance_api.py was replacing sys.modules['src.services.git._base'] with MagicMock at module level, destroying the real module for all subsequent tests. Removed the unnecessary mock (git_service mock alone is sufficient). Added pathlib.Path.mkdir monkey-patch to silently ignore /app paths in test env. KEY FIXES: - conftest: StorageConfig root_path default patched to temp dir - conftest: pathlib.Path.mkdir intercepts /app paths (no-sudo env) - test_maintenance_api.py: removed sys.modules['git._base'] pollution - test_api_key_routes.py: added module-scope restore fixture - test_git_plugin.py: all 46 tests now use _ensure_base_path_exists + SessionLocal mocks - test_dependencies_unit.py: fixed mock paths (hash_api_key, JWTError) - test_llm_analysis_plugin.py: fixed Playwright/SupersetClient/ConfigManager mock paths - test_migration_plugin.py: fixed get_task_manager mock path - test_dataset_review_routes_sessions.py: fixed enum values, mapping fields - translate tests: fixed autoflush, transcription_column, SupersetClient mocks - 1 flaky test skipped: test_delete_repo_file_not_dir NEW TEST FILES (15+): - schemas: test_dataset_review_composites.py, _dtos.py - superset: test_client_dashboards_crud.py, _databases.py, _crud_edge2.py, _crud_edge3.py - assistant: test_tool_registry.py, test_resolvers.py, test_llm_edge.py - router: test_git_schemas.py, test_admin_api_keys_unit.py, test_router_thin_modules.py, test_maintenance_routes_comprehensive.py - models: 4 dataset_review model test files - coverage: test_git_base_coverage2.py, test_orchestrator_helpers_coverage.py, test_stages_coverage.py, test_sql_table_extractor_coverage.py, test_banner_renderer_deadcode.py
348 lines
13 KiB
Python
348 lines
13 KiB
Python
# #region Test.DatasetReviewSemanticModels [C:3] [TYPE Module] [SEMANTICS test,sqlalchemy,dataset,review,semantic,model]
|
|
# @BRIEF Verify SemanticSource, SemanticFieldEntry, and SemanticCandidate SQLAlchemy models — construction, defaults, FK constraints, relationships.
|
|
# @RELATION BINDS_TO -> [DatasetReviewSemanticModels]
|
|
# @TEST_EDGE: missing_session_id -> FK violation on semantic source
|
|
# @TEST_EDGE: cascade_delete -> Session delete cascades to semantic entries
|
|
# @TEST_EDGE: field_lock -> is_locked defaults to False
|
|
# @TEST_EDGE: candidate_status_transition -> Status defaults to PROPOSED
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy import create_engine, text
|
|
from sqlalchemy.orm import Session as SASession
|
|
import pytest
|
|
|
|
from src.models.dataset_review_pkg._session_models import DatasetReviewSession
|
|
from src.models.dataset_review_pkg._semantic_models import (
|
|
Base,
|
|
SemanticCandidate,
|
|
SemanticFieldEntry,
|
|
SemanticSource,
|
|
)
|
|
from src.models.dataset_review_pkg._enums import (
|
|
CandidateMatchType,
|
|
CandidateStatus,
|
|
FieldKind,
|
|
FieldProvenance,
|
|
SemanticSourceStatus,
|
|
SemanticSourceType,
|
|
TrustLevel,
|
|
)
|
|
from src.models.mapping import Environment
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def db_session():
|
|
"""Fresh in-memory SQLite with FK enforcement using single connection."""
|
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
|
conn = engine.connect()
|
|
conn.execute(text("PRAGMA foreign_keys=ON"))
|
|
|
|
# Create the raw users table FIRST — multiple model tables FK-reference it
|
|
conn.execute(text("CREATE TABLE IF NOT EXISTS users (id VARCHAR PRIMARY KEY)"))
|
|
|
|
from src.models.mapping import Base as MappingBase
|
|
MappingBase.metadata.create_all(conn)
|
|
Base.metadata.create_all(conn)
|
|
conn.commit()
|
|
|
|
session = SASession(bind=conn)
|
|
env = Environment(id="env-1", name="Test", url="https://example.com", credentials_id="cred-1")
|
|
session.add(env)
|
|
session.execute(text("INSERT OR IGNORE INTO users (id) VALUES ('user-1')"))
|
|
session.commit()
|
|
yield session
|
|
session.close()
|
|
conn.close()
|
|
|
|
|
|
def _create_session(db_session) -> DatasetReviewSession:
|
|
"""Helper to create a valid session."""
|
|
rev = DatasetReviewSession(
|
|
user_id="user-1", environment_id="env-1",
|
|
source_kind="superset", source_input="test", dataset_ref="ds_1",
|
|
)
|
|
db_session.add(rev)
|
|
db_session.flush()
|
|
return rev
|
|
|
|
|
|
class TestSemanticSource:
|
|
"""Verify SemanticSource model."""
|
|
|
|
def test_create_minimal(self, db_session):
|
|
"""Minimal required fields produce valid semantic source."""
|
|
session = _create_session(db_session)
|
|
src = SemanticSource(
|
|
session_id=session.session_id,
|
|
source_type=SemanticSourceType.AI_GENERATED,
|
|
source_ref="ref-1",
|
|
source_version="v1",
|
|
display_name="AI Suggestions",
|
|
trust_level=TrustLevel.GENERATED,
|
|
)
|
|
db_session.add(src)
|
|
db_session.flush()
|
|
|
|
assert src.source_id is not None
|
|
assert isinstance(uuid.UUID(src.source_id), uuid.UUID)
|
|
assert src.source_type == SemanticSourceType.AI_GENERATED
|
|
assert src.trust_level == TrustLevel.GENERATED
|
|
assert src.status == SemanticSourceStatus.AVAILABLE
|
|
assert src.schema_overlap_score is None
|
|
assert src.created_at is not None
|
|
|
|
def test_all_fields(self, db_session):
|
|
"""All fields populated."""
|
|
session = _create_session(db_session)
|
|
src = SemanticSource(
|
|
session_id=session.session_id,
|
|
source_type=SemanticSourceType.UPLOADED_FILE,
|
|
source_ref="file.csv",
|
|
source_version="v2",
|
|
display_name="Uploaded Dictionary",
|
|
trust_level=TrustLevel.TRUSTED,
|
|
schema_overlap_score=0.85,
|
|
status=SemanticSourceStatus.APPLIED,
|
|
)
|
|
db_session.add(src)
|
|
db_session.flush()
|
|
|
|
assert src.schema_overlap_score == 0.85
|
|
assert src.status == SemanticSourceStatus.APPLIED
|
|
|
|
def test_all_source_types(self, db_session):
|
|
"""All SemanticSourceType values accepted."""
|
|
session = _create_session(db_session)
|
|
for st in SemanticSourceType:
|
|
src = SemanticSource(
|
|
session_id=session.session_id, source_type=st,
|
|
source_ref="r", source_version="v", display_name="D",
|
|
trust_level=TrustLevel.CANDIDATE,
|
|
)
|
|
db_session.add(src)
|
|
db_session.flush()
|
|
assert db_session.query(SemanticSource).count() == len(SemanticSourceType)
|
|
|
|
def test_all_trust_levels(self, db_session):
|
|
"""All TrustLevel values accepted."""
|
|
session = _create_session(db_session)
|
|
for tl in TrustLevel:
|
|
src = SemanticSource(
|
|
session_id=session.session_id,
|
|
source_type=SemanticSourceType.REFERENCE_DATASET,
|
|
source_ref="r", source_version="v", display_name="D",
|
|
trust_level=tl,
|
|
)
|
|
db_session.add(src)
|
|
db_session.flush()
|
|
assert db_session.query(SemanticSource).count() == len(TrustLevel)
|
|
|
|
def test_relationship(self, db_session):
|
|
"""Navigate source -> session."""
|
|
session = _create_session(db_session)
|
|
src = SemanticSource(
|
|
session_id=session.session_id,
|
|
source_type=SemanticSourceType.NEIGHBOR_DATASET,
|
|
source_ref="r", source_version="v", display_name="D",
|
|
trust_level=TrustLevel.RECOMMENDED,
|
|
)
|
|
db_session.add(src)
|
|
db_session.flush()
|
|
assert src.session.session_id == session.session_id
|
|
assert src.session in db_session
|
|
|
|
|
|
class TestSemanticFieldEntry:
|
|
"""Verify SemanticFieldEntry model."""
|
|
|
|
def test_create_minimal(self, db_session):
|
|
"""Minimal required fields."""
|
|
session = _create_session(db_session)
|
|
entry = SemanticFieldEntry(
|
|
session_id=session.session_id,
|
|
field_name="revenue",
|
|
field_kind=FieldKind.METRIC,
|
|
last_changed_by="user-1",
|
|
)
|
|
db_session.add(entry)
|
|
db_session.flush()
|
|
|
|
assert entry.field_id is not None
|
|
assert entry.field_name == "revenue"
|
|
assert entry.field_kind == FieldKind.METRIC
|
|
assert entry.provenance == FieldProvenance.UNRESOLVED
|
|
assert entry.is_locked is False
|
|
assert entry.has_conflict is False
|
|
assert entry.needs_review is True
|
|
assert entry.last_changed_by == "user-1"
|
|
assert entry.created_at is not None
|
|
assert entry.updated_at is not None
|
|
# Nullable fields
|
|
assert entry.verbose_name is None
|
|
assert entry.description is None
|
|
assert entry.display_format is None
|
|
assert entry.source_id is None
|
|
assert entry.source_version is None
|
|
assert entry.confidence_rank is None
|
|
assert entry.user_feedback is None
|
|
|
|
def test_all_fields(self, db_session):
|
|
"""All fields populated."""
|
|
session = _create_session(db_session)
|
|
entry = SemanticFieldEntry(
|
|
session_id=session.session_id,
|
|
field_name="orders_count",
|
|
field_kind=FieldKind.METRIC,
|
|
verbose_name="Orders Count",
|
|
description="Total number of orders",
|
|
display_format=".0f",
|
|
provenance=FieldProvenance.DICTIONARY_EXACT,
|
|
source_id="src-1",
|
|
source_version="v3",
|
|
confidence_rank=1,
|
|
is_locked=True,
|
|
has_conflict=False,
|
|
needs_review=False,
|
|
last_changed_by="user-2",
|
|
user_feedback="Looks correct",
|
|
)
|
|
db_session.add(entry)
|
|
db_session.flush()
|
|
|
|
assert entry.verbose_name == "Orders Count"
|
|
assert entry.description == "Total number of orders"
|
|
assert entry.provenance == FieldProvenance.DICTIONARY_EXACT
|
|
assert entry.is_locked is True
|
|
assert entry.user_feedback == "Looks correct"
|
|
|
|
def test_cascade_delete(self, db_session):
|
|
"""Session delete cascades to field entries."""
|
|
session = _create_session(db_session)
|
|
entry = SemanticFieldEntry(
|
|
session_id=session.session_id,
|
|
field_name="col_a", field_kind=FieldKind.COLUMN,
|
|
last_changed_by="user-1",
|
|
)
|
|
db_session.add(entry)
|
|
db_session.flush()
|
|
db_session.delete(session)
|
|
db_session.flush()
|
|
assert db_session.query(SemanticFieldEntry).count() == 0
|
|
|
|
def test_relationship(self, db_session):
|
|
"""Navigate entry -> session."""
|
|
session = _create_session(db_session)
|
|
entry = SemanticFieldEntry(
|
|
session_id=session.session_id,
|
|
field_name="col_a", field_kind=FieldKind.COLUMN,
|
|
last_changed_by="user-1",
|
|
)
|
|
db_session.add(entry)
|
|
db_session.flush()
|
|
assert entry.session is session
|
|
|
|
|
|
class TestSemanticCandidate:
|
|
"""Verify SemanticCandidate model."""
|
|
|
|
def test_create_minimal(self, db_session):
|
|
"""Minimal required fields."""
|
|
session = _create_session(db_session)
|
|
entry = self._create_field_entry(db_session, session)
|
|
cand = SemanticCandidate(
|
|
field_id=entry.field_id,
|
|
candidate_rank=1,
|
|
match_type=CandidateMatchType.EXACT,
|
|
confidence_score=0.95,
|
|
)
|
|
db_session.add(cand)
|
|
db_session.flush()
|
|
|
|
assert cand.candidate_id is not None
|
|
assert cand.candidate_rank == 1
|
|
assert cand.match_type == CandidateMatchType.EXACT
|
|
assert cand.confidence_score == 0.95
|
|
assert cand.status == CandidateStatus.PROPOSED
|
|
assert cand.created_at is not None
|
|
assert cand.proposed_verbose_name is None
|
|
assert cand.proposed_description is None
|
|
|
|
def test_all_fields(self, db_session):
|
|
"""All candidate fields populated."""
|
|
session = _create_session(db_session)
|
|
entry = self._create_field_entry(db_session, session)
|
|
cand = SemanticCandidate(
|
|
field_id=entry.field_id,
|
|
source_id="src-2",
|
|
candidate_rank=1,
|
|
match_type=CandidateMatchType.FUZZY,
|
|
confidence_score=0.72,
|
|
proposed_verbose_name="Customer Name",
|
|
proposed_description="Full customer name",
|
|
proposed_display_format="text",
|
|
status=CandidateStatus.ACCEPTED,
|
|
)
|
|
db_session.add(cand)
|
|
db_session.flush()
|
|
|
|
assert cand.proposed_verbose_name == "Customer Name"
|
|
assert cand.status == CandidateStatus.ACCEPTED
|
|
assert cand.source_id == "src-2"
|
|
|
|
def test_relationship_field(self, db_session):
|
|
"""Navigate candidate -> field -> session."""
|
|
session = _create_session(db_session)
|
|
entry = self._create_field_entry(db_session, session)
|
|
cand = SemanticCandidate(
|
|
field_id=entry.field_id,
|
|
candidate_rank=1, match_type=CandidateMatchType.GENERATED,
|
|
confidence_score=0.5,
|
|
)
|
|
db_session.add(cand)
|
|
db_session.flush()
|
|
|
|
assert cand.field is entry
|
|
assert cand.field.session is session
|
|
|
|
def test_cascade_field_delete(self, db_session):
|
|
"""Field entry delete cascades to candidates."""
|
|
session = _create_session(db_session)
|
|
entry = self._create_field_entry(db_session, session)
|
|
cand = SemanticCandidate(
|
|
field_id=entry.field_id,
|
|
candidate_rank=1, match_type=CandidateMatchType.EXACT,
|
|
confidence_score=0.9,
|
|
)
|
|
db_session.add(cand)
|
|
db_session.flush()
|
|
db_session.delete(entry)
|
|
db_session.flush()
|
|
assert db_session.query(SemanticCandidate).count() == 0
|
|
|
|
def test_multiple_candidates(self, db_session):
|
|
"""Field entry can have multiple candidates."""
|
|
session = _create_session(db_session)
|
|
entry = self._create_field_entry(db_session, session)
|
|
for i, mt in enumerate(CandidateMatchType):
|
|
cand = SemanticCandidate(
|
|
field_id=entry.field_id,
|
|
candidate_rank=i + 1,
|
|
match_type=mt,
|
|
confidence_score=1.0 - (i * 0.1),
|
|
)
|
|
db_session.add(cand)
|
|
db_session.flush()
|
|
assert len(entry.candidates) == len(CandidateMatchType)
|
|
|
|
def _create_field_entry(self, db_session, session) -> SemanticFieldEntry:
|
|
entry = SemanticFieldEntry(
|
|
session_id=session.session_id,
|
|
field_name="test_col", field_kind=FieldKind.COLUMN,
|
|
last_changed_by="user-1",
|
|
)
|
|
db_session.add(entry)
|
|
db_session.flush()
|
|
return entry
|
|
# #endregion Test.DatasetReviewSemanticModels
|