feat(server): REST /clarify route for headless clarification answers (#42)
Discovery-stage clarifications could only be answered over WebSocket
(ClientMessage.ClarificationResponse), so the curl-based headless QA
driver parked forever at discovery.
Add POST /sessions/{id}/clarify mirroring approveStageRoute. The server
resolves the live (stageId, requestId) from the session id via
SessionOrchestrator.pendingClarificationFor() — the newest still-live,
unanswered ClarificationRequestedEvent from the event log — so a curl
caller need not know the requestId. Empty answers = free-text skip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -181,6 +181,39 @@ class ClarificationLoopTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pendingClarificationFor resolves the live request server-side, then clears once answered`():
|
||||
Unit = runBlocking {
|
||||
// Backs the REST /clarify route (#42): a headless caller has no ClarificationRequestedEvent,
|
||||
// so the server must resolve (stageId, requestId) from just the session id, and stop
|
||||
// resolving it once answered.
|
||||
val sessionId = SessionId("clarify-rest")
|
||||
val orchestrator = orchestrator(listOf(withQuestions, clean))
|
||||
val runJob = launch { orchestrator.run(sessionId, graph(), config) }
|
||||
|
||||
val pending = withTimeout(5_000) {
|
||||
var p: ClarificationRequestedEvent? = null
|
||||
while (p == null) {
|
||||
p = orchestrator.pendingClarificationFor(sessionId)
|
||||
if (p == null) yield()
|
||||
}
|
||||
p
|
||||
}
|
||||
assertEquals(analystStage, pending.stageId)
|
||||
assertEquals("Which stack?", pending.questions.single().prompt)
|
||||
|
||||
orchestrator.submitClarification(
|
||||
sessionId, pending.stageId, pending.requestId,
|
||||
listOf(ClarificationAnswer(questionId = "stack", value = "React")),
|
||||
)
|
||||
runJob.join()
|
||||
|
||||
assertEquals(
|
||||
null, orchestrator.pendingClarificationFor(sessionId),
|
||||
"Answered clarification must no longer resolve as pending",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a stage that emits questions parks once and is never re-run`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("clarify-no-rerun")
|
||||
|
||||
Reference in New Issue
Block a user