refactor(approval): wire ApprovalCoordinator end-to-end
ApprovalCoordinator (added in epic-13) was never instantiated. Its four capabilities — multi-client fan-out, single-resolver dedupe, request timeout, and a unified domain mapper — were duplicated piecemeal across GlobalStreamHandler and SessionStreamHandler, with a STEER-outcome divergence between them (coordinator mapped STEER → APPROVED while GlobalStreamHandler mapped STEER → REJECTED). This refactor wires the coordinator and removes the duplication. Protocol: drop ApprovalDecision.STEER. STEER is no longer a discriminator; it's expressed as (APPROVE | REJECT) + non-null steeringNote, so the user can steer alongside either choice. TUI's current single-keybind STEER input flow now sends APPROVE + note (matching the original epic-13 intent); a future overlay UX will let the user pick APPROVE or REJECT explicitly. Mapper: ClientMessage.ApprovalResponse → DomainApprovalDecision lives in a single shared `toDomain` helper (apps/server/approval/ ApprovalResponseMapper.kt). Both stream handlers and the coordinator use it. Config: delete the duplicate apps/server/approval/ApprovalConfig.kt; ApprovalCoordinator now takes core.config.ApprovalConfig (already had timeoutMs=300_000L). ConfigLoader already populates it. Wiring: - ServerModule owns a SupervisorJob-backed moduleScope, constructs ApprovalCoordinator, and on start() subscribes eventStore.subscribeAll() to route ApprovalRequestedEvent payloads to onApprovalRequested. - GlobalStreamHandler delegates ApprovalResponse to approvalCoordinator.handleResponse and registers/unregisters via the new registerGlobalClient/unregisterGlobalClient (global sockets aren't bound to one SessionId, so broadcasts union session and global sets). - SessionStreamHandler delegates ApprovalResponse and registers per-session via registerClient/unregisterClient. - Timeout is on by default at 5 minutes via config.timeoutMs. Tests: ApprovalCoordinatorWiringTest covers delegate path, dedupe producing ProtocolError, and event-stream subscription routing.
This commit is contained in:
@@ -69,7 +69,7 @@ object InputReducer {
|
||||
Effect.SendWs(
|
||||
ClientMessage.ApprovalResponse(
|
||||
requestId = ApprovalRequestId(active.requestId),
|
||||
decision = ApprovalDecision.STEER,
|
||||
decision = ApprovalDecision.APPROVE,
|
||||
steeringNote = text,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -132,7 +132,7 @@ class InputReducerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubmitInput in STEER mode with active approval emits ApprovalResponse STEER`() {
|
||||
fun `SubmitInput in STEER mode with active approval emits ApprovalResponse APPROVE with steering note`() {
|
||||
val (state, effects) = reduce(
|
||||
state = stateWithActiveApproval(inputMode = InputMode.STEER, inputBuffer = "steer this way"),
|
||||
action = Action.SubmitInput,
|
||||
@@ -141,7 +141,7 @@ class InputReducerTest {
|
||||
assertEquals(1, effects.size)
|
||||
val effect = effects[0] as Effect.SendWs
|
||||
val msg = effect.message as ClientMessage.ApprovalResponse
|
||||
assertEquals(ApprovalDecision.STEER, msg.decision)
|
||||
assertEquals(ApprovalDecision.APPROVE, msg.decision)
|
||||
assertEquals("steer this way", msg.steeringNote)
|
||||
assertEquals("req-1", msg.requestId.value)
|
||||
}
|
||||
@@ -155,7 +155,7 @@ class InputReducerTest {
|
||||
assertEquals(null, state.sessions.sessions[0].pendingApproval)
|
||||
assertEquals(1, effects.size)
|
||||
val msg = (effects[0] as Effect.SendWs).message as ClientMessage.ApprovalResponse
|
||||
assertEquals(ApprovalDecision.STEER, msg.decision)
|
||||
assertEquals(ApprovalDecision.APPROVE, msg.decision)
|
||||
assertEquals("go left", msg.steeringNote)
|
||||
assertEquals("req-1", msg.requestId.value)
|
||||
}
|
||||
|
||||
+2
-2
@@ -46,14 +46,14 @@ class ApprovalResponseGlobalSocketRoundTripTest {
|
||||
fun `ApprovalResponse with steering note round-trips`() {
|
||||
val original: ClientMessage = ClientMessage.ApprovalResponse(
|
||||
requestId = ApprovalRequestId("req-steer"),
|
||||
decision = ApprovalDecision.STEER,
|
||||
decision = ApprovalDecision.APPROVE,
|
||||
steeringNote = "please retry with smaller batch",
|
||||
)
|
||||
|
||||
val wire = clientJson.encodeToString(original)
|
||||
val decoded = ProtocolSerializer.decodeClientMessage(wire) as ClientMessage.ApprovalResponse
|
||||
|
||||
assertEquals(ApprovalDecision.STEER, decoded.decision)
|
||||
assertEquals(ApprovalDecision.APPROVE, decoded.decision)
|
||||
assertEquals("please retry with smaller batch", decoded.steeringNote)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user