fix(hands,jobs): watchdog window reads the room tail, not the pending cursor

This commit is contained in:
hh committed 2026-09-23 18:18:27 +00:00
1 parent 91bd50a66d
commit 47c31ac356
6 files changed
+341 -37

No files matched your search

+64 -1
View File
@@ -1,10 +1,12 @@
from urllib.parse import parse_qsl, urlsplit
import pytest
from beaver_gateway.mcp.types import McpServer
from beaver_gateway.mcp.wrap import build_python_tool_server
from fastmcp import Client
from fastmcp.exceptions import ToolError
from beaver_agent.hands.vibegram import Vibegram, _cards, _event, _work
from beaver_agent.hands.vibegram import PAGE, Vibegram, _cards, _event, _work
def test_events_render_and_unknown_kinds_are_skipped():
@@ -64,3 +66,64 @@ async def test_tool_schema_lists_actions_and_needs_text_for_send():
)
with pytest.raises(ToolError, match="text"):
await client.call_tool("vibegram", {"action": "send"})
def _raw(eid, kind, body=None):
return {
"id": eid,
"roomId": "r",
"nick": "claude-kai",
"kind": kind,
"payload": {} if body is None else {"body": body},
"createdAt": "2026-09-23T05:00:02.000Z",
}
def _hub(monkeypatch, events):
calls = []
async def call(self, method, path, body):
calls.append(path)
if path == "/api/room":
return {"room": {"id": "r", "name": "kai-rem-watch", "viewToken": "v 1"}}
query = dict(parse_qsl(urlsplit(path).query))
assert query["view"] == "v 1" # ключ комнаты уезжает экранированным
after = int(query.get("after", 0))
return {"events": [e for e in events if e["id"] > after][:PAGE]}
monkeypatch.setattr(Vibegram, "_call", call)
return calls, Vibegram(hub="http://hub", token="t", nick="beaver-test")
async def test_window_pages_forward_to_the_end_and_drops_joins(monkeypatch):
# 520 событий комнаты - три страницы по 200; join/leave в окно не идут
events = [
_raw(i, "message" if i % 4 else "agent_leave", f"строка {i}")
for i in range(1, 521)
]
calls, room = _hub(monkeypatch, events)
window = await room.window(25)
assert window.full
assert len(window.events) == 25
assert window.events[-1].text.endswith("строка 519")
assert all("вошёл" not in e.text and "вышел" not in e.text for e in window.events)
assert calls.count("/api/room") == 1
assert sum(p.startswith("/api/state") for p in calls) == 3
assert "view=v%201" in calls[1]
assert not any("pending" in p for p in calls) # курсор сторожу не окно
async def test_window_that_ran_out_of_pages_says_it_is_not_full(monkeypatch):
events = [_raw(i, "message", f"строка {i}") for i in range(1, 521)]
_, room = _hub(monkeypatch, events)
assert not (await room.window(25, pages=2)).full
assert (await room.window(25, pages=3)).full
async def test_window_of_an_empty_room_is_full_and_empty(monkeypatch):
_, room = _hub(monkeypatch, [])
window = await room.window(25)
assert window.full and window.events == []