feat(tui-03): wire SnapshotPhaseReducer into RootReducer with replay and cursor tracking
SnapshotComplete now flips snapshotPhase, replays buffered events through SnapshotPhaseReducer for dedup/cursor updates, then through sub-reducers.
This commit is contained in:
@@ -12,6 +12,34 @@ object RootReducer {
|
||||
state: TuiState,
|
||||
action: Action,
|
||||
clock: () -> Long = System::currentTimeMillis,
|
||||
): Pair<TuiState, List<Effect>> {
|
||||
val phase = SnapshotPhaseReducer.process(state, action)
|
||||
var current = phase.state
|
||||
val allEffects = mutableListOf<Effect>()
|
||||
val isReplay = action is Action.ServerEventReceived &&
|
||||
action.message is ServerMessage.SnapshotComplete
|
||||
for (a in phase.actionsToDispatch) {
|
||||
if (isReplay) {
|
||||
val inner = SnapshotPhaseReducer.process(current, a)
|
||||
current = inner.state
|
||||
for (ia in inner.actionsToDispatch) {
|
||||
val (next, effects) = reduceThroughSubReducers(current, ia, clock)
|
||||
current = next
|
||||
allEffects += effects
|
||||
}
|
||||
} else {
|
||||
val (next, effects) = reduceThroughSubReducers(current, a, clock)
|
||||
current = next
|
||||
allEffects += effects
|
||||
}
|
||||
}
|
||||
return current to allEffects
|
||||
}
|
||||
|
||||
private fun reduceThroughSubReducers(
|
||||
state: TuiState,
|
||||
action: Action,
|
||||
clock: () -> Long,
|
||||
): Pair<TuiState, List<Effect>> {
|
||||
log.debug("action={}", action::class.simpleName)
|
||||
val quitEffects: List<Effect> = if (action is Action.Quit) listOf(Effect.Quit) else emptyList()
|
||||
|
||||
@@ -47,7 +47,7 @@ class RootReducerTest {
|
||||
|
||||
@Test
|
||||
fun `NavigateUp with sessions adjusts selected id`() {
|
||||
val state0 = TuiState()
|
||||
val state0 = TuiState(snapshotPhase = false)
|
||||
val startMsg = com.correx.apps.server.protocol.ServerMessage.SessionStarted(
|
||||
sessionId = com.correx.core.events.types.SessionId("s1"),
|
||||
workflowId = "wf",
|
||||
@@ -71,6 +71,36 @@ class RootReducerTest {
|
||||
assertEquals("s2", state3.sessions.selectedId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SnapshotComplete flips snapshotPhase and replays buffered events in order`() {
|
||||
val startMsg1 = com.correx.apps.server.protocol.ServerMessage.SessionStarted(
|
||||
sessionId = com.correx.core.events.types.SessionId("s1"),
|
||||
workflowId = "wf",
|
||||
sequence = 1L,
|
||||
sessionSequence = 1L,
|
||||
)
|
||||
val startMsg2 = com.correx.apps.server.protocol.ServerMessage.SessionStarted(
|
||||
sessionId = com.correx.core.events.types.SessionId("s2"),
|
||||
workflowId = "wf",
|
||||
sequence = 2L,
|
||||
sessionSequence = 1L,
|
||||
)
|
||||
val initial = TuiState(
|
||||
snapshotPhase = true,
|
||||
pendingEvents = listOf(startMsg1, startMsg2),
|
||||
)
|
||||
val (result, _) = RootReducer.reduce(
|
||||
initial,
|
||||
Action.ServerEventReceived(com.correx.apps.server.protocol.ServerMessage.SnapshotComplete),
|
||||
fixedClock,
|
||||
)
|
||||
assertEquals(false, result.snapshotPhase)
|
||||
assertTrue(result.pendingEvents.isEmpty())
|
||||
assertEquals(2, result.sessions.sessions.size)
|
||||
assertEquals(1L, result.cursors["s1"])
|
||||
assertEquals(1L, result.cursors["s2"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AppendChar followed by Backspace returns to original buffer`() {
|
||||
val initial = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "ab")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.correx.apps.tui.reducer
|
||||
|
||||
import com.correx.apps.server.protocol.SessionStateDto
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
import com.correx.apps.tui.input.Action
|
||||
import com.correx.apps.tui.state.TuiState
|
||||
@@ -31,4 +32,32 @@ class SnapshotPhaseReducerTest {
|
||||
assertTrue(allDispatched.isEmpty())
|
||||
assertTrue(state.sessions.sessions.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `during snapshotPhase SessionSnapshot installs cursor and is forwarded to sub-reducers`() {
|
||||
val state = TuiState(snapshotPhase = true)
|
||||
val snapshot = ServerMessage.SessionSnapshot(
|
||||
sessionId = SessionId("s1"),
|
||||
workflowId = "wf",
|
||||
status = "ACTIVE",
|
||||
currentStageId = null,
|
||||
pauseReason = null,
|
||||
pendingApproval = false,
|
||||
approvalRequestId = null,
|
||||
approvalTier = null,
|
||||
approvalToolName = null,
|
||||
approvalPreview = null,
|
||||
state = SessionStateDto(status = "ACTIVE", currentStageId = null, pauseReason = null),
|
||||
pendingApprovals = emptyList(),
|
||||
lastSequence = 1L,
|
||||
lastSessionSequence = 42L,
|
||||
)
|
||||
val action = Action.ServerEventReceived(snapshot)
|
||||
|
||||
val result = SnapshotPhaseReducer.process(state, action)
|
||||
|
||||
assertEquals(42L, result.state.cursors["s1"])
|
||||
assertEquals(listOf(action), result.actionsToDispatch)
|
||||
assertTrue(result.state.pendingEvents.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user