fix(webui): reconcile threads after browser resume

This commit is contained in:
chengyongru
2026-07-28 16:25:08 +08:00
committed by chengyongru
parent 78cf68c291
commit ae089aa3ae
39 changed files with 6151 additions and 243 deletions
+139
View File
@@ -389,6 +389,7 @@ def test_thread_response_does_not_mark_completed_message_tool_tail_pending(
assert out is not None
assert out["has_pending_tool_calls"] is False
assert out["completed_turn_ids"] == [turn_id]
assert out["messages"][-1]["kind"] == "trace"
assert out["messages"][-2]["content"] == "Cron test"
@@ -410,6 +411,144 @@ def test_thread_response_marks_unfinished_tool_tail_pending(tmp_path, monkeypatc
assert out is not None
assert out["has_pending_tool_calls"] is True
assert out["completed_turn_ids"] == []
def test_thread_response_reports_active_registry_without_transcript(
tmp_path,
monkeypatch,
) -> None:
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
out = build_webui_thread_response(
"websocket:active-without-transcript",
active_turn_started_at=1_700_000_000.0,
active_turn_id="turn-active",
)
assert out is not None
assert out["messages"] == []
assert out["completed_turn_ids"] == []
assert out["has_pending_tool_calls"] is True
assert out["active_turn_id"] == "turn-active"
def test_thread_response_reports_explicit_completion_without_assistant_row(
tmp_path,
monkeypatch,
) -> None:
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
key = "websocket:empty-answer"
turn_id = "turn-empty-answer"
append_transcript_object(
key,
{"event": "user", "chat_id": "empty-answer", "text": "stop", "turn_id": turn_id},
)
append_transcript_object(
key,
{"event": "turn_end", "chat_id": "empty-answer", "turn_id": turn_id},
)
out = build_webui_thread_response(key)
assert out is not None
assert out["messages"][-1]["role"] == "user"
assert out["has_pending_tool_calls"] is False
assert out["completed_turn_ids"] == [turn_id]
def test_incomplete_turn_with_ambiguous_session_match_stays_pending(
tmp_path,
monkeypatch,
) -> None:
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
key = "websocket:ambiguous-incomplete"
turn_id = "turn-ambiguous"
append_transcript_object(
key,
{
"event": "user",
"chat_id": "ambiguous-incomplete",
"text": "repeat",
"turn_id": turn_id,
},
)
append_transcript_object(
key,
{
"event": "turn_end",
"chat_id": "ambiguous-incomplete",
"turn_id": turn_id,
"transcript_incomplete": True,
},
)
out = build_webui_thread_response(
key,
session_messages=[
{"role": "user", "content": "repeat"},
{"role": "assistant", "content": "first answer"},
{"role": "user", "content": "repeat"},
{"role": "assistant", "content": "second answer"},
],
)
assert out is not None
assert [(message["role"], message["content"]) for message in out["messages"]] == [
("user", "repeat"),
]
assert out["completed_turn_ids"] == []
assert out["has_pending_tool_calls"] is True
def test_later_completion_does_not_hide_older_incomplete_turn(
tmp_path,
monkeypatch,
) -> None:
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
key = "websocket:older-incomplete"
for event in (
{"event": "user", "text": "first", "turn_id": "turn-first"},
{
"event": "turn_end",
"turn_id": "turn-first",
"transcript_incomplete": True,
},
{"event": "user", "text": "second", "turn_id": "turn-second"},
{"event": "message", "text": "second answer", "turn_id": "turn-second"},
{"event": "turn_end", "turn_id": "turn-second"},
):
append_transcript_object(
key,
{"chat_id": "older-incomplete", **event},
)
out = build_webui_thread_response(key)
assert out is not None
assert out["completed_turn_ids"] == ["turn-second"]
assert out["has_pending_tool_calls"] is True
def test_active_registry_does_not_hide_a_newer_queued_turn(tmp_path, monkeypatch) -> None:
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
key = "websocket:queued-tail"
for event in (
{"event": "user", "text": "first", "turn_id": "turn-old"},
{"event": "message", "text": "done", "turn_id": "turn-old"},
{"event": "turn_end", "turn_id": "turn-old"},
{"event": "user", "text": "queued next", "turn_id": "turn-new"},
):
append_transcript_object(key, {"chat_id": "queued-tail", **event})
out = build_webui_thread_response(
key,
active_turn_started_at=1_700_000_000.0,
active_turn_id="turn-old",
)
assert out is not None
assert out["has_pending_tool_calls"] is True
def test_replay_preserves_turn_metadata(tmp_path, monkeypatch) -> None: