refactor(cleanup-03): delete ApprovalReducer and simplify RootReducer

Remove the approvalAction/NoOp wrong-scope filter and the ApprovalReducer
call site from RootReducer. approvalJustArrived is now derived from the
selected session's pendingApproval delta. Delete ApprovalReducer.kt,
ApprovalState.kt, and ApprovalReducerTest.kt; approval truth is
structurally owned by SessionSummary.
This commit is contained in:
2026-05-25 16:18:12 +04:00
parent 1c67993f5d
commit 7d4468f292
5 changed files with 58 additions and 168 deletions
@@ -1,58 +0,0 @@
package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ApprovalDecision
import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.ApprovalState
import com.correx.apps.tui.state.InputMode
import com.correx.core.events.types.ApprovalRequestId
object ApprovalReducer {
fun reduce(
approval: ApprovalState,
inputMode: InputMode,
action: Action,
): Pair<ApprovalState, List<Effect>> = when (action) {
is Action.ApproveActive -> {
val active = approval.active
if (active != null) {
ApprovalState(active = null) to listOf(
Effect.SendWs(
ClientMessage.ApprovalResponse(
requestId = ApprovalRequestId(active.requestId),
decision = ApprovalDecision.APPROVE,
steeringNote = null,
),
),
)
} else {
approval to emptyList()
}
}
is Action.RejectActive -> {
val active = approval.active
if (active != null) {
ApprovalState(active = null) to listOf(
Effect.SendWs(
ClientMessage.ApprovalResponse(
requestId = ApprovalRequestId(active.requestId),
decision = ApprovalDecision.REJECT,
steeringNote = null,
),
),
)
} else {
approval to emptyList()
}
}
is Action.SubmitInput -> if (inputMode == InputMode.STEER) {
ApprovalState(active = null) to emptyList()
} else {
approval to emptyList()
}
else -> approval to emptyList()
}
}
@@ -2,7 +2,6 @@ package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ServerMessage
import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.ApprovalState
import com.correx.apps.tui.state.TuiState
import org.slf4j.LoggerFactory
@@ -46,33 +45,22 @@ object RootReducer {
val quitEffects: List<Effect> = if (action is Action.Quit) listOf(Effect.Quit) else emptyList()
val prevInputMode = state.inputMode
val prevInputBuffer = state.inputBuffer
val approvalAction = if (
action is Action.ServerEventReceived &&
action.message is ServerMessage.SessionSnapshot &&
action.message.sessionId.value != state.sessions.selectedId
) Action.NoOp else action
log.debug("input state before reducers={}", state.input)
val (afterInput, ie) = InputReducer.reduce(state, action)
log.debug("sessions state before reducers={}", state.sessions)
val (sessions, se) = SessionsReducer.reduce(afterInput.sessions, prevInputMode, prevInputBuffer, action, clock)
val selectedId = afterInput.sessions.selectedId
val bridgedApproval = ApprovalState(
active = afterInput.sessions.sessions.find { it.id == selectedId }?.pendingApproval,
)
val (approval, ae) = ApprovalReducer.reduce(bridgedApproval, prevInputMode, approvalAction)
val approvalJustArrived = approval.active != null && bridgedApproval.active == null
val sessionsWithApproval = sessions.copy(
sessions = sessions.sessions.map { s ->
if (s.id == selectedId) s.copy(pendingApproval = approval.active) else s
},
)
log.debug("connection state before reducers={}", state.connection)
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
log.debug("provider state before reducers={}", state.provider)
val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
val selectedPending = sessions.sessions.firstOrNull { it.id == sessions.selectedId }?.pendingApproval
val prevSelectedPending = afterInput.sessions.sessions.firstOrNull { it.id == afterInput.sessions.selectedId }?.pendingApproval
val approvalJustArrived = selectedPending != null && prevSelectedPending == null
val withSubReducers = afterInput.copy(
sessions = sessionsWithApproval,
sessions = sessions,
connection = connection,
provider = provider,
approvalOverlayVisible = if (approvalJustArrived) true else afterInput.approvalOverlayVisible,
@@ -90,6 +78,6 @@ object RootReducer {
else -> withSubReducers
}
return final to (quitEffects + ie + se + ae + ce + pe)
return final to (quitEffects + ie + se + ce + pe)
}
}
@@ -1,5 +0,0 @@
package com.correx.apps.tui.state
data class ApprovalState(
val active: ApprovalInfo? = null,
)
@@ -1,86 +0,0 @@
package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ApprovalDecision
import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.ApprovalInfo
import com.correx.apps.tui.state.ApprovalState
import com.correx.apps.tui.state.InputMode
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class ApprovalReducerTest {
private val activeInfo = ApprovalInfo(
requestId = "req-1",
sessionId = "sess-1",
tier = "HIGH",
riskSummary = "HIGH",
toolName = "bash",
preview = null,
)
private fun reduce(
approval: ApprovalState = ApprovalState(),
inputMode: InputMode = InputMode.ROUTER,
action: Action,
) = ApprovalReducer.reduce(approval, inputMode, action)
@Test
fun `ApproveActive emits APPROVE response and clears active`() {
val (state, effects) = reduce(
approval = ApprovalState(active = activeInfo),
action = Action.ApproveActive,
)
assertNull(state.active)
assertEquals(1, effects.size)
val effect = effects[0] as Effect.SendWs
val msg = effect.message as ClientMessage.ApprovalResponse
assertEquals(ApprovalDecision.APPROVE, msg.decision)
assertEquals("req-1", msg.requestId.value)
assertNull(msg.steeringNote)
}
@Test
fun `ApproveActive with no active approval is no-op`() {
val (state, effects) = reduce(action = Action.ApproveActive)
assertNull(state.active)
assertTrue(effects.isEmpty())
}
@Test
fun `RejectActive emits REJECT response and clears active`() {
val (state, effects) = reduce(
approval = ApprovalState(active = activeInfo),
action = Action.RejectActive,
)
assertNull(state.active)
assertEquals(1, effects.size)
val effect = effects[0] as Effect.SendWs
val msg = effect.message as ClientMessage.ApprovalResponse
assertEquals(ApprovalDecision.REJECT, msg.decision)
}
@Test
fun `SubmitInput in SteeringNote mode clears active approval`() {
val (state, effects) = reduce(
approval = ApprovalState(active = activeInfo),
inputMode = InputMode.STEER,
action = Action.SubmitInput,
)
assertNull(state.active)
assertTrue(effects.isEmpty())
}
@Test
fun `SubmitInput in non-SteeringNote mode is no-op`() {
val (state, _) = reduce(
approval = ApprovalState(active = activeInfo),
inputMode = InputMode.NAVIGATE,
action = Action.SubmitInput,
)
assertEquals(activeInfo, state.active)
}
}
@@ -1,10 +1,17 @@
package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.server.protocol.RiskSummaryDto
import com.correx.apps.server.protocol.ServerMessage
import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.ConnectionState
import com.correx.apps.tui.state.InputMode
import com.correx.apps.tui.state.SessionSummary
import com.correx.apps.tui.state.SessionsState
import com.correx.apps.tui.state.TuiState
import com.correx.core.approvals.Tier
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.SessionId
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@@ -138,4 +145,48 @@ class RootReducerTest {
val (s2, _) = RootReducer.reduce(s1, Action.Backspace, fixedClock)
assertEquals("ab", s2.inputBuffer)
}
private fun approvalRequiredMsg(sessionId: String, requestId: String = "r1") =
ServerMessage.ApprovalRequired(
sessionId = SessionId(sessionId),
requestId = ApprovalRequestId(requestId),
tier = Tier.T2,
riskSummary = RiskSummaryDto(level = "HIGH", factors = emptyList(), recommendedAction = "review"),
toolName = "bash",
preview = null,
sequence = 1L,
sessionSequence = 1L,
)
private fun session(id: String) = SessionSummary(id = id, status = "ACTIVE", workflowId = "wf", lastEventAt = 0L)
@Test
fun `ApprovalRequired on selected session auto-opens approvalOverlayVisible`() {
val initial = TuiState(
snapshotPhase = false,
sessions = SessionsState(sessions = listOf(session("s1")), selectedId = "s1"),
)
val (state, _) = RootReducer.reduce(
initial, Action.ServerEventReceived(approvalRequiredMsg("s1")), fixedClock,
)
assertEquals(true, state.approvalOverlayVisible)
assertEquals("r1", state.sessions.sessions[0].pendingApproval?.requestId)
}
@Test
fun `ApprovalRequired on non-selected session does not open approvalOverlayVisible`() {
val initial = TuiState(
snapshotPhase = false,
sessions = SessionsState(
sessions = listOf(session("s1"), session("s2")),
selectedId = "s1",
),
)
val (state, _) = RootReducer.reduce(
initial, Action.ServerEventReceived(approvalRequiredMsg("s2")), fixedClock,
)
assertEquals(false, state.approvalOverlayVisible)
assertEquals(null, state.sessions.sessions.find { it.id == "s1" }?.pendingApproval)
assertEquals("r1", state.sessions.sessions.find { it.id == "s2" }?.pendingApproval?.requestId)
}
}