@@ -956,6 +956,366 @@ describe("process inbox item service", () => {
956956 ) ;
957957 } ) ;
958958
959+ it ( "returns a non-empty clarification reply when the planner explicitly asks to clarify" , async ( ) => {
960+ seedInboxItemForProcessingTests ( {
961+ id : "inbox-archive-clarify" ,
962+ userId : "123" ,
963+ sourceEventId : "event-archive-clarify" ,
964+ rawText : "archive that" ,
965+ normalizedText : "archive that" ,
966+ processingStatus : "received" ,
967+ linkedTaskIds : [ ]
968+ } ) ;
969+
970+ const result = await processInboxItem (
971+ { inboxItemId : "inbox-archive-clarify" } ,
972+ {
973+ calendar : getDefaultCalendarAdapter ( ) ,
974+ planner : async ( ) => ( {
975+ confidence : 0.32 ,
976+ summary : "Need clarification before archiving." ,
977+ actions : [
978+ {
979+ type : "clarify" ,
980+ reason : "I couldn't safely apply that update. Tell me the exact task and what you'd like me to change."
981+ }
982+ ]
983+ } )
984+ }
985+ ) ;
986+
987+ expect ( result . outcome ) . toBe ( "needs_clarification" ) ;
988+ expect ( result . followUpMessage ) . toBe (
989+ "I couldn't safely apply that update. Tell me the exact task and what you'd like me to change."
990+ ) ;
991+ } ) ;
992+
993+ it ( "asks for clarification when multiple existing tasks share the same title" , async ( ) => {
994+ const store = getDefaultInboxProcessingStore ( ) ;
995+
996+ seedInboxItemForProcessingTests ( {
997+ id : "inbox-duplicate-1" ,
998+ userId : "123" ,
999+ sourceEventId : "event-duplicate-1" ,
1000+ rawText : "Car maintenance" ,
1001+ normalizedText : "Car maintenance" ,
1002+ processingStatus : "received" ,
1003+ linkedTaskIds : [ ] ,
1004+ createdAt : "2026-03-20T16:00:00.000Z"
1005+ } ) ;
1006+
1007+ await processInboxItem (
1008+ { inboxItemId : "inbox-duplicate-1" } ,
1009+ {
1010+ store,
1011+ calendar : getDefaultCalendarAdapter ( ) ,
1012+ planner : async ( ) => ( {
1013+ confidence : 0.9 ,
1014+ summary : "Scheduled Car maintenance." ,
1015+ actions : [
1016+ {
1017+ type : "create_task" ,
1018+ alias : "new_task_1" ,
1019+ title : "Car maintenance" ,
1020+ priority : "medium" ,
1021+ urgency : "medium"
1022+ } ,
1023+ {
1024+ type : "create_schedule_block" ,
1025+ taskRef : {
1026+ kind : "created_task" ,
1027+ alias : "new_task_1"
1028+ } ,
1029+ scheduleConstraint : {
1030+ dayReference : null ,
1031+ weekday : null ,
1032+ weekOffset : null ,
1033+ explicitHour : 9 ,
1034+ minute : 0 ,
1035+ preferredWindow : null ,
1036+ sourceText : "default next slot"
1037+ } ,
1038+ reason : "Schedule the new task."
1039+ }
1040+ ]
1041+ } )
1042+ }
1043+ ) ;
1044+
1045+ seedInboxItemForProcessingTests ( {
1046+ id : "inbox-duplicate-2" ,
1047+ userId : "123" ,
1048+ sourceEventId : "event-duplicate-2" ,
1049+ rawText : "Car maintenance" ,
1050+ normalizedText : "Car maintenance" ,
1051+ processingStatus : "received" ,
1052+ linkedTaskIds : [ ] ,
1053+ createdAt : "2026-03-20T17:00:00.000Z"
1054+ } ) ;
1055+
1056+ await processInboxItem (
1057+ { inboxItemId : "inbox-duplicate-2" } ,
1058+ {
1059+ store,
1060+ calendar : getDefaultCalendarAdapter ( ) ,
1061+ planner : async ( ) => ( {
1062+ confidence : 0.9 ,
1063+ summary : "Scheduled Car maintenance." ,
1064+ actions : [
1065+ {
1066+ type : "create_task" ,
1067+ alias : "new_task_1" ,
1068+ title : "Car maintenance" ,
1069+ priority : "medium" ,
1070+ urgency : "medium"
1071+ } ,
1072+ {
1073+ type : "create_schedule_block" ,
1074+ taskRef : {
1075+ kind : "created_task" ,
1076+ alias : "new_task_1"
1077+ } ,
1078+ scheduleConstraint : {
1079+ dayReference : null ,
1080+ weekday : null ,
1081+ weekOffset : null ,
1082+ explicitHour : 10 ,
1083+ minute : 0 ,
1084+ preferredWindow : null ,
1085+ sourceText : "default next slot"
1086+ } ,
1087+ reason : "Schedule the new task."
1088+ }
1089+ ]
1090+ } )
1091+ }
1092+ ) ;
1093+
1094+ seedInboxItemForProcessingTests ( {
1095+ id : "inbox-duplicate-complete" ,
1096+ userId : "123" ,
1097+ sourceEventId : "event-duplicate-complete" ,
1098+ rawText : "car maintenance is done" ,
1099+ normalizedText : "car maintenance is done" ,
1100+ processingStatus : "received" ,
1101+ linkedTaskIds : [ ] ,
1102+ createdAt : "2026-03-20T18:00:00.000Z"
1103+ } ) ;
1104+
1105+ const result = await processInboxItem (
1106+ {
1107+ inboxItemId : "inbox-duplicate-complete" ,
1108+ planningInboxTextOverride : {
1109+ text : "Mark car maintenance as done."
1110+ }
1111+ } ,
1112+ {
1113+ store,
1114+ calendar : getDefaultCalendarAdapter ( ) ,
1115+ planner : async ( ) => ( {
1116+ confidence : 0.61 ,
1117+ summary : "Mark the existing car maintenance task as done." ,
1118+ actions : [
1119+ {
1120+ type : "complete_task" ,
1121+ taskRef : {
1122+ kind : "existing_task" ,
1123+ alias : "existing_task_1"
1124+ } ,
1125+ reason : "The user said car maintenance is done."
1126+ }
1127+ ]
1128+ } )
1129+ }
1130+ ) ;
1131+
1132+ expect ( result . outcome ) . toBe ( "needs_clarification" ) ;
1133+ expect ( result . followUpMessage ) . toContain ( "I found multiple tasks named 'Car maintenance'" ) ;
1134+ expect ( result . followUpMessage ) . toContain ( "1. scheduled for" ) ;
1135+ expect ( result . followUpMessage ) . toContain ( "2. scheduled for" ) ;
1136+ } ) ;
1137+
1138+ it ( "does not ask for clarification when duplicate titles only exist on closed tasks" , async ( ) => {
1139+ const store = getDefaultInboxProcessingStore ( ) ;
1140+
1141+ seedInboxItemForProcessingTests ( {
1142+ id : "inbox-car-open" ,
1143+ userId : "123" ,
1144+ sourceEventId : "event-car-open" ,
1145+ rawText : "Car maintenance" ,
1146+ normalizedText : "Car maintenance" ,
1147+ processingStatus : "received" ,
1148+ linkedTaskIds : [ ] ,
1149+ createdAt : "2026-03-20T16:00:00.000Z"
1150+ } ) ;
1151+
1152+ await processInboxItem (
1153+ { inboxItemId : "inbox-car-open" } ,
1154+ {
1155+ store,
1156+ calendar : getDefaultCalendarAdapter ( ) ,
1157+ planner : async ( ) => ( {
1158+ confidence : 0.9 ,
1159+ summary : "Scheduled Car maintenance." ,
1160+ actions : [
1161+ {
1162+ type : "create_task" ,
1163+ alias : "new_task_1" ,
1164+ title : "Car maintenance" ,
1165+ priority : "medium" ,
1166+ urgency : "medium"
1167+ } ,
1168+ {
1169+ type : "create_schedule_block" ,
1170+ taskRef : {
1171+ kind : "created_task" ,
1172+ alias : "new_task_1"
1173+ } ,
1174+ scheduleConstraint : {
1175+ dayReference : null ,
1176+ weekday : null ,
1177+ weekOffset : null ,
1178+ explicitHour : 9 ,
1179+ minute : 0 ,
1180+ preferredWindow : null ,
1181+ sourceText : "default next slot"
1182+ } ,
1183+ reason : "Schedule the new task."
1184+ }
1185+ ]
1186+ } )
1187+ }
1188+ ) ;
1189+
1190+ seedInboxItemForProcessingTests ( {
1191+ id : "inbox-car-closed" ,
1192+ userId : "123" ,
1193+ sourceEventId : "event-car-closed" ,
1194+ rawText : "Car maintenance is done" ,
1195+ normalizedText : "Car maintenance is done" ,
1196+ processingStatus : "received" ,
1197+ linkedTaskIds : [ ] ,
1198+ createdAt : "2026-03-20T17:00:00.000Z"
1199+ } ) ;
1200+
1201+ await processInboxItem (
1202+ {
1203+ inboxItemId : "inbox-car-closed" ,
1204+ planningInboxTextOverride : {
1205+ text : "Mark car maintenance as done."
1206+ }
1207+ } ,
1208+ {
1209+ store,
1210+ calendar : getDefaultCalendarAdapter ( ) ,
1211+ planner : async ( ) => ( {
1212+ confidence : 0.71 ,
1213+ summary : "Marked the remaining car maintenance task as done." ,
1214+ actions : [
1215+ {
1216+ type : "complete_task" ,
1217+ taskRef : {
1218+ kind : "existing_task" ,
1219+ alias : "existing_task_1"
1220+ } ,
1221+ reason : "The user said car maintenance is done."
1222+ }
1223+ ]
1224+ } )
1225+ }
1226+ ) ;
1227+
1228+ seedInboxItemForProcessingTests ( {
1229+ id : "inbox-car-second-open" ,
1230+ userId : "123" ,
1231+ sourceEventId : "event-car-second-open" ,
1232+ rawText : "Car maintenance" ,
1233+ normalizedText : "Car maintenance" ,
1234+ processingStatus : "received" ,
1235+ linkedTaskIds : [ ] ,
1236+ createdAt : "2026-03-20T18:00:00.000Z"
1237+ } ) ;
1238+
1239+ await processInboxItem (
1240+ { inboxItemId : "inbox-car-second-open" } ,
1241+ {
1242+ store,
1243+ calendar : getDefaultCalendarAdapter ( ) ,
1244+ planner : async ( ) => ( {
1245+ confidence : 0.9 ,
1246+ summary : "Scheduled Car maintenance." ,
1247+ actions : [
1248+ {
1249+ type : "create_task" ,
1250+ alias : "new_task_1" ,
1251+ title : "Car maintenance" ,
1252+ priority : "medium" ,
1253+ urgency : "medium"
1254+ } ,
1255+ {
1256+ type : "create_schedule_block" ,
1257+ taskRef : {
1258+ kind : "created_task" ,
1259+ alias : "new_task_1"
1260+ } ,
1261+ scheduleConstraint : {
1262+ dayReference : null ,
1263+ weekday : null ,
1264+ weekOffset : null ,
1265+ explicitHour : 10 ,
1266+ minute : 0 ,
1267+ preferredWindow : null ,
1268+ sourceText : "default next slot"
1269+ } ,
1270+ reason : "Schedule the new task."
1271+ }
1272+ ]
1273+ } )
1274+ }
1275+ ) ;
1276+
1277+ seedInboxItemForProcessingTests ( {
1278+ id : "inbox-car-complete-open" ,
1279+ userId : "123" ,
1280+ sourceEventId : "event-car-complete-open" ,
1281+ rawText : "Car maintenance is done" ,
1282+ normalizedText : "Car maintenance is done" ,
1283+ processingStatus : "received" ,
1284+ linkedTaskIds : [ ] ,
1285+ createdAt : "2026-03-20T19:00:00.000Z"
1286+ } ) ;
1287+
1288+ const result = await processInboxItem (
1289+ {
1290+ inboxItemId : "inbox-car-complete-open" ,
1291+ planningInboxTextOverride : {
1292+ text : "Mark car maintenance as done."
1293+ }
1294+ } ,
1295+ {
1296+ store,
1297+ calendar : getDefaultCalendarAdapter ( ) ,
1298+ planner : async ( ) => ( {
1299+ confidence : 0.71 ,
1300+ summary : "Marked the remaining car maintenance task as done." ,
1301+ actions : [
1302+ {
1303+ type : "complete_task" ,
1304+ taskRef : {
1305+ kind : "existing_task" ,
1306+ alias : "existing_task_2"
1307+ } ,
1308+ reason : "The user said car maintenance is done."
1309+ }
1310+ ]
1311+ } )
1312+ }
1313+ ) ;
1314+
1315+ expect ( result . outcome ) . toBe ( "completed_tasks" ) ;
1316+ expect ( result . followUpMessage ) . toBe ( "Marked 'Car maintenance' as done." ) ;
1317+ } ) ;
1318+
9591319 it ( "resets processing state and records a failed planner run when calendar creation fails" , async ( ) => {
9601320 const store = getDefaultInboxProcessingStore ( ) ;
9611321
0 commit comments