"""Live operator-note injection into a running kanban worker. ``tools.kanban_tools.inject_new_comments_from_env`` polls the worker's task for comments added *after* the run started and folds them into the live turn via the agent's OUT-OF-BAND steer channel — so a user can talk to a running task without the block→comment→unblock dance or a restart. Verifies: no-op off a worker, watermark seeding (history isn't re-injected), new comments steer, and own-authored comments are skipped. """ from __future__ import annotations import sys from pathlib import Path import pytest _WORKTREE = Path(__file__).resolve().parents[2] if str(_WORKTREE) not in sys.path: sys.path.insert(0, str(_WORKTREE)) from hermes_cli import kanban_db as kb import tools.kanban_tools as kt class FakeAgent: def __init__(self): self.steers: list[str] = [] def steer(self, text: str) -> bool: self.steers.append(text) return True @pytest.fixture def worker_home(tmp_path, monkeypatch): home = tmp_path / "hermes_home" home.mkdir() monkeypatch.setenv("HERMES_HOME", str(home)) monkeypatch.setattr(Path, "home", lambda: tmp_path) for var in ("HERMES_KANBAN_DB", "HERMES_KANBAN_WORKSPACES_ROOT", "HERMES_KANBAN_HOME", "HERMES_KANBAN_BOARD"): monkeypatch.delenv(var, raising=False) try: import hermes_constants hermes_constants._cached_default_hermes_root = None # type: ignore[attr-defined] except Exception: pass kb._INITIALIZED_PATHS.clear() # Reset module-level poll state so tests don't leak into each other. kt._comment_watermark.clear() kt._comment_poll_last_attempt = 0.0 return home def _unthrottle(): """Bypass the inter-poll rate limit for deterministic tests.""" kt._comment_poll_last_attempt = 0.0 def test_noop_without_worker_env(worker_home, monkeypatch): monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False) agent = FakeAgent() assert kt.inject_new_comments_from_env(agent) is False assert agent.steers == [] def test_seed_then_inject_new_comment(worker_home, monkeypatch): conn = kb.connect() try: tid = kb.create_task(conn, title="live task") kb.add_comment(conn, tid, author="desktop", body="pre-existing note") finally: conn.close() monkeypatch.setenv("HERMES_KANBAN_TASK", tid) monkeypatch.setenv("HERMES_PROFILE", "worker-bot") agent = FakeAgent() # First poll seeds the watermark past the existing thread — no injection. _unthrottle() assert kt.inject_new_comments_from_env(agent) is False assert agent.steers == [] conn = kb.connect() try: kb.add_comment(conn, tid, author="desktop", body="actually use the v2 API") finally: conn.close() _unthrottle() assert kt.inject_new_comments_from_env(agent) is True assert len(agent.steers) == 1 assert "v2 API" in agent.steers[0] # Watermark advanced — a re-poll with no new comments injects nothing. _unthrottle() assert kt.inject_new_comments_from_env(agent) is False assert len(agent.steers) == 1 def test_skips_own_authored_comments(worker_home, monkeypatch): conn = kb.connect() try: tid = kb.create_task(conn, title="echo guard") finally: conn.close() monkeypatch.setenv("HERMES_KANBAN_TASK", tid) monkeypatch.setenv("HERMES_PROFILE", "worker-bot") agent = FakeAgent() _unthrottle() kt.inject_new_comments_from_env(agent) # seed conn = kb.connect() try: kb.add_comment(conn, tid, author="worker-bot", body="i did a thing") finally: conn.close() _unthrottle() assert kt.inject_new_comments_from_env(agent) is False assert agent.steers == []