personal memory agent
0
fork

Configure Feed

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

apps/utils: fall back to get_journal(), require absolute journal root

get_app_storage_path() previously built app storage paths directly from state.journal_root, which defaults to an empty string before convey boots and could silently redirect writes into the current working directory. This change falls back to think.utils.get_journal() when state.journal_root is empty and raises RuntimeError when the resolved root is not absolute, so that failure happens loudly at the shared helper every app uses. Tests cover state-backed paths, get_journal() fallback, the non-absolute-root failure, and invalid app-name rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+67 -2
+10 -2
apps/utils.py
··· 9 9 from typing import Any 10 10 11 11 from convey import state 12 + from think.utils import get_journal 12 13 13 14 # Compiled pattern for app name validation 14 15 APP_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$") ··· 28 29 ensure_exists: Create directory if it doesn't exist (default: True) 29 30 30 31 Returns: 31 - Path to <journal>/apps/<app_name>/<sub_dirs>/ 32 + Absolute path to <journal>/apps/<app_name>/<sub_dirs>/. 33 + Falls back to think.utils.get_journal() when state.journal_root is empty. 32 34 33 35 Raises: 34 36 ValueError: If app_name contains invalid characters 37 + RuntimeError: If the resolved journal root is not absolute 35 38 36 39 Examples: 37 40 get_app_storage_path("search") # → Path("<journal>/apps/search") ··· 42 45 raise ValueError(f"Invalid app name: {app_name}") 43 46 44 47 # Build path 45 - path = Path(state.journal_root) / "apps" / app_name 48 + root = state.journal_root or get_journal() 49 + if not Path(root).is_absolute(): 50 + raise RuntimeError( 51 + f"get_app_storage_path: resolved journal root is not absolute: {root}" 52 + ) 53 + path = Path(root) / "apps" / app_name 46 54 for sub_dir in sub_dirs: 47 55 path = path / sub_dir 48 56
+57
tests/test_app_utils.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from pathlib import Path 5 + 6 + import pytest 7 + 8 + from apps.utils import get_app_storage_path 9 + from convey import state 10 + 11 + 12 + def test_get_app_storage_path_uses_state_journal_root(tmp_path, monkeypatch): 13 + monkeypatch.setattr("convey.state.journal_root", str(tmp_path)) 14 + assert state.journal_root == str(tmp_path) 15 + 16 + result = get_app_storage_path("sampleapp", ensure_exists=False) 17 + 18 + assert result == tmp_path / "apps" / "sampleapp" 19 + assert result.is_absolute() 20 + 21 + 22 + def test_get_app_storage_path_falls_back_to_get_journal_when_state_empty( 23 + tmp_path, monkeypatch 24 + ): 25 + monkeypatch.setattr("convey.state.journal_root", "") 26 + other_dir = tmp_path / "other" 27 + other_dir.mkdir(parents=True) 28 + monkeypatch.chdir(other_dir) 29 + fake_journal = tmp_path / "journal" 30 + fake_journal.mkdir(parents=True) 31 + monkeypatch.setattr("apps.utils.get_journal", lambda: str(fake_journal)) 32 + 33 + result = get_app_storage_path("sampleapp", ensure_exists=False) 34 + 35 + assert result.is_absolute() 36 + assert result == fake_journal / "apps" / "sampleapp" 37 + assert Path.cwd() not in result.parents 38 + assert result != Path.cwd() / "apps" / "sampleapp" 39 + 40 + 41 + def test_get_app_storage_path_raises_on_non_absolute_root(tmp_path, monkeypatch): 42 + monkeypatch.setattr("convey.state.journal_root", "apps") 43 + 44 + with pytest.raises(RuntimeError) as excinfo: 45 + get_app_storage_path("sampleapp", ensure_exists=False) 46 + 47 + assert ( 48 + str(excinfo.value) 49 + == "get_app_storage_path: resolved journal root is not absolute: apps" 50 + ) 51 + 52 + 53 + def test_get_app_storage_path_rejects_invalid_app_name(tmp_path, monkeypatch): 54 + monkeypatch.setattr("convey.state.journal_root", str(tmp_path)) 55 + 56 + with pytest.raises(ValueError): 57 + get_app_storage_path("Bad-Name")