- 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
191 lines
7.1 KiB
Python
191 lines
7.1 KiB
Python
# #region Test.Api.DashboardTesting.Fixtures [C:3] [TYPE Module] [SEMANTICS testing,api,dashboard-testing,fixtures,auth]
|
|
# @defgroup Shared deterministic fixtures and builders for dashboard-testing API tests.
|
|
# @LAYER Test
|
|
# @RELATION BINDS_TO -> [Api.DashboardTesting]
|
|
from __future__ import annotations
|
|
|
|
from itertools import count
|
|
import pytest
|
|
from uuid import uuid4
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.app import app
|
|
from src.dependencies import get_current_user
|
|
from src.models.agent_run import AgentRun
|
|
from src.models.auth import Role, User
|
|
from src.models.git import GitRepository, GitServerConfig
|
|
|
|
_VERIFICATION_REPOSITORY_DASHBOARD_IDS = count(9001)
|
|
|
|
|
|
# #region Test.Api.DashboardTesting.Fixtures.MakeAdminUser [C:1] [TYPE Function] [SEMANTICS test,api,dashboard-testing,auth]
|
|
def make_dashboard_testing_admin_user() -> User:
|
|
admin_role = Role(id="admin-role-test-001", name="Admin", is_admin=True)
|
|
user = User(id="test-user-lifecycle", username="tester", email="tester@test.com")
|
|
user.roles = [admin_role]
|
|
return user
|
|
# #endregion Test.Api.DashboardTesting.Fixtures.MakeAdminUser
|
|
|
|
|
|
# #region Test.Api.DashboardTesting.Fixtures.CreateCaptureArtifact [C:2] [TYPE Function] [SEMANTICS test,api,dashboard-testing,capture,artifact]
|
|
# @BRIEF Create a server-issued capture execution DraftArtifact in the database with deterministic hash.
|
|
# @POST Returns (artifact_id, sha256) tuple. The artifact is persisted to session.
|
|
# @INVARIANT The artifact has kind="capture_execution" and valid sha256 matching mock raw bytes.
|
|
def create_dashboard_testing_capture_artifact(session, run_id: str, result_key: str = "count") -> tuple[str, str]:
|
|
"""Create a capture execution DraftArtifact for lifecycle test fixtures.
|
|
|
|
Returns (artifact_id, sha256) with deterministic hash computed from mock raw bytes.
|
|
"""
|
|
import hashlib
|
|
|
|
from src.models.agent_run import DraftArtifact
|
|
|
|
mock_raw = f'{{"result": "{result_key}", "value": 100}}'.encode()
|
|
sha256 = hashlib.sha256(mock_raw).hexdigest()
|
|
|
|
artifact = DraftArtifact(
|
|
run_id=run_id,
|
|
kind="capture_execution",
|
|
name=f"capture:{result_key}:dev",
|
|
intended_path="",
|
|
content_ref=f"draft:{run_id}:{sha256}",
|
|
sha256=sha256,
|
|
validation_status="valid",
|
|
capture_meta={
|
|
"kind": "capture_execution",
|
|
"environment_id": "dev",
|
|
"dashboard_id": 42,
|
|
"chart_id": 1,
|
|
"dataset_id": None,
|
|
"result_key": result_key,
|
|
"source_response_hash": sha256,
|
|
"repo_key": "test-repo",
|
|
"dash_key": "test-dash",
|
|
"normalized_value": {
|
|
"kind": "integer",
|
|
"canonical_value": "100",
|
|
},
|
|
},
|
|
)
|
|
session.add(artifact)
|
|
session.flush()
|
|
return artifact.id, sha256
|
|
# #endregion Test.Api.DashboardTesting.Fixtures.CreateCaptureArtifact
|
|
|
|
|
|
# #region Test.Api.DashboardTesting.Fixtures.MakeCandidatePayload [C:2] [TYPE Function] [SEMANTICS test,api,dashboard-testing,candidate]
|
|
# @BRIEF Build a CandidateRequest dict for testing, optionally with a server-issued capture artifact ref.
|
|
# @INVARIANT For kind=metric, caller MUST provide capture_artifact_ref to satisfy server-side validation.
|
|
# source_response_hash must match the capture artifact's sha256.
|
|
def make_dashboard_testing_candidate_payload(
|
|
agent_run_id: str,
|
|
capture_artifact_ref: str | None = None,
|
|
source_response_hash: str | None = None,
|
|
) -> dict:
|
|
payload: dict = {
|
|
"environment_id": "dev",
|
|
"dashboard_id": 42,
|
|
"repository_key": "test-repo",
|
|
"dashboard_key": "test-dash",
|
|
"chart_id": 1,
|
|
"result_key": "count",
|
|
"label": "test-candidate",
|
|
"normalized_filters": {
|
|
"filters": [],
|
|
"filters_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
},
|
|
"candidate_value": {
|
|
"kind": "integer",
|
|
"raw_value": 100,
|
|
"canonical_value": "100",
|
|
"display_value": "100",
|
|
},
|
|
"source_response_hash": source_response_hash or ("a" * 64),
|
|
"comparison_policy": {
|
|
"type": "exact",
|
|
},
|
|
"provenance": {
|
|
"environment": "dev",
|
|
"actor": "tester",
|
|
"agent_run_id": agent_run_id,
|
|
},
|
|
"agent_run_id": agent_run_id,
|
|
}
|
|
if capture_artifact_ref:
|
|
payload["capture_artifact_ref"] = capture_artifact_ref
|
|
return payload
|
|
# #endregion Test.Api.DashboardTesting.Fixtures.MakeCandidatePayload
|
|
|
|
|
|
# #region Test.Api.DashboardTesting.Fixtures.CreateAgentRun [C:1] [TYPE Function] [SEMANTICS test,api,dashboard-testing,agent-run]
|
|
def create_dashboard_testing_agent_run(
|
|
session, user_id: str = "test-user-lifecycle"
|
|
) -> AgentRun:
|
|
run = AgentRun(
|
|
user_id=user_id,
|
|
dashboard_id="42",
|
|
environment_id="dev",
|
|
context_snapshot={},
|
|
status="CREATED",
|
|
)
|
|
session.add(run)
|
|
session.flush()
|
|
return run
|
|
# #endregion Test.Api.DashboardTesting.Fixtures.CreateAgentRun
|
|
|
|
|
|
# #region Test.Api.DashboardTesting.Fixtures.VerificationRepository [C:2] [TYPE Function] [SEMANTICS test,api,dashboard-testing,verification,repository]
|
|
# @BRIEF Create a real minimal GitServerConfig/GitRepository pair in the API database.
|
|
@pytest.fixture
|
|
def dashboard_testing_verification_repository_id() -> str:
|
|
from src.core.database import SessionLocal
|
|
|
|
session = SessionLocal()
|
|
try:
|
|
server = GitServerConfig(
|
|
id=str(uuid4()),
|
|
name="verification-api-server",
|
|
provider="GITHUB",
|
|
url="https://git.example.test",
|
|
pat="verification-api-token",
|
|
)
|
|
session.add(server)
|
|
session.flush()
|
|
repository = GitRepository(
|
|
id=str(uuid4()),
|
|
dashboard_id=next(_VERIFICATION_REPOSITORY_DASHBOARD_IDS),
|
|
config_id=server.id,
|
|
remote_url="https://git.example.test/org/verification-api.git",
|
|
local_path="/tmp/verification-api-repository",
|
|
)
|
|
session.add(repository)
|
|
session.commit()
|
|
return repository.id
|
|
finally:
|
|
session.close()
|
|
# #endregion Test.Api.DashboardTesting.Fixtures.VerificationRepository
|
|
|
|
|
|
# #region Test.Api.DashboardTesting.Fixtures.MockUser [C:2] [TYPE Function] [SEMANTICS test,api,dashboard-testing,fixture]
|
|
# @BRIEF Provide an admin user whose permission check passes through the admin bypass.
|
|
@pytest.fixture
|
|
def dashboard_testing_mock_user() -> User:
|
|
return make_dashboard_testing_admin_user()
|
|
# #endregion Test.Api.DashboardTesting.Fixtures.MockUser
|
|
|
|
|
|
# #region Test.Api.DashboardTesting.Fixtures.Client [C:2] [TYPE Function] [SEMANTICS test,api,dashboard-testing,fixture]
|
|
# @BRIEF Provide a TestClient with only the current-user dependency overridden.
|
|
@pytest.fixture
|
|
def dashboard_testing_client(dashboard_testing_mock_user: User):
|
|
app.dependency_overrides[get_current_user] = lambda: dashboard_testing_mock_user
|
|
try:
|
|
yield TestClient(app)
|
|
finally:
|
|
app.dependency_overrides.pop(get_current_user, None)
|
|
# #endregion Test.Api.DashboardTesting.Fixtures.Client
|
|
|
|
|
|
# #endregion Test.Api.DashboardTesting.Fixtures
|