diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/narration/NarrationSubscriber.kt b/apps/server/src/main/kotlin/com/correx/apps/server/narration/NarrationSubscriber.kt index 36e5b3e1..c58de559 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/narration/NarrationSubscriber.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/narration/NarrationSubscriber.kt @@ -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)), diff --git a/apps/server/src/test/kotlin/com/correx/apps/server/narration/NarrationSubscriberTest.kt b/apps/server/src/test/kotlin/com/correx/apps/server/narration/NarrationSubscriberTest.kt index aab3b8f7..c0136a19 100644 --- a/apps/server/src/test/kotlin/com/correx/apps/server/narration/NarrationSubscriberTest.kt +++ b/apps/server/src/test/kotlin/com/correx/apps/server/narration/NarrationSubscriberTest.kt @@ -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", + ) + } }