refactor(cleanup-02): move ApprovalRequired write-side into SessionsReducer
SessionsReducer now handles ServerMessage.ApprovalRequired and populates SessionSummary.pendingApproval directly on the matched session. The SessionSnapshot handler also propagates approval fields to pendingApproval. ApprovalReducer's ServerEventReceived branch is removed; approval truth is now strictly scoped to a single SessionSummary.
This commit is contained in:
@@ -2,15 +2,10 @@ package com.correx.apps.tui.reducer
|
||||
|
||||
import com.correx.apps.server.protocol.ApprovalDecision
|
||||
import com.correx.apps.server.protocol.ClientMessage
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
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 com.correx.core.events.types.ApprovalRequestId
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
private val log = LoggerFactory.getLogger(ApprovalReducer::class.simpleName)
|
||||
|
||||
object ApprovalReducer {
|
||||
fun reduce(
|
||||
@@ -58,43 +53,6 @@ object ApprovalReducer {
|
||||
approval to emptyList()
|
||||
}
|
||||
|
||||
is Action.ServerEventReceived -> when (val msg = action.message) {
|
||||
is ServerMessage.ApprovalRequired -> {
|
||||
log.debug(
|
||||
"ApprovalRequired received requestId={} sessionId={} toolName={}",
|
||||
msg.requestId.value, msg.sessionId.value, msg.toolName,
|
||||
)
|
||||
val info = ApprovalInfo(
|
||||
requestId = msg.requestId.value,
|
||||
sessionId = msg.sessionId.value,
|
||||
tier = msg.tier.name,
|
||||
riskSummary = msg.riskSummary.level,
|
||||
toolName = msg.toolName,
|
||||
preview = msg.preview,
|
||||
)
|
||||
ApprovalState(active = info) to emptyList()
|
||||
}
|
||||
|
||||
is ServerMessage.SessionSnapshot -> {
|
||||
val requestId = msg.approvalRequestId
|
||||
if (msg.pendingApproval && requestId != null) {
|
||||
val info = ApprovalInfo(
|
||||
requestId = requestId,
|
||||
sessionId = msg.sessionId.value,
|
||||
tier = msg.approvalTier ?: "T2",
|
||||
riskSummary = "unknown",
|
||||
toolName = msg.approvalToolName,
|
||||
preview = msg.approvalPreview,
|
||||
)
|
||||
ApprovalState(active = info) to emptyList()
|
||||
} else {
|
||||
approval to emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
else -> approval to emptyList()
|
||||
}
|
||||
|
||||
else -> approval to emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.correx.apps.server.protocol.ClientMessage
|
||||
import com.correx.apps.server.protocol.PauseReason
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
import com.correx.apps.tui.input.Action
|
||||
import com.correx.apps.tui.state.ApprovalInfo
|
||||
import com.correx.apps.tui.state.InputMode
|
||||
import com.correx.apps.tui.state.SessionSummary
|
||||
import com.correx.apps.tui.state.SessionsState
|
||||
@@ -107,6 +108,7 @@ object SessionsReducer {
|
||||
is ServerMessage.ToolFailed -> processToolFailedMessage(msg, sessions)
|
||||
is ServerMessage.ToolRejected -> processToolRejectedMessage(clock, sessions, msg)
|
||||
is ServerMessage.SessionSnapshot -> processSessionSnapshotMessage(clock, sessions, msg)
|
||||
is ServerMessage.ApprovalRequired -> processApprovalRequiredMessage(sessions, msg)
|
||||
else -> sessions to emptyList()
|
||||
}.also { log.debug("processed server message: {}", msg) }
|
||||
|
||||
@@ -115,6 +117,17 @@ object SessionsReducer {
|
||||
sessionState: SessionsState,
|
||||
msg: ServerMessage.SessionSnapshot,
|
||||
): Pair<SessionsState, List<Effect>> {
|
||||
val approvalRequestId = msg.approvalRequestId
|
||||
val approvalInfo = if (msg.pendingApproval && approvalRequestId != null) {
|
||||
ApprovalInfo(
|
||||
requestId = approvalRequestId,
|
||||
sessionId = msg.sessionId.value,
|
||||
tier = msg.approvalTier ?: "T2",
|
||||
riskSummary = "unknown",
|
||||
toolName = msg.approvalToolName,
|
||||
preview = msg.approvalPreview,
|
||||
)
|
||||
} else null
|
||||
val summary = SessionSummary(
|
||||
id = msg.sessionId.value,
|
||||
status = when {
|
||||
@@ -126,6 +139,7 @@ object SessionsReducer {
|
||||
lastEventAt = clock(),
|
||||
currentStage = msg.currentStageId,
|
||||
currentStageId = msg.currentStageId,
|
||||
pendingApproval = approvalInfo,
|
||||
)
|
||||
val selected = sessionState.selectedId ?: msg.sessionId.value
|
||||
return sessionState.copy(
|
||||
@@ -134,6 +148,25 @@ object SessionsReducer {
|
||||
) to emptyList()
|
||||
}
|
||||
|
||||
private fun processApprovalRequiredMessage(
|
||||
sessions: SessionsState,
|
||||
msg: ServerMessage.ApprovalRequired,
|
||||
): Pair<SessionsState, List<Effect>> {
|
||||
val info = ApprovalInfo(
|
||||
requestId = msg.requestId.value,
|
||||
sessionId = msg.sessionId.value,
|
||||
tier = msg.tier.name,
|
||||
riskSummary = msg.riskSummary.level,
|
||||
toolName = msg.toolName,
|
||||
preview = msg.preview,
|
||||
)
|
||||
return sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) s.copy(pendingApproval = info) else s
|
||||
},
|
||||
) to emptyList()
|
||||
}
|
||||
|
||||
private fun processToolRejectedMessage(
|
||||
clock: () -> Long,
|
||||
sessions: SessionsState,
|
||||
|
||||
@@ -2,15 +2,10 @@ package com.correx.apps.tui.reducer
|
||||
|
||||
import com.correx.apps.server.protocol.ApprovalDecision
|
||||
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.ApprovalInfo
|
||||
import com.correx.apps.tui.state.ApprovalState
|
||||
import com.correx.apps.tui.state.InputMode
|
||||
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.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
@@ -88,23 +83,4 @@ class ApprovalReducerTest {
|
||||
)
|
||||
assertEquals(activeInfo, state.active)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ServerEventReceived ApprovalRequired sets active approval`() {
|
||||
val msg = ServerMessage.ApprovalRequired(
|
||||
sessionId = SessionId("sess-1"),
|
||||
requestId = ApprovalRequestId("req-2"),
|
||||
tier = Tier.T2,
|
||||
riskSummary = RiskSummaryDto(level = "HIGH", factors = emptyList(), recommendedAction = "review"),
|
||||
toolName = "bash",
|
||||
preview = "rm -rf /",
|
||||
sequence = 1L,
|
||||
sessionSequence = 1L,
|
||||
)
|
||||
val (state, effects) = reduce(action = Action.ServerEventReceived(msg))
|
||||
assertEquals("req-2", state.active?.requestId)
|
||||
assertEquals("sess-1", state.active?.sessionId)
|
||||
assertEquals("bash", state.active?.toolName)
|
||||
assertTrue(effects.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.correx.apps.tui.reducer
|
||||
|
||||
import com.correx.apps.server.protocol.PauseReason
|
||||
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.InputMode
|
||||
import com.correx.apps.tui.state.SessionSummary
|
||||
import com.correx.apps.tui.state.SessionsState
|
||||
import com.correx.core.approvals.Tier
|
||||
import com.correx.core.events.types.ApprovalRequestId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -247,4 +250,87 @@ class SessionsReducerTest {
|
||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||
assertEquals("PAUSED awaiting approval", state.sessions[0].status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ApprovalRequired sets pendingApproval on matching session`() {
|
||||
val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1")
|
||||
val msg = ServerMessage.ApprovalRequired(
|
||||
sessionId = SessionId("s1"),
|
||||
requestId = ApprovalRequestId("r1"),
|
||||
tier = Tier.T2,
|
||||
riskSummary = RiskSummaryDto(level = "HIGH", factors = emptyList(), recommendedAction = "review"),
|
||||
toolName = "bash",
|
||||
preview = "rm -rf /",
|
||||
sequence = 1L,
|
||||
sessionSequence = 1L,
|
||||
)
|
||||
val (state, effects) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||
assertEquals("r1", state.sessions[0].pendingApproval?.requestId)
|
||||
assertEquals("s1", state.sessions[0].pendingApproval?.sessionId)
|
||||
assertEquals("bash", state.sessions[0].pendingApproval?.toolName)
|
||||
assertTrue(effects.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ApprovalRequired for unknown session is no-op`() {
|
||||
val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1")
|
||||
val msg = ServerMessage.ApprovalRequired(
|
||||
sessionId = SessionId("s2"),
|
||||
requestId = ApprovalRequestId("r1"),
|
||||
tier = Tier.T2,
|
||||
riskSummary = RiskSummaryDto(level = "HIGH", factors = emptyList(), recommendedAction = "review"),
|
||||
toolName = "bash",
|
||||
preview = null,
|
||||
sequence = 1L,
|
||||
sessionSequence = 1L,
|
||||
)
|
||||
val (state, effects) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||
assertEquals(s.sessions, state.sessions)
|
||||
assertTrue(effects.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SessionSnapshot with pendingApproval populates pendingApproval on summary`() {
|
||||
val msg = ServerMessage.SessionSnapshot(
|
||||
sessionId = SessionId("s1"),
|
||||
workflowId = "wf",
|
||||
status = "PAUSED",
|
||||
currentStageId = null,
|
||||
pauseReason = null,
|
||||
pendingApproval = true,
|
||||
approvalRequestId = "r9",
|
||||
approvalTier = "T2",
|
||||
approvalToolName = "bash",
|
||||
approvalPreview = "preview text",
|
||||
state = com.correx.apps.server.protocol.SessionStateDto("PAUSED", null, null),
|
||||
pendingApprovals = emptyList(),
|
||||
lastSequence = 1L,
|
||||
lastSessionSequence = 1L,
|
||||
)
|
||||
val (state, _) = reduce(action = Action.ServerEventReceived(msg))
|
||||
assertEquals("r9", state.sessions[0].pendingApproval?.requestId)
|
||||
assertEquals("bash", state.sessions[0].pendingApproval?.toolName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SessionSnapshot without pendingApproval leaves pendingApproval null`() {
|
||||
val msg = ServerMessage.SessionSnapshot(
|
||||
sessionId = SessionId("s1"),
|
||||
workflowId = "wf",
|
||||
status = "ACTIVE",
|
||||
currentStageId = null,
|
||||
pauseReason = null,
|
||||
pendingApproval = false,
|
||||
approvalRequestId = null,
|
||||
approvalTier = null,
|
||||
approvalToolName = null,
|
||||
approvalPreview = null,
|
||||
state = com.correx.apps.server.protocol.SessionStateDto("PAUSED", null, null),
|
||||
pendingApprovals = emptyList(),
|
||||
lastSequence = 1L,
|
||||
lastSessionSequence = 1L,
|
||||
)
|
||||
val (state, _) = reduce(action = Action.ServerEventReceived(msg))
|
||||
assertEquals(null, state.sessions[0].pendingApproval)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user