- Authoritative candidate capture with server-issued artifacts and raw-byte
immutability hashing (source_response_hash server-owned)
- Closed-period lifecycle: request-hash bound approvals, persisted closure
immutability violations, byte-for-byte catalog stability on reclosure
- Verification runs: persisted VerificationRun model + FK migration,
publish gate (block_publish), scheduled observability runs (02:00 UTC)
- FR-013 baseline inheritance: prior_release_id migration, plan_inheritance/
execute_inheritance classification and re-extraction, API endpoints
- Visual executor bound to release-deployment environment; caller mismatch
rejected; visual SSIM/reconciliation modules
- Query execution decomposed: envelope/model/executor split, no direct SQL
- AgentRun approvals extracted to submodule; evidence adapter; _utils
- Dashboard testing service decomposed into 30+ modules (all <400 LOC)
- Five Feature-037 agent tools with permission guards (tools_037.py)
- API readiness endpoint; Alembic env/migrations; test fixture repos
- Specs 036/037 contracts, openapi.yaml, schema.json, tasks/traceability
updated; semantic index rebuilt with 0 parse warnings
- Fix ADR-0003 parser ambiguity: remove [DEF🆔ADR] prose example
- Add axiom-mcp-agent-feedback.md: agent findings for MCP rework plan
- Tests: 298 service + 1464 API + 45 agent passing; ruff clean
595 lines
25 KiB
Python
595 lines
25 KiB
Python
# #region Test.BaselineEngine.StructureDiff.Service [C:4] [TYPE Module] [SEMANTICS test,structure-diff,service,deterministic,snapshot]
|
|
# @defgroup Test.BaselineEngine.StructureDiff Service-level tests for snapshot-loaded structure diff.
|
|
# @LAYER Test
|
|
# @RELATION BINDS_TO -> [BaselineEngine.StructureDiff.Service]
|
|
# @RELATION VERIFIES -> [BaselineEngine.StructureDiff.ComputeDiff]
|
|
# @TEST_EDGE same_snapshot -> zero changes, pass=1, blocked=False
|
|
# @TEST_EDGE chart_removed -> critical chart_removed change, blocked=True
|
|
# @TEST_EDGE filter_scope_lost -> critical filter_scope_narrowed change
|
|
# @TEST_EDGE column_reorder -> warning column_order_changed, xlsx_export affected
|
|
# @TEST_EDGE missing_snapshot -> blocked diff with critical missing-snapshot change, NOT synthetic empty diff
|
|
# @TEST_EDGE malformed_snapshot -> ValueError raised
|
|
# @TEST_EDGE identical_snapshots -> deterministic hash matches
|
|
# @TEST_EDGE affected_artifacts -> correct artifacts per change kind
|
|
# @INVARIANT The diff is computed from persisted DashboardQueryModel snapshots,
|
|
# never from version strings or release metadata.
|
|
# @INVARIANT Missing snapshots produce blocked=True with critical severity,
|
|
# never a synthetic empty diff.
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import pytest
|
|
|
|
from src.schemas.dashboard_testing import DiffKind, DiffSeverity, StructureDiffRequest
|
|
from src.services.dashboard_testing.structure_diff_service import compute_structure_diff, set_snapshot_base_path
|
|
|
|
# ── Fixture path ────────────────────────────────────────────────
|
|
|
|
FIXTURE_DIR = Path(__file__).parents[1] / "fixtures" / "structure_diff"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _use_fixture_base():
|
|
"""Point the snapshot loader at our test fixture directory."""
|
|
set_snapshot_base_path(FIXTURE_DIR)
|
|
yield
|
|
# Reset to None after each test to avoid cross-test contamination
|
|
set_snapshot_base_path(Path.cwd())
|
|
|
|
|
|
# ── Helper ──────────────────────────────────────────────────────
|
|
|
|
def _make_request(
|
|
release_from: str,
|
|
release_to: str,
|
|
environment_id: str = "ss-preprod",
|
|
dashboard_id: int = 42,
|
|
repository_key: str | None = None,
|
|
dashboard_key: str | None = None,
|
|
) -> StructureDiffRequest:
|
|
"""Build a StructureDiffRequest pointing at fixture snapshots."""
|
|
return StructureDiffRequest(
|
|
environment_id=environment_id,
|
|
dashboard_id=dashboard_id,
|
|
release_version_from=release_from,
|
|
release_version_to=release_to,
|
|
repository_key=repository_key,
|
|
dashboard_key=dashboard_key,
|
|
)
|
|
|
|
|
|
def _count_by_severity(changes: list) -> dict[str, int]:
|
|
"""Quick severity tally from a change list."""
|
|
counts: dict[str, int] = {"critical": 0, "warning": 0, "info": 0}
|
|
for c in changes:
|
|
counts[c.severity] += 1
|
|
return counts
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.IdenticalSnapshots [C:2] [TYPE Class]
|
|
class TestIdenticalSnapshots:
|
|
"""Both release versions point to the same snapshot → zero changes."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.IdenticalSnapshots.TestSameSnapshot
|
|
def test_same_snapshot_yields_empty_diff(self):
|
|
"""T037: same snapshot from/to → no changes, pass=1, blocked=False."""
|
|
request = _make_request("v1.0.0", "v1.0.0")
|
|
diff = compute_structure_diff(request)
|
|
|
|
assert diff.release_from == "v1.0.0"
|
|
assert diff.release_to == "v1.0.0"
|
|
assert diff.query_model_hash_from == diff.query_model_hash_to
|
|
assert diff.changes == []
|
|
assert diff.summary["pass"] == 1
|
|
assert diff.summary["critical"] == 0
|
|
assert diff.summary["warning"] == 0
|
|
assert diff.summary["info"] == 0
|
|
assert diff.blocked is False
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.IdenticalSnapshots.TestDeterministicHash
|
|
def test_deterministic_hash_across_calls(self):
|
|
"""T037: same snapshot file produces same hash every time."""
|
|
request = _make_request("v1.0.0", "v1.0.0")
|
|
diff1 = compute_structure_diff(request)
|
|
diff2 = compute_structure_diff(request)
|
|
|
|
assert diff1.query_model_hash_from == diff2.query_model_hash_from
|
|
assert diff1.query_model_hash_to == diff2.query_model_hash_to
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.IdenticalSnapshots.TestHashFormat
|
|
def test_hash_format(self):
|
|
"""T037: hash starts with 'sha256:' and is 71 chars (prefix + 64 hex)."""
|
|
request = _make_request("v1.0.0", "v1.0.0")
|
|
diff = compute_structure_diff(request)
|
|
h = diff.query_model_hash_from
|
|
assert h is not None
|
|
assert h.startswith("sha256:")
|
|
assert len(h) == 71 # "sha256:" (7) + 64 hex chars
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.ChartRemoved [C:2] [TYPE Class]
|
|
class TestChartRemoved:
|
|
"""Target snapshot has chart 129 removed → critical change."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.ChartRemoved.TestChartRemovedCritical
|
|
def test_chart_removed_is_critical(self):
|
|
"""T037: chart removed → critical severity, blocked=True."""
|
|
request = _make_request("v1.0.0", "v1.1.0-chart-removed")
|
|
diff = compute_structure_diff(request)
|
|
|
|
assert diff.summary["critical"] >= 1
|
|
assert diff.blocked is True
|
|
|
|
# Find the CHART_REMOVED change
|
|
chart_changes = [c for c in diff.changes if c.kind == DiffKind.CHART_REMOVED]
|
|
assert len(chart_changes) == 1
|
|
|
|
cc = chart_changes[0]
|
|
assert cc.severity == DiffSeverity.CRITICAL
|
|
assert "129" in cc.target or "Total Revenue KPI" in cc.detail
|
|
assert cc.before is not None
|
|
assert cc.before["chart_id"] == 129
|
|
assert cc.after is None
|
|
assert "screenshot_evidence" in cc.affected_artifacts
|
|
assert "metric_assertion" in cc.affected_artifacts
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.ChartRemoved.TestHashesDiffer
|
|
def test_hashes_differ_when_chart_removed(self):
|
|
"""T037: different snapshots produce different hashes."""
|
|
request = _make_request("v1.0.0", "v1.1.0-chart-removed")
|
|
diff = compute_structure_diff(request)
|
|
assert diff.query_model_hash_from != diff.query_model_hash_to
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.FilterScopeLost [C:2] [TYPE Class]
|
|
class TestFilterScopeLost:
|
|
"""Target snapshot has chart 128 losing its filter scope → critical."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.FilterScopeLost.TestFilterScopeNarrowed
|
|
def test_filter_scope_narrowed_is_critical(self):
|
|
"""T037: filter scope narrowed on chart 128 → critical."""
|
|
request = _make_request("v1.0.0", "v1.1.0-filter-scope-lost")
|
|
diff = compute_structure_diff(request)
|
|
|
|
# Should have at least one FILTER_SCOPE_NARROWED
|
|
narrow_changes = [
|
|
c for c in diff.changes
|
|
if c.kind == DiffKind.FILTER_SCOPE_NARROWED
|
|
]
|
|
assert len(narrow_changes) >= 1
|
|
|
|
nc = narrow_changes[0]
|
|
assert nc.severity == DiffSeverity.CRITICAL
|
|
assert "NATIVE_FILTER-date" in nc.detail or "NATIVE_FILTER-region" in nc.detail
|
|
assert "metric_assertion" in nc.affected_artifacts
|
|
assert diff.blocked is True
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.ColumnReorder [C:2] [TYPE Class]
|
|
class TestColumnReorder:
|
|
"""Target snapshot has reordered dataset columns → warning."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.ColumnReorder.TestColumnOrderChanged
|
|
def test_column_order_changed_is_warning(self):
|
|
"""T037: column order changed → warning severity, xlsx_export affected."""
|
|
request = _make_request("v1.0.0", "v1.1.0-column-reorder")
|
|
diff = compute_structure_diff(request)
|
|
|
|
order_changes = [
|
|
c for c in diff.changes
|
|
if c.kind == DiffKind.COLUMN_ORDER_CHANGED
|
|
]
|
|
assert len(order_changes) >= 1
|
|
|
|
oc = order_changes[0]
|
|
assert oc.severity == DiffSeverity.WARNING
|
|
assert oc.before is not None
|
|
assert oc.after is not None
|
|
assert "xlsx_export" in oc.affected_artifacts
|
|
assert "screenshot_evidence" in oc.affected_artifacts
|
|
assert diff.blocked is False # column reorder is warning, not critical
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.MissingSnapshot [C:2] [TYPE Class]
|
|
class TestMissingSnapshot:
|
|
"""One or both snapshots do not exist → blocked error, NOT synthetic success."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.MissingSnapshot.TestBaseMissing
|
|
def test_missing_base_snapshot_blocks(self):
|
|
"""T037: missing base snapshot → blocked=True with critical explanation."""
|
|
request = _make_request("v999.0.0-nonexistent", "v1.0.0")
|
|
diff = compute_structure_diff(request)
|
|
|
|
assert diff.blocked is True
|
|
assert diff.summary["critical"] >= 1
|
|
assert diff.summary["pass"] == 0
|
|
# Verify it's NOT an empty diff
|
|
assert len(diff.changes) >= 1
|
|
# The change should explain the missing snapshot
|
|
assert "not found" in diff.changes[0].detail.lower()
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.MissingSnapshot.TestTargetMissing
|
|
def test_missing_target_snapshot_blocks(self):
|
|
"""T037: missing target snapshot → blocked=True."""
|
|
request = _make_request("v1.0.0", "v999.0.0-nonexistent")
|
|
diff = compute_structure_diff(request)
|
|
|
|
assert diff.blocked is True
|
|
assert diff.summary["critical"] >= 1
|
|
assert diff.summary["pass"] == 0
|
|
assert "not found" in diff.changes[0].detail.lower()
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.MissingSnapshot.TestNoSyntheticSuccess
|
|
def test_missing_snapshot_not_synthetic_success(self):
|
|
"""T037: CRITICAL: never return pass=1 when snapshots are missing."""
|
|
request = _make_request("v999.0.0-nonexistent", "v1.0.0")
|
|
diff = compute_structure_diff(request)
|
|
# This asserts the forbidden behavior: no synthetic empty diff
|
|
assert not (diff.summary["pass"] == 1 and diff.changes == [])
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.MalformedSnapshot [C:2] [TYPE Class]
|
|
class TestMalformedSnapshot:
|
|
"""Snapshot file exists but contains invalid JSON → ValueError."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.MalformedSnapshot.TestMalformedFile
|
|
def test_malformed_snapshot_raises(self):
|
|
"""T037: malformed JSON snapshot raises ValueError."""
|
|
request = _make_request("v1.0.0", "v1.1.0-malformed")
|
|
with pytest.raises(ValueError, match="Malformed snapshot"):
|
|
compute_structure_diff(request)
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.FullDiffScenarios [C:3] [TYPE Class]
|
|
class TestFullDiffScenarios:
|
|
"""End-to-end diff scenarios with all classification dimensions."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.FullDiffScenarios.TestSummaryCounts
|
|
def test_chart_removed_summary_counts(self):
|
|
"""T037: chart removed diff has correct severity breakdown."""
|
|
request = _make_request("v1.0.0", "v1.1.0-chart-removed")
|
|
diff = compute_structure_diff(request)
|
|
|
|
assert diff.summary["critical"] >= 1 # chart_removed + filter scope changes
|
|
assert diff.summary["pass"] == 0
|
|
# Verify total changes equals sum of severity counts
|
|
total = diff.summary["critical"] + diff.summary["warning"] + diff.summary["info"]
|
|
assert total == len(diff.changes)
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.FullDiffScenarios.TestAffectedArtifactsMapping
|
|
def test_affected_artifacts_mapped_correctly(self):
|
|
"""T037: every change has appropriate affected_artifacts."""
|
|
request = _make_request("v1.0.0", "v1.1.0-chart-removed")
|
|
diff = compute_structure_diff(request)
|
|
|
|
for change in diff.changes:
|
|
# Every change must have at least one affected artifact
|
|
assert len(change.affected_artifacts) >= 1, (
|
|
f"Change {change.kind} on {change.target} has no affected_artifacts"
|
|
)
|
|
# All values must be valid
|
|
for artifact in change.affected_artifacts:
|
|
assert artifact in ("xlsx_export", "screenshot_evidence", "metric_assertion")
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.FullDiffScenarios.TestAllChangesHaveRationale
|
|
def test_all_changes_have_rationale(self):
|
|
"""T037: every change must have a rationale string."""
|
|
request = _make_request("v1.0.0", "v1.1.0-chart-removed")
|
|
diff = compute_structure_diff(request)
|
|
|
|
for change in diff.changes:
|
|
assert change.rationale is not None and len(change.rationale) > 0, (
|
|
f"Change {change.kind} on {change.target} missing rationale"
|
|
)
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Deterministic [C:2] [TYPE Class]
|
|
class TestDeterministicBehavior:
|
|
"""Repeated calls with same inputs produce identical results."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Deterministic.TestRepeatedIdentical
|
|
def test_identical_snapshot_always_empty(self):
|
|
"""T037: repeated calls with same snapshot always return empty diff."""
|
|
req = _make_request("v1.0.0", "v1.0.0")
|
|
for _ in range(3):
|
|
diff = compute_structure_diff(req)
|
|
assert diff.changes == []
|
|
assert diff.summary["pass"] == 1
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Deterministic.TestRepeatedWithChanges
|
|
def test_repeated_diff_produces_identical_changes(self):
|
|
"""T037: repeated calls with different snapshots produce identical change lists."""
|
|
req = _make_request("v1.0.0", "v1.1.0-chart-removed")
|
|
ref = compute_structure_diff(req)
|
|
|
|
for _ in range(3):
|
|
diff = compute_structure_diff(req)
|
|
assert len(diff.changes) == len(ref.changes)
|
|
assert [c.kind for c in diff.changes] == [c.kind for c in ref.changes]
|
|
assert [c.severity for c in diff.changes] == [c.severity for c in ref.changes]
|
|
assert diff.summary == ref.summary
|
|
assert diff.blocked == ref.blocked
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.APIBehavior [C:2] [TYPE Class]
|
|
class TestAPIBehavior:
|
|
"""Ensure the API-facing contract works correctly with the new implementation."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.APIBehavior.TestRepositoryKeyInference
|
|
def test_repository_key_fallback(self):
|
|
"""T037: when no repository_key given, uses env+id based fallback."""
|
|
request = _make_request(
|
|
"v1.0.0", "v1.0.0",
|
|
environment_id="ss-preprod", dashboard_id=42,
|
|
)
|
|
# Should not raise — builds default key from env+dashboard
|
|
diff = compute_structure_diff(request)
|
|
assert diff.changes == []
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.APIBehavior.TestRequestExtraFields
|
|
def test_request_accepts_repository_and_dashboard_keys(self):
|
|
"""T037: optional repository_key/dashboard_key accepted (via schema)."""
|
|
from src.schemas.dashboard_testing import StructureDiffRequest
|
|
# Schema accepts the keys — this is the contract test
|
|
req = StructureDiffRequest(
|
|
environment_id="dev",
|
|
dashboard_id=1,
|
|
release_version_from="v1.0.0",
|
|
release_version_to="v2.0.0",
|
|
repository_key="my_repo",
|
|
dashboard_key="my_dashboard",
|
|
)
|
|
assert req.repository_key == "my_repo"
|
|
assert req.dashboard_key == "my_dashboard"
|
|
# Service uses them for path resolution; fixture doesn't exist
|
|
# but that's OK — this test only validates schema acceptance
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.SchemaValidation [C:2] [TYPE Class]
|
|
class TestSchemaValidation:
|
|
"""New schema fields are populated correctly."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.SchemaValidation.TestAffectedArtifactsOnStructureChange
|
|
def test_structure_change_has_affected_artifacts(self):
|
|
"""T037: StructureChange has affected_artifacts field."""
|
|
from src.schemas.dashboard_testing import StructureChange
|
|
sc = StructureChange(
|
|
target="charts[128]",
|
|
kind=DiffKind.CHART_REMOVED,
|
|
severity=DiffSeverity.CRITICAL,
|
|
detail="Test change",
|
|
affected_artifacts=["screenshot_evidence"],
|
|
)
|
|
assert sc.affected_artifacts == ["screenshot_evidence"]
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.SchemaValidation.TestStructureDiffRequestHasKeys
|
|
def test_structure_diff_request_has_optional_keys(self):
|
|
"""T037: StructureDiffRequest has optional repository_key/dashboard_key."""
|
|
from src.schemas.dashboard_testing import StructureDiffRequest
|
|
req = StructureDiffRequest(
|
|
environment_id="dev",
|
|
dashboard_id=1,
|
|
release_version_from="v1.0.0",
|
|
release_version_to="v2.0.0",
|
|
repository_key="my_repo",
|
|
dashboard_key="my_dashboard",
|
|
)
|
|
assert req.repository_key == "my_repo"
|
|
assert req.dashboard_key == "my_dashboard"
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Classifier [C:2] [TYPE Class]
|
|
class TestClassifier:
|
|
"""Unit tests for severity classification and artifact mapping."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Classifier.TestSeverityCritical
|
|
def test_classify_critical(self):
|
|
from src.schemas.dashboard_testing import DiffKind, DiffSeverity
|
|
from src.services.dashboard_testing.structure_diff_classifier import classify_severity
|
|
critical_kinds = [DiffKind.FILTER_SCOPE_NARROWED, DiffKind.FILTER_OPERATOR_CHANGED,
|
|
DiffKind.CHART_REMOVED, DiffKind.FILTER_REMOVED]
|
|
for k in critical_kinds:
|
|
assert classify_severity(k) == DiffSeverity.CRITICAL
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Classifier.TestSeverityWarning
|
|
def test_classify_warning(self):
|
|
from src.schemas.dashboard_testing import DiffKind, DiffSeverity
|
|
from src.services.dashboard_testing.structure_diff_classifier import classify_severity
|
|
warning_kinds = [DiffKind.COLUMN_REORDER, DiffKind.COLUMN_ORDER_CHANGED,
|
|
DiffKind.COLUMN_REMOVED, DiffKind.GROUP_BY_CHANGE,
|
|
DiffKind.VIZ_TYPE_CHANGE]
|
|
for k in warning_kinds:
|
|
assert classify_severity(k) == DiffSeverity.WARNING
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Classifier.TestSeverityInfo
|
|
def test_classify_info(self):
|
|
from src.schemas.dashboard_testing import DiffKind, DiffSeverity
|
|
from src.services.dashboard_testing.structure_diff_classifier import classify_severity
|
|
info_kinds = [DiffKind.CHART_ADDED, DiffKind.COLUMN_ADDED,
|
|
DiffKind.FILTER_ADDED, DiffKind.METRIC_ADDED,
|
|
DiffKind.METRIC_REMOVED, DiffKind.DATASET_CHANGED]
|
|
for k in info_kinds:
|
|
assert classify_severity(k) == DiffSeverity.INFO
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Classifier.TestAffectedArtifacts
|
|
def test_affected_artifacts(self):
|
|
from src.schemas.dashboard_testing import DiffKind
|
|
from src.services.dashboard_testing.structure_diff_classifier import affected_artifacts_for
|
|
# CHART_REMOVED affects screenshot (not metric_assertion by classifier contract)
|
|
arts = affected_artifacts_for(DiffKind.CHART_REMOVED)
|
|
assert "screenshot_evidence" in arts
|
|
# COLUMN_REMOVED affects xlsx
|
|
arts = affected_artifacts_for(DiffKind.COLUMN_REMOVED)
|
|
assert "xlsx_export" in arts
|
|
assert "screenshot_evidence" not in arts
|
|
# FILTER_REMOVED affects metric_assertion
|
|
arts = affected_artifacts_for(DiffKind.FILTER_REMOVED)
|
|
assert "metric_assertion" in arts
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.Classifier.TestBuildMissingSnapshot
|
|
def test_build_missing_snapshot_diff(self):
|
|
from src.services.dashboard_testing.structure_diff_classifier import build_missing_snapshot_diff
|
|
diff = build_missing_snapshot_diff("v1.0.0", "v2.0.0", "v1.0.0", "/tmp/notfound.json")
|
|
assert diff.blocked is True
|
|
assert diff.summary["critical"] == 1
|
|
assert diff.summary["pass"] == 0
|
|
assert "not found" in diff.changes[0].detail.lower()
|
|
assert diff.query_model_hash_from is None
|
|
assert diff.query_model_hash_to is None
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.PersistSnapshot [C:2] [TYPE Class]
|
|
class TestPersistSnapshot:
|
|
"""Tests for atomic snapshot persistence."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.PersistSnapshot.TestPersistAndLoad
|
|
def test_persist_and_load_snapshot(self, tmp_path):
|
|
from src.schemas.dashboard_testing import DashboardQueryModel
|
|
from src.services.dashboard_testing.snapshot_loader import load_snapshot, persist_snapshot
|
|
model = DashboardQueryModel(
|
|
environment_id="test-env",
|
|
dashboard_id=1,
|
|
title="Test Dashboard",
|
|
charts=[],
|
|
datasets=[],
|
|
native_filters=[],
|
|
query_model_fingerprint="sha256:test123",
|
|
)
|
|
path = persist_snapshot(
|
|
model, "test-repo", "test-dash", "v1.0.0", base_path=str(tmp_path),
|
|
)
|
|
assert path.exists()
|
|
assert path.name == "v1.0.0.json"
|
|
assert "test-repo" in str(path)
|
|
assert "test-dash" in str(path)
|
|
|
|
loaded = load_snapshot(path, "v1.0.0")
|
|
assert loaded.environment_id == "test-env"
|
|
assert loaded.dashboard_id == 1
|
|
assert loaded.title == "Test Dashboard"
|
|
assert loaded.query_model_fingerprint == "sha256:test123"
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.PersistSnapshot.TestPersistPathContainment
|
|
def test_persist_invalid_path_raises(self, tmp_path):
|
|
from src.schemas.dashboard_testing import DashboardQueryModel
|
|
from src.services.dashboard_testing.snapshot_loader import persist_snapshot
|
|
model = DashboardQueryModel(
|
|
environment_id="e", dashboard_id=1, title="T",
|
|
query_model_fingerprint="sha256:x",
|
|
)
|
|
with pytest.raises(ValueError, match="must not contain"):
|
|
persist_snapshot(
|
|
model, "../escape", "dash", "v1.0.0", base_path=str(tmp_path),
|
|
)
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.PersistSnapshot.TestPersistAtomicity
|
|
def test_persist_snapshot_file_content(self, tmp_path):
|
|
import json
|
|
|
|
from src.schemas.dashboard_testing import DashboardQueryModel
|
|
from src.services.dashboard_testing.snapshot_loader import persist_snapshot
|
|
model = DashboardQueryModel(
|
|
environment_id="e2", dashboard_id=2, title="Atomic Test",
|
|
slug="atomic-test",
|
|
query_model_fingerprint="sha256:atomic_fp",
|
|
)
|
|
path = persist_snapshot(
|
|
model, "atomic-repo", "atomic-dash", "v2.0.0", base_path=str(tmp_path),
|
|
)
|
|
raw = json.loads(path.read_text())
|
|
assert raw["environment_id"] == "e2"
|
|
assert raw["dashboard_id"] == 2
|
|
assert raw["title"] == "Atomic Test"
|
|
assert raw["query_model_fingerprint"] == "sha256:atomic_fp"
|
|
assert raw["schema_version"] == 1
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.CaptureResponse [C:2] [TYPE Class]
|
|
class TestCaptureResponseSchema:
|
|
"""Schema validation for SnapshotCaptureResponse."""
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.CaptureResponse.TestCaptureResponse
|
|
def test_capture_response_schema(self):
|
|
from src.schemas.dashboard_testing import SnapshotCaptureResponse
|
|
resp = SnapshotCaptureResponse(
|
|
snapshot_path="/tmp/snap.json",
|
|
environment_id="prod",
|
|
dashboard_id=42,
|
|
release_version="v1.0.0",
|
|
repository_key="my-repo",
|
|
dashboard_key="my-dash",
|
|
charts_count=3,
|
|
filters_count=2,
|
|
datasets_count=1,
|
|
query_model_fingerprint="sha256:abc123",
|
|
warnings=0,
|
|
)
|
|
assert resp.snapshot_path == "/tmp/snap.json"
|
|
assert resp.charts_count == 3
|
|
assert resp.filters_count == 2
|
|
assert resp.datasets_count == 1
|
|
assert resp.query_model_fingerprint == "sha256:abc123"
|
|
# #endregion
|
|
|
|
# #region Test.BaselineEngine.StructureDiff.CaptureResponse.TestCaptureResponseDefaults
|
|
def test_capture_response_defaults(self):
|
|
from src.schemas.dashboard_testing import SnapshotCaptureResponse
|
|
resp = SnapshotCaptureResponse(
|
|
snapshot_path="/tmp/s.json",
|
|
environment_id="e",
|
|
dashboard_id=1,
|
|
release_version="v1",
|
|
repository_key="r",
|
|
dashboard_key="d",
|
|
)
|
|
assert resp.charts_count == 0
|
|
assert resp.filters_count == 0
|
|
assert resp.datasets_count == 0
|
|
assert resp.query_model_fingerprint == ""
|
|
assert resp.warnings == 0
|
|
# #endregion
|
|
# #endregion
|
|
|
|
|
|
# #endregion Test.BaselineEngine.StructureDiff.Service
|