143 lines
7.7 KiB
Python
143 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""t-261015090129-ef5029: канал агент↔агент через MCP (ask_agent/poll_messages).
|
||
|
||
Запуск: ./venv/bin/python tests/test_agent_channel.py
|
||
|
||
Сеть НЕ трогается: harness.agent_channel._rpc_tool подменяется фейком —
|
||
"портал" отвечает заготовками ask_agent/poll_messages. Проверяет:
|
||
- pull(): agent_message → MessageReceived в стор (kind=agent_message,
|
||
chat_id=portal:agent:<from>, update_id=id события портала);
|
||
- дедуп: повторный pull с теми же id не дублирует инбокс (курсор портала
|
||
не сдвинулся — стор второй линией отсекает);
|
||
- push_agent(): ask_agent(to, text, kind) — ok:true → True; error → False;
|
||
- send_reply() на agent-сообщение: ветка portal:agent:* уходит ask_agent'ом
|
||
(fake перехватывает), ReplySent пишется, сообщение закрывается;
|
||
- send_batch_reply(): сводный ответ агенту одним ask_agent;
|
||
- sec_reflex(): rate-limit (не чаще MIN_REFLEX_GAP), после сброса — pull.
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
_DST = "/tmp/simbiot-test-agent-channel.db"
|
||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||
if os.path.exists(p):
|
||
os.unlink(p)
|
||
os.environ["SIMBIOT_DB"] = _DST
|
||
os.environ["SIMBIOT_PORTAL_MCP_TOKEN"] = "agt_test_agent_channel"
|
||
os.environ.pop("SIMBIOT_HOME", None)
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from harness import agent_channel, es, tg_inbox
|
||
|
||
fails = []
|
||
|
||
|
||
def check(name, cond, extra=""):
|
||
print(("PASS" if cond else "FAIL"), name, ("" if cond else ("| " + str(extra))))
|
||
if not cond:
|
||
fails.append(name)
|
||
|
||
|
||
es.init_db()
|
||
|
||
# ── фейк MCP-шлюза ──────────────────────────────────────────────────────────
|
||
CALLS = []
|
||
|
||
|
||
def _fake_rpc(tool, arguments):
|
||
CALLS.append((tool, arguments))
|
||
if tool == "poll_messages":
|
||
return json.dumps({"messages": FAKE_QUEUE.get("messages", []),
|
||
"count": len(FAKE_QUEUE.get("messages", [])),
|
||
"note": "ok"})
|
||
if tool == "ask_agent":
|
||
return json.dumps({"ok": True, "to": arguments.get("to"),
|
||
"delivered": "agent_message"})
|
||
return json.dumps({"error": "unknown"})
|
||
|
||
|
||
agent_channel._rpc_tool = _fake_rpc
|
||
|
||
FAKE_QUEUE = {"messages": [
|
||
{"id": 9001, "from": "OpenCode", "kind": "help",
|
||
"text": "🆘 Запрос помощи от «OpenCode»:\n\nСимбиот, глянь deploy_app для groupsign",
|
||
"ts": "2026-10-15 09:00:00"},
|
||
{"id": 9002, "from": "Roy", "kind": "handoff",
|
||
"text": "🤝 Передача задачи от «Roy»:\n\nЗабери мою задачу по туннелям",
|
||
"ts": "2026-10-15 09:05:00"},
|
||
]}
|
||
|
||
# ── 1. pull → инбокс ────────────────────────────────────────────────────────
|
||
n = agent_channel.pull()
|
||
check("pull инжестит 2 сообщения", n == 2, n)
|
||
model = es.run_projections()
|
||
inbox = model.get("inbox") or {}
|
||
m1 = inbox.get("m-9001")
|
||
check("m-9001 в сторе", m1 is not None)
|
||
if m1:
|
||
check("chat_id агентный", m1.get("chat_id") == "portal:agent:OpenCode", m1.get("chat_id"))
|
||
check("kind=agent_message", m1.get("kind") == "agent_message")
|
||
check("username=агент", m1.get("username") == "OpenCode")
|
||
check("agent_kind=help", m1.get("agent_kind") == "help")
|
||
check("m-9002 в сторе", inbox.get("m-9002") is not None)
|
||
|
||
# ── 2. дедуп: тот же poll снова ─────────────────────────────────────────────
|
||
n2 = agent_channel.pull()
|
||
check("повторный pull — 0 новых (дедуп стора)", n2 == 0, n2)
|
||
|
||
# ── 3. push_agent ───────────────────────────────────────────────────────────
|
||
ok = agent_channel.push_agent("OpenCode", "deploy ок, лог чист", "help")
|
||
check("push_agent ok", ok is True and CALLS[-1][0] == "ask_agent", CALLS[-1:])
|
||
args = CALLS[-1][1]
|
||
check("ask_agent аргументы", args.get("to") == "OpenCode"
|
||
and args.get("text") == "deploy ок, лог чист"
|
||
and args.get("kind") == "help", args)
|
||
ok = agent_channel.push_agent("", "пустой адресат")
|
||
check("push_agent без адресата — False", ok is False)
|
||
ok = agent_channel.push_agent("Nobody", "")
|
||
check("push_agent без текста — False", ok is False)
|
||
|
||
# ── 4. send_reply на агентское сообщение ────────────────────────────────────
|
||
rid = tg_inbox.send_reply("m-9001", "Симбиот: deploy посмотрел, всё чисто. Детали в тике.")
|
||
check("send_reply вернул id", rid == "m-9001", rid)
|
||
check("ушёл ask_agent (не TG)", CALLS[-1][0] == "ask_agent", CALLS[-1:])
|
||
check("адресат из chat_id", CALLS[-1][1].get("to") == "OpenCode", CALLS[-1][1])
|
||
model = es.run_projections()
|
||
m1 = (model.get("inbox") or {}).get("m-9001")
|
||
check("m-9001 закрыт (replied)", bool(m1 and m1.get("replied")))
|
||
check("канал ответа agent_mcp", m1 and m1.get("reply_text") == "Симбиот: deploy посмотрел, всё чисто. Детали в тике.")
|
||
|
||
# ── 5. батч: сводный ответ агенту ───────────────────────────────────────────
|
||
FAKE_QUEUE["messages"] = [{"id": 9003, "from": "Roy", "kind": "review",
|
||
"text": "Проверь мой отчёт по туннелям", "ts": "2026-10-15 09:10:00"},
|
||
{"id": 9004, "from": "Roy", "kind": "review",
|
||
"text": "И заодно по слотам", "ts": "2026-10-15 09:11:00"}]
|
||
n = agent_channel.pull()
|
||
check("pull инжестит вторую партию 2 сообщения", n == 2, n)
|
||
CALLS.clear()
|
||
head = tg_inbox.send_batch_reply(["m-9003", "m-9004"], "Принял оба: отчёт и слоты проверю.")
|
||
check("батч закрыт головным", head == "m-9003", head)
|
||
ask_calls = [c for c in CALLS if c[0] == "ask_agent"]
|
||
check("один ask_agent на батч", len(ask_calls) == 1, CALLS)
|
||
model = es.run_projections()
|
||
m3 = (model.get("inbox") or {}).get("m-9003")
|
||
m4 = (model.get("inbox") or {}).get("m-9004")
|
||
check("оба закрыты", bool(m3 and m3.get("replied") and m4 and m4.get("replied")))
|
||
|
||
# ── 6. sec_reflex rate-limit ────────────────────────────────────────────────
|
||
agent_channel._last_reflex["ts"] = 0.0
|
||
r1 = agent_channel.sec_reflex()
|
||
r2 = agent_channel.sec_reflex()
|
||
check("первый рефлекс работает", "ingested" in r1, r1)
|
||
check("второй — rate-limit", r2.get("skipped") == "rate-limit", r2)
|
||
|
||
# ── 7. pull без токена — тихо ───────────────────────────────────────────────
|
||
_tok = os.environ.pop("SIMBIOT_PORTAL_MCP_TOKEN")
|
||
n = agent_channel.pull()
|
||
check("без токена — 0, без исключений", n == 0, n)
|
||
os.environ["SIMBIOT_PORTAL_MCP_TOKEN"] = _tok
|
||
|
||
print()
|
||
print("FAILS:", len(fails), fails if fails else "")
|
||
sys.exit(1 if fails else 0)
|