# #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment [C:3] [TYPE Module] [SEMANTICS test,api,openapi,yaml,alignment,spec] # @defgroup Compare checked-in OpenAPI YAML against generated FastAPI schema. # @LAYER Test # @RELATION VERIFIES -> [Api.DashboardTesting] # @TEST_EDGE: yaml_vs_generated_paths -> Both specs define the same dashboard-testing paths. # @TEST_EDGE: yaml_approval_fields -> All three required fields in ApprovalGateRequest. # @TEST_EDGE: yaml_approval_patterns -> Patterns match exact v-SemVer and 40-char SHA. # @TEST_EDGE: yaml_decision_enum -> decision enum values match Python schema. # @TEST_EDGE: yaml_semver_patterns -> release_version uses SemVer pattern; release_commit_hash uses 40 hex. # @TEST_EDGE: yaml_snapshot_endpoints -> YAML has /structure-snapshot/capture and /diff. # @TEST_EDGE: yaml_capture_env_required -> environment_id required in YAML capture endpoint. # @TEST_EDGE: yaml_verification_schemas -> VerificationRun has id, repository_id, trigger, environment_id, overall_status. # @TEST_EDGE: yaml_status_codes -> Verify 201 for create-candidate, approval-gate, verification-runs. from __future__ import annotations from pathlib import Path import pytest import re as _re import yaml as _yaml from src.app import app @pytest.fixture def openapi_schema(): """Extract the OpenAPI schema from the FastAPI app.""" return app.openapi() _SPEC_PATH = Path(__file__).parent.parent.parent.parent / "specs" / "037-superset-baseline-engine" / "contracts" / "dashboard-testing.openapi.yaml" # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.Aligner [C:2] [TYPE Class] [SEMANTICS test,api,openapi,yaml,alignment] class TestOpenApiYamlAlignment: """Compare checked-in OpenAPI YAML against generated FastAPI schema. Ensures the spec file at specs/037-.../dashboard-testing.openapi.yaml matches what FastAPI generates at runtime for all dashboard-testing paths, methods, success statuses, approval required fields, and SemVer/SHA patterns. """ # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestPathsMatch [C:2] [TYPE Function] # @BRIEF All paths in the checked-in YAML are also present in the generated spec (and vice versa). # @TEST_EDGE: yaml_vs_generated_paths -> Both specs define the same dashboard-testing paths. # @RATIONALE YAML uses camelCase path params (candidateId) while FastAPI generates snake_case # (candidate_id). The normalization replaces {camelCase} with {snake_case} before # comparison. This is a deliberately different naming convention — YAML is hand-authored # for external API consumers, FastAPI auto-generates from Python parameter names. def test_yaml_paths_match_generated(self, openapi_schema): """T037: Checked-in YAML paths match generated FastAPI OpenAPI paths.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") def _normalize_path(p: str) -> str: """Convert {camelCase} to {snake_case} in path params for comparison.""" def _to_snake(m: _re.Match) -> str: name = m.group(1) snake = _re.sub(r'(? All three required fields in ApprovalGateRequest. def test_yaml_approval_required_fields(self): """T037: Checked-in YAML ApprovalGateRequest requires agent_run_id, release_version, release_commit_hash.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) schemas = yaml_spec.get("components", {}).get("schemas", {}) gate_schema = schemas.get("ApprovalGateRequest", {}) required = gate_schema.get("required", []) for field in ("agent_run_id", "release_version", "release_commit_hash"): assert field in required, ( f"'{field}' must be required in YAML ApprovalGateRequest. Required: {required}" ) # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestApprovalRequiredFields # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestApprovalFieldPatterns [C:2] [TYPE Function] # @BRIEF ApprovalGateRequest release_version has v-SemVer pattern; release_commit_hash has 40-hex pattern. # @TEST_EDGE: yaml_approval_patterns -> Patterns match exact v-SemVer and 40-char SHA. def test_yaml_approval_field_patterns(self): """T037: Checked-in YAML ApprovalGateRequest has v-SemVer and 40-hex patterns.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) schemas = yaml_spec.get("components", {}).get("schemas", {}) gate_schema = schemas.get("ApprovalGateRequest", {}) props = gate_schema.get("properties", {}) version_props = props.get("release_version", {}) assert "pattern" in version_props, ( "release_version in YAML ApprovalGateRequest must have a pattern" ) ver_pattern = version_props["pattern"] assert ver_pattern.startswith("^v"), ( f"release_version pattern must start with ^v for v-prefixed SemVer, got: {ver_pattern}" ) hash_props = props.get("release_commit_hash", {}) assert hash_props.get("pattern") == "^[a-f0-9]{40}$", ( f"release_commit_hash pattern must be ^[a-f0-9]{40}$, got: {hash_props.get('pattern')}" ) assert hash_props.get("minLength") == 40, "release_commit_hash minLength must be 40" assert hash_props.get("maxLength") == 40, "release_commit_hash maxLength must be 40" # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestApprovalFieldPatterns # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestDecisionEnum [C:2] [TYPE Function] # @BRIEF Decision enum in YAML is exactly [confirm, deny] matching Python Literal. # @TEST_EDGE: yaml_decision_enum -> decision enum values match Python schema. def test_yaml_decision_enum(self): """T037: Decision enum in YAML decide endpoint matches Python Literal['confirm', 'deny'].""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) paths = yaml_spec.get("paths", {}) decide_path = next((p for p in paths if "decide" in p), None) assert decide_path is not None, "Decide path not found in YAML" request_body = paths[decide_path].get("post", {}).get("requestBody", {}) content = request_body.get("content", {}) json_schema = content.get("application/json", {}).get("schema", {}) decision_prop = json_schema.get("properties", {}).get("decision", {}) decision_enum = decision_prop.get("enum", []) assert decision_enum == ["confirm", "deny"], ( f"decision enum must be ['confirm', 'deny'], got: {decision_enum}" ) # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestDecisionEnum # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestSemverPatterns [C:2] [TYPE Function] # @BRIEF Consume endpoint in YAML uses canonical SemVer and 40-char commit hash patterns. # @TEST_EDGE: yaml_semver_patterns -> release_version uses SemVer pattern; release_commit_hash uses 40 hex. def test_yaml_consume_semver_commit_hash_patterns(self): """T037: YAML consume endpoint has SemVer and canonical 40-char commit hash patterns.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) paths = yaml_spec.get("paths", {}) consume_path = next(((p, v) for p, v in paths.items() if "consume" in p), None) assert consume_path is not None, "Consume path not found in YAML" _path, item = consume_path params = item.get("post", {}).get("parameters", []) version_param = next((p for p in params if p.get("name") == "release_version"), None) hash_param = next((p for p in params if p.get("name") == "release_commit_hash"), None) assert version_param is not None, "release_version parameter missing from consume endpoint in YAML" assert hash_param is not None, "release_commit_hash parameter missing from consume endpoint in YAML" version_schema = version_param.get("schema", {}) assert "pattern" in version_schema, "SemVer pattern missing from release_version in YAML" hash_schema = hash_param.get("schema", {}) assert hash_schema.get("pattern") == "^[a-f0-9]{40}$", ( f"commit hash pattern should require 40 lowercase hex chars, got: {hash_schema.get('pattern')}" ) assert hash_schema.get("minLength") == 40, "minLength should be 40 for commit hash" assert hash_schema.get("maxLength") == 40, "maxLength should be 40 for commit hash" # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestSemverPatterns # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestSnapshotEndpoints [C:2] [TYPE Function] # @BRIEF Release-bound snapshot endpoints exist in YAML with correct response schemas. # @TEST_EDGE: yaml_snapshot_endpoints -> YAML has /structure-snapshot/capture and /diff. def test_yaml_snapshot_endpoints(self): """T037: Checked-in YAML includes /dashboard-testing/structure-snapshot/* endpoints.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) paths = yaml_spec.get("paths", {}) capture_path = "/dashboard-testing/structure-snapshot/capture" assert capture_path in paths, ( f"Path {capture_path} missing from YAML spec. Available: {list(paths.keys())}" ) assert "post" in paths[capture_path], "capture endpoint missing POST" diff_path = "/dashboard-testing/structure-snapshot/diff" assert diff_path in paths, ( f"Path {diff_path} missing from YAML spec." ) assert "post" in paths[diff_path], "diff endpoint missing POST" # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestSnapshotEndpoints # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestCaptureEnvIdRequired [C:2] [TYPE Function] # @BRIEF Capture endpoint in YAML has environment_id as REQUIRED parameter (not optional). # @TEST_EDGE: yaml_capture_env_required -> environment_id required in YAML capture endpoint. def test_yaml_capture_env_id_required(self): """T037: Checked-in YAML capture endpoint environment_id is required.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) paths = yaml_spec.get("paths", {}) capture_path = "/dashboard-testing/structure-snapshot/capture" assert capture_path in paths, "Capture path missing from YAML" params = paths[capture_path].get("post", {}).get("parameters", []) env_param = next((p for p in params if p.get("name") == "environment_id"), None) assert env_param is not None, "environment_id parameter missing in YAML capture endpoint" assert env_param.get("required") is True, ( "environment_id MUST be required in YAML capture endpoint" ) # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestCaptureEnvIdRequired # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestVerificationRunSchemas [C:2] [TYPE Function] # @BRIEF VerificationRun and CategoryOutcome schemas exist in YAML with required fields. # @TEST_EDGE: yaml_verification_schemas -> VerificationRun has id, repository_id, trigger, environment_id, overall_status. def test_yaml_verification_run_schema(self): """T037: Checked-in YAML VerificationRun schema has required fields.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) schemas = yaml_spec.get("components", {}).get("schemas", {}) ver_schema = schemas.get("VerificationRun", {}) required = ver_schema.get("required", []) for field in ("id", "repository_id", "trigger", "environment_id", "overall_status", "created_at"): assert field in required, ( f"'{field}' must be required in YAML VerificationRun. Required: {required}" ) overall_status = ver_schema.get("properties", {}).get("overall_status", {}) status_enum = overall_status.get("enum", []) assert "pass" in status_enum assert "fail" in status_enum assert "blocked" in status_enum # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestVerificationRunSchemas # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestResponseStatusCodes [C:2] [TYPE Function] # @BRIEF Creation endpoints use 201, validation errors use 422 in YAML spec. # @TEST_EDGE: yaml_status_codes -> Verify 201 for create-candidate, approval-gate, verification-runs. def test_yaml_creation_endpoints_have_201(self): """T037: Checked-in YAML creation endpoints have 201 responses matching generated spec.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) paths = yaml_spec.get("paths", {}) creation_paths = [ "/dashboard-testing/baseline-candidates", "/dashboard-testing/baseline-candidates/{candidateId}/approval-gate", "/dashboard-testing/structure-snapshot/capture", "/dashboard-testing/verification-runs", ] for p in creation_paths: assert p in paths, f"Path {p} missing from YAML" post_op = paths[p].get("post", {}) responses = post_op.get("responses", {}) assert "201" in responses, ( f"201 response missing for {p} in YAML. Available: {list(responses.keys())}" ) # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestResponseStatusCodes # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestApprovalGateRequestProperties [C:2] [TYPE Function] [SEMANTICS test,api,openapi,approval,gate] # @BRIEF ApprovalGateRequest schema in YAML includes close_period, reason, reason_required fields. # @TEST_EDGE yaml_approval_gate_properties -> All properties match Python schema. def test_yaml_approval_gate_request_properties(self): """YAML ApprovalGateRequest has close_period, reason, reason_required matching Python.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) schemas = yaml_spec.get("components", {}).get("schemas", {}) gate_schema = schemas.get("ApprovalGateRequest", {}) props = gate_schema.get("properties", {}) # Must have close_period with correct description assert "close_period" in props, "close_period missing from YAML ApprovalGateRequest" assert "period identifier" in props["close_period"]["description"].lower(), ( "close_period description must mention period identifier" ) # Must have reason and reason_required assert "reason" in props, "reason missing from YAML ApprovalGateRequest" assert props["reason"].get("maxLength") == 500, "reason maxLength must be 500" assert "reason_required" in props, "reason_required missing from YAML ApprovalGateRequest" # Verify the decide endpoint 200 response decide_path = next((p for p in yaml_spec.get("paths", {}) if "decide" in p), None) assert decide_path is not None post_op = yaml_spec["paths"][decide_path].get("post", {}) responses = post_op.get("responses", {}) assert "200" in responses, "200 response missing for decide endpoint" content = responses["200"].get("content", {}) schema_ref = content.get("application/json", {}).get("schema", {}) assert "$ref" in schema_ref, "decide 200 must reference a schema" # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestApprovalGateRequestProperties # #region Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestCaptureEndpointStatuses [C:2] [TYPE Function] [SEMANTICS test,api,openapi,capture,statuses] # @BRIEF Capture endpoint has 201, 422, 500 responses in YAML. def test_yaml_capture_endpoint_statuses(self): """YAML capture endpoint has 201/422/500 statuses.""" if not _SPEC_PATH.exists(): pytest.skip(f"Spec file not found: {_SPEC_PATH}") yaml_spec = _yaml.safe_load(_SPEC_PATH.read_text()) paths = yaml_spec.get("paths", {}) capture_path = "/dashboard-testing/baseline-candidates/capture" assert capture_path in paths, "Capture path missing from YAML" responses = paths[capture_path]["post"]["responses"] for status in ("201", "422", "500"): assert status in responses, f"{status} response missing for capture endpoint" # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.TestCaptureEndpointStatuses # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment.Aligner # #endregion Test.Api.DashboardTesting.OpenAPI.YamlAlignment