From d701e3cbf30ca5e2d2678547137313cee3cb535e Mon Sep 17 00:00:00 2001 From: kami Date: Tue, 26 May 2026 13:39:11 +0400 Subject: [PATCH] fix(tui): use takeLast(3) for events, add bridge/TUI tests for snapshot recentEvents - Change EventHistoryStrip take(3) to takeLast(3) so it shows the 3 most recent events instead of the 3 oldest (chronological order is oldest-first) - Add bridge test confirming replaySnapshot populates recentEvents from the event store (with 3 different event types mapped correctly) - Add TUI SessionsReducer test confirming processSessionSnapshotMessage maps EventEntryDto entries from the snapshot into TuiEventEntry on the summary --- .../server/bridge/SessionEventBridgeTest.kt | 35 +++++++++++++++++++ .../apps/tui/components/EventHistoryStrip.kt | 2 +- .../apps/tui/reducer/SessionsReducerTest.kt | 26 ++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/apps/server/src/test/kotlin/com/correx/apps/server/bridge/SessionEventBridgeTest.kt b/apps/server/src/test/kotlin/com/correx/apps/server/bridge/SessionEventBridgeTest.kt index 0b82568b..e9f059f4 100644 --- a/apps/server/src/test/kotlin/com/correx/apps/server/bridge/SessionEventBridgeTest.kt +++ b/apps/server/src/test/kotlin/com/correx/apps/server/bridge/SessionEventBridgeTest.kt @@ -1,5 +1,6 @@ package com.correx.apps.server.bridge +import com.correx.apps.server.protocol.EventEntryDto import com.correx.apps.server.protocol.ServerMessage import com.correx.apps.server.registry.WorkflowRegistry import com.correx.core.approvals.ApprovalProjector @@ -10,7 +11,9 @@ import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.EventPayload import com.correx.core.events.events.NewEvent import com.correx.core.events.events.OrchestrationResumedEvent +import com.correx.core.events.events.StageCompletedEvent import com.correx.core.events.events.StoredEvent +import com.correx.core.events.events.TransitionExecutedEvent import com.correx.core.events.events.WorkflowCompletedEvent import com.correx.core.events.events.WorkflowStartedEvent import com.correx.core.events.orchestration.OrchestrationState @@ -20,6 +23,7 @@ import com.correx.core.events.types.ArtifactId import com.correx.core.events.types.EventId import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId +import com.correx.core.events.types.TransitionId import com.correx.core.kernel.orchestration.OrchestrationRepository import com.correx.core.sessions.projections.replay.DefaultEventReplayer import com.correx.core.tools.contract.Tool @@ -139,6 +143,37 @@ class SessionEventBridgeTest { assertEquals(ServerMessage.SnapshotComplete, sent[1]) } + @Test + fun `replaySnapshot populates recentEvents from event store`() = runTest { + val events = listOf( + storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L), + storedEvent(TransitionExecutedEvent(sessionId, stageId, StageId("stage-2"), TransitionId("t1")), seq = 2L), + storedEvent(StageCompletedEvent(sessionId, stageId, TransitionId("t2")), seq = 3L), + storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 4L), + ) + val store = fakeEventStore(allEventsList = events) + val orchRepo = activeOrchestrationRepository(status = OrchestrationStatus.COMPLETED) + val sent = mutableListOf() + val bridge = SessionEventBridge( + store, + noopArtifactStore, + orchRepo, + approvalRepository, + noopWorkflowRegistry, + noopToolRegistry, + ) { sent.add(it) } + + bridge.replaySnapshot() + + assertEquals(2, sent.size) + val snapshot = sent[0] as ServerMessage.SessionSnapshot + // WorkflowStartedEvent is filtered out by eventToEntry (maps to null) + assertEquals(3, snapshot.recentEvents.size) + assertEquals(EventEntryDto(timestamp.toEpochMilliseconds(), "StageStarted", "stage-2"), snapshot.recentEvents[0]) + assertEquals(EventEntryDto(timestamp.toEpochMilliseconds(), "StageCompleted", stageId.value), snapshot.recentEvents[1]) + assertEquals(EventEntryDto(timestamp.toEpochMilliseconds(), "SessionCompleted", ""), snapshot.recentEvents[2]) + } + @Test fun `replaySnapshot skips unmapped events`() = runTest { val events = listOf( diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/EventHistoryStrip.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/EventHistoryStrip.kt index d2c86091..f6ad7ed4 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/EventHistoryStrip.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/EventHistoryStrip.kt @@ -36,7 +36,7 @@ fun eventHistoryStripWidget(state: TuiState): Paragraph { // Recent events (plain text, compact format) — capped to fit allocated height. // Line 0 = stage info, lines 1-3 = events (max 3). - val events = selectedSession.recentEvents.take(3) + val events = selectedSession.recentEvents.takeLast(3) if (events.isEmpty()) { add(Line.from(Span.styled("no events", dimStyle))) } else { diff --git a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/SessionsReducerTest.kt b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/SessionsReducerTest.kt index 07320bdb..15a6322d 100644 --- a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/SessionsReducerTest.kt +++ b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/SessionsReducerTest.kt @@ -3,6 +3,7 @@ package com.correx.apps.tui.reducer import com.correx.apps.server.protocol.ApprovalDecision import com.correx.apps.server.protocol.ApprovalDto import com.correx.apps.server.protocol.ClientMessage +import com.correx.apps.server.protocol.EventEntryDto import com.correx.apps.server.protocol.PauseReason import com.correx.apps.server.protocol.RiskSummaryDto import com.correx.apps.server.protocol.ServerMessage @@ -363,4 +364,29 @@ class SessionsReducerTest { val (state, _) = reduce(action = Action.ServerEventReceived(msg)) assertEquals(null, state.sessions[0].pendingApproval) } + + @Test + fun `SessionSnapshot recentEvents are mapped to TuiEventEntry on summary`() { + val msg = ServerMessage.SessionSnapshot( + sessionId = SessionId("s1"), + workflowId = "wf-1", + state = SessionStateDto("COMPLETED", null, null), + pendingApprovals = emptyList(), + recentEvents = listOf( + EventEntryDto(timestamp = 1000L, type = "StageStarted", detail = "stage-1"), + EventEntryDto(timestamp = 2000L, type = "InferenceCompleted", detail = "stage-1"), + EventEntryDto(timestamp = 3000L, type = "SessionCompleted", detail = ""), + ), + lastSequence = 1L, + lastSessionSequence = 1L, + ) + val (state, _) = reduce(action = Action.ServerEventReceived(msg)) + val events = state.sessions[0].recentEvents + assertEquals(3, events.size) + assertEquals("StageStarted", events[0].type) + assertEquals("stage-1", events[0].detail) + assertEquals("InferenceCompleted", events[1].type) + assertEquals("SessionCompleted", events[2].type) + assertEquals("", events[2].detail) + } }