Files
hermes-webui/tests/test_anchor_scene_persistence.py
T
nesquena-hermes 6951d10ae5 rebase #4411 onto master (v0.51.542) for deep-review — Stable Assistant Turn Anchors capstone
franksong2702's activity_scene_v1 anchor-scene model: live-stream/settled/refresh/re-entry
show the same ordered turn. Rebased clean (no conflicts) onto current master.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-20 21:48:55 +00:00

865 lines
31 KiB
Python

import json
from collections import OrderedDict
from types import SimpleNamespace
def _client_anchor_scene_message_ref(message):
content = message.get("content") if isinstance(message, dict) else ""
if isinstance(content, list):
parts = []
for part in content:
if isinstance(part, dict):
parts.append(str(part.get("text") or part.get("content") or part.get("input_text") or ""))
else:
parts.append(str(part or ""))
content = "\n".join(parts)
payload = {
"role": str(message.get("role") or ""),
"content": " ".join(str(content or "").split()),
"timestamp": message.get("_ts") or message.get("timestamp") or "",
}
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
def test_anchor_scene_persistence_round_trip_outside_provider_messages(tmp_path, monkeypatch):
from api import models, routes
from api.models import Session
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
monkeypatch.setattr(models, "SESSIONS", OrderedDict())
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SESSIONS", models.SESSIONS)
session = Session(
session_id="anchorpersist1",
title="Anchor persistence",
messages=[
{"role": "user", "content": "question"},
{"role": "assistant", "content": "final answer", "timestamp": 10.0},
],
)
session.save(skip_index=True)
scene = {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"activity_rows": [
{
"row_id": "tool-1",
"role": "tool",
"tool_call_id": "call-1",
"tool": {"id": "call-1", "name": "terminal", "args": {"command": "git status"}},
}
],
"final_answer": "final answer",
}
request_body = {
"session_id": "anchorpersist1",
"stream_id": "stream-1",
"message_index": 1,
"message_ref": "stale-ref-after-content-normalization",
"scene": scene,
}
captured = {}
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
monkeypatch.setattr(routes, "read_body", lambda handler: request_body)
monkeypatch.setattr(
routes,
"j",
lambda handler, payload, status=200, extra_headers=None: captured.update(
payload=payload, status=status
) or True,
)
assert routes.handle_post(SimpleNamespace(command="POST"), SimpleNamespace(path="/api/session/anchor-scene")) is True
assert captured["status"] == 200
assert captured["payload"]["ok"] is True
assert captured["payload"]["message_index"] == 1
raw = json.loads((session_dir / "anchorpersist1.json").read_text(encoding="utf-8"))
assert "_anchor_activity_scene" not in raw["messages"][1]
assert raw["messages"][1]["content"] == "final answer"
records = raw["anchor_activity_scenes"]
assert len(records) == 1
record = next(iter(records.values()))
assert record["message_index"] == 1
assert record["stream_id"] == "stream-1"
assert record["scene"]["version"] == "activity_scene_v1"
loaded = Session.load("anchorpersist1")
hydrated = routes._hydrate_anchor_activity_scenes(
loaded.messages,
loaded.anchor_activity_scenes,
message_offset=0,
)
assert "_anchor_activity_scene" not in loaded.messages[1]
assert hydrated[1]["_anchor_stream_id"] == "stream-1"
assert hydrated[1]["_anchor_activity_scene"]["final_answer"] == "final answer"
assert hydrated[1]["_anchor_activity_scene"]["activity_rows"][0]["tool_call_id"] == "call-1"
def test_anchor_scene_hydration_rejects_stale_index_fallback_when_final_answer_mismatches():
from api import routes
messages = [
{"role": "user", "content": "old question"},
{"role": "assistant", "content": "old final"},
{"role": "user", "content": "new question"},
{"role": "assistant", "content": "new final"},
]
records = {
"stale-ref": {
"message_index": 3,
"message_ref": "missing-ref-after-window-shift",
"stream_id": "stream-1",
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"final_answer": "old final",
"activity_rows": [
{"row_id": "old-tool", "role": "tool", "tool_call_id": "old-call"}
],
},
}
}
hydrated = routes._hydrate_anchor_activity_scenes(messages, records)
assert "_anchor_activity_scene" not in hydrated[3]
def test_anchor_scene_persistence_rejects_invalid_scene(tmp_path, monkeypatch):
from api import models, routes
from api.models import Session
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
monkeypatch.setattr(models, "SESSIONS", OrderedDict())
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SESSIONS", models.SESSIONS)
Session(
session_id="anchorpersist2",
messages=[{"role": "assistant", "content": "answer"}],
).save(skip_index=True)
captured = {}
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
monkeypatch.setattr(
routes,
"read_body",
lambda handler: {
"session_id": "anchorpersist2",
"message_index": 0,
"scene": {"version": "wrong", "activity_rows": []},
},
)
monkeypatch.setattr(
routes,
"bad",
lambda handler, msg, status=400: captured.update(error=msg, status=status) or True,
)
assert routes.handle_post(SimpleNamespace(command="POST"), SimpleNamespace(path="/api/session/anchor-scene")) is True
assert captured["status"] == 400
assert "activity_scene_v1" in captured["error"]
def test_anchor_scene_persistence_prefers_unique_ref_over_stale_index(tmp_path, monkeypatch):
from api import models, routes
from api.models import Session
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
monkeypatch.setattr(models, "SESSIONS", OrderedDict())
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SESSIONS", models.SESSIONS)
messages = [
{"role": "user", "content": "old question"},
{"role": "assistant", "content": "old final"},
{"role": "user", "content": "new question"},
{"role": "assistant", "content": "new final"},
]
Session(session_id="anchorpersist_ref", messages=messages).save(skip_index=True)
client_ref = _client_anchor_scene_message_ref(messages[3])
assert client_ref != routes._assistant_anchor_scene_message_ref(messages[3])
captured = {}
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
monkeypatch.setattr(
routes,
"read_body",
lambda handler: {
"session_id": "anchorpersist_ref",
"message_index": 1,
"message_ref": client_ref,
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"activity_rows": [{"row_id": "tool-1", "role": "tool"}],
"final_answer": "new final",
},
},
)
monkeypatch.setattr(
routes,
"j",
lambda handler, payload, status=200, extra_headers=None: captured.update(
payload=payload, status=status
) or True,
)
assert routes.handle_post(SimpleNamespace(command="POST"), SimpleNamespace(path="/api/session/anchor-scene")) is True
assert captured["payload"]["message_index"] == 3
raw = json.loads((session_dir / "anchorpersist_ref.json").read_text(encoding="utf-8"))
record = next(iter(raw["anchor_activity_scenes"].values()))
assert record["message_index"] == 3
assert record["scene"]["final_answer"] == "new final"
def test_anchor_scene_persistence_rejects_duplicate_client_ref_over_stale_index(tmp_path, monkeypatch):
from api import models, routes
from api.models import Session
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
monkeypatch.setattr(models, "SESSIONS", OrderedDict())
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SESSIONS", models.SESSIONS)
messages = [
{"role": "user", "content": "old question"},
{"role": "assistant", "content": "same final"},
{"role": "user", "content": "new question"},
{"role": "assistant", "content": "same final"},
]
Session(session_id="anchorpersist_duplicate_ref", messages=messages).save(skip_index=True)
captured = {}
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
monkeypatch.setattr(
routes,
"read_body",
lambda handler: {
"session_id": "anchorpersist_duplicate_ref",
"message_index": 1,
"message_ref": _client_anchor_scene_message_ref(messages[3]),
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"activity_rows": [{"row_id": "tool-1", "role": "tool"}],
"final_answer": "same final",
},
},
)
monkeypatch.setattr(
routes,
"bad",
lambda handler, msg, status=400: captured.update(error=msg, status=status) or True,
)
assert routes.handle_post(SimpleNamespace(command="POST"), SimpleNamespace(path="/api/session/anchor-scene")) is True
assert captured["status"] == 404
assert captured["error"] == "Assistant message not found"
raw = json.loads((session_dir / "anchorpersist_duplicate_ref.json").read_text(encoding="utf-8"))
assert not raw.get("anchor_activity_scenes")
def test_anchor_scene_persistence_converts_window_index_to_full_index(tmp_path, monkeypatch):
from api import models, routes
from api.models import Session
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
monkeypatch.setattr(models, "SESSIONS", OrderedDict())
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SESSIONS", models.SESSIONS)
Session(
session_id="anchorpersist_window",
messages=[
{"role": "user", "content": "old question"},
{"role": "assistant", "content": "old final"},
{"role": "user", "content": "new question"},
{"role": "assistant", "content": "new final"},
],
).save(skip_index=True)
captured = {}
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
monkeypatch.setattr(
routes,
"read_body",
lambda handler: {
"session_id": "anchorpersist_window",
"message_index": 1,
"message_window_index": 1,
"message_offset": 2,
"message_ref": "stale-ref-after-content-normalization",
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"activity_rows": [{"row_id": "tool-1", "role": "tool"}],
"final_answer": "new final",
},
},
)
monkeypatch.setattr(
routes,
"j",
lambda handler, payload, status=200, extra_headers=None: captured.update(
payload=payload, status=status
) or True,
)
assert routes.handle_post(SimpleNamespace(command="POST"), SimpleNamespace(path="/api/session/anchor-scene")) is True
assert captured["payload"]["message_index"] == 3
raw = json.loads((session_dir / "anchorpersist_window.json").read_text(encoding="utf-8"))
record = next(iter(raw["anchor_activity_scenes"].values()))
assert record["message_index"] == 3
assert record["message_ref"] == routes._assistant_anchor_scene_message_ref(raw["messages"][3])
def test_anchor_scene_persistence_rejects_unmatched_ref_without_index(tmp_path, monkeypatch):
from api import models, routes
from api.models import Session
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
monkeypatch.setattr(models, "SESSIONS", OrderedDict())
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SESSIONS", models.SESSIONS)
Session(
session_id="anchorpersist_no_index",
messages=[
{"role": "user", "content": "question"},
{"role": "assistant", "content": "final"},
],
).save(skip_index=True)
captured = {}
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
monkeypatch.setattr(
routes,
"read_body",
lambda handler: {
"session_id": "anchorpersist_no_index",
"message_ref": "missing-ref",
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"activity_rows": [{"row_id": "tool-1", "role": "tool"}],
},
},
)
monkeypatch.setattr(
routes,
"bad",
lambda handler, msg, status=400: captured.update(error=msg, status=status) or True,
)
assert routes.handle_post(SimpleNamespace(command="POST"), SimpleNamespace(path="/api/session/anchor-scene")) is True
assert captured["status"] == 404
assert captured["error"] == "Assistant message not found"
def test_anchor_scene_persistence_rejects_ref_miss_stale_index_mismatch(tmp_path, monkeypatch):
from api import models, routes
from api.models import Session
session_dir = tmp_path / "sessions"
session_dir.mkdir()
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
monkeypatch.setattr(models, "SESSION_INDEX_FILE", session_dir / "_index.json")
monkeypatch.setattr(models, "SESSIONS", OrderedDict())
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SESSIONS", models.SESSIONS)
Session(
session_id="anchorpersist_stale_index_mismatch",
messages=[
{"role": "user", "content": "old question"},
{"role": "assistant", "content": "old final"},
{"role": "user", "content": "new question"},
{"role": "assistant", "content": "new final"},
],
).save(skip_index=True)
captured = {}
monkeypatch.setattr(routes, "_check_csrf", lambda handler: True)
monkeypatch.setattr(
routes,
"read_body",
lambda handler: {
"session_id": "anchorpersist_stale_index_mismatch",
"message_index": 1,
"message_ref": "stale-ref-after-content-normalization",
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"activity_rows": [{"row_id": "tool-1", "role": "tool"}],
"final_answer": "new final",
},
},
)
monkeypatch.setattr(
routes,
"bad",
lambda handler, msg, status=400: captured.update(error=msg, status=status) or True,
)
assert routes.handle_post(SimpleNamespace(command="POST"), SimpleNamespace(path="/api/session/anchor-scene")) is True
assert captured["status"] == 404
assert captured["error"] == "Assistant message not found"
raw = json.loads((session_dir / "anchorpersist_stale_index_mismatch.json").read_text(encoding="utf-8"))
assert not raw.get("anchor_activity_scenes")
def test_anchor_scene_hydration_repairs_tail_only_scene_from_full_turn():
from api import routes
final_answer = (
"final answer with enough detail to be the answer and a shared verification paragraph "
"about cron proxy fallback removal, 127.0.0.1:7890, direct git pulls, and durable Worklog ordering"
)
stale_final_draft = (
"draft answer with enough detail to be the answer and a shared verification paragraph "
"about cron proxy fallback removal, 127.0.0.1:7890, direct git pulls, and durable Worklog ordering"
)
short_stale_final_draft = "cron 127.0.0.1:7890 fallback " + ("x " * 45) + "Worklog"
messages = [
{"role": "user", "content": "question"},
{
"role": "assistant",
"content": "first progress",
"reasoning": "thinking near first progress",
"tool_calls": [{"id": "call-1", "function": {"name": "terminal"}}],
},
{"role": "tool", "tool_call_id": "call-1", "content": "ok"},
{"role": "assistant", "content": "second progress"},
{"role": "assistant", "content": final_answer},
]
old_scene = {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"final_answer": "",
"activity_rows": [
{
"row_id": "tail-prose",
"role": "prose",
"kind": "process_prose",
"source_event_type": "token",
"text": "second progress",
},
{
"row_id": "bad-final-prefix",
"role": "thinking",
"kind": "reasoning",
"source_event_type": "reasoning",
"text": final_answer,
},
{
"row_id": "tail-thinking",
"role": "thinking",
"kind": "reasoning",
"source_event_type": "reasoning",
"text": "thinking near first progress",
},
{
"row_id": "tail-final-draft",
"role": "prose",
"kind": "process_prose",
"source_event_type": "token",
"text": stale_final_draft,
},
{
"row_id": "tail-short-final-draft",
"role": "prose",
"kind": "process_prose",
"source_event_type": "token",
"text": short_stale_final_draft,
},
{"row_id": "done", "role": "terminal", "kind": "terminal_status", "source_event_type": "done"},
],
}
records = {
"record": {
"message_index": 4,
"message_ref": routes._assistant_anchor_scene_message_ref(messages[4]),
"stream_id": "stream-1",
"scene": old_scene,
}
}
hydrated = routes._hydrate_anchor_activity_scenes(
messages,
records,
message_offset=0,
tool_calls=[
{
"assistant_msg_idx": 1,
"tid": "call-1",
"name": "terminal",
"preview": "running",
"snippet": "ok",
}
],
)
scene = hydrated[4]["_anchor_activity_scene"]
rows = scene["activity_rows"]
prose_texts = [row.get("text") for row in rows if row.get("role") == "prose"]
tool_ids = [row.get("tool_call_id") for row in rows if row.get("role") == "tool"]
thinking_texts = [row.get("text") for row in rows if row.get("role") == "thinking"]
chronological = [
(row.get("role"), row.get("text") or row.get("tool_call_id"))
for row in rows
if row.get("role") != "terminal"
]
assert scene["final_answer"] == final_answer
assert prose_texts == ["first progress", "second progress"]
assert tool_ids == ["call-1"]
assert thinking_texts == ["thinking near first progress"]
assert chronological[:4] == [
("prose", "first progress"),
("thinking", "thinking near first progress"),
("tool", "call-1"),
("prose", "second progress"),
]
assert rows[-1]["role"] == "terminal"
def test_anchor_scene_hydration_backfills_turn_duration_from_final_message():
from api import routes
messages = [
{"role": "user", "content": "question"},
{"role": "assistant", "content": "final answer", "_turnDuration": 731.2},
]
records = {
"record": {
"message_index": 1,
"message_ref": routes._assistant_anchor_scene_message_ref(messages[1]),
"stream_id": "stream-1",
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"final_answer": "final answer",
"activity_rows": [
{"row_id": "done", "role": "terminal", "kind": "terminal_status"}
],
},
}
}
hydrated = routes._hydrate_anchor_activity_scenes(messages, records)
assert hydrated[1]["_anchor_activity_scene"]["turn_duration"] == 731.2
def test_anchor_scene_hydration_dedupes_compression_lifecycle_rows():
from api import routes
messages = [
{"role": "user", "content": "question"},
{"role": "assistant", "content": "thinking text", "reasoning": "thinking text"},
{"role": "assistant", "content": "final answer"},
]
records = {
"record": {
"message_index": 2,
"message_ref": routes._assistant_anchor_scene_message_ref(messages[2]),
"stream_id": "stream-1",
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"final_answer": "final answer",
"activity_rows": [
{
"row_id": "compressing-1",
"role": "lifecycle",
"kind": "lifecycle_status",
"source_event_type": "compressing",
"status": "running",
"text": "Compressing context",
},
{
"row_id": "compressing-2",
"role": "lifecycle",
"kind": "lifecycle_status",
"source_event_type": "compressing",
"status": "running",
"text": "Compressing context",
},
{
"row_id": "compressed",
"role": "lifecycle",
"kind": "lifecycle_status",
"source_event_type": "compressed",
"status": "completed",
"text": "Context auto-compressed",
},
{"row_id": "done", "role": "terminal", "kind": "terminal_status", "source_event_type": "done"},
],
},
}
}
hydrated = routes._hydrate_anchor_activity_scenes(messages, records)
rows = hydrated[2]["_anchor_activity_scene"]["activity_rows"]
compression_rows = [
row
for row in rows
if row.get("role") == "lifecycle"
and row.get("source_event_type") in {"compressing", "compressed"}
]
assert len(compression_rows) == 1
assert compression_rows[0]["source_event_type"] == "compressed"
assert compression_rows[0]["order_index"] == compression_rows[0]["seq"]
def test_anchor_scene_hydration_drops_stale_live_running_thinking_when_settled_thinking_exists():
from api import routes
messages = [
{"role": "user", "content": "question"},
{
"role": "assistant",
"content": "process update",
"reasoning": "settled thinking from transcript",
},
{"role": "assistant", "content": "final answer"},
]
records = {
"record": {
"message_index": 2,
"message_ref": routes._assistant_anchor_scene_message_ref(messages[2]),
"stream_id": "stream-1",
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"final_answer": "final answer",
"activity_rows": [
{
"row_id": "live-reasoning:stream-1:2",
"local_id": "live-reasoning:stream-1:2",
"role": "thinking",
"kind": "reasoning",
"source_event_type": "reasoning",
"status": "running",
"text": "stale live reasoning text",
},
{"row_id": "done", "role": "terminal", "kind": "terminal_status", "source_event_type": "done"},
],
},
}
}
hydrated = routes._hydrate_anchor_activity_scenes(messages, records)
rows = hydrated[2]["_anchor_activity_scene"]["activity_rows"]
thinking_rows = [row for row in rows if row.get("role") == "thinking"]
assert [row.get("text") for row in thinking_rows] == ["settled thinking from transcript"]
assert not any(str(row.get("row_id") or "").startswith("live-reasoning:") for row in rows)
assert not any(
row.get("role") in {"thinking", "prose", "tool"} and row.get("status") == "running"
for row in rows
)
def test_anchor_scene_hydration_seals_unmatched_live_running_activity_rows():
from api import routes
messages = [
{"role": "user", "content": "question"},
{"role": "assistant", "content": "final answer"},
]
records = {
"record": {
"message_index": 1,
"message_ref": routes._assistant_anchor_scene_message_ref(messages[1]),
"stream_id": "stream-1",
"scene": {
"version": "activity_scene_v1",
"mode": "compact_worklog",
"final_answer": "final answer",
"activity_rows": [
{
"row_id": "live-reasoning:stream-1:1",
"local_id": "live-reasoning:stream-1:1",
"role": "thinking",
"kind": "reasoning",
"source_event_type": "reasoning",
"status": "running",
"text": "only live reasoning",
},
{
"row_id": "live-prose:stream-1:1",
"local_id": "live-prose:stream-1:1",
"role": "prose",
"kind": "process_prose",
"source_event_type": "token",
"status": "running",
"text": "only live prose",
},
{
"row_id": "live-tool:stream-1:call-1",
"local_id": "live-tool:stream-1:call-1",
"role": "tool",
"kind": "tool_started",
"source_event_type": "tool",
"status": "running",
"tool_call_id": "call-1",
"tool": {"id": "call-1", "name": "terminal", "done": False},
},
{"row_id": "done", "role": "terminal", "kind": "terminal_status", "source_event_type": "done"},
],
},
}
}
hydrated = routes._hydrate_anchor_activity_scenes(messages, records)
rows = hydrated[1]["_anchor_activity_scene"]["activity_rows"]
activity_rows = [row for row in rows if row.get("role") in {"thinking", "prose", "tool"}]
assert [row.get("status") for row in activity_rows] == ["completed", "completed", "completed"]
assert [row.get("role") for row in activity_rows] == ["thinking", "prose", "tool"]
def test_runtime_journal_snapshot_includes_live_anchor_activity_scene(monkeypatch):
from api import routes
stream_id = "stream-live-scene"
events = [
{
"event": "token",
"seq": 1,
"event_id": f"{stream_id}:1",
"created_at": 1.0,
"payload": {"text": "first progress"},
},
{
"event": "reasoning",
"seq": 2,
"event_id": f"{stream_id}:2",
"created_at": 2.0,
"payload": {"text": "thinking through plan"},
},
{
"event": "interim_assistant",
"seq": 3,
"event_id": f"{stream_id}:3",
"created_at": 3.0,
"payload": {"text": "checkpoint"},
},
{
"event": "tool",
"seq": 4,
"event_id": f"{stream_id}:4",
"created_at": 4.0,
"payload": {"name": "terminal", "tid": "call-1", "args": {"command": "pytest"}},
},
{
"event": "tool_complete",
"seq": 5,
"event_id": f"{stream_id}:5",
"created_at": 5.0,
"payload": {"name": "terminal", "tid": "call-1", "preview": "ok"},
},
{
"event": "token",
"seq": 6,
"event_id": f"{stream_id}:6",
"created_at": 6.0,
"payload": {"text": " tail"},
},
]
monkeypatch.setattr(
routes,
"find_run_summary",
lambda sid: {
"session_id": "session-live-scene",
"last_seq": 6,
"last_event_id": f"{stream_id}:6",
},
)
monkeypatch.setattr(
routes,
"read_run_events",
lambda session_id, run_id: {"events": events},
)
snapshot = routes._run_journal_live_snapshot(stream_id)
scene = snapshot["anchor_activity_scene"]
rows = scene["activity_rows"]
assert scene["version"] == "activity_scene_v1"
assert snapshot["last_assistant_text"] == "first progress\n\ncheckpoint tail"
assert snapshot["last_reasoning_text"] == "thinking through plan"
assert [row["role"] for row in rows] == ["prose", "thinking", "tool", "prose"]
assert rows[0]["local_id"] == f"live-prose:{stream_id}:1"
assert rows[1]["local_id"] == f"live-thinking:{stream_id}:1"
assert rows[1]["thinking"]["text"] == "thinking through plan"
assert rows[2]["tool_call_id"] == "call-1"
assert rows[2]["tool"]["done"] is True
assert rows[3]["status"] == "running"
def test_runtime_journal_snapshot_has_running_anchor_row_before_first_token(monkeypatch):
from api import routes
stream_id = "stream-live-empty"
monkeypatch.setattr(
routes,
"find_run_summary",
lambda sid: {
"session_id": "session-live-empty",
"last_seq": 1,
"last_event_id": f"{stream_id}:1",
},
)
monkeypatch.setattr(
routes,
"read_run_events",
lambda session_id, run_id: {
"events": [
{
"event": "context_status",
"seq": 1,
"event_id": f"{stream_id}:1",
"created_at": 1.0,
"payload": {"session_id": "session-live-empty"},
}
]
},
)
snapshot = routes._run_journal_live_snapshot(stream_id)
rows = snapshot["anchor_activity_scene"]["activity_rows"]
assert rows
assert rows[0]["role"] == "lifecycle"
assert rows[0]["status"] == "running"