"""Shared fixtures and factories for the mnema test suite. Conventions used throughout the tests: * Synthetic evidence is built with ``make_evidence`` (exposed as the ``evidence_factory`` fixture) so that every test constructs Evidence the same way. * Synthetic claims are built with ``make_claim`` (exposed as ``claim_factory``); the claim id doubles as a readable graph-node name ("A", "B", ...) in cascade scenarios. * All timestamps are timezone-aware UTC and serialised as ISO-8601 with a trailing ``Z``. """ from __future__ import annotations from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Union import pytest from mnema.core.keys import KeyPair from mnema.derive.model import Claim, Evidence, Provenance SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "samples" # --------------------------------------------------------------------------- # Plain helpers (importable by tests via fixtures below) # --------------------------------------------------------------------------- def iso(dt: datetime) -> str: """ISO-8601 UTC with a trailing Z.""" return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") def make_evidence( evidence_id: str, source: str, kind: str, observed_at: Union[str, datetime], payload: Dict[str, Any], ) -> Evidence: if isinstance(observed_at, datetime): observed_at = iso(observed_at) return Evidence( evidence_id=evidence_id, source=source, kind=kind, observed_at=observed_at, payload=payload, ) def make_claim( claim_id: str, inputs: Sequence[str], *, predicate: str = "test.fact", value: Optional[Dict[str, Any]] = None, confidence: float = 0.8, deriver: str = "test-deriver", rule: str = "test-rule", ) -> Claim: provenance = Provenance( deriver=deriver, deriver_version="0.0.0", rule=rule, inputs=list(inputs), ) return Claim( claim_id=claim_id, subject="self", predicate=predicate, value=value if value is not None else {"label": claim_id}, confidence=confidence, provenance=provenance, ) # --------------------------------------------------------------------------- # Generic fixtures # --------------------------------------------------------------------------- @pytest.fixture def keypair() -> KeyPair: return KeyPair.generate() @pytest.fixture def evidence_factory(): return make_evidence @pytest.fixture def claim_factory(): return make_claim @pytest.fixture def samples_dir() -> Path: assert SAMPLES_DIR.is_dir(), f"sample data directory missing: {SAMPLES_DIR}" return SAMPLES_DIR # --------------------------------------------------------------------------- # Synthetic calendar evidence: a strong Tue/Thu 07:00 gym routine, March 2025 # --------------------------------------------------------------------------- GYM_DAYS = (4, 6, 11, 13, 18, 20, 25, 27) # Tuesdays and Thursdays, March 2025 def _calendar_event(eid: str, start: datetime, title: str, location: str) -> Evidence: end = start + timedelta(hours=1) return make_evidence( eid, "calendar", "calendar.event", start, { "uid": f"uid-{eid}", "title": title, "start": iso(start), "end": iso(end), "location": location, }, ) @pytest.fixture def gym_calendar_evidence() -> List[Evidence]: """Eight gym sessions: every Tuesday and Thursday of March 2025 at 07:00.""" out = [] for i, day in enumerate(GYM_DAYS): start = datetime(2025, 3, day, 7, 0, tzinfo=timezone.utc) out.append(_calendar_event(f"ev-cal-gym-{i:02d}", start, "Gym session", "Iron Temple Gym")) return out @pytest.fixture def noisy_calendar_evidence(gym_calendar_evidence) -> List[Evidence]: """The gym routine plus one-off events that must NOT become routines.""" extras = [ ("ev-cal-extra-00", datetime(2025, 3, 5, 10, 0, tzinfo=timezone.utc), "Coffee with recruiter", "Ritual Coffee"), ("ev-cal-extra-01", datetime(2025, 3, 12, 15, 0, tzinfo=timezone.utc), "Dentist appointment", "Smile Dental"), ("ev-cal-extra-02", datetime(2025, 3, 21, 9, 30, tzinfo=timezone.utc), "Flight to NYC", "SFO"), ] out = list(gym_calendar_evidence) for eid, start, title, location in extras: out.append(_calendar_event(eid, start, title, location)) return out # --------------------------------------------------------------------------- # Synthetic photo evidence: a repeated cafe cluster plus a one-off location # --------------------------------------------------------------------------- BLUE_BOTTLE = (37.77630, -122.42320) _CAFE_VISITS = ( (3, 3, 8, 31), (3, 8, 9, 2), (3, 10, 8, 27), (3, 17, 8, 33), (3, 24, 8, 29), ) def _photo(eid: str, taken: datetime, lat: float, lon: float, place_label: str, people: List[str]) -> Evidence: return make_evidence( eid, "photos", "photo.meta", taken, { "id": eid, "taken_at": iso(taken), "lat": lat, "lon": lon, "place_label": place_label, "people": people, "camera": "phone-rear", }, ) @pytest.fixture def cafe_photo_evidence() -> List[Evidence]: """Five morning photos at the same cafe cluster, plus one one-off photo.""" out = [] for i, (month, day, hour, minute) in enumerate(_CAFE_VISITS): taken = datetime(2025, month, day, hour, minute, tzinfo=timezone.utc) jitter = (i - 2) * 0.00002 out.append(_photo( f"ev-photo-cafe-{i:02d}", taken, BLUE_BOTTLE[0] + jitter, BLUE_BOTTLE[1] - jitter, "Blue Bottle Coffee", [], )) out.append(_photo( "ev-photo-oneoff-00", datetime(2025, 3, 15, 11, 10, tzinfo=timezone.utc), 37.83242, -122.47951, "Golden Gate Overlook", ["Sam", "Priya"], )) return out # --------------------------------------------------------------------------- # Synthetic note evidence: repeated mentions of a person, and preferences # --------------------------------------------------------------------------- def _note(eid: str, created: datetime, text: str) -> Evidence: return make_evidence( eid, "notes", "note", created, {"text": text, "created_at": iso(created)}, ) @pytest.fixture def relationship_notes_evidence() -> List[Evidence]: """Three notes mentioning Sam (strong signal), one mentioning Priya (weak).""" return [ _note("ev-note-rel-00", datetime(2025, 3, 9, 16, 0, tzinfo=timezone.utc), "Great afternoon with Sam at Dolores Park. Sam brought sandwiches."), _note("ev-note-rel-01", datetime(2025, 3, 14, 21, 15, tzinfo=timezone.utc), "Sam recommended a book on urban planning, should pick it up."), _note("ev-note-rel-02", datetime(2025, 3, 26, 8, 40, tzinfo=timezone.utc), "Call Sam about the camping trip before Friday."), _note("ev-note-rel-03", datetime(2025, 3, 16, 12, 5, tzinfo=timezone.utc), "Priya suggested the Golden Gate overlook, photos came out great."), ] @pytest.fixture def preference_notes_evidence() -> List[Evidence]: """Repeated positive mentions of oat milk lattes, one negative of cilantro.""" return [ _note("ev-note-pref-00", datetime(2025, 3, 3, 8, 45, tzinfo=timezone.utc), "I really love oat milk lattes - best way to start the day."), _note("ev-note-pref-01", datetime(2025, 3, 10, 8, 50, tzinfo=timezone.utc), "Oat milk latte again this morning. Worth it."), _note("ev-note-pref-02", datetime(2025, 3, 24, 9, 0, tzinfo=timezone.utc), "Another oat milk latte. I love this place."), _note("ev-note-pref-03", datetime(2025, 3, 18, 19, 30, tzinfo=timezone.utc), "Dinner note: I hate cilantro, remember to ask for none."), _note("ev-note-pref-04", datetime(2025, 3, 20, 7, 55, tzinfo=timezone.utc), "Pick up dry cleaning tomorrow."), ]