feat(history): seed Logs + Status Feed with history on connect

Both panels were empty on Connect / agent select — only new events since
subscribe appeared. Backend already persists per-agent logs to
logs/<id>/YYYY-MM-DD.jsonl AND now keeps the last 100 status diffs in
a ring buffer (agents_and_robots 71b3b2b).

Frontend changes:
- fetch_log_history(s, agent_id, n) → GET /agents/{id}/logs?n=N, fills
  s.log_lines BEFORE SSE subscribe so context appears instantly.
  Handles the {count,id,lines:[...]} response shape from the backend.
- start_log_sse now spawns this fetch on entry; SSE adds new lines on top.
- fetch_status_history(s, n) → GET /status/recent?n=N, fills
  s.status_events with [hist]-tagged entries before the live SSE attaches.
- Connect handler dispatches fetch_status_history() in a worker thread
  alongside the existing start_status_sse + fetch_agents_async.

E2E (4 new, 29 total):
- test_status_recent_history_endpoint   — shape contract
- test_status_recent_captures_stop_start_events — drives stop/start on
  test-bot, asserts events appear in /status/recent within 5s. This is
  the "send actions and observe feed" loop the user requested.
- test_agent_logs_history_endpoint      — {count,id,lines} contract +
  lines>0 for long-running assistant-bot
- test_status_recent_unauthorized_without_bearer — auth boundary

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 23:43:16 +02:00
parent 9cade2f2f8
commit e4a1c20fc2
2 changed files with 128 additions and 1 deletions
+66
View File
@@ -82,6 +82,72 @@ def test_connect_succeeds_with_valid_apikey():
assert n > 0, f"expected at least 1 agent, got {n}"
def test_status_recent_history_endpoint():
"""GET /status/recent returns a JSON array (may be empty after restart)."""
apikey = _apikey()
r = _curl([
"-fsS",
"-H", f"Authorization: Bearer {apikey}",
f"{_url()}/status/recent?n=10",
])
assert r.returncode == 0, r.stderr
body = json.loads(r.stdout)
assert isinstance(body, list)
def test_status_recent_captures_stop_start_events():
"""Triggering stop/start must produce 2 entries in /status/recent.
Drives the end-to-end "send actions and observe feed" flow the user
asked for: this is the same data path the C++ frontend uses to seed
its Status Feed panel on Connect.
"""
apikey = _apikey()
hdr = ["-H", f"Authorization: Bearer {apikey}"]
# Snapshot history length before
r0 = _curl(["-fsS", *hdr, f"{_url()}/status/recent?n=100"])
before = json.loads(r0.stdout)
# Drive events: stop then start test-bot.
_curl(["-sS", "-X", "POST", *hdr, f"{_url()}/agents/test-bot/stop"])
import time
time.sleep(2.5) # poller emits diff at most every 2s
_curl(["-sS", "-X", "POST", *hdr, f"{_url()}/agents/test-bot/start"])
time.sleep(2.5)
r1 = _curl(["-fsS", *hdr, f"{_url()}/status/recent?n=100"])
after = json.loads(r1.stdout)
# At least one new event captured (poller may coalesce or already had recents)
assert len(after) > len(before) or any(
e.get("agent_id") == "test-bot" for e in after[-5:]
), f"no test-bot event in feed; before={len(before)} after={len(after)}"
def test_agent_logs_history_endpoint():
"""GET /agents/{id}/logs?n=N returns {count, id, lines:[...]} — historical tail."""
apikey = _apikey()
r = _curl([
"-fsS",
"-H", f"Authorization: Bearer {apikey}",
f"{_url()}/agents/assistant-bot/logs?n=50",
])
assert r.returncode == 0, r.stderr
body = json.loads(r.stdout)
assert isinstance(body, dict)
assert body.get("id") == "assistant-bot"
lines = body.get("lines")
assert isinstance(lines, list)
# assistant-bot is long-running with persistent log file (logs/<id>/YYYY-MM-DD.jsonl)
assert len(lines) > 0, "expected at least 1 historical log line"
assert isinstance(lines[0], str)
def test_status_recent_unauthorized_without_bearer():
r = _curl(["-s", "-o", "/dev/null", "-w", "%{http_code}",
f"{_url()}/status/recent"])
assert r.stdout == "401", f"expected 401 got {r.stdout!r}"
def test_app_survives_auto_refresh_cycle():
"""Regression: app must NOT crash on Refresh Agents button click.