From 992a48879cbf42dd5731feb7382a56a0deeaeda9 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Wed, 22 Apr 2026 22:50:27 -0600 Subject: [PATCH] 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) --- apps/utils.py | 12 +++++++-- tests/test_app_utils.py | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tests/test_app_utils.py diff --git a/apps/utils.py b/apps/utils.py index 095dedfa0..2fdac7a70 100644 --- a/apps/utils.py +++ b/apps/utils.py @@ -9,6 +9,7 @@ from pathlib import Path from typing import Any from convey import state +from think.utils import get_journal # Compiled pattern for app name validation APP_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$") @@ -28,10 +29,12 @@ def get_app_storage_path( ensure_exists: Create directory if it doesn't exist (default: True) Returns: - Path to /apps/// + Absolute path to /apps///. + Falls back to think.utils.get_journal() when state.journal_root is empty. Raises: ValueError: If app_name contains invalid characters + RuntimeError: If the resolved journal root is not absolute Examples: get_app_storage_path("search") # → Path("/apps/search") @@ -42,7 +45,12 @@ def get_app_storage_path( raise ValueError(f"Invalid app name: {app_name}") # Build path - path = Path(state.journal_root) / "apps" / app_name + root = state.journal_root or get_journal() + if not Path(root).is_absolute(): + raise RuntimeError( + f"get_app_storage_path: resolved journal root is not absolute: {root}" + ) + path = Path(root) / "apps" / app_name for sub_dir in sub_dirs: path = path / sub_dir diff --git a/tests/test_app_utils.py b/tests/test_app_utils.py new file mode 100644 index 000000000..dee2c52ef --- /dev/null +++ b/tests/test_app_utils.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from pathlib import Path + +import pytest + +from apps.utils import get_app_storage_path +from convey import state + + +def test_get_app_storage_path_uses_state_journal_root(tmp_path, monkeypatch): + monkeypatch.setattr("convey.state.journal_root", str(tmp_path)) + assert state.journal_root == str(tmp_path) + + result = get_app_storage_path("sampleapp", ensure_exists=False) + + assert result == tmp_path / "apps" / "sampleapp" + assert result.is_absolute() + + +def test_get_app_storage_path_falls_back_to_get_journal_when_state_empty( + tmp_path, monkeypatch +): + monkeypatch.setattr("convey.state.journal_root", "") + other_dir = tmp_path / "other" + other_dir.mkdir(parents=True) + monkeypatch.chdir(other_dir) + fake_journal = tmp_path / "journal" + fake_journal.mkdir(parents=True) + monkeypatch.setattr("apps.utils.get_journal", lambda: str(fake_journal)) + + result = get_app_storage_path("sampleapp", ensure_exists=False) + + assert result.is_absolute() + assert result == fake_journal / "apps" / "sampleapp" + assert Path.cwd() not in result.parents + assert result != Path.cwd() / "apps" / "sampleapp" + + +def test_get_app_storage_path_raises_on_non_absolute_root(tmp_path, monkeypatch): + monkeypatch.setattr("convey.state.journal_root", "apps") + + with pytest.raises(RuntimeError) as excinfo: + get_app_storage_path("sampleapp", ensure_exists=False) + + assert ( + str(excinfo.value) + == "get_app_storage_path: resolved journal root is not absolute: apps" + ) + + +def test_get_app_storage_path_rejects_invalid_app_name(tmp_path, monkeypatch): + monkeypatch.setattr("convey.state.journal_root", str(tmp_path)) + + with pytest.raises(ValueError): + get_app_storage_path("Bad-Name") -- 2.51.2