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.ApprovalDecision
|
||||||
import com.correx.apps.server.protocol.ClientMessage
|
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.input.Action
|
||||||
import com.correx.apps.tui.state.ApprovalInfo
|
|
||||||
import com.correx.apps.tui.state.ApprovalState
|
import com.correx.apps.tui.state.ApprovalState
|
||||||
import com.correx.apps.tui.state.InputMode
|
import com.correx.apps.tui.state.InputMode
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(ApprovalReducer::class.simpleName)
|
|
||||||
|
|
||||||
object ApprovalReducer {
|
object ApprovalReducer {
|
||||||
fun reduce(
|
fun reduce(
|
||||||
@@ -58,43 +53,6 @@ object ApprovalReducer {
|
|||||||
approval to emptyList()
|
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()
|
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.PauseReason
|
||||||
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.ApprovalInfo
|
||||||
import com.correx.apps.tui.state.InputMode
|
import com.correx.apps.tui.state.InputMode
|
||||||
import com.correx.apps.tui.state.SessionSummary
|
import com.correx.apps.tui.state.SessionSummary
|
||||||
import com.correx.apps.tui.state.SessionsState
|
import com.correx.apps.tui.state.SessionsState
|
||||||
@@ -107,6 +108,7 @@ object SessionsReducer {
|
|||||||
is ServerMessage.ToolFailed -> processToolFailedMessage(msg, sessions)
|
is ServerMessage.ToolFailed -> processToolFailedMessage(msg, sessions)
|
||||||
is ServerMessage.ToolRejected -> processToolRejectedMessage(clock, sessions, msg)
|
is ServerMessage.ToolRejected -> processToolRejectedMessage(clock, sessions, msg)
|
||||||
is ServerMessage.SessionSnapshot -> processSessionSnapshotMessage(clock, sessions, msg)
|
is ServerMessage.SessionSnapshot -> processSessionSnapshotMessage(clock, sessions, msg)
|
||||||
|
is ServerMessage.ApprovalRequired -> processApprovalRequiredMessage(sessions, msg)
|
||||||
else -> sessions to emptyList()
|
else -> sessions to emptyList()
|
||||||
}.also { log.debug("processed server message: {}", msg) }
|
}.also { log.debug("processed server message: {}", msg) }
|
||||||
|
|
||||||
@@ -115,6 +117,17 @@ object SessionsReducer {
|
|||||||
sessionState: SessionsState,
|
sessionState: SessionsState,
|
||||||
msg: ServerMessage.SessionSnapshot,
|
msg: ServerMessage.SessionSnapshot,
|
||||||
): Pair<SessionsState, List<Effect>> {
|
): 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(
|
val summary = SessionSummary(
|
||||||
id = msg.sessionId.value,
|
id = msg.sessionId.value,
|
||||||
status = when {
|
status = when {
|
||||||
@@ -126,6 +139,7 @@ object SessionsReducer {
|
|||||||
lastEventAt = clock(),
|
lastEventAt = clock(),
|
||||||
currentStage = msg.currentStageId,
|
currentStage = msg.currentStageId,
|
||||||
currentStageId = msg.currentStageId,
|
currentStageId = msg.currentStageId,
|
||||||
|
pendingApproval = approvalInfo,
|
||||||
)
|
)
|
||||||
val selected = sessionState.selectedId ?: msg.sessionId.value
|
val selected = sessionState.selectedId ?: msg.sessionId.value
|
||||||
return sessionState.copy(
|
return sessionState.copy(
|
||||||
@@ -134,6 +148,25 @@ object SessionsReducer {
|
|||||||
) to emptyList()
|
) 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(
|
private fun processToolRejectedMessage(
|
||||||
clock: () -> Long,
|
clock: () -> Long,
|
||||||
sessions: SessionsState,
|
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.ApprovalDecision
|
||||||
import com.correx.apps.server.protocol.ClientMessage
|
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.input.Action
|
||||||
import com.correx.apps.tui.state.ApprovalInfo
|
import com.correx.apps.tui.state.ApprovalInfo
|
||||||
import com.correx.apps.tui.state.ApprovalState
|
import com.correx.apps.tui.state.ApprovalState
|
||||||
import com.correx.apps.tui.state.InputMode
|
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.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
@@ -88,23 +83,4 @@ class ApprovalReducerTest {
|
|||||||
)
|
)
|
||||||
assertEquals(activeInfo, state.active)
|
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
|
package com.correx.apps.tui.reducer
|
||||||
|
|
||||||
import com.correx.apps.server.protocol.PauseReason
|
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.server.protocol.ServerMessage
|
||||||
import com.correx.apps.tui.input.Action
|
import com.correx.apps.tui.input.Action
|
||||||
import com.correx.apps.tui.state.InputMode
|
import com.correx.apps.tui.state.InputMode
|
||||||
import com.correx.apps.tui.state.SessionSummary
|
import com.correx.apps.tui.state.SessionSummary
|
||||||
import com.correx.apps.tui.state.SessionsState
|
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.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@@ -247,4 +250,87 @@ class SessionsReducerTest {
|
|||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals("PAUSED awaiting approval", state.sessions[0].status)
|
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