personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""Self-contained fixtures for todos app tests.
5
6These fixtures are fully standalone and only depend on pytest builtins.
7No shared dependencies from the root conftest.py are required.
8"""
9
10from __future__ import annotations
11
12import json
13from datetime import datetime
14
15import pytest
16
17
18@pytest.fixture(autouse=True)
19def _skip_supervisor_check(monkeypatch):
20 """Allow app CLI tests to run without a live solstone supervisor."""
21 monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1")
22
23
24@pytest.fixture
25def todo_env(tmp_path, monkeypatch):
26 """Create a temporary journal facet with optional todo entries.
27
28 This fixture is self-contained and provides a complete test environment
29 for todo operations without any external dependencies.
30
31 Usage:
32 def test_example(todo_env):
33 day, facet, todo_path = todo_env([
34 {"text": "First item"},
35 {"text": "Second item", "completed": True}
36 ])
37 # Now _SOLSTONE_JOURNAL_OVERRIDE is set and todo file exists
38 """
39
40 def _create(
41 entries: list[dict] | None = None,
42 day: str | None = None,
43 facet: str = "personal",
44 ):
45 if day is None:
46 day = datetime.now().strftime("%Y%m%d")
47 todos_dir = tmp_path / "facets" / facet / "todos"
48 todos_dir.mkdir(parents=True, exist_ok=True)
49 todo_path = todos_dir / f"{day}.jsonl"
50 if entries is not None:
51 lines = [json.dumps(e, ensure_ascii=False) for e in entries]
52 todo_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
53 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path))
54 return day, facet, todo_path
55
56 return _create
57
58
59@pytest.fixture
60def facet_env(tmp_path, monkeypatch):
61 """Create a temporary facet with full structure for testing.
62
63 Includes facet.json configuration, todos directory, and logs directory.
64 Suitable for testing action logging and other facet-scoped operations.
65 """
66 journal = tmp_path / "journal"
67 journal.mkdir()
68
69 def _create(facet: str = "test_facet"):
70 facet_path = journal / "facets" / facet
71 facet_path.mkdir(parents=True)
72
73 # Create facet.json
74 facet_json = facet_path / "facet.json"
75 facet_json.write_text(
76 json.dumps({"title": f"Test {facet}", "description": "Test facet"}),
77 encoding="utf-8",
78 )
79
80 # Create todos directory
81 (facet_path / "todos").mkdir()
82
83 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal))
84 return journal, facet
85
86 return _create
87
88
89@pytest.fixture
90def move_env(tmp_path, monkeypatch):
91 """Create a two-facet environment for move tests."""
92 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path))
93
94 def _create(
95 entries: list[dict] | None = None,
96 day: str = "20240101",
97 src_facet: str = "work",
98 dst_facet: str = "personal",
99 ):
100 for facet in [src_facet, dst_facet]:
101 facet_dir = tmp_path / "facets" / facet
102 facet_dir.mkdir(parents=True, exist_ok=True)
103 (facet_dir / "facet.json").write_text(
104 json.dumps({"title": f"Test {facet}", "description": "Test facet"}),
105 encoding="utf-8",
106 )
107
108 todos_dir = tmp_path / "facets" / src_facet / "todos"
109 todos_dir.mkdir(parents=True, exist_ok=True)
110 todo_path = todos_dir / f"{day}.jsonl"
111 if entries:
112 now_ms = int(datetime.now().timestamp() * 1000)
113 lines = []
114 for entry in entries:
115 data = {
116 "text": entry["text"],
117 "created_at": entry.get("created_at", now_ms),
118 "updated_at": entry.get("updated_at", now_ms),
119 }
120 if entry.get("cancelled"):
121 data["cancelled"] = True
122 if entry.get("completed"):
123 data["completed"] = True
124 if entry.get("nudge"):
125 data["nudge"] = entry["nudge"]
126 lines.append(json.dumps(data, ensure_ascii=False))
127 todo_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
128
129 return tmp_path, src_facet, dst_facet
130
131 return _create