feat(server): narrate execution-plan rejection

This commit is contained in:
2026-06-10 21:30:07 +04:00
parent 0553b1b6ee
commit 04836b751c
2 changed files with 33 additions and 0 deletions
@@ -1,6 +1,7 @@
package com.correx.apps.server.narration
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ExecutionPlanRejectedEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
@@ -65,6 +66,10 @@ class NarrationSubscriber(
sid,
NarrationTrigger(kind = "workflow_failed", instruction = "The workflow failed: ${p.reason}. Explain the failure to the user."),
)
is ExecutionPlanRejectedEvent -> enqueue(
sid,
NarrationTrigger(kind = "execution_plan_rejected", instruction = "The execution plan was rejected: ${p.reason}. Explain to the user what happened and what they can do next."),
)
is OrchestrationPausedEvent -> enqueue(
sid,
NarrationTrigger(kind = "paused", instruction = pauseInstruction(sid, p)),
@@ -4,6 +4,7 @@ import com.correx.core.approvals.Tier
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.ExecutionPlanRejectedEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.StageCompletedEvent
@@ -199,4 +200,31 @@ class NarrationSubscriberTest {
assertEquals(1, facade.calls.size)
assertEquals("stage_completed", facade.calls[0])
}
@Test
fun `execution plan rejection triggers narration with rejection reason`(): Unit = runBlocking {
val facade = RecordingRouterFacade()
NarrationSubscriber(fakeStore, facade, scope).start()
while (liveFlow.subscriptionCount.value == 0) yield()
liveFlow.emit(storedEvent(WorkflowStartedEvent(sessionId, "wf", StageId("s0")), seq = 1L))
liveFlow.emit(
storedEvent(
ExecutionPlanRejectedEvent(sessionId, reason = "Missing required artifact", source = "missing_content"),
seq = 2L,
),
)
withTimeout(2_000L) {
while (facade.calls.isEmpty()) yield()
}
assertEquals(1, facade.calls.size)
assertEquals("execution_plan_rejected", facade.calls[0].second.kind)
assertTrue(
facade.calls[0].second.instruction.contains("Missing required artifact"),
"instruction must include the rejection reason",
)
}
}