11import { describe , expect , it } from "vitest" ;
2+ import type { ConversationStateSnapshot } from "@atlas/core" ;
3+ import type { ProcessedInboxResult } from "@atlas/db" ;
24
3- import { deriveConversationReplyState } from "./conversation-state" ;
5+ import { deriveConversationReplyState , deriveMutationState } from "./conversation-state" ;
46
5- function buildSnapshot ( ) {
7+ function buildSnapshot ( ) : ConversationStateSnapshot {
68 return {
79 conversation : {
810 id : "conversation-1" ,
@@ -21,86 +23,283 @@ function buildSnapshot() {
2123 last_user_mentioned_entity_ids : [ ] ,
2224 last_presented_items : [ ] ,
2325 pending_clarifications : [ ] ,
24- mode : "clarifying " as const
26+ mode : "planning " as const
2527 }
2628 } ;
2729}
2830
2931describe ( "deriveConversationReplyState" , ( ) => {
30- it ( "keeps clarifications as clarifications when required slots are still missing" , ( ) => {
32+ it ( "persists a clarification only when structured interpretation still has a real missing slot " , ( ) => {
3133 const result = deriveConversationReplyState ( {
3234 snapshot : buildSnapshot ( ) ,
33- policyAction : "ask_clarification" ,
35+ policy : {
36+ action : "ask_clarification"
37+ } ,
3438 interpretation : {
3539 turnType : "planning_request" ,
3640 confidence : 0.58 ,
3741 resolvedEntityIds : [ ] ,
3842 ambiguity : "high" ,
3943 missingSlots : [ "time" ]
4044 } ,
41- reply : "It sounds like you want me to block out time for planning your Malaysia trip. I can proceed with that now once I know the time. " ,
45+ reply : "What time should I schedule the Malaysia trip planning? " ,
4246 userTurnText : "schedule malaysia trip planning tomorrow" ,
4347 summaryText : null ,
4448 occurredAt : "2026-03-22T16:05:00.000Z"
4549 } ) ;
4650
47- expect ( result . mode ) . toBe ( "conversation_then_mutation" ) ;
4851 expect ( result . entityRegistry ) . toHaveLength ( 1 ) ;
4952 expect ( result . entityRegistry [ 0 ] ) . toMatchObject ( {
5053 kind : "clarification" ,
51- status : "active"
54+ status : "active" ,
55+ data : {
56+ reason : "time"
57+ }
5258 } ) ;
59+ expect ( result . discourseState ?. pending_clarifications ) . toEqual ( [
60+ expect . objectContaining ( {
61+ slot : "time" ,
62+ status : "pending" ,
63+ blocking : true
64+ } )
65+ ] ) ;
5366 expect ( result . discourseState ?. mode ) . toBe ( "clarifying" ) ;
5467 } ) ;
5568
56- it ( "does not upgrade to proposal based on wording alone " , ( ) => {
69+ it ( "does not persist clarification state for ask_clarification without a real blocking slot " , ( ) => {
5770 const result = deriveConversationReplyState ( {
5871 snapshot : buildSnapshot ( ) ,
59- policyAction : "ask_clarification" ,
72+ policy : {
73+ action : "ask_clarification" ,
74+ clarificationSlots : [ "proposal" ]
75+ } ,
6076 interpretation : {
61- turnType : "planning_request " ,
62- confidence : 0.91 ,
77+ turnType : "confirmation " ,
78+ confidence : 0.32 ,
6379 resolvedEntityIds : [ ] ,
64- ambiguity : "none "
80+ ambiguity : "high "
6581 } ,
66- reply : "It sounds like you want me to block out time for planning your Malaysia trip at 3:15 PM for 15 minutes. I can proceed with that now. " ,
67- userTurnText : "schedule malaysia trip planning at 3:15pm for 15 minutes " ,
82+ reply : "Which proposal do you want me to apply? " ,
83+ userTurnText : "ok " ,
6884 summaryText : null ,
6985 occurredAt : "2026-03-22T16:05:00.000Z"
7086 } ) ;
7187
72- expect ( result . entityRegistry [ 0 ] ) . toMatchObject ( {
73- kind : "clarification"
74- } ) ;
75- expect ( result . discourseState ?. mode ) . toBe ( "clarifying" ) ;
88+ expect ( result . entityRegistry ) . toHaveLength ( 0 ) ;
89+ expect ( result . discourseState ?. pending_clarifications ) . toEqual ( [ ] ) ;
90+ expect ( result . discourseState ?. mode ) . toBe ( "planning" ) ;
7691 } ) ;
7792
78- it ( "persists proposal state only when policy already says present_proposal " , ( ) => {
93+ it ( "rejects slot unknown as a persistable blocking clarification " , ( ) => {
7994 const result = deriveConversationReplyState ( {
8095 snapshot : buildSnapshot ( ) ,
81- policyAction : "present_proposal" ,
96+ policy : {
97+ action : "ask_clarification"
98+ } ,
8299 interpretation : {
83- turnType : "edit_request" ,
84- confidence : 0.94 ,
100+ turnType : "unknown" ,
101+ confidence : 0.42 ,
102+ resolvedEntityIds : [ ] ,
103+ ambiguity : "high" ,
104+ missingSlots : [ "unknown" ]
105+ } ,
106+ reply : "Can you clarify what you want me to change?" ,
107+ userTurnText : "do it" ,
108+ summaryText : null ,
109+ occurredAt : "2026-03-22T16:05:00.000Z"
110+ } ) ;
111+
112+ expect ( result . entityRegistry ) . toHaveLength ( 0 ) ;
113+ expect ( result . discourseState ?. pending_clarifications ) . toEqual ( [ ] ) ;
114+ expect ( result . discourseState ?. mode ) . toBe ( "planning" ) ;
115+ } ) ;
116+
117+ it ( "clears active clarification state when a proposal is presented" , ( ) => {
118+ const snapshot = buildSnapshot ( ) ;
119+ snapshot . entityRegistry = [
120+ {
121+ id : "clar-1" ,
122+ conversationId : "conversation-1" ,
123+ kind : "clarification" ,
124+ label : "Need a time" ,
125+ status : "active" ,
126+ createdAt : "2026-03-22T16:01:00.000Z" ,
127+ updatedAt : "2026-03-22T16:01:00.000Z" ,
128+ data : {
129+ prompt : "What time should I schedule it?" ,
130+ reason : "time" ,
131+ open : true
132+ }
133+ }
134+ ] ;
135+ snapshot . discourseState = {
136+ focus_entity_id : "clar-1" ,
137+ currently_editable_entity_id : null ,
138+ last_user_mentioned_entity_ids : [ ] ,
139+ last_presented_items : [ ] ,
140+ pending_clarifications : [
141+ {
142+ id : "clar-1" ,
143+ slot : "time" ,
144+ question : "What time should I schedule it?" ,
145+ status : "pending" ,
146+ blocking : true ,
147+ createdAt : "2026-03-22T16:01:00.000Z" ,
148+ createdTurnId : "assistant:1"
149+ }
150+ ] ,
151+ mode : "clarifying"
152+ } ;
153+
154+ const result = deriveConversationReplyState ( {
155+ snapshot,
156+ policy : {
157+ action : "present_proposal"
158+ } ,
159+ interpretation : {
160+ turnType : "clarification_answer" ,
161+ confidence : 0.93 ,
85162 resolvedEntityIds : [ "task-1" ] ,
86163 ambiguity : "none"
87164 } ,
88- reply : "I can move the planning block to 3:15 PM for 15 minutes. Would you like me to do that now?" ,
89- userTurnText : "move it to 3:15pm for 15 minutes " ,
165+ reply : "I can schedule the Malaysia trip planning at 5 PM. Want me to do that now?" ,
166+ userTurnText : "5pm " ,
90167 summaryText : null ,
91168 occurredAt : "2026-03-22T16:05:00.000Z"
92169 } ) ;
93170
94- expect ( result . entityRegistry ) . toHaveLength ( 1 ) ;
95- expect ( result . entityRegistry [ 0 ] ) . toMatchObject ( {
96- kind : "proposal_option" ,
97- status : "active" ,
98- data : {
99- targetEntityId : "task-1" ,
100- policyAction : "present_proposal" ,
101- confirmationRequired : true
171+ expect ( result . entityRegistry ) . toEqual (
172+ expect . arrayContaining ( [
173+ expect . objectContaining ( {
174+ id : "clar-1" ,
175+ kind : "clarification" ,
176+ status : "resolved"
177+ } ) ,
178+ expect . objectContaining ( {
179+ kind : "proposal_option" ,
180+ status : "active"
181+ } )
182+ ] )
183+ ) ;
184+ expect ( result . discourseState ?. pending_clarifications ) . toEqual ( [
185+ expect . objectContaining ( {
186+ id : "clar-1" ,
187+ status : "resolved"
188+ } )
189+ ] ) ;
190+ expect ( result . discourseState ?. mode ) . toBe ( "confirming" ) ;
191+ } ) ;
192+ } ) ;
193+
194+ describe ( "deriveMutationState" , ( ) => {
195+ it ( "clears pending clarifications after execution" , ( ) => {
196+ const snapshot = buildSnapshot ( ) ;
197+ snapshot . entityRegistry = [
198+ {
199+ id : "clar-1" ,
200+ conversationId : "conversation-1" ,
201+ kind : "clarification" ,
202+ label : "Need a time" ,
203+ status : "active" ,
204+ createdAt : "2026-03-22T16:01:00.000Z" ,
205+ updatedAt : "2026-03-22T16:01:00.000Z" ,
206+ data : {
207+ prompt : "What time should I schedule it?" ,
208+ reason : "time" ,
209+ open : true
210+ }
102211 }
212+ ] ;
213+ snapshot . discourseState = {
214+ focus_entity_id : "clar-1" ,
215+ currently_editable_entity_id : null ,
216+ last_user_mentioned_entity_ids : [ ] ,
217+ last_presented_items : [ ] ,
218+ pending_clarifications : [
219+ {
220+ id : "clar-1" ,
221+ slot : "time" ,
222+ question : "What time should I schedule it?" ,
223+ status : "pending" ,
224+ blocking : true ,
225+ createdAt : "2026-03-22T16:01:00.000Z" ,
226+ createdTurnId : "assistant:1"
227+ }
228+ ] ,
229+ mode : "clarifying"
230+ } ;
231+
232+ const processing : ProcessedInboxResult = {
233+ outcome : "planned" ,
234+ inboxItem : {
235+ id : "inbox-1" ,
236+ userId : "user-1" ,
237+ rawText : "schedule malaysia trip planning" ,
238+ normalizedText : "schedule malaysia trip planning" ,
239+ processingStatus : "planned" ,
240+ linkedTaskIds : [ "task-1" ] ,
241+ createdAt : "2026-03-22T16:00:00.000Z"
242+ } ,
243+ plannerRun : {
244+ id : "run-1" ,
245+ userId : "user-1" ,
246+ inboxItemId : "inbox-1" ,
247+ version : "test" ,
248+ modelInput : { } ,
249+ modelOutput : { } ,
250+ confidence : 0.92
251+ } ,
252+ createdTasks : [
253+ {
254+ id : "task-1" ,
255+ userId : "user-1" ,
256+ sourceInboxItemId : "inbox-1" ,
257+ lastInboxItemId : "inbox-1" ,
258+ title : "Malaysia trip planning" ,
259+ lifecycleState : "scheduled" ,
260+ externalCalendarEventId : null ,
261+ externalCalendarId : null ,
262+ scheduledStartAt : "2026-03-23T00:00:00.000Z" ,
263+ scheduledEndAt : "2026-03-23T00:30:00.000Z" ,
264+ calendarSyncStatus : "in_sync" ,
265+ calendarSyncUpdatedAt : null ,
266+ rescheduleCount : 0 ,
267+ lastFollowupAt : null ,
268+ followupReminderSentAt : null ,
269+ completedAt : null ,
270+ archivedAt : null ,
271+ priority : "medium" ,
272+ urgency : "medium"
273+ }
274+ ] ,
275+ scheduleBlocks : [
276+ {
277+ id : "block-1" ,
278+ userId : "user-1" ,
279+ taskId : "task-1" ,
280+ startAt : "2026-03-23T00:00:00.000Z" ,
281+ endAt : "2026-03-23T00:30:00.000Z" ,
282+ confidence : 0.92 ,
283+ reason : "User requested 5 PM." ,
284+ rescheduleCount : 0 ,
285+ externalCalendarId : null
286+ }
287+ ] ,
288+ followUpMessage : "Scheduled it for 5 PM."
289+ } ;
290+
291+ const result = deriveMutationState ( {
292+ snapshot,
293+ processing,
294+ occurredAt : "2026-03-22T16:10:00.000Z"
103295 } ) ;
104- expect ( result . discourseState ?. mode ) . toBe ( "confirming" ) ;
296+
297+ expect ( result . discourseState . pending_clarifications ) . toEqual ( [
298+ expect . objectContaining ( {
299+ id : "clar-1" ,
300+ status : "resolved"
301+ } )
302+ ] ) ;
303+ expect ( result . discourseState . mode ) . toBe ( "editing" ) ;
105304 } ) ;
106305} ) ;
0 commit comments