# SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc import json import re from datetime import UTC, datetime from solstone.think.surfaces.types import Cadence def _configure_env(tmp_path, monkeypatch) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1") import solstone.think.utils as think_utils think_utils._journal_path_cache = None def _write_journal_entity( tmp_path, entity_id: str, name: str, *, entity_type: str = "Person", aka: list[str] | None = None, is_principal: bool = False, ) -> None: entity_dir = tmp_path / "entities" / entity_id entity_dir.mkdir(parents=True, exist_ok=True) payload: dict[str, object] = {"id": entity_id, "name": name, "type": entity_type} if aka: payload["aka"] = aka if is_principal: payload["is_principal"] = True (entity_dir / "entity.json").write_text(json.dumps(payload), encoding="utf-8") def _write_facet_relationship( tmp_path, facet: str, entity_id: str, *, description: str = "" ) -> None: relationship_dir = tmp_path / "facets" / facet / "entities" / entity_id relationship_dir.mkdir(parents=True, exist_ok=True) (relationship_dir / "entity.json").write_text( json.dumps({"entity_id": entity_id, "description": description}), encoding="utf-8", ) def _minimal_facet_tree( tmp_path, facets=("work",), *, muted_facets=(), journal_entities: tuple[dict[str, object], ...] = (), ) -> None: for facet in facets: facet_dir = tmp_path / "facets" / facet facet_dir.mkdir(parents=True, exist_ok=True) (facet_dir / "activities").mkdir(exist_ok=True) (facet_dir / "facet.json").write_text( json.dumps( { "title": facet.title(), "description": "", "color": "", "emoji": "", "muted": facet in set(muted_facets), } ), encoding="utf-8", ) for entity in journal_entities: _write_journal_entity( tmp_path, str(entity["id"]), str(entity["name"]), entity_type=str(entity.get("type", "Person")), aka=list(entity.get("aka", [])) if isinstance(entity.get("aka"), list) else None, is_principal=bool(entity.get("is_principal", False)), ) def _utc_ms(day: str, hour: int = 12) -> int: parsed = datetime.strptime(f"{day} {hour:02d}:00:00", "%Y%m%d %H:%M:%S") return int(parsed.replace(tzinfo=UTC).timestamp() * 1000) def _participant( entity_id: str | None, *, name: str | None = None, role: str = "attendee", source: str = "screen", confidence: float = 0.9, context: str = "test participant", ) -> dict[str, object]: display_name = name or ( entity_id.replace("_", " ").title() if entity_id else "Unknown" ) return { "name": display_name, "role": role, "source": source, "confidence": confidence, "context": context, "entity_id": entity_id, } def _activity_record( day: str, participation: list[dict[str, object]], **kwargs ) -> dict: activity = str(kwargs.pop("activity", "meeting")) record_id = str(kwargs.pop("record_id", f"{activity}_{day}_120000")) title = str(kwargs.pop("title", f"{record_id} title")) record = { "id": record_id, "activity": activity, "title": title, "description": str(kwargs.pop("description", title)), "details": str(kwargs.pop("details", "")), "participation": participation, "segments": list(kwargs.pop("segments", [])), "active_entities": list(kwargs.pop("active_entities", [])), "created_at": int(kwargs.pop("created_at", _utc_ms(day))), "source": str(kwargs.pop("source", "user")), "hidden": bool(kwargs.pop("hidden", False)), "edits": list(kwargs.pop("edits", [])), } record.update(kwargs) return record def _append_activity(facet: str, day: str, record: dict) -> None: from solstone.think.activities import append_activity_record append_activity_record(facet, day, record) def _commitment( *, owner: str = "Mina", owner_entity_id: str | None = "mina", action: str = "send proposal", counterparty: str = "Ravi", counterparty_entity_id: str | None = "ravi", when: str = "tomorrow", context: str = "Commitment context.", ) -> dict[str, object]: return { "owner": owner, "owner_entity_id": owner_entity_id, "action": action, "counterparty": counterparty, "counterparty_entity_id": counterparty_entity_id, "when": when, "context": context, } def _decision( *, owner: str = "Ravi", owner_entity_id: str | None = "ravi", action: str = "move launch review", context: str = "Decision context.", ) -> dict[str, object]: return { "owner": owner, "owner_entity_id": owner_entity_id, "action": action, "context": context, } def _write_story_activity( facet: str, day: str, record_id: str, created_at: int, *, commitments: list[dict[str, object]] | None = None, closures: list[dict[str, object]] | None = None, decisions: list[dict[str, object]] | None = None, ) -> None: from solstone.think.activities import append_activity_record, merge_story_fields append_activity_record( facet, day, _activity_record( day, [], record_id=record_id, created_at=created_at, title=f"{record_id} title", ), ) merge_story_fields( facet, day, record_id, story={ "talent": "story", "body": f"{record_id} summary", "topics": ["profile"], "confidence": 0.9, }, commitments=commitments or [], closures=closures or [], decisions=decisions or [], relations=[], actor="story", ) def test_cadence_zero_interactions(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") assert profile_surface.cadence("Ravi") == Cadence(0, None, None, None) def test_cadence_single_day(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _append_activity( "work", "20260418", _activity_record( "20260418", [_participant("ravi", name="Ravi")], record_id="meeting_20260418_a", ), ) cadence = profile_surface.cadence("Ravi") assert cadence == Cadence(1, "20260418", None, None) def test_cadence_multi_day(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") for index, day in enumerate(["20260410", "20260414", "20260418"], start=1): _append_activity( "work", day, _activity_record( day, [_participant("ravi", name="Ravi")], record_id=f"meeting_{index}", ), ) cadence = profile_surface.cadence("Ravi") assert cadence is not None assert cadence.recent_interactions_count_30d == 3 assert cadence.last_seen == "20260418" assert cadence.avg_interval_days == 4.0 assert cadence.gone_quiet_since is None def test_cadence_gone_quiet_threshold(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=( {"id": "quiet_person", "name": "Quiet Person", "type": "Person"}, {"id": "boundary_person", "name": "Boundary Person", "type": "Person"}, ), ) _write_facet_relationship(tmp_path, "work", "quiet_person", description="Quiet") _write_facet_relationship( tmp_path, "work", "boundary_person", description="Boundary" ) monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") for day in ["20260401", "20260406"]: _append_activity( "work", day, _activity_record( day, [_participant("quiet_person", name="Quiet Person")], record_id=f"quiet_{day}", ), ) for day in ["20260405", "20260410"]: _append_activity( "work", day, _activity_record( day, [_participant("boundary_person", name="Boundary Person")], record_id=f"boundary_{day}", ), ) quiet = profile_surface.cadence("Quiet Person") boundary = profile_surface.cadence("Boundary Person") assert quiet is not None assert quiet.gone_quiet_since == 14 assert boundary is not None assert boundary.gone_quiet_since is None def test_cadence_gone_quiet_returns_int_days(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=( {"id": "far_person", "name": "Far Person", "type": "Person"}, {"id": "equal_person", "name": "Equal Person", "type": "Person"}, {"id": "recent_person", "name": "Recent Person", "type": "Person"}, ), ) for entity_id, description in ( ("far_person", "Far"), ("equal_person", "Equal"), ("recent_person", "Recent"), ): _write_facet_relationship(tmp_path, "work", entity_id, description=description) monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") for entity_id, days in ( ("far_person", ("20260330", "20260404")), ("equal_person", ("20260405", "20260410")), ("recent_person", ("20260406", "20260411")), ): for day in days: _append_activity( "work", day, _activity_record( day, [_participant(entity_id, name=entity_id.replace("_", " ").title())], record_id=f"{entity_id}_{day}", ), ) far = profile_surface.cadence("Far Person") equal = profile_surface.cadence("Equal Person") recent = profile_surface.cadence("Recent Person") assert far is not None assert far.gone_quiet_since == 16 assert equal is not None assert equal.gone_quiet_since is None assert recent is not None assert recent.gone_quiet_since is None def test_cadence_distinct_days_vs_record_count(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") for suffix in ("a", "b", "c"): _append_activity( "work", "20260418", _activity_record( "20260418", [_participant("ravi", name="Ravi")], record_id=f"meeting_20260418_{suffix}", ), ) cadence = profile_surface.cadence("Ravi") assert cadence == Cadence(3, "20260418", None, None) def test_resolve_exact_and_slug_and_aka_and_fuzzy(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=( { "id": "john_borthwick", "name": "John Borthwick", "type": "Person", "aka": ["JB"], }, ), ) _write_facet_relationship( tmp_path, "work", "john_borthwick", description="Investor" ) queries = ["John Borthwick", "john_borthwick", "JB", "John Borthwik"] resolved_ids = { profile_surface._resolve_target(query).entity_id # noqa: SLF001 for query in queries } assert resolved_ids == {"john_borthwick"} def test_facets_filter_narrows_display_not_cadence(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, facets=("personal", "work"), journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Work contact") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _append_activity( "work", "20260418", _activity_record( "20260418", [_participant("ravi", name="Ravi")], record_id="meeting_20260418", ), ) profile = profile_surface.full("Ravi", facets=["personal"]) assert profile is not None assert profile.facets == () assert profile.description is None assert profile.cadence.recent_interactions_count_30d == 1 def test_include_mentions_toggle(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _append_activity( "work", "20260418", _activity_record( "20260418", [_participant("ravi", name="Ravi", role="mentioned")], record_id="meeting_20260418", ), ) assert profile_surface.cadence("Ravi") == Cadence(0, None, None, None) assert profile_surface.cadence("Ravi", include_mentions=True) == Cadence( 1, "20260418", None, None ) def test_self_view_returns_is_self_true(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=( { "id": "romeo_montague", "name": "Romeo Montague", "type": "Person", "aka": ["RM"], "is_principal": True, }, ), ) _write_facet_relationship(tmp_path, "work", "romeo_montague", description="Founder") profile = profile_surface.full("RM") assert profile is not None assert profile.is_self is True def test_full_composes_ledger_open_loops(tmp_path, monkeypatch): from solstone.think.surfaces import ledger as ledger_surface from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") _write_story_activity( "work", "20260418", "meeting_090000_300", _utc_ms("20260418", 9), commitments=[_commitment(counterparty="Ravi", counterparty_entity_id="ravi")], ) expected = tuple(ledger_surface.list(state="open", counterparty="ravi")) profile = profile_surface.full("Ravi") assert profile is not None assert profile.open_with_them == expected def test_full_composes_ledger_closed_30d(tmp_path, monkeypatch): from solstone.think.surfaces import ledger as ledger_surface from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _write_story_activity( "work", "20260410", "meeting_090000_300", _utc_ms("20260410", 9), commitments=[_commitment(counterparty="Ravi", counterparty_entity_id="ravi")], ) _write_story_activity( "work", "20260415", "meeting_100000_300", _utc_ms("20260415", 10), closures=[ { "owner": "Mina", "owner_entity_id": "mina", "action": "send proposal", "counterparty": "Ravi", "counterparty_entity_id": "ravi", "resolution": "sent", "context": "Closure context.", } ], ) _write_story_activity( "work", "20260301", "meeting_080000_300", _utc_ms("20260301", 8), commitments=[ _commitment( action="archive contract", counterparty="Ravi", counterparty_entity_id="ravi", ) ], ) _write_story_activity( "work", "20260305", "meeting_083000_300", _utc_ms("20260305", 8), closures=[ { "owner": "Mina", "owner_entity_id": "mina", "action": "archive contract", "counterparty": "Ravi", "counterparty_entity_id": "ravi", "resolution": "archived", "context": "Old closure context.", } ], ) expected = tuple( ledger_surface.list( state="closed", closed_since=profile_surface._day_minus(30), # noqa: SLF001 counterparty="ravi", ) ) profile = profile_surface.full("Ravi") assert profile is not None assert profile.closed_with_them_30d == expected assert len(profile.closed_with_them_30d) >= 1 assert all( item.action != "archive contract" for item in profile.closed_with_them_30d ) def test_full_composes_ledger_decisions(tmp_path, monkeypatch): from solstone.think.surfaces import ledger as ledger_surface from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _write_story_activity( "work", "20260419", "meeting_090000_300", _utc_ms("20260419", 9), decisions=[_decision(owner="Ravi", owner_entity_id="ravi")], ) expected = tuple(ledger_surface.decisions(involving="ravi")) profile = profile_surface.full("Ravi") assert profile is not None assert profile.decisions_involving_them == expected assert len(profile.decisions_involving_them) >= 1 def test_full_decisions_involving_them_includes_old(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _write_story_activity( "work", "20260301", "meeting_090000_300", _utc_ms("20260301", 9), decisions=[_decision(owner="Ravi", owner_entity_id="ravi")], ) profile = profile_surface.full("Ravi") assert profile is not None assert any( decision.day == "20260301" for decision in profile.decisions_involving_them ) def test_not_found_returns_none(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) assert profile_surface.full("missing") is None def test_brief_shape_and_counts(tmp_path, monkeypatch): from solstone.think.surfaces import ledger as ledger_surface from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "work", "ravi", description="Customer") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _write_story_activity( "work", "20260418", "meeting_090000_300", _utc_ms("20260418", 9), commitments=[_commitment(counterparty="Ravi", counterparty_entity_id="ravi")], ) _write_story_activity( "work", "20260419", "meeting_100000_300", _utc_ms("20260419", 10), decisions=[_decision(owner="Ravi", owner_entity_id="ravi")], ) brief = profile_surface.brief("Ravi") assert brief is not None assert tuple(brief.__dataclass_fields__) == ( "entity_id", "name", "type", "description", "last_seen", "open_loop_count", "decisions_count_30d", ) assert brief.entity_id == "ravi" assert brief.name == "Ravi" assert brief.type == "Person" assert brief.description == "Customer" assert brief.last_seen is None assert brief.open_loop_count == len( ledger_surface.list(state="open", counterparty="ravi") ) assert brief.decisions_count_30d == len( ledger_surface.decisions(involving="ravi", since="20260321") ) assert not hasattr(brief, "is_self") assert not hasattr(brief, "generated_at") def test_list_active_sort_dedup_window(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path, facets=("personal", "work")) monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _append_activity( "work", "20260418", _activity_record( "20260418", [ _participant("zoe", name="Zoe"), _participant("anna", name="Anna"), _participant(None, name="Unknown"), ], record_id="meeting_work", ), ) _append_activity( "personal", "20260419", _activity_record( "20260419", [_participant("anna", name="Anna")], record_id="meeting_personal", ), ) _append_activity( "work", "20260301", _activity_record( "20260301", [_participant("outside_window", name="Outside")], record_id="meeting_old", ), ) assert profile_surface.list_active(window_days=30) == ["anna", "zoe"] def test_list_active_excludes_mentioned(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _append_activity( "work", "20260418", _activity_record( "20260418", [_participant("ravi", name="Ravi", role="mentioned")], record_id="meeting_mentioned", ), ) assert profile_surface.list_active(window_days=30) == [] def test_muted_facet_included_in_cadence(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, facets=("quiet",), muted_facets=("quiet",), journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), ) _write_facet_relationship(tmp_path, "quiet", "ravi", description="Muted facet") monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") _append_activity( "quiet", "20260418", _activity_record( "20260418", [_participant("ravi", name="Ravi")], record_id="meeting_quiet", ), ) cadence = profile_surface.cadence("Ravi") assert cadence == Cadence(1, "20260418", None, None) def test_utc_day_math(tmp_path, monkeypatch): from solstone.think.surfaces import profile as profile_surface _configure_env(tmp_path, monkeypatch) assert re.fullmatch(r"\d{8}", profile_surface._today_day()) is not None # noqa: SLF001 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420") assert profile_surface._day_minus(30) == "20260321" # noqa: SLF001