diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-23 23:57:58 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-23 23:57:58 -0500 |
| commit | a053e9d1ecbf583b4a6f054c34f64ad1f359365e (patch) | |
| tree | 27d60e5b02df6fdfad8094886c08952c82d5c70f | |
| parent | 0f91a8e0cd0a4b5b351efe407987bb8bb6ab125e (diff) | |
| download | rulesets-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.py | 20 | ||||
| -rw-r--r-- | .ai/scripts/tests/test_inbox_send.py | 42 | ||||
| -rwxr-xr-x | claude-templates/.ai/scripts/inbox-send.py | 20 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/tests/test_inbox_send.py | 42 |
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 |
