fix(ui): stabilize workplace chat scrolling

This commit is contained in:
LZH-YS1998
2026-07-13 23:02:39 +08:00
parent 4e7aa75ba5
commit 4bc18dcd27
46 changed files with 5937 additions and 1258 deletions
+81
View File
@@ -0,0 +1,81 @@
from __future__ import annotations
import unittest
from opc.plugins.office_ui.chat_store import ChatStore
class ChatStoreProgressFoldingTests(unittest.TestCase):
def test_stream_fold_preserves_first_timestamp(self) -> None:
first = {
"timestamp": 1_700_000_000.0,
"type": "thinking",
"summary": "Thinking",
"detail": "Need ",
"turn_id": "runtime-1:1",
"item_id": "runtime-1:1:thinking",
"seq": 1,
}
deltas = [
{
"timestamp": 1_700_000_000.1,
"type": "thinking",
"summary": "Thinking",
"detail": "more ",
"turn_id": "runtime-1:1",
"item_id": "runtime-1:1:thinking",
"seq": 2,
},
{
"timestamp": 1_700_000_000.2,
"type": "thinking",
"summary": "Thinking",
"detail": "context",
"turn_id": "runtime-1:1",
"item_id": "runtime-1:1:thinking",
"seq": 3,
},
]
folded = ChatStore._fold_progress_entries([first], deltas)
self.assertEqual(len(folded), 1)
self.assertEqual(folded[0]["timestamp"], first["timestamp"])
self.assertEqual(folded[0]["detail"], "Need more context")
self.assertEqual(folded[0]["seq"], 3)
# Folding builds a replacement row and must not mutate the persisted
# value supplied by the caller.
self.assertEqual(first["detail"], "Need ")
def test_stream_fold_within_one_batch_keeps_creation_timestamp(self) -> None:
folded = ChatStore._fold_progress_entries(
[],
[
{
"timestamp": 10.0,
"type": "assistant",
"summary": "Part one",
"detail": "Part one ",
"turn_id": "runtime-2:1",
"stream_id": "runtime-2:1:assistant",
"seq": 1,
},
{
"timestamp": 11.0,
"type": "assistant",
"summary": "part two",
"detail": "part two",
"turn_id": "runtime-2:1",
"stream_id": "runtime-2:1:assistant",
"seq": 2,
},
],
)
self.assertEqual(len(folded), 1)
self.assertEqual(folded[0]["timestamp"], 10.0)
self.assertEqual(folded[0]["detail"], "Part one part two")
if __name__ == "__main__":
unittest.main()
+7 -2
View File
@@ -679,7 +679,11 @@ async def test_session_detail_routes_by_request_project_id() -> None:
chat_store = SimpleNamespace(
create_session_channel=AsyncMock(return_value={"channel_id": "session:task-b"}),
backfill_messages=AsyncMock(return_value=[]),
get_channel_messages_page=AsyncMock(return_value=[]),
get_channel_messages_page_info=AsyncMock(return_value={
"messages": [],
"has_more": False,
"total_count": 0,
}),
get_channel_messages=AsyncMock(return_value=[]),
)
handler = WSHandler(engine_a, MagicMock(), chat_store, _ui_event_adapter())
@@ -708,7 +712,8 @@ async def test_session_detail_routes_by_request_project_id() -> None:
"Project B Session",
project_id="project-b",
)
assert chat_store.get_channel_messages_page.await_args.kwargs["project_id"] == "project-b"
assert chat_store.get_channel_messages_page_info.await_args.kwargs["project_id"] == "project-b"
assert chat_store.get_channel_messages_page_info.await_args.kwargs["detail_level"] == "summary"
@_async_test
+94 -4
View File
@@ -21,10 +21,9 @@ from types import SimpleNamespace
from typing import Any
from unittest.mock import ANY, AsyncMock, MagicMock, patch
import aiosqlite
from opc.core.attachment_store import AttachmentRef, AttachmentStore
from opc.core.models import DelegationRun, ExecutionCheckpoint, Task, TaskStatus
from opc.database.store import _SQLiteConnectionAdapter
from opc.layer2_organization import comms as file_comms
from opc.plugins.office_ui.event_adapter import EventAdapter
from opc.plugins.office_ui.chat_store import ChatStore
@@ -207,8 +206,8 @@ def _make_engine(store: StubStore | None = None, memory: StubMemory | None = Non
async def _make_chat_store() -> ChatStore:
"""Create an in-memory ChatStore for testing."""
db = await aiosqlite.connect(":memory:")
cs = ChatStore(db)
db = _SQLiteConnectionAdapter(":memory:")
cs = ChatStore(db) # type: ignore[arg-type]
await cs.initialize()
return cs
@@ -4444,6 +4443,97 @@ class TestWSHandlerSessionDetail(unittest.IsolatedAsyncioTestCase):
["oldest"],
)
async def test_session_detail_pages_transcript_and_ui_only_messages_together(self) -> None:
ws = MagicMock()
ws.send_json = AsyncMock()
base_time = datetime.now()
task = Task(
id="mixed-page-task-1",
title="Mixed source page",
project_id="test-project",
session_id="mixed-page-session-1",
)
await self.store.save_task(task)
self.store._transcripts["mixed-page-session-1"] = [
{
"message": SimpleNamespace(
message_id="transcript-old",
role="assistant",
agent_id="agent-reviewer",
created_at=base_time,
summary_flag=False,
metadata={"kind": "top_level_reply"},
),
"parts": [SimpleNamespace(part_type="text", payload={"text": "Persisted reply"})],
},
]
channel_id = "session:mixed-page-task-1"
await self.chat_store.create_session_channel(
task.id,
task.title,
project_id="test-project",
)
await self.chat_store.insert_message(
channel_id,
"system",
"OPC",
"Approval required",
metadata={
"source": "ui",
"detail_visibility": "summary",
"kind": "ui_only_notice",
},
message_id="ui-only-mid",
project_id="test-project",
created_at=base_time.timestamp() + 1,
)
await self.chat_store.insert_message(
channel_id,
"system",
"OPC",
"Legacy execution notice",
metadata={"source": "ui", "detail_visibility": "summary"},
message_id="ui-only-new",
project_id="test-project",
created_at=base_time.timestamp() + 2,
)
await self.handler._handle_session_detail(
ws,
{"project_id": "test-project", "task_id": task.id, "limit": 2},
)
first_payload = ws.send_json.await_args_list[0].args[0]["payload"]
self.assertEqual(first_payload["message_count"], 3)
self.assertEqual(first_payload["loaded_count"], 2)
self.assertTrue(first_payload["has_more"])
self.assertEqual(
[message["message_id"] for message in first_payload["messages"]],
["ui-only-mid", "ui-only-new"],
)
oldest_loaded = first_payload["messages"][0]
await self.handler._handle_session_detail(
ws,
{
"project_id": "test-project",
"task_id": task.id,
"limit": 2,
"before_created_at": oldest_loaded["created_at"],
"before_message_id": oldest_loaded["message_id"],
},
)
second_payload = ws.send_json.await_args_list[1].args[0]["payload"]
self.assertEqual(second_payload["message_count"], 3)
self.assertEqual(second_payload["loaded_count"], 1)
self.assertFalse(second_payload["has_more"])
self.assertEqual(
[message["message_id"] for message in second_payload["messages"]],
["transcript-old"],
)
async def test_session_detail_returns_silently_when_shutdown_closes_chat_db(self) -> None:
ws = MagicMock()
ws.send_json = AsyncMock()
+691
View File
@@ -0,0 +1,691 @@
from __future__ import annotations
import asyncio
import random
import tempfile
import unittest
from datetime import datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
from opc.core.models import SessionMessageRecord, SessionPartRecord, SessionRecord
from opc.core.transcript_visibility import transcript_metadata_visible
from opc.database.store import OPCStore, _SQLiteConnectionAdapter
from opc.plugins.office_ui.chat_store import (
ChatStore,
_MessageMatchIndex,
_MessageMatchState,
)
from opc.plugins.office_ui.snapshot_builder import build_transcript_ui_messages
from opc.plugins.office_ui.ws_handler import WSHandler
class TranscriptStorePaginationTests(unittest.IsolatedAsyncioTestCase):
async def test_summary_page_filters_full_detail_rows_before_limit(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
store = OPCStore(Path(tmpdir) / "tasks.db")
await store.initialize()
try:
session_id = "summary-pagination-session"
task_id = "summary-pagination-task"
base = datetime(2026, 7, 13, 12, 0, 0)
await store.save_session(SessionRecord(
session_id=session_id,
project_id="test-project",
title="Summary pagination",
created_at=base,
updated_at=base,
))
async def save(
message_id: str,
offset: int,
kind: str,
*,
company_final_turn: bool = False,
summary_flag: bool = False,
) -> None:
metadata = {"kind": kind}
if company_final_turn:
metadata["company_final_turn"] = True
created_at = base + timedelta(seconds=offset)
await store.save_session_message(SessionMessageRecord(
message_id=message_id,
session_id=session_id,
task_id=task_id,
role="assistant",
agent_id="agent-reviewer",
summary_flag=summary_flag,
metadata=metadata,
created_at=created_at,
))
await store.save_session_part(SessionPartRecord(
part_id=f"part-{message_id}",
message_id=message_id,
session_id=session_id,
part_type="text",
payload={"text": f"content:{message_id}"},
created_at=created_at,
))
await save("summary-old", 0, "top_level_reply")
hidden_kinds = (
"runtime_v2_user_turn",
"runtime_v2_intermediate_assistant",
"runtime_v2_company_assistant",
"runtime_v2_tool_output",
)
# More than 8 * page size: post-LIMIT filtering used to return
# an empty page even though summary-old remained reachable.
for index in range(24):
await save(f"full-only-{index:02d}", index + 1, hidden_kinds[index % len(hidden_kinds)])
await save("assistant-final", 25, "runtime_v2_assistant")
await save(
"company-final",
26,
"runtime_v2_company_assistant",
company_final_turn=True,
)
await save("canonical-result", 27, "child_result")
await save("compaction-summary", 28, "top_level_reply", summary_flag=True)
latest = await store.get_session_transcript_page(
session_id,
limit=2,
detail_level="summary",
)
self.assertEqual(latest["total_count"], 4)
self.assertTrue(latest["has_more"])
self.assertEqual(
[item["message"].message_id for item in latest["messages"]],
["company-final", "canonical-result"],
)
self.assertEqual(
[message["message_id"] for message in build_transcript_ui_messages(
latest["messages"],
channel_id=f"session:{task_id}",
task_id=task_id,
detail_level="summary",
)],
["company-final", "canonical-result"],
)
older = await store.get_session_transcript_page(
session_id,
limit=2,
before_created_at=base + timedelta(seconds=26),
before_message_id="company-final",
detail_level="summary",
)
self.assertEqual(older["total_count"], 4)
self.assertFalse(older["has_more"])
self.assertEqual(
[item["message"].message_id for item in older["messages"]],
["summary-old", "assistant-final"],
)
full = await store.get_session_transcript_page(
session_id,
limit=2,
detail_level="full",
)
self.assertEqual(full["total_count"], 28)
self.assertTrue(full["has_more"])
finally:
await store.close()
async def test_rendered_page_reads_past_empty_and_collapsed_raw_rows(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
store = OPCStore(Path(tmpdir) / "tasks.db")
await store.initialize()
try:
session_id = "rendered-pagination-session"
task_id = "rendered-pagination-task"
base = datetime(2026, 7, 13, 13, 0, 0)
await store.save_session(SessionRecord(
session_id=session_id,
project_id="test-project",
title="Rendered pagination",
created_at=base,
updated_at=base,
))
async def save(
message_id: str,
offset: int,
kind: str,
content: str | None,
) -> None:
created_at = base + timedelta(seconds=offset)
await store.save_session_message(SessionMessageRecord(
message_id=message_id,
session_id=session_id,
task_id=task_id,
role="assistant",
agent_id="agent-reviewer",
metadata={"kind": kind},
created_at=created_at,
))
if content is not None:
await store.save_session_part(SessionPartRecord(
part_id=f"part-{message_id}",
message_id=message_id,
session_id=session_id,
part_type="text",
payload={"text": content},
created_at=created_at,
))
await save("visible-old", 0, "top_level_reply", "older unique")
await save("duplicate-low", 1, "top_level_reply", "same result")
await save("duplicate-high", 2, "child_result", "same result")
await save("empty-latest", 3, "top_level_reply", None)
handler = WSHandler.__new__(WSHandler)
handler.engine = SimpleNamespace(store=store)
page, total_count, has_more = await handler._load_session_transcript_page(
SimpleNamespace(id=task_id, session_id=session_id),
limit=2,
detail_level="summary",
)
self.assertEqual(
[message["message_id"] for message in page],
["visible-old", "duplicate-high"],
)
self.assertEqual(
[message["content"] for message in page],
["older unique", "same result"],
)
self.assertGreaterEqual(total_count, 2)
self.assertFalse(has_more)
finally:
await store.close()
def test_renderer_and_store_share_company_final_visibility(self) -> None:
self.assertFalse(transcript_metadata_visible(
{"kind": "runtime_v2_company_assistant"},
detail_level="summary",
))
self.assertTrue(transcript_metadata_visible(
{"kind": "runtime_v2_company_assistant", "company_final_turn": True},
detail_level="summary",
))
self.assertTrue(transcript_metadata_visible(
{"kind": "runtime_v2_assistant"},
detail_level="summary",
))
class ChatStorePaginationTests(unittest.TestCase):
@staticmethod
def _legacy_dedupe(
store: ChatStore,
messages: list[dict[str, object]],
) -> list[dict[str, object]]:
"""Reference implementation retained only for equivalence testing."""
deduped: list[dict[str, object]] = []
for message in sorted(messages, key=store._message_timestamp):
match_index = next(
(
index
for index in range(len(deduped) - 1, -1, -1)
if store._messages_semantically_match(deduped[index], message)
),
None,
)
if match_index is None:
deduped.append(message)
else:
deduped[match_index] = store._merge_duplicate_messages(
deduped[match_index],
message,
)
return deduped
def test_indexed_dedupe_matches_legacy_semantics(self) -> None:
randomizer = random.Random(20260713)
messages: list[dict[str, object]] = []
content_variants = (
"Repeated result",
"Repeated result\n\nVerification: passed",
"Unique body ",
"**Narrative heading**: " + ("long body " * 20),
)
result_kinds = (
"child_result",
"company_role_result",
"top_level_reply",
"",
)
for index in range(600):
metadata: dict[str, object] = {}
if randomizer.random() < 0.55:
metadata["source"] = "engine"
result_kind = randomizer.choice(result_kinds)
if result_kind:
metadata["transcript_kind"] = result_kind
# Exercise identity merges which can replace the semantic bucket of
# an already-indexed row.
if messages and randomizer.random() < 0.12:
identity_source = randomizer.choice(messages)
metadata["ui_message_id"] = identity_source["message_id"]
content = randomizer.choice(content_variants)
if content == "Unique body ":
content += str(index % 31)
messages.append({
"message_id": f"random-{index:04d}",
"channel_id": f"session:{randomizer.randrange(2)}",
"sender": "user" if randomizer.random() < 0.18 else "assistant",
"sender_name": "OPC",
"content": content,
# Include zero/negative sentinel values because the historical
# matcher deliberately treats a zero timestamp as unbounded.
"created_at": float(randomizer.randrange(-6, 45)) / 3.0,
"reply_to_id": f"reply-{randomizer.randrange(4)}",
"mentions": [],
"metadata": metadata,
})
randomizer.shuffle(messages)
store = ChatStore(None) # type: ignore[arg-type]
expected = self._legacy_dedupe(store, messages)
actual = store._dedupe_messages(messages)
self.assertEqual(actual, expected)
def test_indexed_dedupe_normalizes_long_content_once_per_row(self) -> None:
class CountingChatStore(ChatStore):
normalize_calls = 0
@classmethod
def _normalize_duplicate_content(cls, content: object) -> str:
cls.normalize_calls += 1
return ChatStore._normalize_duplicate_content(content)
store = CountingChatStore(None) # type: ignore[arg-type]
long_content = "x" * 8192
message_count = 4000
messages = [
{
"message_id": f"scale-{index:05d}",
"channel_id": "session:scale",
"sender": "assistant",
"sender_name": "OPC",
"content": long_content,
"created_at": float(index),
"reply_to_id": None,
"mentions": [],
# Without an engine source these equal-content rows deliberately
# do not merge; the legacy reverse scan normalized O(n^2) pairs.
"metadata": {},
}
for index in range(message_count)
]
deduped = store._dedupe_messages(messages)
self.assertEqual(len(deduped), message_count)
self.assertEqual(CountingChatStore.normalize_calls, message_count)
def test_timed_backfill_index_does_not_scan_out_of_window_rows(self) -> None:
store = ChatStore(None) # type: ignore[arg-type]
row_count = 1000
def message(prefix: str, index: int, timestamp: float) -> dict[str, object]:
return {
"message_id": f"{prefix}-{index:04d}",
"channel_id": "session:timed-backfill-scale",
"sender": "assistant",
"sender_name": "OPC",
"content": "Ordinary engine update",
"created_at": timestamp,
"reply_to_id": "same-turn",
"mentions": [],
# Intentionally not a result surface: only the exact 2-second
# ordinary-message window may match these rows.
"metadata": {"source": "engine"},
}
existing = [
message("existing", index, 100_001.0 + index * 10.0)
for index in range(row_count)
]
incoming = [
message("incoming", index, 1.0 + index * 10.0)
for index in range(row_count)
]
# Prepared-state reference of the former reversed scan. Every incoming
# row misses and is appended, causing 1000 + ... + 1999 comparisons.
legacy_rows = list(existing)
legacy_states = [
_MessageMatchState.from_message(store, item)
for item in legacy_rows
]
legacy_matches: list[int | None] = []
legacy_checks = 0
for item in incoming:
candidate = _MessageMatchState.from_message(store, item)
match_index: int | None = None
for index in range(len(legacy_states) - 1, -1, -1):
legacy_checks += 1
if legacy_states[index].matches(
candidate,
duplicate_window=store._DUPLICATE_WINDOW_SECONDS,
):
match_index = index
break
legacy_matches.append(match_index)
if match_index is None:
legacy_rows.append(item)
legacy_states.append(candidate)
indexed_rows = list(existing)
timed_index = _MessageMatchIndex(store, indexed_rows)
indexed_matches: list[int | None] = []
indexed_checks = 0
original_matches = _MessageMatchState.matches
def counted_matches(
existing_state: _MessageMatchState,
candidate_state: _MessageMatchState,
*,
duplicate_window: float,
) -> bool:
nonlocal indexed_checks
indexed_checks += 1
return original_matches(
existing_state,
candidate_state,
duplicate_window=duplicate_window,
)
_MessageMatchState.matches = counted_matches
try:
for item in incoming:
candidate = timed_index.prepare(item)
match_index = timed_index.latest_match(
item,
prepared_state=candidate,
)
indexed_matches.append(match_index)
if match_index is None:
timed_index.append(item, prepared_state=candidate)
finally:
_MessageMatchState.matches = original_matches
self.assertEqual(indexed_matches, legacy_matches)
self.assertEqual(legacy_checks, 1_499_500)
self.assertEqual(indexed_checks, 0)
def test_timed_index_preserves_float_rounding_at_window_boundary(self) -> None:
store = ChatStore(None) # type: ignore[arg-type]
def message(message_id: str, timestamp: float) -> dict[str, object]:
return {
"message_id": message_id,
"channel_id": "session:float-window-boundary",
"sender": "assistant",
"sender_name": "OPC",
"content": "Boundary update",
"created_at": timestamp,
"reply_to_id": "same-turn",
"mentions": [],
"metadata": {"source": "engine"},
}
existing = message("existing", -1e-300)
candidate = message("candidate", 2.0)
self.assertTrue(store._messages_semantically_match(existing, candidate))
rows = [existing]
index = _MessageMatchIndex(store, rows)
self.assertEqual(index.latest_match(candidate), 0)
def test_backfill_semantic_matches_remain_one_to_one(self) -> None:
asyncio.run(self._exercise_backfill_semantic_matches_one_to_one())
async def _exercise_backfill_semantic_matches_one_to_one(self) -> None:
tmpdir = tempfile.TemporaryDirectory()
db = _SQLiteConnectionAdapter(str(Path(tmpdir.name) / "ui-state.db"))
store = ChatStore(db) # type: ignore[arg-type]
await store.initialize()
channel_id = "session:backfill-scale"
project_id = "test-project"
content = "Canonical result " + ("detail " * 1000)
try:
for index in range(200):
await store.insert_message(
channel_id,
"assistant",
"OPC",
content,
metadata={
"source": "engine",
"transcript_kind": "child_result",
},
message_id=f"existing-result-{index:03d}",
project_id=project_id,
created_at=float(index + 1),
)
backfill = [
{
"message_id": f"backfill-result-{index:03d}",
"channel_id": channel_id,
"sender": "assistant",
"sender_name": "OPC",
"content": content,
"created_at": float(index + 1000),
"metadata": {
"source": "engine",
"transcript_kind": "child_result",
},
}
for index in range(201)
]
inserted = await store.backfill_messages(
channel_id,
backfill,
project_id=project_id,
)
self.assertEqual(
[message["message_id"] for message in inserted],
["backfill-result-200"],
)
cursor = await db.execute(
"SELECT COUNT(*) FROM messages WHERE channel_id = ? AND project_id = ?",
(channel_id, project_id),
)
self.assertEqual((await cursor.fetchone())[0], 201)
finally:
await db.close()
tmpdir.cleanup()
def test_summary_cache_page_filters_before_raw_fetch_limit(self) -> None:
asyncio.run(self._exercise_summary_cache_page())
async def _exercise_summary_cache_page(self) -> None:
tmpdir = tempfile.TemporaryDirectory()
db = _SQLiteConnectionAdapter(str(Path(tmpdir.name) / "ui-state.db"))
store = ChatStore(db) # type: ignore[arg-type]
await store.initialize()
channel_id = "session:summary-cache-task"
project_id = "test-project"
async def insert(message_id: str, timestamp: float, visibility: str) -> None:
await store.insert_message(
channel_id,
"agent-reviewer",
"Reviewer",
f"content:{message_id}",
metadata={"detail_visibility": visibility},
message_id=message_id,
project_id=project_id,
created_at=timestamp,
)
try:
await insert("summary-old", 1.0, "summary")
for index in range(24):
await insert(f"full-only-{index:02d}", float(index + 2), "full")
await insert("summary-new", 26.0, "summary")
page = await store.get_channel_messages_page(
channel_id,
limit=2,
detail_level="summary",
project_id=project_id,
)
self.assertEqual(
[message["message_id"] for message in page],
["summary-old", "summary-new"],
)
self.assertEqual(
await store.get_channel_visible_message_count(
channel_id,
project_id=project_id,
detail_level="summary",
),
2,
)
self.assertEqual(
await store.get_channel_visible_message_count(
channel_id,
project_id=project_id,
detail_level="full",
),
26,
)
older = await store.get_channel_messages_page(
channel_id,
limit=2,
before_timestamp=26.0,
before_message_id="summary-new",
detail_level="summary",
project_id=project_id,
)
self.assertEqual([message["message_id"] for message in older], ["summary-old"])
finally:
await db.close()
tmpdir.cleanup()
def test_cache_page_dedupes_before_paging_and_keeps_ui_only_rows(self) -> None:
asyncio.run(self._exercise_cache_page_with_ui_only_rows())
async def _exercise_cache_page_with_ui_only_rows(self) -> None:
tmpdir = tempfile.TemporaryDirectory()
db = _SQLiteConnectionAdapter(str(Path(tmpdir.name) / "ui-state.db"))
store = ChatStore(db) # type: ignore[arg-type]
await store.initialize()
channel_id = "session:mixed-cache-task"
project_id = "test-project"
async def insert(
message_id: str,
timestamp: float,
content: str,
metadata: dict[str, object],
) -> None:
await store.insert_message(
channel_id,
"assistant",
"OPC",
content,
metadata=metadata,
message_id=message_id,
project_id=project_id,
created_at=timestamp,
)
try:
# These two UI-owned rows have no authoritative transcript row, but
# must still contribute to the page cursor, total, and has_more.
await insert(
"approval-card",
1.0,
"",
{
"checkpoint_id": "checkpoint-1",
"checkpoint_type": "tool_approval",
"checkpoint_status": "pending",
},
)
await insert(
"legacy-notice",
2.0,
"Legacy execution notice",
{"kind": "legacy_notice"},
)
# More than the old ``limit * 8`` lookahead collapses to one final
# result surface. Raw-row pagination therefore used to hide both
# older UI-only rows and incorrectly report the end of history.
for index in range(24):
await insert(
f"result-surface-{index:02d}",
float(index + 3),
"Canonical child result",
{
"source": "engine",
"transcript_kind": "child_result",
"detail_visibility": "summary",
},
)
await insert(
"latest-message",
27.0,
"Latest committed reply",
{"detail_visibility": "summary"},
)
for index in range(20):
await insert(
f"full-only-{index:02d}",
float(index + 28),
f"Runtime row {index}",
{"detail_visibility": "full"},
)
page = await store.get_channel_messages_page_info(
channel_id,
limit=2,
detail_level="summary",
project_id=project_id,
)
self.assertEqual(page["total_count"], 4)
self.assertTrue(page["has_more"])
self.assertEqual(
[message["message_id"] for message in page["messages"]],
["result-surface-00", "latest-message"],
)
result_message = page["messages"][0]
older = await store.get_channel_messages_page_info(
channel_id,
limit=2,
before_timestamp=result_message["created_at"],
before_message_id=result_message["message_id"],
detail_level="summary",
project_id=project_id,
)
self.assertFalse(older["has_more"])
self.assertEqual(older["total_count"], 4)
self.assertEqual(
[message["message_id"] for message in older["messages"]],
["approval-card", "legacy-notice"],
)
compatible = await store.get_channel_messages_page(
channel_id,
limit=2,
detail_level="summary",
project_id=project_id,
)
self.assertEqual(compatible, page["messages"])
finally:
await db.close()
tmpdir.cleanup()
if __name__ == "__main__":
unittest.main()