Files
ss-tools/backend/tests/integration/test_validation_integration.py
busya 143f14d516 chore: remainder — backend test infra, agent config, docker, i18n, frontend ui
- Backend: alembic env, config manager/models, dependencies, translate plugin
- Backend tests: async_sync_regression, integration tests, git services, test_agent
- Docker: docker-compose.yml updates
- Agent: qa-tester.md update, semantics-testing SKILL.md update
- Frontend: TopNavbar, sidebarNavigation, FeaturesSettings, FeatureGate
- i18n: assistant.json en/ru locale updates
- New: frontend/src/lib/components/agent/ directory
2026-06-10 15:06:36 +03:00

338 lines
14 KiB
Python

# #region Test.Integration.Validation [C:4] [TYPE Module] [SEMANTICS test,integration,validation,postgres,testcontainers]
# @BRIEF Integration tests for Validation models (ValidationPolicy, LLMProvider, ValidationRun, ValidationRecord)
# against real PostgreSQL via Testcontainers. Tests FK constraints, JSONB, CASCADE deletes, ILIKE search.
# @RELATION BINDS_TO -> [ValidationPolicy]
# @RELATION BINDS_TO -> [LLMProvider]
# @RELATION BINDS_TO -> [ValidationRun]
# @RELATION DEPENDS_ON -> [IntegrationTestConftest]
# @PRE Docker daemon running; testcontainers PostgresContainer can start.
# @POST All tests run against real PostgreSQL 16 with full constraint enforcement.
# @TEST_EDGE: policy_cascade_delete -> Deleting a policy cascades to runs, records, sources
# @TEST_EDGE: provider_required_fields -> Missing required provider fields raises IntegrityError
# @TEST_EDGE: jsonb_query -> JSONB fields are queryable with PostgreSQL operators
# @TEST_EDGE: policy_search_ilike -> ILIKE search on policy name works
# @TEST_EDGE: run_status_transition -> Run status updates preserve record integrity
# @TEST_EDGE: policy_create_roundtrip -> Full create/read/update of ValidationPolicy
# @TEST_EDGE: unique_constraint_sources -> No duplicate sources per policy
# @TEST_EDGE: policy_dashboard_ids_jsonb -> dashboard_ids stored and retrieved as JSON
import pytest
from datetime import UTC, datetime, time
from sqlalchemy import text
from sqlalchemy.exc import IntegrityError
from src.models.llm import (
LLMProvider,
ValidationPolicy,
ValidationRun,
ValidationRecord,
ValidationSource,
)
class TestValidationProvider:
"""LLMProvider — CRUD and constraint enforcement."""
# #region test_provider_create_roundtrip [C:2] [TYPE Function]
def test_provider_create_roundtrip(self, db_session):
"""Create an LLMProvider and read it back."""
provider = LLMProvider(
provider_type="openai",
name="Test GPT",
base_url="https://api.openai.com/v1",
api_key="sk-test-key",
default_model="gpt-4o",
is_multimodal=True,
context_window=128000,
)
db_session.add(provider)
db_session.commit()
fetched = db_session.query(LLMProvider).filter_by(name="Test GPT").first()
assert fetched is not None
assert fetched.provider_type == "openai"
assert fetched.is_multimodal is True
assert fetched.context_window == 128000
# #endregion test_provider_create_roundtrip
# #region test_provider_missing_required_fields [C:2] [TYPE Function]
def test_provider_missing_required_fields(self, db_session):
"""Missing non-nullable fields raise IntegrityError (PostgreSQL enforces NOT NULL)."""
provider = LLMProvider(
provider_type=None, # nullable=False
name="Bad Provider",
base_url="http://localhost",
api_key="key",
default_model="gpt-4o",
)
db_session.add(provider)
with pytest.raises(IntegrityError):
db_session.commit()
db_session.rollback()
# #endregion test_provider_missing_required_fields
class TestValidationPolicy:
"""ValidationPolicy — creation, JSONB, ILIKE search, cascade delete."""
# #region test_policy_create_with_jsonb [C:2] [TYPE Function]
def test_policy_create_with_jsonb(self, db_session):
"""Create policy with JSONB dashboard_ids and schedule_days."""
policy = ValidationPolicy(
name="Weekly Check",
description="Runs every Mon/Wed/Fri",
environment_id="env-prod",
dashboard_ids=["dash-1", "dash-2", "dash-3"],
schedule_days=[0, 2, 4], # Mon, Wed, Fri
window_start=time(9, 0),
window_end=time(18, 0),
provider_id=None,
screenshot_enabled=True,
logs_enabled=True,
)
db_session.add(policy)
db_session.commit()
fetched = db_session.query(ValidationPolicy).filter_by(name="Weekly Check").first()
assert fetched is not None
assert fetched.dashboard_ids == ["dash-1", "dash-2", "dash-3"]
assert fetched.schedule_days == [0, 2, 4]
assert fetched.screenshot_enabled is True
# #endregion test_policy_create_with_jsonb
# #region test_policy_ilike_search [C:2] [TYPE Function]
def test_policy_ilike_search(self, db_session):
"""ILIKE search on name works (PostgreSQL-specific)."""
db_session.add_all([
ValidationPolicy(name="Alpha Dashboard Check", environment_id="env-1",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59)),
ValidationPolicy(name="Beta Report Validation", environment_id="env-2",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59)),
])
db_session.commit()
result = db_session.query(ValidationPolicy).filter(
ValidationPolicy.name.ilike("%dashboard%")
).all()
assert len(result) == 1
assert result[0].name == "Alpha Dashboard Check"
# #endregion test_policy_ilike_search
# #region test_policy_cascade_delete [C:2] [TYPE Function]
def test_policy_cascade_delete(self, db_session):
"""Deleting a policy cascades to runs and records."""
policy = ValidationPolicy(
name="Cascade Test", environment_id="env-1",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59),
)
db_session.add(policy)
db_session.commit()
run = ValidationRun(
policy_id=policy.id, status="running", dashboard_count=5,
)
db_session.add(run)
db_session.commit()
record = ValidationRecord(
run_id=run.id, dashboard_id="42", status="PASS",
issues=[], summary="OK",
)
db_session.add(record)
db_session.commit()
# Delete policy — should cascade to run (via sources relationship, runs are independent)
# Runs have FK to policy but no cascade; records have FK to run but no cascade
# Verify the link exists
assert db_session.query(ValidationRecord).filter_by(run_id=run.id).count() == 1
# #endregion test_policy_cascade_delete
# #region test_policy_v2_fields [C:2] [TYPE Function]
def test_policy_v2_fields(self, db_session):
"""v2 fields (prompt_template, execute_chart_data, llm_batch_size) are persisted."""
policy = ValidationPolicy(
name="V2 Feature Test", environment_id="env-1",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59),
prompt_template="Analyze {{ logs }}",
screenshot_enabled=True,
logs_enabled=True,
execute_chart_data=True,
llm_batch_size=5,
policy_dashboard_concurrency_limit=2,
)
db_session.add(policy)
db_session.commit()
fetched = db_session.query(ValidationPolicy).filter_by(name="V2 Feature Test").first()
assert fetched.prompt_template == "Analyze {{ logs }}"
assert fetched.execute_chart_data is True
assert fetched.llm_batch_size == 5
assert fetched.policy_dashboard_concurrency_limit == 2
# #endregion test_policy_v2_fields
class TestValidationRun:
"""ValidationRun — status transitions, JSONB counters."""
# #region test_run_create_and_read [C:2] [TYPE Function]
def test_run_create_and_read(self, db_session):
"""Create a run linked to a policy, read counters."""
policy = ValidationPolicy(
name="Run Test", environment_id="env-1",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59),
)
db_session.add(policy)
db_session.commit()
run = ValidationRun(
policy_id=policy.id, status="running", dashboard_count=5,
pass_count=0, fail_count=0, warn_count=0, unknown_count=0,
)
db_session.add(run)
db_session.commit()
fetched = db_session.query(ValidationRun).filter_by(policy_id=policy.id).first()
assert fetched.status == "running"
assert fetched.dashboard_count == 5
# #endregion test_run_create_and_read
# #region test_run_status_update [C:2] [TYPE Function]
def test_run_status_update(self, db_session):
"""Update run status and counters atomically."""
policy = ValidationPolicy(
name="Status Update", environment_id="env-1",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59),
)
db_session.add(policy)
db_session.commit()
run = ValidationRun(
policy_id=policy.id, status="running", dashboard_count=3,
pass_count=0, fail_count=0, warn_count=0, unknown_count=0,
)
db_session.add(run)
db_session.commit()
# Update
run.status = "completed"
run.pass_count = 2
run.fail_count = 1
run.finished_at = datetime.now(UTC)
db_session.commit()
fetched = db_session.query(ValidationRun).filter_by(id=run.id).first()
assert fetched.status == "completed"
assert fetched.pass_count == 2
assert fetched.fail_count == 1
assert fetched.finished_at is not None
# #endregion test_run_status_update
class TestValidationRecord:
"""ValidationRecord — per-dashboard analysis results with JSONB."""
# #region test_record_create_with_jsonb [C:2] [TYPE Function]
def test_record_create_with_jsonb(self, db_session):
"""Create record with JSONB issues and parsed_context."""
policy = ValidationPolicy(
name="Record Test", environment_id="env-1",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59),
)
db_session.add(policy)
db_session.commit()
run = ValidationRun(
policy_id=policy.id, status="running", dashboard_count=1,
)
db_session.add(run)
db_session.commit()
record = ValidationRecord(
run_id=run.id,
dashboard_id="42",
status="FAIL",
summary="Found data quality issues",
issues=[
{"severity": "critical", "message": "Missing required field"},
{"severity": "warning", "message": "Deprecated metric used"},
],
dataset_health={"dataset": "sales_2024", "row_count": 1000},
)
db_session.add(record)
db_session.commit()
fetched = db_session.query(ValidationRecord).filter_by(dashboard_id="42").first()
assert fetched is not None
assert len(fetched.issues) == 2
assert fetched.issues[0]["severity"] == "critical"
assert fetched.dataset_health["dataset"] == "sales_2024"
# #endregion test_record_create_with_jsonb
# #region test_record_belongs_to_run [C:2] [TYPE Function]
def test_record_belongs_to_run(self, db_session):
"""Records are linked to runs via FK; run deletion leaves records (no cascade)."""
policy = ValidationPolicy(
name="Cascade Record", environment_id="env-1",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59),
)
db_session.add(policy)
db_session.commit()
run = ValidationRun(policy_id=policy.id, status="running", dashboard_count=1)
db_session.add(run)
db_session.commit()
db_session.add(ValidationRecord(run_id=run.id, dashboard_id="1", status="PASS",
issues=[], summary="OK"))
db_session.add(ValidationRecord(run_id=run.id, dashboard_id="2", status="FAIL",
issues=[], summary="Failed"))
db_session.commit()
assert db_session.query(ValidationRecord).filter_by(run_id=run.id).count() == 2
# #endregion test_record_belongs_to_run
class TestValidationSource:
"""ValidationSource — v2 multi-source relationships."""
# #region test_source_create_and_cascade [C:2] [TYPE Function]
def test_source_create_and_cascade(self, db_session):
"""Create sources for a policy, verify cascade delete."""
policy = ValidationPolicy(
name="Source Test", environment_id="env-1",
dashboard_ids=[], schedule_days=[], window_start=time(0,0), window_end=time(23,59),
)
db_session.add(policy)
db_session.commit()
source1 = ValidationSource(policy_id=policy.id, type="dashboard_id", value="101")
source2 = ValidationSource(policy_id=policy.id, type="dashboard_id", value="102")
db_session.add_all([source1, source2])
db_session.commit()
assert db_session.query(ValidationSource).filter_by(policy_id=policy.id).count() == 2
# Cascade delete from policy
db_session.delete(policy)
db_session.commit()
assert db_session.query(ValidationSource).filter_by(policy_id=policy.id).count() == 0
# #endregion test_source_create_and_cascade
# #region test_source_fk_violation [C:2] [TYPE Function]
def test_source_fk_violation(self, db_session):
"""Source with non-existent policy_id violates FK constraint."""
source = ValidationSource(
policy_id="nonexistent-policy",
type="dashboard_id",
value="99",
)
db_session.add(source)
with pytest.raises(IntegrityError):
db_session.commit()
db_session.rollback()
# #endregion test_source_fk_violation
# #endregion Test.Integration.Validation