personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_ledger_routes.py
10 kB 331 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import json 7 8from solstone.convey import create_app 9from tests._baseline_harness import make_test_client 10from tests.test_surfaces_ledger import ( 11 _commitment, 12 _minimal_facet_tree, 13 _utc_ms, 14 _write_story_activity, 15) 16 17PREFIX = "/api/ledger" 18 19 20def _assert_error(response, status: int) -> dict: 21 assert response.status_code == status 22 data = response.get_json() 23 assert data["reason_code"] 24 if status == 400: 25 assert data["detail"] 26 return data 27 28 29def _configure_journal(tmp_path, monkeypatch) -> None: 30 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 31 config_dir = tmp_path / "config" 32 config_dir.mkdir(parents=True, exist_ok=True) 33 (config_dir / "journal.json").write_text( 34 json.dumps({"setup": {"completed_at": 1}}), 35 encoding="utf-8", 36 ) 37 38 39def _configure_unset_journal(tmp_path, monkeypatch) -> None: 40 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 41 42 43def _decision( 44 *, 45 owner: str = "Mina", 46 owner_entity_id: str | None = "mina", 47 action: str = "move launch review", 48 context: str = "Decision context.", 49) -> dict: 50 return { 51 "owner": owner, 52 "owner_entity_id": owner_entity_id, 53 "action": action, 54 "context": context, 55 } 56 57 58def test_ledger_list_collection_envelope_and_bound(tmp_path, monkeypatch): 59 _configure_journal(tmp_path, monkeypatch) 60 _minimal_facet_tree(tmp_path) 61 for index in range(25): 62 _write_story_activity( 63 "work", 64 "20260410", 65 f"meeting_{index:06d}_300", 66 _utc_ms(f"2026-04-10T09:{index:02d}:00Z"), 67 commitments=[_commitment(action=f"action number {index}")], 68 ) 69 client = make_test_client(tmp_path) 70 71 response = client.get(PREFIX) 72 73 assert response.status_code == 200 74 data = response.get_json() 75 assert isinstance(data, dict) 76 assert len(data["items"]) == 20 77 assert data["total"] == 25 78 79 80def test_ledger_list_counterparty_filter_narrows_total(tmp_path, monkeypatch): 81 _configure_journal(tmp_path, monkeypatch) 82 _minimal_facet_tree(tmp_path) 83 for index in range(3): 84 _write_story_activity( 85 "work", 86 "20260410", 87 f"ravi_{index:06d}_300", 88 _utc_ms(f"2026-04-10T09:{index:02d}:00Z"), 89 commitments=[_commitment(action=f"ravi action {index}")], 90 ) 91 for index in range(2): 92 _write_story_activity( 93 "work", 94 "20260410", 95 f"imani_{index:06d}_300", 96 _utc_ms(f"2026-04-10T10:{index:02d}:00Z"), 97 commitments=[ 98 _commitment( 99 action=f"imani action {index}", 100 counterparty="Imani", 101 counterparty_entity_id="imani", 102 ) 103 ], 104 ) 105 client = make_test_client(tmp_path) 106 107 all_response = client.get(PREFIX) 108 filtered_response = client.get(f"{PREFIX}?counterparty=Ravi") 109 110 assert all_response.status_code == 200 111 assert filtered_response.status_code == 200 112 assert filtered_response.get_json()["total"] < all_response.get_json()["total"] 113 114 115def test_ledger_list_bad_state_returns_invalid_request_value(tmp_path, monkeypatch): 116 _configure_journal(tmp_path, monkeypatch) 117 _minimal_facet_tree(tmp_path) 118 client = make_test_client(tmp_path) 119 120 data = _assert_error(client.get(f"{PREFIX}?state=bogus"), 400) 121 122 assert data["reason_code"] == "invalid_request_value" 123 124 125def test_ledger_list_bad_sort_returns_invalid_request_value(tmp_path, monkeypatch): 126 _configure_journal(tmp_path, monkeypatch) 127 _minimal_facet_tree(tmp_path) 128 client = make_test_client(tmp_path) 129 130 data = _assert_error(client.get(f"{PREFIX}?sort=bogus"), 400) 131 132 assert data["reason_code"] == "invalid_request_value" 133 134 135def test_ledger_list_bad_age_days_gte_returns_invalid_request_value( 136 tmp_path, monkeypatch 137): 138 _configure_journal(tmp_path, monkeypatch) 139 _minimal_facet_tree(tmp_path) 140 client = make_test_client(tmp_path) 141 142 data = _assert_error(client.get(f"{PREFIX}?age_days_gte=abc"), 400) 143 144 assert data["reason_code"] == "invalid_request_value" 145 146 147def test_ledger_list_bad_closed_since_returns_invalid_day(tmp_path, monkeypatch): 148 _configure_journal(tmp_path, monkeypatch) 149 _minimal_facet_tree(tmp_path) 150 client = make_test_client(tmp_path) 151 152 notaday = _assert_error(client.get(f"{PREFIX}?closed_since=notaday"), 400) 153 bad_month = _assert_error(client.get(f"{PREFIX}?closed_since=20261301"), 400) 154 155 assert notaday["reason_code"] == "invalid_day" 156 assert bad_month["reason_code"] == "invalid_day" 157 158 159def test_ledger_decisions_collection_envelope_and_routing(tmp_path, monkeypatch): 160 _configure_journal(tmp_path, monkeypatch) 161 _minimal_facet_tree(tmp_path) 162 _write_story_activity( 163 "work", 164 "20260410", 165 "meeting_090000_300", 166 _utc_ms("2026-04-10T09:00:00Z"), 167 decisions=[_decision(action="move launch review")], 168 ) 169 client = make_test_client(tmp_path) 170 171 response = client.get(f"{PREFIX}/decisions") 172 173 assert response.status_code == 200 174 data = response.get_json() 175 assert isinstance(data, dict) 176 assert data["total"] == 1 177 assert data["items"][0]["action"] == "move launch review" 178 179 180def test_ledger_decisions_bad_since_returns_invalid_day(tmp_path, monkeypatch): 181 _configure_journal(tmp_path, monkeypatch) 182 _minimal_facet_tree(tmp_path) 183 client = make_test_client(tmp_path) 184 185 data = _assert_error(client.get(f"{PREFIX}/decisions?since=bad"), 400) 186 187 assert data["reason_code"] == "invalid_day" 188 189 190def test_ledger_get_item_returns_recursive_dataclass_dict(tmp_path, monkeypatch): 191 _configure_journal(tmp_path, monkeypatch) 192 _minimal_facet_tree(tmp_path) 193 _write_story_activity( 194 "work", 195 "20260410", 196 "meeting_090000_300", 197 _utc_ms("2026-04-10T09:00:00Z"), 198 commitments=[_commitment()], 199 ) 200 client = make_test_client(tmp_path) 201 item_id = client.get(PREFIX).get_json()["items"][0]["id"] 202 203 response = client.get(f"{PREFIX}/{item_id}") 204 205 assert response.status_code == 200 206 data = response.get_json() 207 assert data["id"] == item_id 208 assert data["sources"] 209 assert "facet" in data["sources"][0] 210 211 212def test_ledger_get_item_missing_returns_ledger_item_not_found(tmp_path, monkeypatch): 213 _configure_journal(tmp_path, monkeypatch) 214 _minimal_facet_tree(tmp_path) 215 client = make_test_client(tmp_path) 216 217 data = _assert_error(client.get(f"{PREFIX}/does-not-exist"), 404) 218 219 assert data["reason_code"] == "ledger_item_not_found" 220 221 222def test_ledger_close_flips_state(tmp_path, monkeypatch): 223 _configure_journal(tmp_path, monkeypatch) 224 _minimal_facet_tree(tmp_path) 225 _write_story_activity( 226 "work", 227 "20260410", 228 "meeting_090000_300", 229 _utc_ms("2026-04-10T09:00:00Z"), 230 commitments=[_commitment()], 231 ) 232 client = make_test_client(tmp_path) 233 item_id = client.get(PREFIX).get_json()["items"][0]["id"] 234 235 closed_response = client.post(f"{PREFIX}/{item_id}/close", json={"note": "done"}) 236 dropped_response = client.post( 237 f"{PREFIX}/{item_id}/close", 238 json={"note": "drop", "as_state": "dropped"}, 239 ) 240 241 assert closed_response.status_code == 200 242 assert closed_response.get_json()["state"] == "closed" 243 assert dropped_response.status_code == 200 244 assert dropped_response.get_json()["state"] == "dropped" 245 246 247def test_ledger_close_decision_id_returns_ledger_item_not_found(tmp_path, monkeypatch): 248 _configure_journal(tmp_path, monkeypatch) 249 _minimal_facet_tree(tmp_path) 250 _write_story_activity( 251 "work", 252 "20260410", 253 "meeting_090000_300", 254 _utc_ms("2026-04-10T09:00:00Z"), 255 decisions=[_decision()], 256 ) 257 client = make_test_client(tmp_path) 258 decision_id = client.get(f"{PREFIX}/decisions").get_json()["items"][0]["id"] 259 260 data = _assert_error( 261 client.post(f"{PREFIX}/{decision_id}/close", json={"note": "x"}), 404 262 ) 263 264 assert data["reason_code"] == "ledger_item_not_found" 265 266 267def test_ledger_close_no_body_returns_missing_request_body(tmp_path, monkeypatch): 268 _configure_journal(tmp_path, monkeypatch) 269 _minimal_facet_tree(tmp_path) 270 client = make_test_client(tmp_path) 271 272 data = _assert_error(client.post(f"{PREFIX}/does-not-exist/close"), 400) 273 274 assert data["reason_code"] == "missing_request_body" 275 276 277def test_ledger_close_non_json_returns_invalid_json_request(tmp_path, monkeypatch): 278 _configure_journal(tmp_path, monkeypatch) 279 _minimal_facet_tree(tmp_path) 280 client = make_test_client(tmp_path) 281 282 data = _assert_error( 283 client.post( 284 f"{PREFIX}/does-not-exist/close", 285 data="not json", 286 content_type="application/json", 287 ), 288 400, 289 ) 290 291 assert data["reason_code"] == "invalid_json_request" 292 293 294def test_ledger_close_empty_note_returns_missing_required_field(tmp_path, monkeypatch): 295 _configure_journal(tmp_path, monkeypatch) 296 _minimal_facet_tree(tmp_path) 297 client = make_test_client(tmp_path) 298 299 data = _assert_error( 300 client.post(f"{PREFIX}/does-not-exist/close", json={"note": " "}), 400 301 ) 302 303 assert data["reason_code"] == "missing_required_field" 304 305 306def test_ledger_close_bad_as_state_returns_invalid_request_value(tmp_path, monkeypatch): 307 _configure_journal(tmp_path, monkeypatch) 308 _minimal_facet_tree(tmp_path) 309 client = make_test_client(tmp_path) 310 311 data = _assert_error( 312 client.post( 313 f"{PREFIX}/does-not-exist/close", 314 json={"note": "x", "as_state": "weird"}, 315 ), 316 400, 317 ) 318 319 assert data["reason_code"] == "invalid_request_value" 320 321 322def test_ledger_redirects_to_init_when_setup_incomplete(tmp_path, monkeypatch): 323 _configure_unset_journal(tmp_path, monkeypatch) 324 app = create_app(journal=str(tmp_path)) 325 app.config["TESTING"] = True 326 client = app.test_client() 327 328 response = client.get(PREFIX) 329 330 assert response.status_code == 302 331 assert "/init" in response.headers["Location"]