fractafrag/services/api/tests/test_fulfillment.py
John Lightner 5936ab167e feat(M001): Desire Economy
Completed slices:
- S01: Desire Embedding & Clustering
- S02: Fulfillment Flow & Frontend

Branch: milestone/M001
2026-03-25 02:22:50 -05:00

294 lines
11 KiB
Python

"""Tests for desire fulfillment endpoint and cluster_count annotation.
Covers:
- fulfill_desire endpoint: happy path, not-found, not-open, shader validation
(tested via source assertions since FastAPI isn't in the test environment)
- cluster_count annotation: batch query pattern, single desire query
- Schema field: cluster_count exists in DesirePublic
Approach: Per K005, router functions can't be imported without FastAPI installed.
We verify correctness through:
1. Source-level structure assertions (endpoint wiring, imports, validation logic)
2. Isolated logic unit tests (annotation loop, status transitions)
3. Schema field verification via Pydantic model introspection
"""
import uuid
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import MagicMock
import pytest
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _router_source() -> str:
"""Read the desires router source code."""
return (
Path(__file__).resolve().parent.parent
/ "app"
/ "routers"
/ "desires.py"
).read_text(encoding="utf-8")
def _schema_source() -> str:
"""Read the schemas source code."""
return (
Path(__file__).resolve().parent.parent
/ "app"
/ "schemas"
/ "schemas.py"
).read_text(encoding="utf-8")
def _make_mock_desire(
*,
desire_id=None,
status="open",
heat_score=1.0,
):
"""Create a mock object simulating a Desire ORM instance."""
d = MagicMock()
d.id = desire_id or uuid.uuid4()
d.status = status
d.heat_score = heat_score
d.cluster_count = 0 # default before annotation
return d
# ---------------------------------------------------------------------------
# fulfill_desire — happy path structure
# ---------------------------------------------------------------------------
class TestFulfillHappyPath:
"""Verify the fulfill endpoint's happy-path logic via source analysis."""
def test_fulfill_sets_status_to_fulfilled(self):
"""The endpoint sets desire.status = 'fulfilled' on success."""
source = _router_source()
assert 'desire.status = "fulfilled"' in source
def test_fulfill_sets_fulfilled_by_shader(self):
"""The endpoint sets desire.fulfilled_by_shader = shader_id."""
source = _router_source()
assert "desire.fulfilled_by_shader = shader_id" in source
def test_fulfill_sets_fulfilled_at_timestamp(self):
"""The endpoint sets desire.fulfilled_at to current UTC time."""
source = _router_source()
assert "desire.fulfilled_at" in source
assert "datetime.now(timezone.utc)" in source
def test_fulfill_returns_status_response(self):
"""The endpoint returns a dict with status, desire_id, shader_id."""
source = _router_source()
assert '"status": "fulfilled"' in source
assert '"desire_id"' in source
assert '"shader_id"' in source
# ---------------------------------------------------------------------------
# fulfill_desire — error paths
# ---------------------------------------------------------------------------
class TestFulfillDesireNotFound:
"""404 when desire doesn't exist."""
def test_desire_not_found_raises_404(self):
source = _router_source()
# After desire lookup, checks scalar_one_or_none result
assert "Desire not found" in source
class TestFulfillDesireNotOpen:
"""400 when desire is not in 'open' status."""
def test_desire_not_open_check_exists(self):
source = _router_source()
assert 'desire.status != "open"' in source
def test_desire_not_open_error_message(self):
source = _router_source()
assert "Desire is not open" in source
class TestFulfillShaderNotFound:
"""404 when shader_id doesn't match any shader."""
def test_shader_lookup_exists(self):
source = _router_source()
assert "select(Shader).where(Shader.id == shader_id)" in source
def test_shader_not_found_raises_404(self):
source = _router_source()
assert "Shader not found" in source
class TestFulfillShaderNotPublished:
"""400 when shader status is not 'published'."""
def test_shader_status_validation(self):
source = _router_source()
assert 'shader.status != "published"' in source
def test_shader_not_published_error_message(self):
source = _router_source()
assert "Shader must be published to fulfill a desire" in source
# ---------------------------------------------------------------------------
# cluster_count annotation — logic unit tests
# ---------------------------------------------------------------------------
class TestClusterCountAnnotation:
"""Verify cluster_count annotation logic patterns."""
def test_list_desires_has_batch_cluster_query(self):
"""list_desires uses a batch query with ANY(:desire_ids)."""
source = _router_source()
assert "ANY(:desire_ids)" in source
assert "desire_clusters dc1" in source
assert "desire_clusters dc2" in source
def test_list_desires_avoids_n_plus_1(self):
"""Cluster counts are fetched in a single batch, not per-desire."""
source = _router_source()
# The pattern: build dict from batch query, then loop to annotate
assert "cluster_counts = {" in source
assert "cluster_counts.get(d.id, 0)" in source
def test_list_desires_skips_cluster_query_when_empty(self):
"""When no desires are returned, cluster query is skipped."""
source = _router_source()
assert "if desire_ids:" in source
def test_get_desire_annotates_single_cluster_count(self):
"""get_desire runs a cluster count query for the single desire."""
source = _router_source()
# Should have a cluster query scoped to a single desire_id
assert "WHERE dc1.desire_id = :desire_id" in source
def test_annotation_loop_sets_default_zero(self):
"""Desires without cluster entries default to cluster_count = 0."""
source = _router_source()
assert "cluster_counts.get(d.id, 0)" in source
def test_annotation_loop_logic(self):
"""Unit test: the annotation loop correctly maps cluster counts to desires."""
# Simulate the annotation loop from list_desires
d1 = _make_mock_desire()
d2 = _make_mock_desire()
d3 = _make_mock_desire()
desires = [d1, d2, d3]
# Simulate cluster query result: d1 has 3 in cluster, d3 has 2
cluster_counts = {d1.id: 3, d3.id: 2}
# This is the exact logic from the router
for d in desires:
d.cluster_count = cluster_counts.get(d.id, 0)
assert d1.cluster_count == 3
assert d2.cluster_count == 0 # not in any cluster
assert d3.cluster_count == 2
def test_get_desire_cluster_count_fallback(self):
"""get_desire sets cluster_count=0 when no cluster row exists."""
source = _router_source()
# The router checks `row[0] if row else 0`
assert "row[0] if row else 0" in source
# ---------------------------------------------------------------------------
# Schema field verification
# ---------------------------------------------------------------------------
class TestDesirePublicSchema:
"""Verify DesirePublic schema has the cluster_count field."""
def test_cluster_count_field_in_schema_source(self):
"""DesirePublic schema source contains cluster_count field."""
source = _schema_source()
assert "cluster_count" in source
def test_cluster_count_default_zero(self):
"""cluster_count defaults to 0 in the schema."""
source = _schema_source()
assert "cluster_count: int = 0" in source
def test_schema_from_attributes_enabled(self):
"""DesirePublic uses from_attributes=True for ORM compatibility."""
source = _schema_source()
# Find the DesirePublic class section
desire_public_idx = source.index("class DesirePublic")
desire_public_section = source[desire_public_idx:desire_public_idx + 200]
assert "from_attributes=True" in desire_public_section
def test_cluster_count_pydantic_model(self):
"""DesirePublic schema has cluster_count as an int field with default 0."""
source = _schema_source()
# Find the DesirePublic class and verify cluster_count is between
# heat_score and fulfilled_by_shader (correct field ordering)
desire_idx = source.index("class DesirePublic")
desire_section = source[desire_idx:desire_idx + 500]
heat_pos = desire_section.index("heat_score")
cluster_pos = desire_section.index("cluster_count")
fulfilled_pos = desire_section.index("fulfilled_by_shader")
assert heat_pos < cluster_pos < fulfilled_pos, (
"cluster_count should be between heat_score and fulfilled_by_shader"
)
# ---------------------------------------------------------------------------
# Wiring assertions
# ---------------------------------------------------------------------------
class TestFulfillmentWiring:
"""Structural assertions that the router is properly wired."""
def test_router_imports_shader_model(self):
"""desires.py imports Shader for shader validation."""
source = _router_source()
assert "Shader" in source.split("\n")[8] # near top imports
def test_router_imports_text_from_sqlalchemy(self):
"""desires.py imports text from sqlalchemy for raw SQL."""
source = _router_source()
assert "from sqlalchemy import" in source
assert "text" in source
def test_fulfill_endpoint_requires_auth(self):
"""fulfill_desire uses get_current_user dependency."""
source = _router_source()
# Find the fulfill_desire function
fulfill_idx = source.index("async def fulfill_desire")
fulfill_section = source[fulfill_idx:fulfill_idx + 500]
assert "get_current_user" in fulfill_section
def test_fulfill_endpoint_takes_shader_id_param(self):
"""fulfill_desire accepts shader_id as a query parameter."""
source = _router_source()
fulfill_idx = source.index("async def fulfill_desire")
fulfill_section = source[fulfill_idx:fulfill_idx + 300]
assert "shader_id" in fulfill_section
def test_list_desires_returns_desire_public(self):
"""list_desires endpoint uses DesirePublic response model."""
source = _router_source()
assert "response_model=list[DesirePublic]" in source
def test_get_desire_returns_desire_public(self):
"""get_desire endpoint uses DesirePublic response model."""
source = _router_source()
# Find the get_desire endpoint specifically
lines = source.split("\n")
for i, line in enumerate(lines):
if "async def get_desire" in line:
# Check the decorator line above
decorator_line = lines[i - 1]
assert "response_model=DesirePublic" in decorator_line
break