""" Tests for GitHub issue #609 — Docker workspace path trust and env-var priority. Two independent bugs were fixed: 1. HERMES_WEBUI_DEFAULT_WORKSPACE env var was silently overridden by settings.json at server startup. The env var must always win. 2. resolve_trusted_workspace() rejected paths that are children of DEFAULT_WORKSPACE (e.g. /data/workspace/project) when the default is a Docker volume mount outside the user's home directory. Any path under the boot-time default should be trusted automatically. """ from pathlib import Path import pytest from api.workspace import resolve_trusted_workspace # ── Fix 2: trust paths under DEFAULT_WORKSPACE ─────────────────────────────── def test_subdir_of_boot_default_is_trusted(monkeypatch, tmp_path): """A subdirectory of BOOT_DEFAULT_WORKSPACE must be trusted without being in the saved workspace list and without being under the user's home directory. This is the core Docker case: DEFAULT_WORKSPACE=/data/workspace, and the user tries to open /data/workspace/myproject — should NOT raise ValueError. """ import api.workspace as ws_mod boot_default = tmp_path / "data" / "workspace" boot_default.mkdir(parents=True) sub = boot_default / "myproject" sub.mkdir() monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default)) # Should not raise — sub is under the boot default result = resolve_trusted_workspace(str(sub)) assert result == sub.resolve() def test_boot_default_itself_is_trusted(monkeypatch, tmp_path): """The DEFAULT_WORKSPACE path itself must also be trusted (not only subdirs).""" import api.workspace as ws_mod boot_default = tmp_path / "data" / "workspace" boot_default.mkdir(parents=True) monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default)) result = resolve_trusted_workspace(str(boot_default)) assert result == boot_default.resolve() def test_path_outside_boot_default_and_home_is_rejected(monkeypatch, tmp_path): """A path that is not under home, not in the saved list, and not under DEFAULT_WORKSPACE must still be rejected.""" import api.workspace as ws_mod boot_default = tmp_path / "data" / "workspace" boot_default.mkdir(parents=True) outside = tmp_path / "other_mount" / "secret" outside.mkdir(parents=True) monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default)) with pytest.raises(ValueError, match="outside the user home"): resolve_trusted_workspace(str(outside)) def test_none_path_returns_boot_default(monkeypatch, tmp_path): """resolve_trusted_workspace(None) always returns the boot default unchanged.""" import api.workspace as ws_mod boot_default = tmp_path / "data" / "workspace" boot_default.mkdir(parents=True) monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default)) result = resolve_trusted_workspace(None) assert result == boot_default.resolve() def test_path_traversal_via_dotdot_does_not_escape_boot_default(monkeypatch, tmp_path): """A path that uses `..` to escape DEFAULT_WORKSPACE must not be trusted by (C). `Path.resolve()` collapses `..` before the `relative_to(boot_default)` check runs, so `/data/workspace/../etc` resolves to `/etc` and is rejected (it's also caught earlier by the system-roots block, but this test pins the behavior in case the order of conditions ever changes). """ import api.workspace as ws_mod boot_default = tmp_path / "data" / "workspace" boot_default.mkdir(parents=True) sibling = tmp_path / "data" / "private" sibling.mkdir(parents=True) monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default)) # `boot_default/../private` resolves to `tmp_path/data/private`, which is # NOT a child of boot_default and not under home — must reject. escape = boot_default / ".." / "private" with pytest.raises(ValueError, match="outside the user home"): resolve_trusted_workspace(str(escape))