From 4b768acba2f5b322d9a05c0a95b87fc0d59d97b0 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 22 May 2026 00:37:58 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@claire):=20=E2=9C=A8=20add=20tm?= =?UTF-8?q?ux=5Fname=20validation=20in=20session=20dispatch=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- tests/test_dispatch.py | 8 +++++ tests/test_orchestrator_tools.py | 56 +++++++++++++++++++++++++++++ tests/test_pull_idempotency.py | 60 ++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/tests/test_dispatch.py b/tests/test_dispatch.py index 78ba3a1..dd97f12 100644 --- a/tests/test_dispatch.py +++ b/tests/test_dispatch.py @@ -153,6 +153,14 @@ def test_dispatch_success() -> None: "SELECT COUNT(*) FROM assignments WHERE task_id = ?", (str(task_id),) ).fetchone() assert rows[0] == 1 + # The dispatched session is addressable: its sessions row carries the + # spawned tmux_name (a fresh pane has no `--resume`, so the pull loop + # would never map it — dispatch must record it directly). + sess = conn.execute( + "SELECT tmux_name FROM sessions WHERE uuid = ?", (result.session_uuid,) + ).fetchone() + assert sess is not None + assert sess["tmux_name"] == tmux_name def test_dispatch_refused_host_at_cap() -> None: diff --git a/tests/test_orchestrator_tools.py b/tests/test_orchestrator_tools.py index d4766ed..2dec3a9 100644 --- a/tests/test_orchestrator_tools.py +++ b/tests/test_orchestrator_tools.py @@ -225,3 +225,59 @@ def test_suggest_assignments_skips_already_assigned(conn, gen) -> None: assert out["pairings"] == [] assert out["remaining_tasks"] == [] assert out["remaining_sessions"] == [] + + +# --------------------------------------------------------------------------- +# send_to_session — must target by the resolved tmux_name, not the UUID +# --------------------------------------------------------------------------- + + +class _SendCapturingRclaude: + """Records every `send` call's `match` argument.""" + + def __init__(self) -> None: + self.send_calls: list[dict] = [] + + def send(self, *, text: str, match: str, yes: bool = False, dry_run: bool = False): + self.send_calls.append({"text": text, "match": match, "yes": yes}) + return None + + +def test_send_to_session_targets_resolved_tmux_name( + conn, gen, monkeypatch: pytest.MonkeyPatch +) -> None: + """When the session has a known tmux_name, `--match` gets the tmux name + (not the UUID — `rclaude --match` matches tmux names, never UUIDs). + """ + sid = _uuid.uuid4() + tmux_name = "claude-natalie-Users-natalie-Code-1779419135" + ev.append(conn, gen, ev.SessionObserved( + session_uuid=sid, host="local", tmux_name=tmux_name, + )) + fake = _SendCapturingRclaude() + monkeypatch.setattr("claire.rclaude.Rclaude", lambda: fake) + + out = tools.send_to_session(conn, gen, session_ref=str(sid), text="hello") + assert out["delivered"] is True + assert out["tmux_name"] == tmux_name + assert len(fake.send_calls) == 1 + # The load-bearing assertion: the match is the tmux name, NOT a UUID prefix. + assert fake.send_calls[0]["match"] == tmux_name + assert fake.send_calls[0]["match"] != str(sid)[:8] + + +def test_send_to_session_fails_loudly_when_tmux_name_unknown( + conn, gen, monkeypatch: pytest.MonkeyPatch +) -> None: + """A session with no known tmux_name must raise a clear error, not + silently fire a send that matches nothing. + """ + sid = _uuid.uuid4() + ev.append(conn, gen, ev.SessionObserved(session_uuid=sid, host="local")) + fake = _SendCapturingRclaude() + monkeypatch.setattr("claire.rclaude.Rclaude", lambda: fake) + + with pytest.raises(tools.ToolError, match="no known tmux_name"): + tools.send_to_session(conn, gen, session_ref=str(sid), text="hi") + # No send was attempted. + assert fake.send_calls == [] diff --git a/tests/test_pull_idempotency.py b/tests/test_pull_idempotency.py index a544f50..7c15f55 100644 --- a/tests/test_pull_idempotency.py +++ b/tests/test_pull_idempotency.py @@ -93,3 +93,63 @@ def test_pull_dedup_picks_highest_mtime(conn, gen) -> None: (str(shared_uuid),), ).fetchone() assert row["host"] == "local" + + +def test_pull_populates_tmux_name_from_resumed_uuid(conn, gen) -> None: + """A TmuxRow with resumed_uuid maps that UUID → its tmux session name. + + This is the fix for Clare being blind to its own fleet: without + sessions.tmux_name populated, `send_to_session` can't resolve a session + UUID to anything `rclaude --match` can target. + """ + sess_uuid = UUID("33333333-3333-3333-3333-333333333333") + tmux_name = "claude-natalie-Users-natalie-Code-1779419135" + cwd = "/Users/natalie/Code" + + # The session is on disk (list_sessions) AND backed by a live resumed + # tmux pane (list_tmux carries the resumed_uuid). + sessions = [ + SessionRow(host="local", uuid=sess_uuid, snippet="hi", cwd=cwd, + mtime_epoch=1_700_000_000), + ] + tmux = [ + TmuxRow(host="local", session_name=tmux_name, detail="1 windows", + resumed_uuid=str(sess_uuid)), + ] + fake = _FakeRclaude(sessions=sessions, tmux=tmux) + + first = pull(conn, gen, rclaude=fake) + assert first.errors == [] + # One observation for the disk row + one for the tmux mapping. + assert first.sessions_observed == 2 + + row = conn.execute( + "SELECT tmux_name FROM sessions WHERE uuid = ?", (str(sess_uuid),) + ).fetchone() + assert row["tmux_name"] == tmux_name + + # Re-pull is idempotent — the tmux_name already matches, no new event. + second = pull(conn, gen, rclaude=fake) + assert second.sessions_observed == 0 + + +def test_pull_tmux_name_ignored_for_fresh_session(conn, gen) -> None: + """A TmuxRow without resumed_uuid (fresh spawn) yields no tmux_name mapping.""" + sess_uuid = UUID("44444444-4444-4444-4444-444444444444") + sessions = [ + SessionRow(host="local", uuid=sess_uuid, snippet="hi", + cwd="/tmp/w", mtime_epoch=1_700_000_000), + ] + tmux = [ + TmuxRow(host="local", session_name="claude-natalie-tmp-w-1779419135", + detail="1 windows", resumed_uuid=None), + ] + fake = _FakeRclaude(sessions=sessions, tmux=tmux) + stats = pull(conn, gen, rclaude=fake) + assert stats.errors == [] + # Only the disk-row observation — no tmux mapping for a fresh pane. + assert stats.sessions_observed == 1 + row = conn.execute( + "SELECT tmux_name FROM sessions WHERE uuid = ?", (str(sess_uuid),) + ).fetchone() + assert row["tmux_name"] is None