Delayed chat archive finalization after stream stops.
CI / Formatting (push) Successful in 4s
CI / Linting (push) Successful in 4s
CI / Tests (Python 3.12) (push) Successful in 7s
CI / Tests (Python 3.13) (push) Successful in 7s
CI / Tests (Python 3.14) (push) Successful in 6s
CI / Type Checking (push) Successful in 7s
CI / Spelling (push) Successful in 4s

This commit is contained in:
2026-05-27 11:19:12 -04:00
parent 5d113eecaa
commit c93ffc551d
3 changed files with 198 additions and 19 deletions
+129 -5
View File
@@ -287,12 +287,12 @@ class TestChatArchive:
"chat-archive-20260526T120000Z.json"
]
async def test_stream_stopped_finalizes_archive_and_ignores_later_events(
async def test_stream_stopped_keeps_archive_active_during_grace(
self,
archive_dir: Path,
event_dispatcher: EventDispatcher,
) -> None:
"""STREAM_STOPPED finalizes the active archive and closes capture."""
"""STREAM_STOPPED records stop metadata but keeps capturing grace chat."""
await event_dispatcher.dispatch(
EventType.STREAM_STARTED,
make_stream_started_event(
@@ -312,17 +312,59 @@ class TestChatArchive:
path = _archive_files(archive_dir)[0]
data = _read_archive(path)
assert data["archive_stop_reason"] == "stream_stopped_event"
assert data["archive_stop_reason"] is None
assert data["archive_stopped_at"] is None
assert data["stream_stopped_at"] == "2026-05-26T13:30:00+00:00"
assert data["event_count"] == 1
await event_dispatcher.dispatch(
EventType.CHAT,
make_chat_event(raw_body="grace chat", message_id="msg-grace"),
)
data = _read_archive(path)
assert data["archive_stop_reason"] is None
assert data["event_count"] == 2
assert _events(data)[1]["event_data"]["message_id"] == "msg-grace"
async def test_stream_stopped_finalizes_after_grace_and_ignores_later_events(
self,
archive_dir: Path,
event_dispatcher: EventDispatcher,
module_context: ModuleContext,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The delayed stop task closes capture after the grace period."""
monkeypatch.setattr(chat_archive, "_STOP_GRACE_SECONDS", 0.0)
await event_dispatcher.dispatch(
EventType.STREAM_STARTED,
make_stream_started_event(
timestamp=datetime(2026, 5, 26, 12, tzinfo=UTC),
),
)
stopped_at = datetime(2026, 5, 26, 13, 30, tzinfo=UTC)
await event_dispatcher.dispatch(
EventType.STREAM_STOPPED,
make_stream_stopped_event(timestamp=stopped_at),
)
state = chat_archive._get_state(module_context)
assert state.stop_task is not None
await asyncio.wait_for(state.stop_task, timeout=1)
path = _archive_files(archive_dir)[0]
data = _read_archive(path)
assert data["archive_stop_reason"] == "stream_stopped_event"
assert data["stream_stopped_at"] == "2026-05-26T13:30:00+00:00"
_assert_iso_datetime(data["archive_stopped_at"])
await event_dispatcher.dispatch(
EventType.CHAT,
make_chat_event(raw_body="after stop", message_id="msg-after"),
make_chat_event(raw_body="after grace", message_id="msg-after-grace"),
)
assert _read_archive(path)["event_count"] == 1
assert _read_archive(path)["event_count"] == 0
async def test_new_stream_replaces_active_archive_and_uses_filename_suffix(
self,
@@ -364,6 +406,54 @@ class TestChatArchive:
assert new_archive["event_count"] == 1
assert _events(new_archive)[0]["event_data"]["message_id"] == "msg-new"
async def test_new_stream_during_grace_replaces_stopped_archive(
self,
archive_dir: Path,
event_dispatcher: EventDispatcher,
) -> None:
"""A new stream during the grace period cancels the delayed stop."""
first_started_at = datetime(2026, 5, 26, 12, tzinfo=UTC)
second_started_at = datetime(2026, 5, 26, 14, tzinfo=UTC)
await event_dispatcher.dispatch(
EventType.STREAM_STARTED,
make_stream_started_event(stream_title="First", timestamp=first_started_at),
)
await event_dispatcher.dispatch(
EventType.STREAM_STOPPED,
make_stream_stopped_event(
stream_title="First",
timestamp=datetime(2026, 5, 26, 13, 30, tzinfo=UTC),
),
)
await event_dispatcher.dispatch(
EventType.CHAT,
make_chat_event(raw_body="grace chat", message_id="msg-grace"),
)
await event_dispatcher.dispatch(
EventType.STREAM_STARTED,
make_stream_started_event(
stream_title="Second",
timestamp=second_started_at,
),
)
await event_dispatcher.dispatch(
EventType.CHAT,
make_chat_event(raw_body="new stream message", message_id="msg-new"),
)
old_archive = _read_archive(archive_dir / "chat-archive-20260526T120000Z.json")
assert old_archive["archive_stop_reason"] == "replaced_by_stream_started_event"
assert old_archive["stream_stopped_at"] == "2026-05-26T13:30:00+00:00"
assert old_archive["event_count"] == 1
assert _events(old_archive)[0]["event_data"]["message_id"] == "msg-grace"
new_archive = _read_archive(archive_dir / "chat-archive-20260526T140000Z.json")
assert new_archive["archive_start_reason"] == "stream_started_event"
assert new_archive["archive_stop_reason"] is None
assert new_archive["stream_title"] == "Second"
assert new_archive["event_count"] == 1
assert _events(new_archive)[0]["event_data"]["message_id"] == "msg-new"
class TestSetupAndTeardown:
"""Cases that need direct control around setup and teardown timing."""
@@ -487,6 +577,40 @@ class TestSetupAndTeardown:
assert data["stream_stopped_at"] is None
_assert_iso_datetime(data["archive_stopped_at"])
@pytest.mark.usefixtures("registered_handlers")
async def test_teardown_cancels_pending_stream_stop_grace(
self,
archive_dir: Path,
module_context: ModuleContext,
event_dispatcher: EventDispatcher,
) -> None:
"""Module teardown cancels delayed stop and finalizes immediately."""
await chat_archive.setup(module_context)
await event_dispatcher.dispatch(
EventType.STREAM_STARTED,
make_stream_started_event(
timestamp=datetime(2026, 5, 26, 12, tzinfo=UTC),
),
)
await event_dispatcher.dispatch(
EventType.STREAM_STOPPED,
make_stream_stopped_event(
timestamp=datetime(2026, 5, 26, 13, 30, tzinfo=UTC),
),
)
state = chat_archive._get_state(module_context)
assert state.stop_task is not None
path = _archive_files(archive_dir)[0]
await chat_archive.teardown(module_context)
assert chat_archive._get_state(module_context).stop_task is None
data = _read_archive(path)
assert data["archive_stop_reason"] == "module_teardown"
assert data["stream_stopped_at"] == "2026-05-26T13:30:00+00:00"
_assert_iso_datetime(data["archive_stopped_at"])
class TestConfiguredOutput:
"""Configuration-dependent archive output behavior."""