- 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
338 lines
14 KiB
Python
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
|