Skip to content

Commit fbf82b1

Browse files
fix(commands): reject relative project_path with clear error
Without validation, Path('foo').resolve() silently joined the input to the sidecar's cwd, producing confusing doubled paths in errors like /Users/.../attune-gui/Users/.../attune-ai/.help when a user typed "Users/me/project" instead of "/Users/me/project". _resolve_project_paths now requires absolute paths or ~-prefixed paths for project_path / project_root / help_dir, raising a clear ValueError that is surfaced to the UI as the job's error message. Bump 0.1.0 → 0.1.1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f7827cc commit fbf82b1

3 files changed

Lines changed: 73 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "attune-gui"
7-
version = "0.1.0"
7+
version = "0.1.1"
88
description = "Unified local GUI that drives attune-ai, attune-rag, and attune-author Python libraries via a FastAPI sidecar."
99
readme = "README.md"
1010
requires-python = ">=3.10"

sidecar/attune_gui/commands.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,40 @@
2828
logger = logging.getLogger(__name__)
2929

3030

31+
def _require_absolute(field: str, raw: str) -> None:
32+
"""Reject relative paths up-front so users see a clear error.
33+
34+
Without this, ``Path('foo').resolve()`` silently joins ``foo`` to the
35+
sidecar's cwd, producing confusing doubled paths in downstream errors
36+
(e.g. /Users/.../attune-gui/Users/.../attune-ai/.help).
37+
"""
38+
if raw.startswith("/") or raw.startswith("~"):
39+
return
40+
raise ValueError(
41+
f"{field} must be an absolute path (e.g. /Users/you/project) "
42+
f"or start with ~ (e.g. ~/project), got: {raw!r}",
43+
)
44+
45+
3146
def _resolve_project_paths(args: dict[str, Any]) -> tuple[Path, Path]:
3247
"""Return (project_root, help_dir) from args.
3348
3449
Accepts either a single ``project_path`` convenience key or the legacy
3550
``project_root`` / ``help_dir`` pair. ``project_path`` wins when set.
51+
Relative paths are rejected with a clear error.
3652
"""
3753
project_path_raw = str(args.get("project_path", "")).strip()
3854
if project_path_raw:
55+
_require_absolute("project_path", project_path_raw)
3956
project_root = Path(project_path_raw).expanduser().resolve()
4057
help_dir = project_root / ".help"
4158
else:
4259
help_dir_raw = str(args.get("help_dir", "")).strip() or ".help"
4360
project_root_raw = str(args.get("project_root", "")).strip() or "."
61+
if project_root_raw != ".":
62+
_require_absolute("project_root", project_root_raw)
63+
if help_dir_raw != ".help":
64+
_require_absolute("help_dir", help_dir_raw)
4465
help_dir = Path(help_dir_raw).expanduser().resolve()
4566
project_root = Path(project_root_raw).expanduser().resolve()
4667
return project_root, help_dir
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Tests for _resolve_project_paths path validation."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
from pathlib import Path
7+
8+
import pytest
9+
from attune_gui.commands import _resolve_project_paths
10+
11+
12+
def test_absolute_project_path_resolves():
13+
home = str(Path.home())
14+
root, help_dir = _resolve_project_paths({"project_path": home})
15+
assert root == Path(home).resolve()
16+
assert help_dir == Path(home).resolve() / ".help"
17+
18+
19+
def test_tilde_project_path_expands():
20+
root, help_dir = _resolve_project_paths({"project_path": "~"})
21+
assert root == Path.home().resolve()
22+
assert help_dir == Path.home().resolve() / ".help"
23+
24+
25+
def test_relative_project_path_rejected():
26+
with pytest.raises(ValueError, match="must be an absolute path"):
27+
_resolve_project_paths({"project_path": "Users/me/project"})
28+
29+
30+
def test_dotted_relative_project_path_rejected():
31+
with pytest.raises(ValueError, match="must be an absolute path"):
32+
_resolve_project_paths({"project_path": "./project"})
33+
34+
35+
def test_legacy_relative_project_root_rejected():
36+
with pytest.raises(ValueError, match="project_root must be an absolute path"):
37+
_resolve_project_paths({"project_root": "some/dir"})
38+
39+
40+
def test_legacy_relative_help_dir_rejected():
41+
with pytest.raises(ValueError, match="help_dir must be an absolute path"):
42+
_resolve_project_paths(
43+
{"project_root": str(Path.home()), "help_dir": "rel/help"},
44+
)
45+
46+
47+
def test_legacy_defaults_pass_through():
48+
"""No project_path, no project_root, no help_dir → resolves to cwd-based defaults."""
49+
root, help_dir = _resolve_project_paths({})
50+
assert root == Path(os.getcwd()).resolve()
51+
assert help_dir == Path(os.getcwd()).resolve() / ".help"

0 commit comments

Comments
 (0)