123 lines
4.4 KiB
Python
123 lines
4.4 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tht.config import ConfigError, load_config, workspace_id_from_path
|
|
from tht.paths import resolve_workspace_paths
|
|
|
|
|
|
def _write_config(path: Path, *, sessions: str = "sessions", absolute: Path | None = None) -> Path:
|
|
root = absolute or Path("artifacts")
|
|
path.write_text(
|
|
"dwh:\n"
|
|
" type: postgres_direct\n"
|
|
" connection:\n"
|
|
" database: db\n"
|
|
" schema: public\n"
|
|
" user: user\n"
|
|
" password: secret\n"
|
|
"roots:\n"
|
|
f" artifacts: {root}\n"
|
|
f" indexes: {root if absolute else 'indexes'}\n"
|
|
f" sessions: {absolute if absolute else sessions}\n"
|
|
)
|
|
return path
|
|
|
|
|
|
def test_relative_paths_resolve_under_workspace_root(tmp_path):
|
|
cfg_path = _write_config(tmp_path / "demo.yaml")
|
|
cfg = load_config(cfg_path)
|
|
|
|
resolved = resolve_workspace_paths(cfg_path, cfg, tmp_path / "data")
|
|
|
|
assert resolved.workspace == tmp_path / "data/workspaces/demo"
|
|
assert resolved.sessions == tmp_path / "data/workspaces/demo/sessions"
|
|
assert resolved.artifacts == tmp_path / "data/workspaces/demo/artifacts"
|
|
assert resolved.indexes == tmp_path / "data/workspaces/demo/indexes"
|
|
assert resolved.corpus == tmp_path / "data/workspaces/demo/corpus"
|
|
|
|
|
|
def test_absolute_legacy_paths_are_preserved(tmp_path):
|
|
legacy = tmp_path / "existing-workspace"
|
|
cfg_path = _write_config(tmp_path / "demo.yaml", absolute=legacy)
|
|
cfg = load_config(cfg_path)
|
|
|
|
resolved = resolve_workspace_paths(cfg_path, cfg, tmp_path / "data")
|
|
|
|
assert resolved.sessions == legacy
|
|
assert resolved.artifacts == legacy
|
|
assert resolved.indexes == legacy
|
|
|
|
|
|
def test_path_escape_is_rejected(tmp_path):
|
|
cfg_path = _write_config(tmp_path / "demo.yaml", sessions="../../private")
|
|
cfg = load_config(cfg_path)
|
|
|
|
with pytest.raises(ConfigError, match="outside workspace root"):
|
|
resolve_workspace_paths(cfg_path, cfg, tmp_path / "data")
|
|
|
|
|
|
def test_workspace_symlink_escape_is_rejected(tmp_path):
|
|
cfg_path = _write_config(tmp_path / "demo.yaml")
|
|
cfg = load_config(cfg_path)
|
|
data_root = tmp_path / "data"
|
|
(data_root / "workspaces").mkdir(parents=True)
|
|
(data_root / "workspaces" / "demo").symlink_to(tmp_path / "private", target_is_directory=True)
|
|
|
|
with pytest.raises(ConfigError, match="outside workspaces root"):
|
|
resolve_workspace_paths(cfg_path, cfg, data_root)
|
|
|
|
|
|
def test_nested_root_symlink_escape_is_rejected(tmp_path):
|
|
cfg_path = _write_config(tmp_path / "demo.yaml")
|
|
cfg = load_config(cfg_path)
|
|
workspace = tmp_path / "data/workspaces/demo"
|
|
workspace.mkdir(parents=True)
|
|
(workspace / "sessions").symlink_to(tmp_path / "private", target_is_directory=True)
|
|
|
|
with pytest.raises(ConfigError, match="outside workspace root"):
|
|
resolve_workspace_paths(cfg_path, cfg, tmp_path / "data")
|
|
|
|
|
|
def test_corpus_symlink_escape_is_rejected(tmp_path):
|
|
cfg_path = _write_config(tmp_path / "demo.yaml")
|
|
cfg = load_config(cfg_path)
|
|
workspace = tmp_path / "data/workspaces/demo"
|
|
workspace.mkdir(parents=True)
|
|
(workspace / "corpus").symlink_to(tmp_path / "private", target_is_directory=True)
|
|
|
|
with pytest.raises(ConfigError, match="outside workspace root"):
|
|
resolve_workspace_paths(cfg_path, cfg, tmp_path / "data")
|
|
|
|
|
|
def test_data_root_environment_activates_portable_paths(monkeypatch, tmp_path):
|
|
cfg_path = _write_config(tmp_path / "demo.yaml")
|
|
monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data"))
|
|
|
|
cfg = load_config(cfg_path)
|
|
|
|
assert cfg.paths.sessions == tmp_path / "data/workspaces/demo/sessions"
|
|
assert cfg.paths.artifacts == tmp_path / "data/workspaces/demo/artifacts"
|
|
|
|
|
|
def test_no_data_root_preserves_legacy_relative_paths(monkeypatch, tmp_path):
|
|
cfg_path = _write_config(tmp_path / "demo.yaml")
|
|
monkeypatch.delenv("THT_DATA_ROOT", raising=False)
|
|
|
|
cfg = load_config(cfg_path)
|
|
|
|
assert cfg.paths.sessions == Path("sessions")
|
|
assert cfg.paths.artifacts == Path("artifacts")
|
|
|
|
|
|
def test_workspace_identity_resolves_config_symlink(tmp_path):
|
|
(tmp_path / "workspaces").mkdir()
|
|
workspace = _write_config(tmp_path / "workspaces/local.yaml")
|
|
alias = tmp_path / "config/tht.yaml"
|
|
alias.parent.mkdir()
|
|
alias.symlink_to(workspace)
|
|
|
|
assert workspace_id_from_path(alias) == "local"
|
|
assert load_config(alias)._workspace_id == "local"
|
|
assert load_config(alias)._config_source == workspace.resolve().as_posix()
|