"""Section 15 — Mists: 8-layer test suite. Tests span the ORM model (MusehubMist), the seven Pydantic wire models in musehub.models.mists, and the nine service functions in musehub.services.musehub_mists. Layer 1 Unit - TestUnitPydanticModels: MistResponse, MistListEntry, MistListResponse, MistCreateRequest, MistUpdateRequest, MistForkResponse, MistEmbedResponse construction and field defaults. - TestUnitValidators: MistCreateRequest.validate_visibility, validate_tags (count, length, null-byte, HTML-special), validate_filename (delegates to validate_mist_filename). Layer 2 Integration - TestIntegrationCreate: create_mist persists all fields, returns MistResponse. - TestIntegrationGet: get_mist returns populated response or None. - TestIntegrationList: list_mists — owner filter, secret exclusion, artifact_type filter, total counter, next_cursor pagination. - TestIntegrationFork: fork_mist copies content, sets fork_parent_id, increments source fork_count atomically. - TestIntegrationUpdate: update_mist patches fields, owner guard. - TestIntegrationDelete: delete_mist hard-delete, owner guard. - TestIntegrationCounters: increment_mist_view / increment_mist_embed atomic. - TestIntegrationForkList: get_mist_forks returns direct children. Layer 3 Edge Cases - TestEdgeCases: create duplicate mist_id → IntegrityError; fork with None parent → None; fork depth limit; update content increments version; list with bad cursor string is ignored. Layer 4 Stress - TestStress: 50 mists created, list returns expected totals; 5-level fork chain. Layer 5 Data Integrity - TestDataIntegrity: list total stays consistent after delete; counters are independent per mist; fork inherits tags and symbol_anchors; fork keeps parent visibility. Layer 6 Performance - TestPerformance: list 50 mists <500ms; count 100 forks <500ms. Layer 7 Security - TestSecurity: update/delete rejected for non-owner; secret mists hidden from public list; fork depth limit blocks chain attacks. Layer 8 Docstrings / API - TestDocstrings: every public service function has a docstring; every Pydantic model has a class docstring. """ from __future__ import annotations import secrets import time import pytest from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from datetime import datetime, timezone from musehub.core.genesis import compute_identity_id, compute_repo_id from musehub.db.musehub_repo_models import MusehubMist, MusehubRepo from musehub.models.mists import ( MistCreateRequest, MistEmbedResponse, MistForkResponse, MistListEntry, MistListResponse, MistResponse, MistUpdateRequest, ) from musehub.services.musehub_mists import ( create_mist, delete_mist, fork_mist, get_mist, get_mist_forks, increment_mist_embed, increment_mist_view, list_mists, update_mist, ) # =========================================================================== # Helpers # =========================================================================== _OWNER = "gabriel" _OTHER = "alice" def _uid() -> str: return secrets.token_hex(16) def _mist_id() -> str: return secrets.token_hex(6) async def _repo( session: AsyncSession, slug: str | None = None, owner: str = _OWNER, visibility: str = "public", ) -> MusehubRepo: slug = slug or _uid() created_at = datetime.now(tz=timezone.utc) owner_id = compute_identity_id(owner.encode()) repo = MusehubRepo( repo_id=compute_repo_id(owner_id, slug, "code", created_at.isoformat()), name=slug, owner=owner, slug=slug, visibility=visibility, owner_user_id=owner_id, created_at=created_at, updated_at=created_at, ) session.add(repo) await session.flush() await session.refresh(repo) return repo async def _mist( session: AsyncSession, repo_id: str, *, mist_id: str | None = None, owner: str = _OWNER, filename: str = "hello.py", content: str = "print('hello')", artifact_type: str = "code", language: str = "python", visibility: str = "public", tags: list[str] | None = None, symbol_anchors: list[str] | None = None, title: str = "", description: str = "", ) -> MistResponse: return await create_mist( session, mist_id=mist_id or _mist_id(), filename=filename, content=content, owner=owner, repo_id=repo_id, artifact_type=artifact_type, language=language, size_bytes=len(content.encode()), visibility=visibility, tags=tags or [], symbol_anchors=symbol_anchors or [], title=title, description=description, ) # =========================================================================== # Layer 1 — Unit: Pydantic models # =========================================================================== class TestUnitPydanticModels: """MistResponse and friends construct with expected defaults.""" def test_mist_response_required_fields(self) -> None: from datetime import datetime, timezone now = datetime.now(tz=timezone.utc) resp = MistResponse( mist_id="abc123def456", owner="gabriel", artifact_type="code", filename="script.py", created_at=now, updated_at=now, ) assert resp.mist_id == "abc123def456" assert resp.owner == "gabriel" assert resp.content == "" assert resp.signed is False assert resp.fork_parent_id is None assert resp.fork_depth == 0 assert resp.visibility == "public" assert resp.tags == [] assert resp.symbol_anchors == [] def test_mist_list_entry_primary_symbol_none(self) -> None: from datetime import datetime, timezone now = datetime.now(tz=timezone.utc) entry = MistListEntry( mist_id="abc123def456", owner="gabriel", artifact_type="code", filename="essay.md", created_at=now, updated_at=now, ) assert entry.primary_symbol is None assert entry.language == "" assert entry.title == "" def test_mist_list_response_defaults(self) -> None: resp = MistListResponse() assert resp.total == 0 assert resp.next_cursor is None assert resp.mists == [] def test_mist_create_request_minimal(self) -> None: req = MistCreateRequest(filename="score.mid", content="binary") assert req.visibility == "public" assert req.tags == [] assert req.agent_id == "" assert req.gpg_signature is None def test_mist_update_request_all_none(self) -> None: req = MistUpdateRequest() assert req.title is None assert req.content is None assert req.visibility is None def test_mist_fork_response_fields(self) -> None: from datetime import datetime, timezone now = datetime.now(tz=timezone.utc) resp = MistForkResponse( mist_id="fork11111111", owner="alice", fork_parent_id="orig11111111", artifact_type="code", filename="main.py", created_at=now, ) assert resp.url == "" assert resp.language == "" def test_mist_embed_response_fields(self) -> None: resp = MistEmbedResponse( mist_id="abc123def456", owner="gabriel", iframe="