Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import asyncio
|
||||
|
||||
from gateway.config import Platform
|
||||
from gateway.run import GatewayRunner
|
||||
from hermes_cli import kanban_db as kb
|
||||
|
||||
|
||||
class RecordingAdapter:
|
||||
def __init__(self, *, fail_send=False):
|
||||
self.sent = []
|
||||
self.handled = []
|
||||
self.fail_send = fail_send
|
||||
|
||||
async def send(self, chat_id, text, metadata=None):
|
||||
self.sent.append({"chat_id": chat_id, "text": text, "metadata": metadata or {}})
|
||||
if self.fail_send:
|
||||
raise RuntimeError("transient send failure")
|
||||
|
||||
async def handle_message(self, event):
|
||||
self.handled.append(event)
|
||||
|
||||
|
||||
async def _run_one_tick(monkeypatch, runner):
|
||||
real_sleep = asyncio.sleep
|
||||
|
||||
async def fake_sleep(delay):
|
||||
if delay == 5:
|
||||
return None
|
||||
runner._running = False
|
||||
await real_sleep(0)
|
||||
|
||||
monkeypatch.setattr(asyncio, "sleep", fake_sleep)
|
||||
await runner._kanban_notifier_watcher(interval=1)
|
||||
|
||||
|
||||
def _runner(adapter):
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
runner._running = True
|
||||
runner.adapters = {Platform.TELEGRAM: adapter}
|
||||
runner._kanban_sub_fail_counts = {}
|
||||
runner._kanban_dispatcher_lock_handle = object()
|
||||
return runner
|
||||
|
||||
|
||||
def _create_review_block(delivery_mode, *, reason="Tests need updates"):
|
||||
conn = kb.connect()
|
||||
try:
|
||||
task_id = kb.create_task(
|
||||
conn,
|
||||
title="existing implementation card",
|
||||
assignee="implementer",
|
||||
session_id="agent:main:telegram:thread:chat-1:topic-7",
|
||||
)
|
||||
kb.add_notify_sub(
|
||||
conn,
|
||||
task_id=task_id,
|
||||
platform="telegram",
|
||||
chat_id="chat-1",
|
||||
thread_id="topic-7",
|
||||
chat_type="thread",
|
||||
delivery_mode=delivery_mode,
|
||||
delivery_metadata={"thread_id": "topic-7", "chat_type": "thread"},
|
||||
)
|
||||
kb._append_event(
|
||||
conn,
|
||||
task_id,
|
||||
kind="changes_requested",
|
||||
payload={
|
||||
"reason": reason,
|
||||
"reviewer": "claude-qa",
|
||||
"implementer": "codex-cua",
|
||||
"status": "ready",
|
||||
},
|
||||
)
|
||||
return task_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _unseen(task_id):
|
||||
conn = kb.connect()
|
||||
try:
|
||||
_, events = kb.unseen_events_for_sub(
|
||||
conn,
|
||||
task_id=task_id,
|
||||
platform="telegram",
|
||||
chat_id="chat-1",
|
||||
thread_id="topic-7",
|
||||
kinds=["changes_requested"],
|
||||
)
|
||||
return events
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_changes_requested_notify_wake_is_actionable_and_exactly_routed(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "review-block.db"))
|
||||
kb.init_db()
|
||||
task_id = _create_review_block("notify+wake")
|
||||
adapter = RecordingAdapter()
|
||||
|
||||
asyncio.run(_run_one_tick(monkeypatch, _runner(adapter)))
|
||||
|
||||
assert len(adapter.sent) == 1
|
||||
text = adapter.sent[0]["text"]
|
||||
assert text.startswith(f"🛑 [default] Kanban {task_id} review requested changes/BLOCK: Tests need updates")
|
||||
assert "reviewer @claude-qa → implementer @codex-cua" in text
|
||||
assert adapter.sent[0]["metadata"]["thread_id"] == "topic-7"
|
||||
assert len(adapter.handled) == 1
|
||||
wake = adapter.handled[0]
|
||||
assert wake.source.chat_id == "chat-1"
|
||||
assert wake.source.chat_type == "thread"
|
||||
assert wake.source.thread_id == "topic-7"
|
||||
assert "implementation is not approved" in wake.text
|
||||
assert "Inspect the existing card and its current review run" in wake.text
|
||||
assert "do not create a duplicate task" in wake.text
|
||||
assert wake.text.count("do not create a duplicate task") == 1
|
||||
assert _unseen(task_id) == []
|
||||
|
||||
# A fresh watcher after restart cannot replay an event whose cursor advanced.
|
||||
asyncio.run(_run_one_tick(monkeypatch, _runner(adapter)))
|
||||
assert len(adapter.sent) == 1
|
||||
assert len(adapter.handled) == 1
|
||||
|
||||
|
||||
def test_changes_requested_notify_is_passive_only(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "notify.db"))
|
||||
kb.init_db()
|
||||
task_id = _create_review_block("notify")
|
||||
adapter = RecordingAdapter()
|
||||
|
||||
asyncio.run(_run_one_tick(monkeypatch, _runner(adapter)))
|
||||
|
||||
assert len(adapter.sent) == 1
|
||||
assert adapter.handled == []
|
||||
assert _unseen(task_id) == []
|
||||
|
||||
|
||||
def test_changes_requested_wake_only_has_no_passive_post(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "wake.db"))
|
||||
kb.init_db()
|
||||
task_id = _create_review_block("wake")
|
||||
adapter = RecordingAdapter()
|
||||
|
||||
asyncio.run(_run_one_tick(monkeypatch, _runner(adapter)))
|
||||
|
||||
assert adapter.sent == []
|
||||
assert len(adapter.handled) == 1
|
||||
assert _unseen(task_id) == []
|
||||
|
||||
|
||||
def test_changes_requested_send_failure_retries_without_event_loss(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "retry.db"))
|
||||
kb.init_db()
|
||||
task_id = _create_review_block("notify")
|
||||
failing = RecordingAdapter(fail_send=True)
|
||||
|
||||
asyncio.run(_run_one_tick(monkeypatch, _runner(failing)))
|
||||
assert len(failing.sent) == 1
|
||||
assert len(_unseen(task_id)) == 1
|
||||
|
||||
healthy = RecordingAdapter()
|
||||
asyncio.run(_run_one_tick(monkeypatch, _runner(healthy)))
|
||||
assert len(healthy.sent) == 1
|
||||
assert _unseen(task_id) == []
|
||||
|
||||
|
||||
def test_changes_requested_reason_is_redacted_path_safe_and_truncated(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "redact.db"))
|
||||
kb.init_db()
|
||||
secret = "sk-abcdefghijklmnopqrstuvwxyz123456"
|
||||
reason = f"See /Users/alice/private/review.log token={secret} " + ("x" * 300)
|
||||
_create_review_block("notify", reason=reason)
|
||||
adapter = RecordingAdapter()
|
||||
|
||||
asyncio.run(_run_one_tick(monkeypatch, _runner(adapter)))
|
||||
|
||||
text = adapter.sent[0]["text"]
|
||||
assert "/Users/alice" not in text
|
||||
assert "abcdefghijklmnopqrstuvwxyz" not in text
|
||||
assert "[local path]" in text
|
||||
assert "… — reviewer @claude-qa" in text
|
||||
Reference in New Issue
Block a user