aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-23 23:57:58 -0500
committerCraig Jennings <c@cjennings.net>2026-07-23 23:57:58 -0500
commita053e9d1ecbf583b4a6f054c34f64ad1f359365e (patch)
tree27d60e5b02df6fdfad8094886c08952c82d5c70f
parent0f91a8e0cd0a4b5b351efe407987bb8bb6ab125e (diff)
downloadrulesets-a053e9d1ecbf583b4a6f054c34f64ad1f359365e.tar.gz
rulesets-a053e9d1ecbf583b4a6f054c34f64ad1f359365e.zip
fix(inbox-send): clean error on an unreadable source, dedupe overlapping roots
main caught ValueError and FileNotFoundError around the send, so an unreadable source raised a PermissionError as a raw traceback instead of the clean "inbox-send: <message>" every other failure produces. Widening the catch to OSError covers the unreadable source, the missing source, and any atomic-write failure alike. discover_projects appended without deduping, so a roots config naming both a parent directory and one of its children listed the same project at two indices. It now dedupes on the resolved path, first-seen order preserved.
-rwxr-xr-x.ai/scripts/inbox-send.py20
-rw-r--r--.ai/scripts/tests/test_inbox_send.py42
-rwxr-xr-xclaude-templates/.ai/scripts/inbox-send.py20
-rw-r--r--claude-templates/.ai/scripts/tests/test_inbox_send.py42
4 files changed, 118 insertions, 6 deletions
diff --git a/.ai/scripts/inbox-send.py b/.ai/scripts/inbox-send.py
index 5787a84..663efcb 100755
--- a/.ai/scripts/inbox-send.py
+++ b/.ai/scripts/inbox-send.py
@@ -70,17 +70,28 @@ def discover_projects(roots: list[Path]) -> list[Path]:
a specific project root (included directly if it qualifies).
"""
projects: list[Path] = []
+ seen: set[Path] = set()
+
+ def _add(p: Path) -> None:
+ # Dedupe on the resolved path: a roots config naming both a parent and
+ # one of its children would otherwise list the child project twice, at
+ # two different indices.
+ key = p.resolve()
+ if key not in seen:
+ seen.add(key)
+ projects.append(p)
+
for root in roots:
if not root.is_dir():
continue
if _is_project(root):
- projects.append(root)
+ _add(root)
continue
for child in sorted(root.iterdir()):
if not child.is_dir():
continue
if _is_project(child):
- projects.append(child)
+ _add(child)
return projects
@@ -345,7 +356,10 @@ def main() -> int:
else:
assert args.file is not None
dest = send_file(target_inbox, args.file, source_name, args.name, now)
- except (ValueError, FileNotFoundError) as exc:
+ except (ValueError, OSError) as exc:
+ # OSError covers FileNotFoundError (missing source), PermissionError
+ # (unreadable source), and any atomic-write failure — all should
+ # surface as the clean "inbox-send: <message>" error, never a traceback.
print(f"inbox-send: {exc}", file=sys.stderr)
return 1
diff --git a/.ai/scripts/tests/test_inbox_send.py b/.ai/scripts/tests/test_inbox_send.py
index 02af23c..9b0a8c6 100644
--- a/.ai/scripts/tests/test_inbox_send.py
+++ b/.ai/scripts/tests/test_inbox_send.py
@@ -548,3 +548,45 @@ class TestAtomicWrite:
dest = mod.send_file(inbox, src, "src", None, now)
assert list(inbox.iterdir()) == [dest]
assert dest.read_text() == "payload"
+
+
+class TestSmallerDefects:
+ """Two low-severity defects found reading inbox-send during the 2026-07-23
+ sweep: an unreadable source raised an uncaught traceback instead of the
+ clean error every other failure path produces, and a roots config naming
+ both a parent and one of its children listed the same project twice."""
+
+ def test_unreadable_source_gives_clean_error_not_traceback(
+ self, project_root, run_script, tmp_path
+ ):
+ project_root("sender")
+ project_root("receiver")
+ roots = [tmp_path / "projects"]
+ src = tmp_path / "secret.bin"
+ src.write_text("x")
+ src.chmod(0o000)
+ try:
+ result = run_script(
+ ["receiver", "--file", str(src)],
+ cwd=tmp_path / "projects" / "sender",
+ roots=roots,
+ expect_failure=True,
+ )
+ finally:
+ src.chmod(0o644)
+ assert result.returncode == 1
+ # The clean "inbox-send: <message>" shape, not a Python traceback.
+ assert result.stderr.startswith("inbox-send:")
+ assert "Traceback" not in result.stderr
+
+ def test_discover_projects_dedupes_parent_and_child_root(self, tmp_path):
+ mod = _load_module()
+ # A project directory, reachable both as a child of its parent root and
+ # as a root in its own right.
+ parent = tmp_path / "projects"
+ proj = parent / "app"
+ (proj / ".ai").mkdir(parents=True)
+ (proj / "inbox").mkdir()
+ found = mod.discover_projects([parent, proj])
+ resolved = [p.resolve() for p in found]
+ assert resolved.count(proj.resolve()) == 1
diff --git a/claude-templates/.ai/scripts/inbox-send.py b/claude-templates/.ai/scripts/inbox-send.py
index 5787a84..663efcb 100755
--- a/claude-templates/.ai/scripts/inbox-send.py
+++ b/claude-templates/.ai/scripts/inbox-send.py
@@ -70,17 +70,28 @@ def discover_projects(roots: list[Path]) -> list[Path]:
a specific project root (included directly if it qualifies).
"""
projects: list[Path] = []
+ seen: set[Path] = set()
+
+ def _add(p: Path) -> None:
+ # Dedupe on the resolved path: a roots config naming both a parent and
+ # one of its children would otherwise list the child project twice, at
+ # two different indices.
+ key = p.resolve()
+ if key not in seen:
+ seen.add(key)
+ projects.append(p)
+
for root in roots:
if not root.is_dir():
continue
if _is_project(root):
- projects.append(root)
+ _add(root)
continue
for child in sorted(root.iterdir()):
if not child.is_dir():
continue
if _is_project(child):
- projects.append(child)
+ _add(child)
return projects
@@ -345,7 +356,10 @@ def main() -> int:
else:
assert args.file is not None
dest = send_file(target_inbox, args.file, source_name, args.name, now)
- except (ValueError, FileNotFoundError) as exc:
+ except (ValueError, OSError) as exc:
+ # OSError covers FileNotFoundError (missing source), PermissionError
+ # (unreadable source), and any atomic-write failure — all should
+ # surface as the clean "inbox-send: <message>" error, never a traceback.
print(f"inbox-send: {exc}", file=sys.stderr)
return 1
diff --git a/claude-templates/.ai/scripts/tests/test_inbox_send.py b/claude-templates/.ai/scripts/tests/test_inbox_send.py
index 02af23c..9b0a8c6 100644
--- a/claude-templates/.ai/scripts/tests/test_inbox_send.py
+++ b/claude-templates/.ai/scripts/tests/test_inbox_send.py
@@ -548,3 +548,45 @@ class TestAtomicWrite:
dest = mod.send_file(inbox, src, "src", None, now)
assert list(inbox.iterdir()) == [dest]
assert dest.read_text() == "payload"
+
+
+class TestSmallerDefects:
+ """Two low-severity defects found reading inbox-send during the 2026-07-23
+ sweep: an unreadable source raised an uncaught traceback instead of the
+ clean error every other failure path produces, and a roots config naming
+ both a parent and one of its children listed the same project twice."""
+
+ def test_unreadable_source_gives_clean_error_not_traceback(
+ self, project_root, run_script, tmp_path
+ ):
+ project_root("sender")
+ project_root("receiver")
+ roots = [tmp_path / "projects"]
+ src = tmp_path / "secret.bin"
+ src.write_text("x")
+ src.chmod(0o000)
+ try:
+ result = run_script(
+ ["receiver", "--file", str(src)],
+ cwd=tmp_path / "projects" / "sender",
+ roots=roots,
+ expect_failure=True,
+ )
+ finally:
+ src.chmod(0o644)
+ assert result.returncode == 1
+ # The clean "inbox-send: <message>" shape, not a Python traceback.
+ assert result.stderr.startswith("inbox-send:")
+ assert "Traceback" not in result.stderr
+
+ def test_discover_projects_dedupes_parent_and_child_root(self, tmp_path):
+ mod = _load_module()
+ # A project directory, reachable both as a child of its parent root and
+ # as a root in its own right.
+ parent = tmp_path / "projects"
+ proj = parent / "app"
+ (proj / ".ai").mkdir(parents=True)
+ (proj / "inbox").mkdir()
+ found = mod.discover_projects([parent, proj])
+ resolved = [p.resolve() for p in found]
+ assert resolved.count(proj.resolve()) == 1