ReMe/tests/unit/test_application_config.py
jinliyl e05b201da9
feat(backend): improve workspace support for web clients (#420)
* feat(backend): improve workspace support for web clients

* fix(config): preserve the default workspace directory

* chore(reme): bump version to 0.4.1.5

- Update __version__ from 0.4.1.4 to 0.4.1.5 in initialization file

* fix(chat): disable builtin tools in read-only mode

* fix(agent): make builtin tools opt-in

* fix(list): tolerate files removed during mtime sort

* fix(chat): expose complete read-only job set
2026-08-07 23:52:56 +08:00

157 lines
6.2 KiB
Python

"""Application path configuration contracts."""
# pylint: disable=protected-access
from types import SimpleNamespace
import pytest
from agentscope.message import Msg
from pydantic import ValidationError
from reme.application import Application
from reme.components import ApplicationContext
from reme.schema import ApplicationConfig, FileChunk
from reme.steps.evolve.auto_memory import AutoMemoryStep
from reme.steps.file_io.daily_write import DailyWriteStep
from reme.steps.index._source_format import is_session_path, render_chunk_body
from reme.steps.index._watch_rules import build_watch_rules
def test_workspace_dir_expands_user_home(monkeypatch, tmp_path):
"""Home-relative workspace config is normalized before components consume it."""
monkeypatch.setenv("HOME", str(tmp_path))
config = ApplicationConfig(workspace_dir="~/.copaw/workspaces/default")
assert config.workspace_dir == str(tmp_path / ".copaw/workspaces/default")
def test_dialog_dir_is_not_an_application_config_field():
"""The removed option is absent from schemas and ignored when supplied."""
custom = ApplicationConfig(session_dir="sessions/", dialog_dir="somewhere/else")
assert not hasattr(custom, "dialog_dir")
assert "dialog_dir" not in custom.model_dump()
assert "dialog_dir" not in ApplicationConfig.model_json_schema()["properties"]
@pytest.mark.parametrize("session_dir", ["/tmp/sessions", "C:\\sessions", "\\\\server\\share\\sessions"])
def test_application_rejects_absolute_session_dir_before_workspace_setup(tmp_path, session_dir):
"""Application initialization rejects session directories outside the workspace."""
workspace = tmp_path / "workspace"
with pytest.raises(ValidationError, match="session_dir must be a workspace-relative path"):
Application(workspace_dir=str(workspace), session_dir=session_dir)
assert not workspace.exists()
def test_dialog_watch_rule_follows_custom_session_dir(tmp_path):
"""Nested watch rules resolve the dialog directory below session_dir."""
config = ApplicationConfig(session_dir="sessions")
rules = build_watch_rules(
config,
tmp_path,
watch_dirs=["session_dir/dialog"],
watch_suffixes=["jsonl"],
)
assert len(rules) == 1
assert rules[0].path == tmp_path / "sessions" / "dialog"
assert rules[0].suffixes == ["jsonl"]
def test_dialog_watch_rule_matches_writer_for_normalized_session_dirs(tmp_path):
"""Nested watch rules use the same defaults and normalization as transcript writers."""
for configured in ("", "parent//../sessions"):
app_context = ApplicationContext(session_dir=configured)
auto_memory = AutoMemoryStep(app_context=app_context)
auto_memory.file_store = SimpleNamespace(workspace_path=tmp_path)
rules = build_watch_rules(
app_context.app_config,
tmp_path,
watch_dirs=["session_dir/dialog"],
watch_suffixes=["jsonl"],
)
assert rules[0].path == auto_memory._session_path("s1").parent
def test_absolute_watch_rule_stays_outside_workspace(tmp_path):
"""Absolute literal watch directories retain their original location."""
config = ApplicationConfig()
absolute_dir = tmp_path.parent / "reme-dialogs"
rules = build_watch_rules(
config,
tmp_path,
watch_dirs=[str(absolute_dir)],
watch_suffixes=["jsonl"],
)
assert rules[0].path == absolute_dir
def test_standard_session_paths_and_links_follow_custom_session_dir(tmp_path):
"""Writers derive transcript paths and links from session_dir."""
app_context = ApplicationContext(session_dir="sessions")
auto_memory = AutoMemoryStep(app_context=app_context)
auto_memory.file_store = SimpleNamespace(workspace_path=tmp_path)
daily_write = DailyWriteStep(app_context=app_context)
assert auto_memory._session_path("s1") == tmp_path / "sessions" / "dialog" / "s1.jsonl"
assert auto_memory._session_source_path("s1") == "sessions/dialog/s1.jsonl"
assert auto_memory._session_link("s1") == "[[sessions/dialog/s1.jsonl]]"
assert daily_write._session_link("s1") == "[[sessions/dialog/s1.jsonl]]"
def test_session_paths_and_links_normalize_custom_session_dir(tmp_path):
"""Writers and source links use the same normalized POSIX session root."""
for configured, expected in (
("./sessions", "sessions"),
(".", ""),
("parent//../sessions", "sessions"),
):
app_context = ApplicationContext(session_dir=configured)
auto_memory = AutoMemoryStep(app_context=app_context)
auto_memory.file_store = SimpleNamespace(workspace_path=tmp_path)
daily_write = DailyWriteStep(app_context=app_context)
source_path = f"{expected + '/' if expected else ''}dialog/s1.jsonl"
assert auto_memory._session_path("s1") == tmp_path / source_path
assert auto_memory._session_source_path("s1") == source_path
assert auto_memory._session_link("s1") == f"[[{source_path}]]"
assert daily_write._session_link("s1") == f"[[{source_path}]]"
def test_dialog_classification_excludes_other_session_stores():
"""Only jsonl files below the dialog child directory are transcripts."""
assert is_session_path("sessions/dialog/s1.jsonl", "sessions")
assert not is_session_path("sessions/claude_code/s1.jsonl", "sessions")
assert not is_session_path("session/dialog/s1.jsonl", "sessions")
def test_dialog_classification_normalizes_paths():
"""Session recognition compares normalized path representations."""
assert is_session_path("sessions/dialog/s1.jsonl", "./sessions")
assert is_session_path("./sessions//dialog/s1.jsonl", "sessions")
assert is_session_path("dialog/s1.jsonl", ".")
assert is_session_path("sessions/dialog/s1.jsonl", "parent/../sessions")
def test_session_rendering_follows_custom_session_dir():
"""Session-aware rendering recognizes a custom session root."""
message = Msg(name="user", role="user", content=[{"type": "text", "text": "hello"}])
chunk = FileChunk(
path="sessions/dialog/s1.jsonl",
start_line=1,
end_line=1,
text=message.model_dump_json(),
)
rendered = render_chunk_body(chunk, "sessions")
assert rendered.startswith("[user @")
assert rendered.endswith("] hello")