# #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