Files
ss-tools/backend/tests/api/test_dashboard_testing.py
busya a32ca0631b feat(037): capture, verification lifecycle, inheritance + close 036 stabilization
- 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
2026-07-31 11:28:50 +03:00

314 lines
15 KiB
Python

# #region Test.Api.DashboardTesting [C:4] [TYPE Module] [SEMANTICS testing,api,dashboard-testing,lifecycle,durability]
# @defgroup API contract + lifecycle durability tests for dashboard-testing endpoints.
# @LAYER Test
# @RELATION VERIFIES -> [Api.DashboardTesting]
# @RELATION BINDS_TO -> [BaselineEngine.Candidates.Create]
# @TEST_EDGE: candidate_persistence -> DraftArtifact row exists in DB after 201.
# @TEST_EDGE: request_decide_consume_lifecycle -> Full FSM transitions persist at each step.
# @TEST_EDGE: cross_candidate_gate_rejection -> Gate bound to A cannot consume on B (409).
from __future__ import annotations
from src.core.database import SessionLocal
from src.models.agent_run import ApprovalGate, DraftArtifact
from .conftest import (
create_dashboard_testing_agent_run as _create_agent_run,
create_dashboard_testing_capture_artifact as _create_capture_artifact,
make_dashboard_testing_candidate_payload as _make_candidate_payload,
)
# #region Test.Api.DashboardTesting.RouterRegistered [C:2] [TYPE Function] [SEMANTICS test,api,dashboard-testing,registration]
# @BRIEF The dashboard-testing router is importable and declares its API prefix.
def test_router_registered():
"""T034: Verify router is importable and has correct routes."""
from src.api.routes.dashboard_testing import router
assert len(router.routes) >= 9
paths = [route.path for route in router.routes if hasattr(route, "path")]
assert "/api/dashboard-testing/query-model" in paths
assert "/api/dashboard-testing/structure-diff" in paths
assert "/api/dashboard-testing/verification-runs" in paths
# #endregion Test.Api.DashboardTesting.RouterRegistered
# #region Test.Api.DashboardTesting.LifecycleTests [C:4] [TYPE Class] [SEMANTICS test,api,dashboard-testing,lifecycle,durability]
# @BRIEF Prove DraftArtifact and ApprovalGate durability through HTTP lifecycle.
class TestLifecycleDurability:
"""API-level lifecycle durability — create, request, decide, consume, cross-candidate reject."""
# #region Test.Api.DashboardTesting.LifecycleTests.TestCreateCandidatePersistence [C:2] [TYPE Function] [SEMANTICS test,api,candidate,persistence]
# @BRIEF POST /baseline-candidates → 201 + DraftArtifact exists in fresh DB session.
# @TEST_EDGE: candidate_persistence -> DraftArtifact row queryable after request commits.
def test_create_candidate_persistence(self, dashboard_testing_client):
"""Create baseline candidate, verify DraftArtifact row survives in DB."""
setup_session = SessionLocal()
try:
run = _create_agent_run(setup_session)
run_id = run.id
artifact_id, sha256 = _create_capture_artifact(setup_session, run_id)
setup_session.commit()
finally:
setup_session.close()
payload = _make_candidate_payload(run_id, capture_artifact_ref=artifact_id, source_response_hash=sha256)
response = dashboard_testing_client.post(
"/api/dashboard-testing/baseline-candidates", json=payload
)
assert response.status_code == 201, (
f"Expected 201, got {response.status_code}: {response.text}"
)
data = response.json()
candidate_id = data["candidate_id"]
assert data["status"] == "draft"
verify_session = SessionLocal()
try:
draft = (
verify_session.query(DraftArtifact)
.filter(DraftArtifact.id == candidate_id)
.first()
)
assert draft is not None, f"DraftArtifact {candidate_id} not found in DB"
assert draft.kind == "baseline_candidate"
assert draft.run_id == run_id
assert (draft.capture_meta or {}).get("candidate_status") == "draft"
finally:
verify_session.close()
# #endregion Test.Api.DashboardTesting.LifecycleTests.TestCreateCandidatePersistence
# #region Test.Api.DashboardTesting.LifecycleTests.TestFullLifecyclePersistence [C:2] [TYPE Function] [SEMANTICS test,api,candidate,approval,lifecycle]
# @BRIEF Full request→decide→consume lifecycle, verifying DB state after each step.
# @TEST_EDGE: request_decide_consume_lifecycle -> ApprovalGate FSM persists.
# @TEST_EDGE: 201 returned for approval-gate creation with OpenAPI-compliant body.
def test_full_lifecycle_persistence(self, dashboard_testing_client):
"""Create → request_approval → decide_confirm → consume, verify persistence at each step."""
setup_session = SessionLocal()
try:
run = _create_agent_run(setup_session)
run_id = run.id
artifact_id, sha256 = _create_capture_artifact(setup_session, run_id)
setup_session.commit()
finally:
setup_session.close()
response = dashboard_testing_client.post(
"/api/dashboard-testing/baseline-candidates",
json=_make_candidate_payload(run_id, capture_artifact_ref=artifact_id, source_response_hash=sha256),
)
assert response.status_code == 201, f"Create failed: {response.text}"
candidate_id = response.json()["candidate_id"]
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_id}/approval-gate",
json={
"agent_run_id": run_id,
"release_version": "v1.0.0",
"release_commit_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b",
"reason": "approve this candidate",
"reason_required": False,
},
)
assert response.status_code == 201, f"Request approval failed: {response.text}"
gate_data = response.json()
gate_id = gate_data["gate_id"]
assert gate_data["status"] == "pending"
gate_session = SessionLocal()
try:
gate = gate_session.query(ApprovalGate).filter(ApprovalGate.id == gate_id).first()
assert gate is not None, f"ApprovalGate {gate_id} not in DB"
assert gate.status == "pending"
finally:
gate_session.close()
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_id}/approval-gate/{gate_id}/decide",
json={"decision": "confirm"},
)
assert response.status_code == 200, f"Decide confirm failed: {response.text}"
assert response.json()["status"] == "confirmed"
decide_session = SessionLocal()
try:
gate = decide_session.query(ApprovalGate).filter(ApprovalGate.id == gate_id).first()
assert gate is not None
assert gate.status == "confirmed"
assert gate.actor_id == "test-user-lifecycle"
finally:
decide_session.close()
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_id}/approval-gate/{gate_id}/consume"
"?release_version=v1.0.0&release_commit_hash=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b",
)
assert response.status_code == 200, f"Consume failed: {response.text}"
assert response.json()["consumed"] is True
consume_session = SessionLocal()
try:
gate = consume_session.query(ApprovalGate).filter(ApprovalGate.id == gate_id).first()
assert gate is not None
assert gate.status == "consumed", f"Expected consumed, got {gate.status}"
draft = (
consume_session.query(DraftArtifact)
.filter(DraftArtifact.id == candidate_id)
.first()
)
assert draft is not None
assert draft.persisted_at is not None, "Bound draft was not marked persisted"
metadata = draft.capture_meta or {}
assert metadata.get("bound_release_version") == "v1.0.0"
assert metadata.get("bound_release_commit_hash") == "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b"
finally:
consume_session.close()
# #endregion Test.Api.DashboardTesting.LifecycleTests.TestFullLifecyclePersistence
# #region Test.Api.DashboardTesting.LifecycleTests.TestCrossCandidateGateRejection [C:2] [TYPE Function] [SEMANTICS test,api,candidate,gate,rejection,cross-candidate]
# @BRIEF Gate bound to candidate A cannot be used to consume candidate B (409).
# @TEST_EDGE: cross_candidate_gate_rejection -> Consume with wrong gate returns 409.
def test_cross_candidate_gate_rejection(self, dashboard_testing_client):
"""Gate G1 bound to candidate A cannot consume on candidate B → 409."""
setup_session = SessionLocal()
try:
run = _create_agent_run(setup_session)
run_id = run.id
artifact_a_id, sha256_a = _create_capture_artifact(setup_session, run_id, result_key="count")
artifact_b_id, sha256_b = _create_capture_artifact(setup_session, run_id, result_key="sum")
setup_session.commit()
finally:
setup_session.close()
payload_a = _make_candidate_payload(run_id, capture_artifact_ref=artifact_a_id, source_response_hash=sha256_a)
payload_a["label"] = "candidate-A"
payload_a["result_key"] = "count"
response_a = dashboard_testing_client.post(
"/api/dashboard-testing/baseline-candidates", json=payload_a
)
assert response_a.status_code == 201
candidate_a_id = response_a.json()["candidate_id"]
payload_b = _make_candidate_payload(run_id, capture_artifact_ref=artifact_b_id, source_response_hash=sha256_b)
payload_b["label"] = "candidate-B"
payload_b["result_key"] = "sum"
response_b = dashboard_testing_client.post(
"/api/dashboard-testing/baseline-candidates", json=payload_b
)
assert response_b.status_code == 201
candidate_b_id = response_b.json()["candidate_id"]
gate_body = {
"agent_run_id": run_id,
"release_version": "v1.0.0",
"release_commit_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b",
}
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_a_id}/approval-gate",
json=gate_body,
)
assert response.status_code == 201, f"Gate creation failed: {response.text}"
gate_a_id = response.json()["gate_id"]
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_a_id}/approval-gate/{gate_a_id}/decide",
json={"decision": "confirm"},
)
assert response.status_code == 200
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_b_id}/approval-gate",
json=gate_body,
)
assert response.status_code == 201, f"Gate creation failed: {response.text}"
gate_b_id = response.json()["gate_id"]
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_b_id}/approval-gate/{gate_b_id}/decide",
json={"decision": "confirm"},
)
assert response.status_code == 200
consume_query = "?release_version=v1.0.0&release_commit_hash=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b"
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_b_id}/approval-gate/{gate_a_id}/consume{consume_query}",
)
assert response.status_code == 409, (
f"Expected 409 (conflict) for cross-candidate gate consume, "
f"got {response.status_code}: {response.text}"
)
assert "gate" in response.json().get("detail", "").lower()
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_a_id}/approval-gate/{gate_a_id}/consume{consume_query}",
)
assert response.status_code == 200, (
f"Expected 200 consuming A with own gate, "
f"got {response.status_code}: {response.text}"
)
response = dashboard_testing_client.post(
f"/api/dashboard-testing/baseline-candidates/{candidate_b_id}/approval-gate/{gate_b_id}/consume{consume_query}",
)
assert response.status_code == 200, (
f"Expected 200 consuming B with own gate, "
f"got {response.status_code}: {response.text}"
)
# #endregion Test.Api.DashboardTesting.LifecycleTests.TestCrossCandidateGateRejection
# #endregion Test.Api.DashboardTesting.LifecycleTests
# #region Test.Api.DashboardTesting.EnvironmentResolution [C:3] [TYPE Class] [SEMANTICS test,api,dashboard-testing,environment]
# @BRIEF Environment resolution through ConfigManager — 404 for unknown, success for valid.
class TestEnvironmentResolution:
"""API environment resolution — invalid IDs return 404, valid IDs proceed."""
# #region Test.Api.DashboardTesting.EnvironmentResolution.TestQueryModelUnknownEnv [C:2] [TYPE Function] [SEMANTICS test,api,environment,404]
# @BRIEF GET /query-model with unknown environment_id returns 404.
def test_query_model_unknown_environment(self, dashboard_testing_client):
"""Unknown environment_id returns 404 for inspect-query-model."""
response = dashboard_testing_client.get(
"/api/dashboard-testing/query-model",
params={"environment_id": "nonexistent", "dashboard_id": 1},
)
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
# #endregion Test.Api.DashboardTesting.EnvironmentResolution.TestQueryModelUnknownEnv
# #region Test.Api.DashboardTesting.EnvironmentResolution.TestExecuteQueryUnknownEnv [C:2] [TYPE Function] [SEMANTICS test,api,environment,404]
# @BRIEF POST /queries/execute with unknown environment_id returns 404.
def test_execute_query_unknown_environment(self, dashboard_testing_client):
"""Unknown environment_id returns 404 for execute-query."""
response = dashboard_testing_client.post(
"/api/dashboard-testing/queries/execute",
json={
"environment_id": "nonexistent",
"dashboard_id": 1,
"result_key": "count",
"normalized_filters": {"filters": [], "filters_hash": "sha256:empty"},
},
)
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
# #endregion Test.Api.DashboardTesting.EnvironmentResolution.TestExecuteQueryUnknownEnv
# #region Test.Api.DashboardTesting.EnvironmentResolution.TestNormalizeFiltersUnknownEnv [C:2] [TYPE Function] [SEMANTICS test,api,environment,404]
# @BRIEF POST /filters/normalize with unknown environment_id returns 404.
def test_normalize_filters_unknown_environment(self, dashboard_testing_client):
"""Unknown environment_id returns 404 for normalize-filters."""
response = dashboard_testing_client.post(
"/api/dashboard-testing/filters/normalize",
json={
"environment_id": "nonexistent",
"dashboard_id": 1,
"filter_inputs": [],
},
)
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
# #endregion Test.Api.DashboardTesting.EnvironmentResolution.TestNormalizeFiltersUnknownEnv
# #endregion Test.Api.DashboardTesting.EnvironmentResolution
# #endregion Test.Api.DashboardTesting