personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4import json
5import re
6from datetime import UTC, datetime
7
8from solstone.think.surfaces.types import Cadence
9
10
11def _configure_env(tmp_path, monkeypatch) -> None:
12 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
13 monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1")
14
15 import solstone.think.utils as think_utils
16
17 think_utils._journal_path_cache = None
18
19
20def _write_journal_entity(
21 tmp_path,
22 entity_id: str,
23 name: str,
24 *,
25 entity_type: str = "Person",
26 aka: list[str] | None = None,
27 is_principal: bool = False,
28) -> None:
29 entity_dir = tmp_path / "entities" / entity_id
30 entity_dir.mkdir(parents=True, exist_ok=True)
31 payload: dict[str, object] = {"id": entity_id, "name": name, "type": entity_type}
32 if aka:
33 payload["aka"] = aka
34 if is_principal:
35 payload["is_principal"] = True
36 (entity_dir / "entity.json").write_text(json.dumps(payload), encoding="utf-8")
37
38
39def _write_facet_relationship(
40 tmp_path, facet: str, entity_id: str, *, description: str = ""
41) -> None:
42 relationship_dir = tmp_path / "facets" / facet / "entities" / entity_id
43 relationship_dir.mkdir(parents=True, exist_ok=True)
44 (relationship_dir / "entity.json").write_text(
45 json.dumps({"entity_id": entity_id, "description": description}),
46 encoding="utf-8",
47 )
48
49
50def _minimal_facet_tree(
51 tmp_path,
52 facets=("work",),
53 *,
54 muted_facets=(),
55 journal_entities: tuple[dict[str, object], ...] = (),
56) -> None:
57 for facet in facets:
58 facet_dir = tmp_path / "facets" / facet
59 facet_dir.mkdir(parents=True, exist_ok=True)
60 (facet_dir / "activities").mkdir(exist_ok=True)
61 (facet_dir / "facet.json").write_text(
62 json.dumps(
63 {
64 "title": facet.title(),
65 "description": "",
66 "color": "",
67 "emoji": "",
68 "muted": facet in set(muted_facets),
69 }
70 ),
71 encoding="utf-8",
72 )
73
74 for entity in journal_entities:
75 _write_journal_entity(
76 tmp_path,
77 str(entity["id"]),
78 str(entity["name"]),
79 entity_type=str(entity.get("type", "Person")),
80 aka=list(entity.get("aka", []))
81 if isinstance(entity.get("aka"), list)
82 else None,
83 is_principal=bool(entity.get("is_principal", False)),
84 )
85
86
87def _utc_ms(day: str, hour: int = 12) -> int:
88 parsed = datetime.strptime(f"{day} {hour:02d}:00:00", "%Y%m%d %H:%M:%S")
89 return int(parsed.replace(tzinfo=UTC).timestamp() * 1000)
90
91
92def _participant(
93 entity_id: str | None,
94 *,
95 name: str | None = None,
96 role: str = "attendee",
97 source: str = "screen",
98 confidence: float = 0.9,
99 context: str = "test participant",
100) -> dict[str, object]:
101 display_name = name or (
102 entity_id.replace("_", " ").title() if entity_id else "Unknown"
103 )
104 return {
105 "name": display_name,
106 "role": role,
107 "source": source,
108 "confidence": confidence,
109 "context": context,
110 "entity_id": entity_id,
111 }
112
113
114def _activity_record(
115 day: str, participation: list[dict[str, object]], **kwargs
116) -> dict:
117 activity = str(kwargs.pop("activity", "meeting"))
118 record_id = str(kwargs.pop("record_id", f"{activity}_{day}_120000"))
119 title = str(kwargs.pop("title", f"{record_id} title"))
120 record = {
121 "id": record_id,
122 "activity": activity,
123 "title": title,
124 "description": str(kwargs.pop("description", title)),
125 "details": str(kwargs.pop("details", "")),
126 "participation": participation,
127 "segments": list(kwargs.pop("segments", [])),
128 "active_entities": list(kwargs.pop("active_entities", [])),
129 "created_at": int(kwargs.pop("created_at", _utc_ms(day))),
130 "source": str(kwargs.pop("source", "user")),
131 "hidden": bool(kwargs.pop("hidden", False)),
132 "edits": list(kwargs.pop("edits", [])),
133 }
134 record.update(kwargs)
135 return record
136
137
138def _append_activity(facet: str, day: str, record: dict) -> None:
139 from solstone.think.activities import append_activity_record
140
141 append_activity_record(facet, day, record)
142
143
144def _commitment(
145 *,
146 owner: str = "Mina",
147 owner_entity_id: str | None = "mina",
148 action: str = "send proposal",
149 counterparty: str = "Ravi",
150 counterparty_entity_id: str | None = "ravi",
151 when: str = "tomorrow",
152 context: str = "Commitment context.",
153) -> dict[str, object]:
154 return {
155 "owner": owner,
156 "owner_entity_id": owner_entity_id,
157 "action": action,
158 "counterparty": counterparty,
159 "counterparty_entity_id": counterparty_entity_id,
160 "when": when,
161 "context": context,
162 }
163
164
165def _decision(
166 *,
167 owner: str = "Ravi",
168 owner_entity_id: str | None = "ravi",
169 action: str = "move launch review",
170 context: str = "Decision context.",
171) -> dict[str, object]:
172 return {
173 "owner": owner,
174 "owner_entity_id": owner_entity_id,
175 "action": action,
176 "context": context,
177 }
178
179
180def _write_story_activity(
181 facet: str,
182 day: str,
183 record_id: str,
184 created_at: int,
185 *,
186 commitments: list[dict[str, object]] | None = None,
187 closures: list[dict[str, object]] | None = None,
188 decisions: list[dict[str, object]] | None = None,
189) -> None:
190 from solstone.think.activities import append_activity_record, merge_story_fields
191
192 append_activity_record(
193 facet,
194 day,
195 _activity_record(
196 day,
197 [],
198 record_id=record_id,
199 created_at=created_at,
200 title=f"{record_id} title",
201 ),
202 )
203 merge_story_fields(
204 facet,
205 day,
206 record_id,
207 story={
208 "talent": "story",
209 "body": f"{record_id} summary",
210 "topics": ["profile"],
211 "confidence": 0.9,
212 },
213 commitments=commitments or [],
214 closures=closures or [],
215 decisions=decisions or [],
216 relations=[],
217 actor="story",
218 )
219
220
221def test_cadence_zero_interactions(tmp_path, monkeypatch):
222 from solstone.think.surfaces import profile as profile_surface
223
224 _configure_env(tmp_path, monkeypatch)
225 _minimal_facet_tree(
226 tmp_path,
227 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
228 )
229 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
230
231 assert profile_surface.cadence("Ravi") == Cadence(0, None, None, None)
232
233
234def test_cadence_single_day(tmp_path, monkeypatch):
235 from solstone.think.surfaces import profile as profile_surface
236
237 _configure_env(tmp_path, monkeypatch)
238 _minimal_facet_tree(
239 tmp_path,
240 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
241 )
242 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
243 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
244
245 _append_activity(
246 "work",
247 "20260418",
248 _activity_record(
249 "20260418",
250 [_participant("ravi", name="Ravi")],
251 record_id="meeting_20260418_a",
252 ),
253 )
254
255 cadence = profile_surface.cadence("Ravi")
256 assert cadence == Cadence(1, "20260418", None, None)
257
258
259def test_cadence_multi_day(tmp_path, monkeypatch):
260 from solstone.think.surfaces import profile as profile_surface
261
262 _configure_env(tmp_path, monkeypatch)
263 _minimal_facet_tree(
264 tmp_path,
265 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
266 )
267 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
268 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
269
270 for index, day in enumerate(["20260410", "20260414", "20260418"], start=1):
271 _append_activity(
272 "work",
273 day,
274 _activity_record(
275 day,
276 [_participant("ravi", name="Ravi")],
277 record_id=f"meeting_{index}",
278 ),
279 )
280
281 cadence = profile_surface.cadence("Ravi")
282 assert cadence is not None
283 assert cadence.recent_interactions_count_30d == 3
284 assert cadence.last_seen == "20260418"
285 assert cadence.avg_interval_days == 4.0
286 assert cadence.gone_quiet_since is None
287
288
289def test_cadence_gone_quiet_threshold(tmp_path, monkeypatch):
290 from solstone.think.surfaces import profile as profile_surface
291
292 _configure_env(tmp_path, monkeypatch)
293 _minimal_facet_tree(
294 tmp_path,
295 journal_entities=(
296 {"id": "quiet_person", "name": "Quiet Person", "type": "Person"},
297 {"id": "boundary_person", "name": "Boundary Person", "type": "Person"},
298 ),
299 )
300 _write_facet_relationship(tmp_path, "work", "quiet_person", description="Quiet")
301 _write_facet_relationship(
302 tmp_path, "work", "boundary_person", description="Boundary"
303 )
304 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
305
306 for day in ["20260401", "20260406"]:
307 _append_activity(
308 "work",
309 day,
310 _activity_record(
311 day,
312 [_participant("quiet_person", name="Quiet Person")],
313 record_id=f"quiet_{day}",
314 ),
315 )
316 for day in ["20260405", "20260410"]:
317 _append_activity(
318 "work",
319 day,
320 _activity_record(
321 day,
322 [_participant("boundary_person", name="Boundary Person")],
323 record_id=f"boundary_{day}",
324 ),
325 )
326
327 quiet = profile_surface.cadence("Quiet Person")
328 boundary = profile_surface.cadence("Boundary Person")
329
330 assert quiet is not None
331 assert quiet.gone_quiet_since == 14
332 assert boundary is not None
333 assert boundary.gone_quiet_since is None
334
335
336def test_cadence_gone_quiet_returns_int_days(tmp_path, monkeypatch):
337 from solstone.think.surfaces import profile as profile_surface
338
339 _configure_env(tmp_path, monkeypatch)
340 _minimal_facet_tree(
341 tmp_path,
342 journal_entities=(
343 {"id": "far_person", "name": "Far Person", "type": "Person"},
344 {"id": "equal_person", "name": "Equal Person", "type": "Person"},
345 {"id": "recent_person", "name": "Recent Person", "type": "Person"},
346 ),
347 )
348 for entity_id, description in (
349 ("far_person", "Far"),
350 ("equal_person", "Equal"),
351 ("recent_person", "Recent"),
352 ):
353 _write_facet_relationship(tmp_path, "work", entity_id, description=description)
354 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
355
356 for entity_id, days in (
357 ("far_person", ("20260330", "20260404")),
358 ("equal_person", ("20260405", "20260410")),
359 ("recent_person", ("20260406", "20260411")),
360 ):
361 for day in days:
362 _append_activity(
363 "work",
364 day,
365 _activity_record(
366 day,
367 [_participant(entity_id, name=entity_id.replace("_", " ").title())],
368 record_id=f"{entity_id}_{day}",
369 ),
370 )
371
372 far = profile_surface.cadence("Far Person")
373 equal = profile_surface.cadence("Equal Person")
374 recent = profile_surface.cadence("Recent Person")
375
376 assert far is not None
377 assert far.gone_quiet_since == 16
378 assert equal is not None
379 assert equal.gone_quiet_since is None
380 assert recent is not None
381 assert recent.gone_quiet_since is None
382
383
384def test_cadence_distinct_days_vs_record_count(tmp_path, monkeypatch):
385 from solstone.think.surfaces import profile as profile_surface
386
387 _configure_env(tmp_path, monkeypatch)
388 _minimal_facet_tree(
389 tmp_path,
390 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
391 )
392 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
393 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
394
395 for suffix in ("a", "b", "c"):
396 _append_activity(
397 "work",
398 "20260418",
399 _activity_record(
400 "20260418",
401 [_participant("ravi", name="Ravi")],
402 record_id=f"meeting_20260418_{suffix}",
403 ),
404 )
405
406 cadence = profile_surface.cadence("Ravi")
407 assert cadence == Cadence(3, "20260418", None, None)
408
409
410def test_resolve_exact_and_slug_and_aka_and_fuzzy(tmp_path, monkeypatch):
411 from solstone.think.surfaces import profile as profile_surface
412
413 _configure_env(tmp_path, monkeypatch)
414 _minimal_facet_tree(
415 tmp_path,
416 journal_entities=(
417 {
418 "id": "john_borthwick",
419 "name": "John Borthwick",
420 "type": "Person",
421 "aka": ["JB"],
422 },
423 ),
424 )
425 _write_facet_relationship(
426 tmp_path, "work", "john_borthwick", description="Investor"
427 )
428
429 queries = ["John Borthwick", "john_borthwick", "JB", "John Borthwik"]
430 resolved_ids = {
431 profile_surface._resolve_target(query).entity_id # noqa: SLF001
432 for query in queries
433 }
434
435 assert resolved_ids == {"john_borthwick"}
436
437
438def test_facets_filter_narrows_display_not_cadence(tmp_path, monkeypatch):
439 from solstone.think.surfaces import profile as profile_surface
440
441 _configure_env(tmp_path, monkeypatch)
442 _minimal_facet_tree(
443 tmp_path,
444 facets=("personal", "work"),
445 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
446 )
447 _write_facet_relationship(tmp_path, "work", "ravi", description="Work contact")
448 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
449 _append_activity(
450 "work",
451 "20260418",
452 _activity_record(
453 "20260418",
454 [_participant("ravi", name="Ravi")],
455 record_id="meeting_20260418",
456 ),
457 )
458
459 profile = profile_surface.full("Ravi", facets=["personal"])
460
461 assert profile is not None
462 assert profile.facets == ()
463 assert profile.description is None
464 assert profile.cadence.recent_interactions_count_30d == 1
465
466
467def test_include_mentions_toggle(tmp_path, monkeypatch):
468 from solstone.think.surfaces import profile as profile_surface
469
470 _configure_env(tmp_path, monkeypatch)
471 _minimal_facet_tree(
472 tmp_path,
473 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
474 )
475 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
476 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
477 _append_activity(
478 "work",
479 "20260418",
480 _activity_record(
481 "20260418",
482 [_participant("ravi", name="Ravi", role="mentioned")],
483 record_id="meeting_20260418",
484 ),
485 )
486
487 assert profile_surface.cadence("Ravi") == Cadence(0, None, None, None)
488 assert profile_surface.cadence("Ravi", include_mentions=True) == Cadence(
489 1, "20260418", None, None
490 )
491
492
493def test_self_view_returns_is_self_true(tmp_path, monkeypatch):
494 from solstone.think.surfaces import profile as profile_surface
495
496 _configure_env(tmp_path, monkeypatch)
497 _minimal_facet_tree(
498 tmp_path,
499 journal_entities=(
500 {
501 "id": "romeo_montague",
502 "name": "Romeo Montague",
503 "type": "Person",
504 "aka": ["RM"],
505 "is_principal": True,
506 },
507 ),
508 )
509 _write_facet_relationship(tmp_path, "work", "romeo_montague", description="Founder")
510
511 profile = profile_surface.full("RM")
512
513 assert profile is not None
514 assert profile.is_self is True
515
516
517def test_full_composes_ledger_open_loops(tmp_path, monkeypatch):
518 from solstone.think.surfaces import ledger as ledger_surface
519 from solstone.think.surfaces import profile as profile_surface
520
521 _configure_env(tmp_path, monkeypatch)
522 _minimal_facet_tree(
523 tmp_path,
524 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
525 )
526 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
527 _write_story_activity(
528 "work",
529 "20260418",
530 "meeting_090000_300",
531 _utc_ms("20260418", 9),
532 commitments=[_commitment(counterparty="Ravi", counterparty_entity_id="ravi")],
533 )
534
535 expected = tuple(ledger_surface.list(state="open", counterparty="ravi"))
536 profile = profile_surface.full("Ravi")
537
538 assert profile is not None
539 assert profile.open_with_them == expected
540
541
542def test_full_composes_ledger_closed_30d(tmp_path, monkeypatch):
543 from solstone.think.surfaces import ledger as ledger_surface
544 from solstone.think.surfaces import profile as profile_surface
545
546 _configure_env(tmp_path, monkeypatch)
547 _minimal_facet_tree(
548 tmp_path,
549 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
550 )
551 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
552 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
553 _write_story_activity(
554 "work",
555 "20260410",
556 "meeting_090000_300",
557 _utc_ms("20260410", 9),
558 commitments=[_commitment(counterparty="Ravi", counterparty_entity_id="ravi")],
559 )
560 _write_story_activity(
561 "work",
562 "20260415",
563 "meeting_100000_300",
564 _utc_ms("20260415", 10),
565 closures=[
566 {
567 "owner": "Mina",
568 "owner_entity_id": "mina",
569 "action": "send proposal",
570 "counterparty": "Ravi",
571 "counterparty_entity_id": "ravi",
572 "resolution": "sent",
573 "context": "Closure context.",
574 }
575 ],
576 )
577 _write_story_activity(
578 "work",
579 "20260301",
580 "meeting_080000_300",
581 _utc_ms("20260301", 8),
582 commitments=[
583 _commitment(
584 action="archive contract",
585 counterparty="Ravi",
586 counterparty_entity_id="ravi",
587 )
588 ],
589 )
590 _write_story_activity(
591 "work",
592 "20260305",
593 "meeting_083000_300",
594 _utc_ms("20260305", 8),
595 closures=[
596 {
597 "owner": "Mina",
598 "owner_entity_id": "mina",
599 "action": "archive contract",
600 "counterparty": "Ravi",
601 "counterparty_entity_id": "ravi",
602 "resolution": "archived",
603 "context": "Old closure context.",
604 }
605 ],
606 )
607
608 expected = tuple(
609 ledger_surface.list(
610 state="closed",
611 closed_since=profile_surface._day_minus(30), # noqa: SLF001
612 counterparty="ravi",
613 )
614 )
615 profile = profile_surface.full("Ravi")
616
617 assert profile is not None
618 assert profile.closed_with_them_30d == expected
619 assert len(profile.closed_with_them_30d) >= 1
620 assert all(
621 item.action != "archive contract" for item in profile.closed_with_them_30d
622 )
623
624
625def test_full_composes_ledger_decisions(tmp_path, monkeypatch):
626 from solstone.think.surfaces import ledger as ledger_surface
627 from solstone.think.surfaces import profile as profile_surface
628
629 _configure_env(tmp_path, monkeypatch)
630 _minimal_facet_tree(
631 tmp_path,
632 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
633 )
634 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
635 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
636 _write_story_activity(
637 "work",
638 "20260419",
639 "meeting_090000_300",
640 _utc_ms("20260419", 9),
641 decisions=[_decision(owner="Ravi", owner_entity_id="ravi")],
642 )
643
644 expected = tuple(ledger_surface.decisions(involving="ravi"))
645 profile = profile_surface.full("Ravi")
646
647 assert profile is not None
648 assert profile.decisions_involving_them == expected
649 assert len(profile.decisions_involving_them) >= 1
650
651
652def test_full_decisions_involving_them_includes_old(tmp_path, monkeypatch):
653 from solstone.think.surfaces import profile as profile_surface
654
655 _configure_env(tmp_path, monkeypatch)
656 _minimal_facet_tree(
657 tmp_path,
658 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
659 )
660 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
661 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
662 _write_story_activity(
663 "work",
664 "20260301",
665 "meeting_090000_300",
666 _utc_ms("20260301", 9),
667 decisions=[_decision(owner="Ravi", owner_entity_id="ravi")],
668 )
669
670 profile = profile_surface.full("Ravi")
671
672 assert profile is not None
673 assert any(
674 decision.day == "20260301" for decision in profile.decisions_involving_them
675 )
676
677
678def test_not_found_returns_none(tmp_path, monkeypatch):
679 from solstone.think.surfaces import profile as profile_surface
680
681 _configure_env(tmp_path, monkeypatch)
682 _minimal_facet_tree(tmp_path)
683
684 assert profile_surface.full("missing") is None
685
686
687def test_brief_shape_and_counts(tmp_path, monkeypatch):
688 from solstone.think.surfaces import ledger as ledger_surface
689 from solstone.think.surfaces import profile as profile_surface
690
691 _configure_env(tmp_path, monkeypatch)
692 _minimal_facet_tree(
693 tmp_path,
694 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
695 )
696 _write_facet_relationship(tmp_path, "work", "ravi", description="Customer")
697 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
698 _write_story_activity(
699 "work",
700 "20260418",
701 "meeting_090000_300",
702 _utc_ms("20260418", 9),
703 commitments=[_commitment(counterparty="Ravi", counterparty_entity_id="ravi")],
704 )
705 _write_story_activity(
706 "work",
707 "20260419",
708 "meeting_100000_300",
709 _utc_ms("20260419", 10),
710 decisions=[_decision(owner="Ravi", owner_entity_id="ravi")],
711 )
712
713 brief = profile_surface.brief("Ravi")
714
715 assert brief is not None
716 assert tuple(brief.__dataclass_fields__) == (
717 "entity_id",
718 "name",
719 "type",
720 "description",
721 "last_seen",
722 "open_loop_count",
723 "decisions_count_30d",
724 )
725 assert brief.entity_id == "ravi"
726 assert brief.name == "Ravi"
727 assert brief.type == "Person"
728 assert brief.description == "Customer"
729 assert brief.last_seen is None
730 assert brief.open_loop_count == len(
731 ledger_surface.list(state="open", counterparty="ravi")
732 )
733 assert brief.decisions_count_30d == len(
734 ledger_surface.decisions(involving="ravi", since="20260321")
735 )
736 assert not hasattr(brief, "is_self")
737 assert not hasattr(brief, "generated_at")
738
739
740def test_list_active_sort_dedup_window(tmp_path, monkeypatch):
741 from solstone.think.surfaces import profile as profile_surface
742
743 _configure_env(tmp_path, monkeypatch)
744 _minimal_facet_tree(tmp_path, facets=("personal", "work"))
745 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
746
747 _append_activity(
748 "work",
749 "20260418",
750 _activity_record(
751 "20260418",
752 [
753 _participant("zoe", name="Zoe"),
754 _participant("anna", name="Anna"),
755 _participant(None, name="Unknown"),
756 ],
757 record_id="meeting_work",
758 ),
759 )
760 _append_activity(
761 "personal",
762 "20260419",
763 _activity_record(
764 "20260419",
765 [_participant("anna", name="Anna")],
766 record_id="meeting_personal",
767 ),
768 )
769 _append_activity(
770 "work",
771 "20260301",
772 _activity_record(
773 "20260301",
774 [_participant("outside_window", name="Outside")],
775 record_id="meeting_old",
776 ),
777 )
778
779 assert profile_surface.list_active(window_days=30) == ["anna", "zoe"]
780
781
782def test_list_active_excludes_mentioned(tmp_path, monkeypatch):
783 from solstone.think.surfaces import profile as profile_surface
784
785 _configure_env(tmp_path, monkeypatch)
786 _minimal_facet_tree(tmp_path)
787 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
788 _append_activity(
789 "work",
790 "20260418",
791 _activity_record(
792 "20260418",
793 [_participant("ravi", name="Ravi", role="mentioned")],
794 record_id="meeting_mentioned",
795 ),
796 )
797
798 assert profile_surface.list_active(window_days=30) == []
799
800
801def test_muted_facet_included_in_cadence(tmp_path, monkeypatch):
802 from solstone.think.surfaces import profile as profile_surface
803
804 _configure_env(tmp_path, monkeypatch)
805 _minimal_facet_tree(
806 tmp_path,
807 facets=("quiet",),
808 muted_facets=("quiet",),
809 journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},),
810 )
811 _write_facet_relationship(tmp_path, "quiet", "ravi", description="Muted facet")
812 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
813 _append_activity(
814 "quiet",
815 "20260418",
816 _activity_record(
817 "20260418",
818 [_participant("ravi", name="Ravi")],
819 record_id="meeting_quiet",
820 ),
821 )
822
823 cadence = profile_surface.cadence("Ravi")
824 assert cadence == Cadence(1, "20260418", None, None)
825
826
827def test_utc_day_math(tmp_path, monkeypatch):
828 from solstone.think.surfaces import profile as profile_surface
829
830 _configure_env(tmp_path, monkeypatch)
831
832 assert re.fullmatch(r"\d{8}", profile_surface._today_day()) is not None # noqa: SLF001
833 monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260420")
834 assert profile_surface._day_minus(30) == "20260321" # noqa: SLF001