from __future__ import annotations from pathlib import Path from civicswarm.api import create_app from civicswarm.service import CivicSwarmService def build_service(tmp_path: Path) -> CivicSwarmService: return CivicSwarmService.from_path(tmp_path / "civicswarm.sqlite") def seed_service(service: CivicSwarmService) -> int: proposal = service.create_proposal( city="Springfield", title="School street safety", body=( "Add protected bike lanes near schools.\n" "Install safer crossings for pedestrians.\n" "Preserve bus access for students and elders." ), geography="ward-3", tags=["transport", "schools", "safety"], ) service.register_resident("resident-a", geography="ward-3", interests=["transport", "safety"], lived_experience=["parent"], languages=["en"]) service.register_resident("resident-b", geography="ward-7", interests=["schools"], lived_experience=["bus rider"], languages=["es", "en"]) service.register_resident("resident-c", geography="ward-3", interests=["parks"], lived_experience=["elder"], languages=["en"]) service.add_comment(proposal["id"], "I support safer crossings for children and elders.", resident_key="resident-a", geography="ward-3") service.add_comment(proposal["id"], "We need bus access and safer sidewalks.", resident_key="resident-b", geography="ward-7") service.add_comment(proposal["id"], "I worry about delayed bus service.", resident_key="resident-c", geography="ward-3") service.submit_preference(proposal["id"], "resident-a", 0.9) service.submit_preference(proposal["id"], "resident-b", 0.4) service.submit_preference(proposal["id"], "resident-c", 0.1) return proposal["id"] def test_service_routes_comments_and_exports_brief(tmp_path: Path): service = build_service(tmp_path) proposal_id = seed_service(service) summary = service.summarize_comments(proposal_id) assert summary assert "en" in summary routed = service.route_proposal(proposal_id, top_n=2) assert routed["fragments"] assert routed["fragments"][0]["matches"] dashboard = service.build_dashboard(proposal_id) assert dashboard["preference_aggregate"]["count"] == 3 assert dashboard["consensus_pockets"] brief = service.export_brief(proposal_id) assert brief["proposal"]["title"] == "School street safety" assert brief["provenance"] assert brief["ledger"] def test_dashboard_surface_exposes_api(tmp_path: Path): app = create_app(tmp_path / "api.sqlite") client = __import__("fastapi.testclient", fromlist=["TestClient"]).TestClient(app) proposal = client.post( "/proposals", json={ "city": "Springfield", "title": "Library weekend hours", "body": "Keep the library open later on weekends for students and families.", "geography": "ward-1", "tags": ["libraries", "education"], }, ).json() client.post("/residents", json={"resident_key": "resident-x", "geography": "ward-1", "interests": ["libraries"], "lived_experience": ["student"], "languages": ["en"]}) client.post(f"/proposals/{proposal['id']}/comments", json={"text": "I support later hours.", "resident_key": "resident-x", "channel": "sms"}) brief = client.get(f"/proposals/{proposal['id']}/brief").json() assert brief["proposal"]["title"] == "Library weekend hours" assert brief["summary"] def test_private_preference_aggregation_supports_noise(tmp_path: Path): service = build_service(tmp_path) proposal_id = seed_service(service) clean = service.aggregate_preferences(proposal_id) noisy = service.aggregate_preferences(proposal_id, epsilon=0.75, seed=42) assert clean["count"] == 3 assert noisy["count"] >= 1 assert noisy["mean_score"] != clean["mean_score"] or noisy["count"] != clean["count"]