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,
|
state: TuiState,
|
||||||
action: Action,
|
action: Action,
|
||||||
clock: () -> Long = System::currentTimeMillis,
|
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>> {
|
): Pair<TuiState, List<Effect>> {
|
||||||
log.debug("action={}", action::class.simpleName)
|
log.debug("action={}", action::class.simpleName)
|
||||||
val quitEffects: List<Effect> = if (action is Action.Quit) listOf(Effect.Quit) else emptyList()
|
val quitEffects: List<Effect> = if (action is Action.Quit) listOf(Effect.Quit) else emptyList()
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class RootReducerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateUp with sessions adjusts selected id`() {
|
fun `NavigateUp with sessions adjusts selected id`() {
|
||||||
val state0 = TuiState()
|
val state0 = TuiState(snapshotPhase = false)
|
||||||
val startMsg = com.correx.apps.server.protocol.ServerMessage.SessionStarted(
|
val startMsg = com.correx.apps.server.protocol.ServerMessage.SessionStarted(
|
||||||
sessionId = com.correx.core.events.types.SessionId("s1"),
|
sessionId = com.correx.core.events.types.SessionId("s1"),
|
||||||
workflowId = "wf",
|
workflowId = "wf",
|
||||||
@@ -71,6 +71,36 @@ class RootReducerTest {
|
|||||||
assertEquals("s2", state3.sessions.selectedId)
|
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
|
@Test
|
||||||
fun `AppendChar followed by Backspace returns to original buffer`() {
|
fun `AppendChar followed by Backspace returns to original buffer`() {
|
||||||
val initial = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "ab")
|
val initial = TuiState(inputMode = InputMode.ROUTER, inputBuffer = "ab")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.correx.apps.tui.reducer
|
package com.correx.apps.tui.reducer
|
||||||
|
|
||||||
|
import com.correx.apps.server.protocol.SessionStateDto
|
||||||
import com.correx.apps.server.protocol.ServerMessage
|
import com.correx.apps.server.protocol.ServerMessage
|
||||||
import com.correx.apps.tui.input.Action
|
import com.correx.apps.tui.input.Action
|
||||||
import com.correx.apps.tui.state.TuiState
|
import com.correx.apps.tui.state.TuiState
|
||||||
@@ -31,4 +32,32 @@ class SnapshotPhaseReducerTest {
|
|||||||
assertTrue(allDispatched.isEmpty())
|
assertTrue(allDispatched.isEmpty())
|
||||||
assertTrue(state.sessions.sessions.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