- 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
314 lines
15 KiB
Python
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
|