aboutsummaryrefslogtreecommitdiff
path: root/claude-templates/.ai/scripts/tests/test_inbox_send.py
diff options
context:
space:
mode:
Diffstat (limited to 'claude-templates/.ai/scripts/tests/test_inbox_send.py')
-rw-r--r--claude-templates/.ai/scripts/tests/test_inbox_send.py235
1 files changed, 235 insertions, 0 deletions
diff --git a/claude-templates/.ai/scripts/tests/test_inbox_send.py b/claude-templates/.ai/scripts/tests/test_inbox_send.py
index a0094dc..9b0a8c6 100644
--- a/claude-templates/.ai/scripts/tests/test_inbox_send.py
+++ b/claude-templates/.ai/scripts/tests/test_inbox_send.py
@@ -97,6 +97,52 @@ class TestInboxSendDiscovery:
result = run_script(["--list"], roots=[tmp_path / "does-not-exist"])
assert result.returncode == 0
+ def test_inbox_send_list_displays_dot_stripped_name(self, project_root, run_script, tmp_path):
+ """Dotted project basenames display dot-stripped (.emacs.d → emacsd)."""
+ project_root(".emacs.d")
+ result = run_script(["--list"], roots=[tmp_path / "projects"])
+ assert "emacsd" in result.stdout
+
+
+class TestInboxSendDotAlias:
+ """A dotted project basename resolves both verbatim and dot-stripped."""
+
+ def test_resolves_by_dot_stripped_alias(self, project_root, run_script, tmp_path):
+ """'emacsd' delivers to the .emacs.d project."""
+ project_root(".emacs.d")
+ cwd = project_root("source")
+ run_script(
+ ["emacsd", "--text", "hi"],
+ cwd=cwd, roots=[tmp_path / "projects"],
+ )
+ files = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir())
+ assert len(files) == 1
+
+ def test_resolves_by_exact_dotted_name_still(self, project_root, run_script, tmp_path):
+ """Backward-compat: the verbatim '.emacs.d' target still resolves."""
+ project_root(".emacs.d")
+ cwd = project_root("source")
+ run_script(
+ [".emacs.d", "--text", "hi"],
+ cwd=cwd, roots=[tmp_path / "projects"],
+ )
+ files = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir())
+ assert len(files) == 1
+
+ def test_exact_match_wins_over_alias(self, project_root, run_script, tmp_path):
+ """An exact basename match is preferred over a dot-stripped collision."""
+ project_root("emacsd") # exact
+ project_root(".emacs.d") # would also normalize to 'emacsd'
+ cwd = project_root("source")
+ run_script(
+ ["emacsd", "--text", "hi"],
+ cwd=cwd, roots=[tmp_path / "projects"],
+ )
+ exact = list((tmp_path / "projects" / "emacsd" / "inbox").iterdir())
+ dotted = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir())
+ assert len(exact) == 1
+ assert dotted == []
+
# ----------------------------------------------------------------------
# Slug derivation from text and from filenames
@@ -355,3 +401,192 @@ class TestInboxSendErrors:
assert result.returncode != 0
files = list((tmp_path / "projects" / "target" / "inbox").iterdir())
assert files == []
+
+
+# ----------------------------------------------------------------------
+# Filename collisions (two sends deriving the same name must not overwrite)
+# ----------------------------------------------------------------------
+
+def _load_module():
+ import importlib.util
+ spec = importlib.util.spec_from_file_location("inbox_send", SCRIPT)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+
+
+class TestFilenameCollisions:
+ """Two sends in the same minute with the same leading phrase derived
+ identical filenames and the second silently overwrote the first
+ (a message was lost this way, 2026-07-02)."""
+
+ def test_send_text_same_minute_same_phrase_keeps_both(self, tmp_path):
+ from datetime import datetime
+ mod = _load_module()
+ inbox = tmp_path / "inbox"
+ inbox.mkdir()
+ now = datetime(2026, 7, 2, 5, 42, 0)
+ prefix = "identical leading phrase long enough to fill the whole slug budget entirely"
+ first = mod.send_text(inbox, prefix + " tail one", "archsetup", None, now)
+ second = mod.send_text(inbox, prefix + " tail two", "archsetup", None, now)
+ assert first != second
+ assert first.exists() and second.exists()
+ assert first.name != second.name
+ assert "tail one" in first.read_text()
+ assert "tail two" in second.read_text()
+
+ def test_send_text_collision_suffix_increments(self, tmp_path):
+ from datetime import datetime
+ mod = _load_module()
+ inbox = tmp_path / "inbox"
+ inbox.mkdir()
+ now = datetime(2026, 7, 2, 5, 42, 0)
+ paths = [mod.send_text(inbox, "same lead phrase differs later A", "src", "fixed-slug", now)
+ for _ in range(3)]
+ names = [p.name for p in paths]
+ assert names[0].endswith("fixed-slug.org")
+ assert names[1].endswith("fixed-slug-2.org")
+ assert names[2].endswith("fixed-slug-3.org")
+
+ def test_send_file_collision_preserves_extension(self, tmp_path):
+ from datetime import datetime
+ mod = _load_module()
+ inbox = tmp_path / "inbox"
+ inbox.mkdir()
+ src = tmp_path / "note.org"
+ src.write_text("body one")
+ now = datetime(2026, 7, 2, 5, 42, 0)
+ first = mod.send_file(inbox, src, "src", None, now)
+ src.write_text("body two")
+ second = mod.send_file(inbox, src, "src", None, now)
+ assert second.name.endswith("note-2.org")
+ assert first.read_text() == "body one"
+ assert second.read_text() == "body two"
+
+ def test_cli_two_rapid_sends_lose_nothing(self, project_root, run_script, tmp_path):
+ project_root("sender")
+ target = project_root("receiver")
+ roots = [tmp_path / "projects"]
+ prefix = "identical leading phrase long enough to fill the whole slug budget entirely"
+ run_script(["receiver", "--text", prefix + " message one"],
+ cwd=tmp_path / "projects" / "sender", roots=roots)
+ run_script(["receiver", "--text", prefix + " message two"],
+ cwd=tmp_path / "projects" / "sender", roots=roots)
+ files = list((target / "inbox").iterdir())
+ assert len(files) == 2
+ bodies = "".join(f.read_text() for f in files)
+ assert "message one" in bodies and "message two" in bodies
+
+
+class TestAtomicWrite:
+ """A send wrote straight to the destination path in another project's
+ inbox/, and write_text truncates on open, so any mid-write failure left a
+ zero-byte .org there. inbox-status counts that phantom as a pending
+ handoff, blocking a turn in the receiving project over a file with no
+ content (2026-07-23). The write must be atomic: the inbox sees a complete
+ file or nothing."""
+
+ def test_send_text_writes_utf8(self, tmp_path):
+ from datetime import datetime
+ mod = _load_module()
+ inbox = tmp_path / "inbox"
+ inbox.mkdir()
+ now = datetime(2026, 7, 23, 4, 36, 0)
+ # An em dash and an accented char — both non-ASCII.
+ dest = mod.send_text(inbox, "accent café and dash — here", "src", None, now)
+ # Reading as utf-8 must round-trip; a locale-encoded write would raise
+ # under a C locale, and reading back proves the bytes are utf-8.
+ assert "—" in dest.read_text(encoding="utf-8")
+
+ def test_send_text_no_partial_on_write_failure(self, tmp_path, monkeypatch):
+ from datetime import datetime
+ mod = _load_module()
+ inbox = tmp_path / "inbox"
+ inbox.mkdir()
+ now = datetime(2026, 7, 23, 4, 36, 0)
+ # Force the atomic finalize to fail after the temp file is written.
+ def boom(*a, **k):
+ raise OSError("disk full")
+ monkeypatch.setattr(mod.os, "replace", boom)
+ with pytest.raises(OSError):
+ mod.send_text(inbox, "a message that should never half-land", "src", None, now)
+ # No phantom, no leftover temp: the inbox is empty.
+ assert list(inbox.iterdir()) == []
+
+ def test_send_text_leaves_no_temp_on_success(self, tmp_path):
+ from datetime import datetime
+ mod = _load_module()
+ inbox = tmp_path / "inbox"
+ inbox.mkdir()
+ now = datetime(2026, 7, 23, 4, 36, 0)
+ dest = mod.send_text(inbox, "clean send", "src", None, now)
+ assert list(inbox.iterdir()) == [dest]
+
+ def test_send_file_no_partial_on_write_failure(self, tmp_path, monkeypatch):
+ from datetime import datetime
+ mod = _load_module()
+ inbox = tmp_path / "inbox"
+ inbox.mkdir()
+ src = tmp_path / "note.org"
+ src.write_text("body")
+ now = datetime(2026, 7, 23, 4, 36, 0)
+ def boom(*a, **k):
+ raise OSError("disk full")
+ monkeypatch.setattr(mod.os, "replace", boom)
+ with pytest.raises(OSError):
+ mod.send_file(inbox, src, "src", None, now)
+ assert list(inbox.iterdir()) == []
+
+ def test_send_file_leaves_no_temp_on_success(self, tmp_path):
+ from datetime import datetime
+ mod = _load_module()
+ inbox = tmp_path / "inbox"
+ inbox.mkdir()
+ src = tmp_path / "note.org"
+ src.write_text("payload")
+ now = datetime(2026, 7, 23, 4, 36, 0)
+ 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