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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user