2026-05-05 22:40:11 -05:00
using Jibo.Cloud.Application.Abstractions ;
2026-04-17 17:49:43 -05:00
using Jibo.Cloud.Application.Services ;
using Jibo.Cloud.Infrastructure.Content ;
2026-05-05 22:40:11 -05:00
using Jibo.Cloud.Infrastructure.Persistence ;
2026-04-17 17:49:43 -05:00
using Jibo.Runtime.Abstractions ;
2026-04-18 16:43:38 -05:00
using System.Text.Json ;
2026-04-17 17:49:43 -05:00
namespace Jibo.Cloud.Tests.WebSockets ;
public sealed class JiboInteractionServiceTests
{
2026-05-06 14:50:37 -05:00
private const string PersonalReportStateKey = "personalReportState" ;
private const string PersonalReportNoMatchCountKey = "personalReportNoMatchCount" ;
private const string PersonalReportUserNameKey = "personalReportUserName" ;
private const string PersonalReportUserVerifiedKey = "personalReportUserVerified" ;
private const string PersonalReportWeatherEnabledKey = "personalReportWeatherEnabled" ;
private const string PersonalReportCalendarEnabledKey = "personalReportCalendarEnabled" ;
private const string PersonalReportCommuteEnabledKey = "personalReportCommuteEnabled" ;
private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled" ;
2026-05-14 07:44:46 -05:00
private const string HouseholdListStateKey = "householdListState" ;
private const string HouseholdListTypeKey = "householdListType" ;
2026-05-06 16:01:15 -05:00
private const string ChitchatStateKey = "chitchatState" ;
private const string ChitchatRouteKey = "chitchatRoute" ;
private const string ChitchatEmotionKey = "chitchatEmotion" ;
2026-05-09 23:46:00 -05:00
private const string GreetingRouteKey = "greetingsRoute" ;
private const string GreetingSpeakerKey = "greetingsSpeaker" ;
private const string GreetingLastProactiveUtcKey = "greetingsLastProactiveUtc" ;
private const string GreetingLastReactiveUtcKey = "greetingsLastReactiveUtc" ;
2026-05-06 14:50:37 -05:00
2026-04-17 17:49:43 -05:00
[Fact]
public async Task BuildDecisionAsync_Joke_UsesCatalogBackedRandomContent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "tell me a joke" ,
NormalizedTranscript = "tell me a joke"
} ) ;
Assert . Equal ( "joke" , decision . IntentName ) ;
Assert . Equal ( "@be/joke" , decision . SkillName ) ;
Assert . Equal ( "Why did the robot cross the road? Because it was programmed by the chicken." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_Dance_UsesCatalogBackedAnimation ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do a dance" ,
NormalizedTranscript = "do a dance"
} ) ;
Assert . Equal ( "dance" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
2026-04-21 21:51:04 -05:00
var catalog = await new InMemoryJiboExperienceContentRepository ( ) . GetCatalogAsync ( ) ; // Ensure catalog is loaded for test coverage
Assert . Contains ( decision . ReplyText , catalog . DanceReplies ) ;
2026-04-17 17:49:43 -05:00
Assert . Equal ( "<speak>Okay.<break size='0.2'/> Watch this.<anim cat='dance' filter='music, rom-upbeat' /></speak>" , decision . SkillPayload ! [ "esml" ] ) ;
}
2026-05-05 22:27:28 -05:00
[Fact]
public async Task BuildDecisionAsync_DoYouLikeToDance_UsesQuestionReplyStyleInsteadOfTriggeringDanceAnimation ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do you like to dance" ,
NormalizedTranscript = "do you like to dance"
} ) ;
Assert . Equal ( "dance_question" , decision . IntentName ) ;
Assert . Null ( decision . SkillName ) ;
Assert . Equal ( "I love to dance. Tell me to dance and I will show you a move." , decision . ReplyText ) ;
}
2026-04-22 07:48:36 -05:00
[Fact]
public async Task BuildDecisionAsync_TwerkQuestion_PrefersSpecificTwerkIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "can you twerk" ,
NormalizedTranscript = "can you twerk"
} ) ;
Assert . Equal ( "twerk" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
}
2026-05-05 20:27:58 -05:00
[Fact]
public async Task BuildDecisionAsync_HowOldAreYou_UsesPersonaBirthdayForAgeReply ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "how old are you" ,
NormalizedTranscript = "how old are you" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 05 - 05 T19 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "robot_age" , decision . IntentName ) ;
Assert . Equal ( "I count March 22, 2026 as my birthday, so I am 1 month old." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_WhenIsYourBirthday_UsesPersonaBirthdayReply ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "when's your birthday" ,
NormalizedTranscript = "when's your birthday"
} ) ;
Assert . Equal ( "robot_birthday" , decision . IntentName ) ;
Assert . Equal ( "My birthday is March 22, 2026." , decision . ReplyText ) ;
}
2026-05-05 22:27:28 -05:00
[Fact]
public async Task BuildDecisionAsync_WhatsYourBirthday_DoesNotFallThroughToDateIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's your birthday" ,
NormalizedTranscript = "what's your birthday"
} ) ;
Assert . Equal ( "robot_birthday" , decision . IntentName ) ;
Assert . Equal ( "My birthday is March 22, 2026." , decision . ReplyText ) ;
}
2026-05-07 07:22:33 -05:00
[Fact]
public async Task BuildDecisionAsync_WhatsYourBday_DoesNotFallThroughToDateIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's your bday" ,
NormalizedTranscript = "what's your bday"
} ) ;
Assert . Equal ( "robot_birthday" , decision . IntentName ) ;
Assert . Equal ( "My birthday is March 22, 2026." , decision . ReplyText ) ;
}
2026-05-09 23:46:00 -05:00
[Fact]
public async Task BuildDecisionAsync_GoodMorning_UsesReactiveGreetingWithRememberedName ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
memoryStore . SetName ( new PersonalMemoryTenantScope ( "acct-a" , "loop-a" , "device-a" ) , "jake" ) ;
var service = CreateService ( memoryStore ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "good morning" ,
NormalizedTranscript = "good morning" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "good_morning" , decision . IntentName ) ;
Assert . Equal ( "Good morning, Jake. It is great to see you." , decision . ReplyText ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "ReactiveGreeting" , decision . ContextUpdates ! [ GreetingRouteKey ] ) ;
Assert . Equal ( string . Empty , decision . ContextUpdates [ GreetingSpeakerKey ] ) ;
Assert . True ( DateTimeOffset . TryParse ( decision . ContextUpdates [ GreetingLastReactiveUtcKey ] ? . ToString ( ) , out _ ) ) ;
}
2026-05-14 20:48:55 -05:00
[Fact]
public async Task BuildDecisionAsync_GoodMorning_UsesPersonScopedNameWhenSpeakerIsKnown ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
memoryStore . SetName ( new PersonalMemoryTenantScope ( "acct-a" , "loop-a" , "device-a" , "person-1" ) , "alex" ) ;
var service = CreateService ( memoryStore ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "good morning" ,
NormalizedTranscript = "good morning" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a" ,
["context"] = "" "{" runtime ":{" perception ":{" speaker ":" person - 1 "}," loop ":{" users ":[{" id ":" person - 1 "," firstName ":" jake "}]}}}" ""
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "good_morning" , decision . IntentName ) ;
Assert . Equal ( "Good morning, Alex. It is great to see you." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_WhoAmI_UsesPersonScopedNameWhenSpeakerIsKnown ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
memoryStore . SetName ( new PersonalMemoryTenantScope ( "acct-b" , "loop-b" , "device-b" , "person-2" ) , "sam" ) ;
var service = CreateService ( memoryStore ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "who am i" ,
NormalizedTranscript = "who am i" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-b" ,
["loopId"] = "loop-b" ,
["context"] = "" "{" runtime ":{" perception ":{" speaker ":" person - 2 "}," loop ":{" users ":[{" id ":" person - 2 "," firstName ":" sam "}]}}}" ""
} ,
DeviceId = "device-b"
} ) ;
Assert . Equal ( "memory_get_name" , decision . IntentName ) ;
Assert . Equal ( "I think you are Sam." , decision . ReplyText ) ;
}
2026-05-14 21:39:17 -05:00
[Theory]
[InlineData("do you know me")]
[InlineData("do you remember me")]
[InlineData("who is this")]
[InlineData("can you recognize me")]
public async Task BuildDecisionAsync_IdentityFollowUp_UsesPersonScopedNameWhenSpeakerIsKnown ( string transcript )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
memoryStore . SetName ( new PersonalMemoryTenantScope ( "acct-c" , "loop-c" , "device-c" , "person-3" ) , "taylor" ) ;
var service = CreateService ( memoryStore ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = transcript ,
NormalizedTranscript = transcript ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-c" ,
["loopId"] = "loop-c" ,
["context"] = "" "{" runtime ":{" perception ":{" speaker ":" person - 3 "}," loop ":{" users ":[{" id ":" person - 3 "," firstName ":" taylor "}]}}}" ""
} ,
DeviceId = "device-c"
} ) ;
Assert . Equal ( "memory_get_name" , decision . IntentName ) ;
Assert . Equal ( "I think you are Taylor." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_IdentityFollowUp_RequestsNameWhenSpeakerIsUnknown ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do you know me" ,
NormalizedTranscript = "do you know me"
} ) ;
Assert . Equal ( "memory_get_name" , decision . IntentName ) ;
Assert . Equal ( "I do not know your name yet. You can say, my name is Alex." , decision . ReplyText ) ;
}
2026-05-09 23:46:00 -05:00
[Fact]
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_BuildsProactiveGreetingAndContext ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = string . Empty ,
NormalizedTranscript = string . Empty ,
Attributes = new Dictionary < string , object? >
{
["messageType"] = "TRIGGER" ,
["triggerSource"] = "PRESENCE" ,
["context"] = "" "{" runtime ":{" perception ":{" speaker ":" person - 1 "," peoplePresent ":[{" id ":" person - 1 "}]}," loop ":{" users ":[{" id ":" person - 1 "," firstName ":" jake "}]}}}" ""
}
} ) ;
Assert . Equal ( "proactive_greeting" , decision . IntentName ) ;
Assert . Contains ( "Jake" , decision . ReplyText , StringComparison . Ordinal ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "ProactiveGreeting" , decision . ContextUpdates ! [ GreetingRouteKey ] ) ;
Assert . Equal ( "person-1" , decision . ContextUpdates [ GreetingSpeakerKey ] ) ;
Assert . True ( DateTimeOffset . TryParse ( decision . ContextUpdates [ GreetingLastProactiveUtcKey ] ? . ToString ( ) , out _ ) ) ;
}
[Fact]
public async Task BuildDecisionAsync_TriggerFromSurprise_ReturnsSilentTriggerIgnoredDecision ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = string . Empty ,
NormalizedTranscript = string . Empty ,
Attributes = new Dictionary < string , object? >
{
["messageType"] = "TRIGGER" ,
["triggerSource"] = "SURPRISE" ,
["context"] = "" "{" runtime ":{" perception ":{" speaker ":" person - 1 "}," loop ":{" users ":[{" id ":" person - 1 "," firstName ":" jake "}]}}}" ""
}
} ) ;
Assert . Equal ( "trigger_ignored" , decision . IntentName ) ;
Assert . Equal ( string . Empty , decision . ReplyText ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . NotNull ( decision . SkillPayload ) ;
Assert . Equal ( "completion_only" , decision . SkillPayload ! [ "cloudResponseMode" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_TriggerWithinCooldown_IsIgnored ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = string . Empty ,
NormalizedTranscript = string . Empty ,
Attributes = new Dictionary < string , object? >
{
["messageType"] = "TRIGGER" ,
["triggerSource"] = "PRESENCE" ,
["context"] = "" "{" runtime ":{" perception ":{" speaker ":" person - 1 "}," loop ":{" users ":[{" id ":" person - 1 "," firstName ":" jake "}]}}}" "" ,
[GreetingLastProactiveUtcKey] = DateTimeOffset . UtcNow . ToString ( "O" )
}
} ) ;
Assert . Equal ( "trigger_ignored" , decision . IntentName ) ;
}
2026-05-05 20:27:58 -05:00
[Fact]
public async Task BuildDecisionAsync_DoYouHaveAPersonality_UsesCatalogBackedPersonalityReply ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do you have a personality" ,
NormalizedTranscript = "do you have a personality"
} ) ;
Assert . Equal ( "robot_personality" , decision . IntentName ) ;
Assert . Equal ( "I do. I am curious, playful, and always up for a new experiment." , decision . ReplyText ) ;
}
2026-05-14 21:15:14 -05:00
[Theory]
[InlineData("what is your favorite color")]
[InlineData("what's your favorite color")]
[InlineData("what color do you like")]
public async Task BuildDecisionAsync_FavoriteColor_UsesPersonalityReply ( string transcript )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = transcript ,
NormalizedTranscript = transcript
} ) ;
Assert . Equal ( "robot_favorite_color" , decision . IntentName ) ;
Assert . Equal ( "Blue." , decision . ReplyText ) ;
Assert . Equal ( "ScriptedResponse" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
}
[Theory]
[InlineData("what is your favorite food", "robot_favorite_food", "Pizza. It is hard to argue with pizza.")]
[InlineData("what is your favorite music", "robot_favorite_music", "Something upbeat with a good rhythm.")]
public async Task BuildDecisionAsync_FavoritesFamily_UsesPersonalityReplies (
string transcript ,
string expectedIntent ,
string expectedReply )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = transcript ,
NormalizedTranscript = transcript
} ) ;
Assert . Equal ( expectedIntent , decision . IntentName ) ;
Assert . Equal ( expectedReply , decision . ReplyText ) ;
Assert . Equal ( "ScriptedResponse" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
}
2026-05-14 06:44:22 -05:00
[Theory]
[InlineData("do you pay taxes", "robot_taxes", "From what I understand, robots don't ever pay anything.")]
[InlineData("what do you want", "robot_desire", "Socializing and electricity. I'd also be happy if everyone in the world was nicer to each other. It seems like they should be.")]
[InlineData("what's your name", "robot_name", "Jibo. Just Jibo, no last name. Like Bono")]
[InlineData("who made you", "robot_origin_created", "My story is pretty typical. Some people wanted to create something that would really help people. So they built a robot.")]
[InlineData("where are you from", "robot_origin_from", "Some people think I come from the moon. But they're wrong, I'm from Boston.")]
public async Task BuildDecisionAsync_LegacyBuildAQuestions_UseImportedScriptedReplies (
string transcript ,
string expectedIntent ,
string expectedReply )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = transcript ,
NormalizedTranscript = transcript
} ) ;
Assert . Equal ( expectedIntent , decision . IntentName ) ;
Assert . Equal ( expectedReply , decision . ReplyText ) ;
Assert . Null ( decision . SkillName ) ;
Assert . Equal ( "ScriptedResponse" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
}
2026-05-06 16:01:15 -05:00
[Fact]
public async Task BuildDecisionAsync_Hello_RoutesThroughChitchatScriptedResponse ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "hello" ,
NormalizedTranscript = "hello"
} ) ;
Assert . Equal ( "hello" , decision . IntentName ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "complete" , decision . ContextUpdates ! [ ChitchatStateKey ] ) ;
Assert . Equal ( "ScriptedResponse" , decision . ContextUpdates [ ChitchatRouteKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_AreYouHappy_RoutesThroughEmotionQuerySplit ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "are you happy" ,
NormalizedTranscript = "are you happy"
} ) ;
Assert . Equal ( "emotion_query" , decision . IntentName ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "EmotionQuery" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
Assert . Equal ( string . Empty , decision . ContextUpdates [ ChitchatEmotionKey ] ) ;
}
2026-05-14 06:44:22 -05:00
[Fact]
public async Task BuildDecisionAsync_AreYouHappy_UsesLegacyEmotionResponseWhenEmotionIsKnown ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "are you happy" ,
NormalizedTranscript = "are you happy" ,
Attributes = new Dictionary < string , object? >
{
[ChitchatEmotionKey] = "happy"
}
} ) ;
Assert . Equal ( "emotion_query" , decision . IntentName ) ;
Assert . Equal ( "Yes indeed. Never been better." , decision . ReplyText ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "EmotionQuery" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
}
2026-05-14 07:44:46 -05:00
[Fact]
public async Task BuildDecisionAsync_AreYouHappy_UsesNonBuildAEmotionCatalog ( )
{
var rootDirectory = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( "N" ) ) ;
Directory . CreateDirectory ( Path . Combine ( rootDirectory , "gqa-responses" ) ) ;
try
{
File . WriteAllText (
Path . Combine ( rootDirectory , "gqa-responses" , "GQA_JBO_IsHappy.mim" ) ,
"" "
{
"mim_type" : "announcement" ,
"prompts" : [
{
"condition" : "jibo.emotion==\"JOYFUL\"" ,
"prompt" : "The outside pack says I'm feeling joyful." ,
"prompt_id" : "GQA_JBO_IsHappy_AN_01"
} ,
{
"condition" : "!jibo.emotion || jibo.emotion==\"NEUTRAL\"" ,
"prompt" : "The outside pack says I'm on neutral." ,
"prompt_id" : "GQA_JBO_IsHappy_AN_02"
}
]
}
"" ");
var catalog = LegacyMimCatalogImporter . ImportCatalog ( rootDirectory ) ;
var service = CreateService ( contentRepository : new StaticCatalogRepository ( catalog ) ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "how are you" ,
NormalizedTranscript = "how are you" ,
Attributes = new Dictionary < string , object? >
{
[ChitchatEmotionKey] = "joyful"
}
} ) ;
Assert . Equal ( "how_are_you" , decision . IntentName ) ;
Assert . Equal ( "The outside pack says I'm feeling joyful." , decision . ReplyText ) ;
}
finally
{
Directory . Delete ( rootDirectory , recursive : true ) ;
}
}
2026-05-14 06:48:39 -05:00
[Theory]
[InlineData("joyful", "Yes indeed. Never been better.")]
[InlineData("pleased", "You know it. Life is good.")]
[InlineData("determined", "You're right. I am feeling pretty good at the moment.")]
[InlineData("confident", "All systems are go.")]
[InlineData("insecure", "Yes. Not too shabby.")]
[InlineData("neutral", "All systems are go.")]
public async Task BuildDecisionAsync_EmotionQuery_UsesStateDrivenLegacyReplies (
string emotion ,
string expectedReply )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "how are you" ,
NormalizedTranscript = "how are you" ,
Attributes = new Dictionary < string , object? >
{
[ChitchatEmotionKey] = emotion
}
} ) ;
Assert . Equal ( "how_are_you" , decision . IntentName ) ;
Assert . Equal ( expectedReply , decision . ReplyText ) ;
}
2026-05-06 16:01:15 -05:00
[Fact]
public async Task BuildDecisionAsync_Smile_RoutesThroughEmotionCommandSplit ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "smile" ,
NormalizedTranscript = "smile"
} ) ;
Assert . Equal ( "emotion_command" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . NotNull ( decision . SkillPayload ) ;
Assert . Contains ( "cat='happy'" , decision . SkillPayload ! [ "esml" ] ? . ToString ( ) , StringComparison . Ordinal ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "EmotionCommand" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
Assert . Equal ( "happy" , decision . ContextUpdates [ ChitchatEmotionKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_UnhandledChat_RoutesThroughErrorResponseSplit ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "blargh" ,
NormalizedTranscript = "blargh"
} ) ;
Assert . Equal ( "chat" , decision . IntentName ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "ErrorResponse" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
}
2026-05-06 16:19:45 -05:00
[Fact]
public async Task BuildDecisionAsync_HowAngryAreYou_RoutesThroughPegasusEmotionQueryPhrase ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "how angry are you" ,
NormalizedTranscript = "how angry are you"
} ) ;
Assert . Equal ( "emotion_query" , decision . IntentName ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "EmotionQuery" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_YouSeemSad_RoutesThroughPegasusEmotionAssertionPhrase ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "you seem sad" ,
NormalizedTranscript = "you seem sad"
} ) ;
Assert . Equal ( "emotion_query" , decision . IntentName ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "EmotionQuery" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_YouShouldTryToBeHappy_RoutesThroughPegasusEmotionCommandPhrase ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "you should try to be happy" ,
NormalizedTranscript = "you should try to be happy"
} ) ;
Assert . Equal ( "emotion_command" , decision . IntentName ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "EmotionCommand" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
Assert . Equal ( "happy" , decision . ContextUpdates [ ChitchatEmotionKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_DontBeAngry_RoutesThroughPegasusNegativeEmotionCommandPhrase ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "don't be angry" ,
NormalizedTranscript = "don't be angry"
} ) ;
Assert . Equal ( "emotion_command" , decision . IntentName ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "EmotionCommand" , decision . ContextUpdates ! [ ChitchatRouteKey ] ) ;
Assert . Equal ( "calm" , decision . ContextUpdates [ ChitchatEmotionKey ] ) ;
}
2026-05-05 22:40:11 -05:00
[Fact]
public async Task BuildDecisionAsync_BirthdayMemory_SetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my birthday is April 12" ,
NormalizedTranscript = "my birthday is April 12" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_birthday" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember your birthday is april 12." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "when is my birthday" ,
NormalizedTranscript = "when is my birthday" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_birthday" , recallDecision . IntentName ) ;
Assert . Equal ( "You told me your birthday is april 12." , recallDecision . ReplyText ) ;
}
2026-05-06 09:51:36 -05:00
[Fact]
public async Task BuildDecisionAsync_BirthdaySetAttemptWithoutValue_RoutesToBirthdayPrompt ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my birthday is" ,
NormalizedTranscript = "my birthday is"
} ) ;
Assert . Equal ( "memory_set_birthday" , decision . IntentName ) ;
Assert . Equal ( "I can remember it if you say, my birthday is March 14." , decision . ReplyText ) ;
}
2026-05-07 07:22:33 -05:00
[Fact]
public async Task BuildDecisionAsync_BirthdayMemory_BdayAliasSetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my bday is April 12" ,
NormalizedTranscript = "my bday is April 12" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_birthday" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember your birthday is april 12." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "when is my bday" ,
NormalizedTranscript = "when is my bday" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_birthday" , recallDecision . IntentName ) ;
Assert . Equal ( "You told me your birthday is april 12." , recallDecision . ReplyText ) ;
}
2026-05-05 22:40:11 -05:00
[Fact]
public async Task BuildDecisionAsync_PreferenceMemory_SetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my favorite music is jazz" ,
NormalizedTranscript = "my favorite music is jazz" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_preference" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember your favorite music is jazz." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what is my favorite music" ,
NormalizedTranscript = "what is my favorite music" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_preference" , recallDecision . IntentName ) ;
Assert . Equal ( "You told me your favorite music is jazz." , recallDecision . ReplyText ) ;
}
2026-05-07 07:22:33 -05:00
[Fact]
public async Task BuildDecisionAsync_PreferenceMemory_BareFavoriteSetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my favorite sport football" ,
NormalizedTranscript = "my favorite sport football" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_preference" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember your favorite sport is football." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what is my favorite sport" ,
NormalizedTranscript = "what is my favorite sport" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_preference" , recallDecision . IntentName ) ;
Assert . Equal ( "You told me your favorite sport is football." , recallDecision . ReplyText ) ;
}
2026-05-06 09:51:36 -05:00
[Fact]
public async Task BuildDecisionAsync_PreferenceSetAttemptWithoutValue_RoutesToPreferencePrompt ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my favorite music is" ,
NormalizedTranscript = "my favorite music is"
} ) ;
Assert . Equal ( "memory_set_preference" , decision . IntentName ) ;
Assert . Equal ( "I can remember it if you say, my favorite music is jazz." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_PreferenceSetAttemptSportWithoutValue_RoutesToPreferencePrompt ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my favorite sport." ,
NormalizedTranscript = "my favorite sport."
} ) ;
Assert . Equal ( "memory_set_preference" , decision . IntentName ) ;
Assert . Equal ( "I can remember it if you say, my favorite music is jazz." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_PreferenceRecallAttemptWithoutCategory_RoutesToRecallPrompt ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's my favorite" ,
NormalizedTranscript = "what's my favorite"
} ) ;
Assert . Equal ( "memory_get_preference" , decision . IntentName ) ;
Assert . Equal ( "Ask me like this: what is my favorite music?" , decision . ReplyText ) ;
}
2026-05-05 22:40:11 -05:00
[Fact]
public async Task BuildDecisionAsync_PersonalMemory_IsTenantScoped ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my birthday is April 12" ,
NormalizedTranscript = "my birthday is April 12" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
var otherTenantRecall = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what is my birthday" ,
NormalizedTranscript = "what is my birthday" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-b" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-b"
} ) ;
Assert . Equal ( "memory_get_birthday" , otherTenantRecall . IntentName ) ;
Assert . Equal ( "I do not know your birthday yet. You can say, my birthday is March 14." , otherTenantRecall . ReplyText ) ;
}
2026-05-06 08:13:26 -05:00
[Fact]
public async Task BuildDecisionAsync_NameMemory_SetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my name is Alex" ,
NormalizedTranscript = "my name is Alex" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_name" , setDecision . IntentName ) ;
Assert . Equal ( "Nice to meet you, alex. I will remember your name." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what is my name" ,
NormalizedTranscript = "what is my name" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_name" , recallDecision . IntentName ) ;
2026-05-14 20:48:55 -05:00
Assert . Equal ( "You told me your name is Alex." , recallDecision . ReplyText ) ;
2026-05-06 08:13:26 -05:00
}
[Fact]
public async Task BuildDecisionAsync_ImportantDateMemory_SetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "our anniversary is June 10" ,
NormalizedTranscript = "our anniversary is June 10" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_important_date" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember your anniversary is june 10." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "when is our anniversary" ,
NormalizedTranscript = "when is our anniversary" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_important_date" , recallDecision . IntentName ) ;
Assert . Equal ( "You told me your anniversary is june 10." , recallDecision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_SetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "I dislike mushrooms" ,
NormalizedTranscript = "I dislike mushrooms" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_affinity" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember you dislike mushrooms." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do i dislike mushrooms" ,
NormalizedTranscript = "do i dislike mushrooms" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_affinity" , recallDecision . IntentName ) ;
Assert . Equal ( "Yes. You told me you dislike mushrooms." , recallDecision . ReplyText ) ;
}
2026-05-06 16:49:26 -05:00
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusEnjoyPhrase_SetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "I enjoy country music" ,
NormalizedTranscript = "I enjoy country music" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_affinity" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember you like country music." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do i enjoy country music" ,
NormalizedTranscript = "do i enjoy country music" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_affinity" , recallDecision . IntentName ) ;
Assert . Equal ( "Yes. You told me you like country music." , recallDecision . ReplyText ) ;
}
2026-05-09 23:46:00 -05:00
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusWeLovePhrase_SetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "we love pizza" ,
NormalizedTranscript = "we love pizza" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_affinity" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember you love pizza." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do i love pizza" ,
NormalizedTranscript = "do i love pizza" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_affinity" , recallDecision . IntentName ) ;
Assert . Equal ( "Yes. You told me you love pizza." , recallDecision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusLoathePhrase_SetThenRecallWithinTenant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "I loathe celery" ,
NormalizedTranscript = "I loathe celery" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_affinity" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember you dislike celery." , setDecision . ReplyText ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do i loathe celery" ,
NormalizedTranscript = "do i loathe celery" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_affinity" , recallDecision . IntentName ) ;
Assert . Equal ( "Yes. You told me you dislike celery." , recallDecision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusDoYouThinkLikeLookup_SetsAndRecalls ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "I enjoy country music" ,
NormalizedTranscript = "I enjoy country music" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do you think i like country music" ,
NormalizedTranscript = "do you think i like country music" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_get_affinity" , recallDecision . IntentName ) ;
Assert . Equal ( "Yes. You told me you like country music." , recallDecision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_AffinitySetAttemptWithoutItem_RoutesToAffinityPrompt ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "we like" ,
NormalizedTranscript = "we like"
} ) ;
Assert . Equal ( "memory_set_affinity" , decision . IntentName ) ;
Assert . Equal ( "I can remember it if you say, I like pizza or I dislike mushrooms." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_AffinityRecallAttemptWithoutItem_RoutesToRecallPrompt ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "do you think i like" ,
NormalizedTranscript = "do you think i like"
} ) ;
Assert . Equal ( "memory_get_affinity" , decision . IntentName ) ;
Assert . Equal ( "Ask me like this: do I like pizza?" , decision . ReplyText ) ;
}
2026-05-06 08:13:26 -05:00
[Fact]
public async Task BuildDecisionAsync_PreferenceReversePhrase_ParsesFavoriteVariant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "pizza is my favorite food" ,
NormalizedTranscript = "pizza is my favorite food" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_preference" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember your favorite food is pizza." , setDecision . ReplyText ) ;
}
2026-05-06 16:49:26 -05:00
[Fact]
public async Task BuildDecisionAsync_PreferenceReversePluralPhrase_ParsesFavoriteVariant ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var setDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "dogs are my favorite animals" ,
NormalizedTranscript = "dogs are my favorite animals" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "memory_set_preference" , setDecision . IntentName ) ;
Assert . Equal ( "Got it. I will remember your favorite animals is dogs." , setDecision . ReplyText ) ;
}
2026-05-06 08:13:26 -05:00
[Fact]
public async Task BuildDecisionAsync_Surprise_WithPizzaPreference_UsesPizzaProactivity ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "my favorite food is pizza" ,
NormalizedTranscript = "my favorite food is pizza" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "surprise me" ,
NormalizedTranscript = "surprise me" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "proactive_pizza_preference" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . Equal ( "RA_JBO_MakePizza" , decision . SkillPayload ! [ "mim_id" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_Surprise_OnNationalPizzaDay_UsesHolidayProactivity ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "surprise me" ,
NormalizedTranscript = "surprise me" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 02 - 09 T10 : 45 : 00 - 06 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "proactive_pizza_day" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . Contains ( "National Pizza Day" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
}
[Fact]
public async Task BuildDecisionAsync_PendingPizzaFactOffer_YesMapsToFact ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "yes" ,
NormalizedTranscript = "yes" ,
Attributes = new Dictionary < string , object? >
{
["pendingProactivityOffer"] = "pizza_fact"
}
} ) ;
Assert . Equal ( "proactive_pizza_fact" , decision . IntentName ) ;
Assert . Contains ( "350 slices per second" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
}
2026-05-10 21:22:25 -05:00
[Fact]
public async Task BuildDecisionAsync_PendingPizzaFactOffer_YesWithTailMapsToFact ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "yes I want to" ,
NormalizedTranscript = "yes I want to" ,
Attributes = new Dictionary < string , object? >
{
["pendingProactivityOffer"] = "pizza_fact"
}
} ) ;
Assert . Equal ( "proactive_pizza_fact" , decision . IntentName ) ;
Assert . Contains ( "350 slices per second" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
}
2026-05-06 08:13:26 -05:00
[Fact]
public async Task BuildDecisionAsync_PendingPizzaFactOffer_NoMapsToDecline ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "no" ,
NormalizedTranscript = "no" ,
Attributes = new Dictionary < string , object? >
{
["pendingProactivityOffer"] = "pizza_fact"
}
} ) ;
Assert . Equal ( "proactive_offer_declined" , decision . IntentName ) ;
Assert . Equal ( "No problem. We can save the pizza fact for another time." , decision . ReplyText ) ;
}
2026-05-10 21:22:25 -05:00
[Fact]
public async Task BuildDecisionAsync_PendingPizzaFactOffer_NoWithTailMapsToDecline ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "no I do not" ,
NormalizedTranscript = "no I do not" ,
Attributes = new Dictionary < string , object? >
{
["pendingProactivityOffer"] = "pizza_fact"
}
} ) ;
Assert . Equal ( "proactive_offer_declined" , decision . IntentName ) ;
Assert . Equal ( "No problem. We can save the pizza fact for another time." , decision . ReplyText ) ;
}
2026-05-05 20:27:58 -05:00
[Fact]
public async Task BuildDecisionAsync_MakePizza_UsesOriginalMimStylePayload ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "make a pizza" ,
NormalizedTranscript = "make a pizza"
} ) ;
Assert . Equal ( "pizza" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . Equal ( "One pizza, coming right up." , decision . ReplyText ) ;
Assert . Equal ( "RA_JBO_MakePizza" , decision . SkillPayload ! [ "mim_id" ] ) ;
Assert . Equal ( "RA_JBO_ShowPizzaMaking_AN_01" , decision . SkillPayload [ "prompt_id" ] ) ;
Assert . Contains ( "pizza-making" , decision . SkillPayload [ "esml" ] ? . ToString ( ) , StringComparison . Ordinal ) ;
}
[Fact]
public async Task BuildDecisionAsync_ClientNluRequestMakePizza_UsesOriginalMimStylePayload ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "requestMakePizza" ,
NormalizedTranscript = "requestMakePizza" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "requestMakePizza"
}
} ) ;
Assert . Equal ( "pizza" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . Equal ( "RA_JBO_MakePizza" , decision . SkillPayload ! [ "mim_id" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_CanYouOrderPizza_UsesLegacyOrderPizzaMimPayload ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "can you order pizza" ,
NormalizedTranscript = "can you order pizza"
} ) ;
Assert . Equal ( "order_pizza" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . Equal ( "RA_JBO_OrderPizza" , decision . SkillPayload ! [ "mim_id" ] ) ;
Assert . Equal ( "RA_JBO_OrderPizza_AN_01" , decision . SkillPayload [ "prompt_id" ] ) ;
Assert . Contains ( "I can't do that yet" , decision . SkillPayload [ "esml" ] ? . ToString ( ) , StringComparison . Ordinal ) ;
}
2026-05-05 22:27:28 -05:00
[Fact]
public async Task BuildDecisionAsync_OrderAPizza_UsesLegacyOrderPizzaMimPayload ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "order a pizza" ,
NormalizedTranscript = "order a pizza"
} ) ;
Assert . Equal ( "order_pizza" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . Equal ( "RA_JBO_OrderPizza" , decision . SkillPayload ! [ "mim_id" ] ) ;
}
2026-05-06 09:51:36 -05:00
[Fact]
public async Task BuildDecisionAsync_CanYouOrderAPizzaWithPunctuation_UsesLegacyOrderPizzaMimPayload ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "Can you order a pizza?" ,
NormalizedTranscript = "Can you order a pizza?"
} ) ;
Assert . Equal ( "order_pizza" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . Equal ( "RA_JBO_OrderPizza" , decision . SkillPayload ! [ "mim_id" ] ) ;
}
2026-05-05 20:27:58 -05:00
[Fact]
public async Task BuildDecisionAsync_ClientNluRequestOrderPizza_UsesLegacyOrderPizzaMimPayload ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "requestOrderPizza" ,
NormalizedTranscript = "requestOrderPizza" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "requestOrderPizza"
}
} ) ;
Assert . Equal ( "order_pizza" , decision . IntentName ) ;
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
Assert . Equal ( "RA_JBO_OrderPizza" , decision . SkillPayload ! [ "mim_id" ] ) ;
}
2026-05-06 14:50:37 -05:00
[Fact]
public async Task BuildDecisionAsync_PersonalReport_StartsOptInStateMachine ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "personal report" ,
NormalizedTranscript = "personal report"
} ) ;
Assert . Equal ( "personal_report_opt_in" , decision . IntentName ) ;
Assert . Equal ( "Would you like your personal report now?" , decision . ReplyText ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "awaiting_opt_in" , decision . ContextUpdates ! [ PersonalReportStateKey ] ) ;
Assert . Equal ( true , decision . ContextUpdates [ PersonalReportWeatherEnabledKey ] ) ;
Assert . Equal ( true , decision . ContextUpdates [ PersonalReportCalendarEnabledKey ] ) ;
Assert . Equal ( true , decision . ContextUpdates [ PersonalReportCommuteEnabledKey ] ) ;
Assert . Equal ( true , decision . ContextUpdates [ PersonalReportNewsEnabledKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_OptInYesWithKnownName_AsksForIdentityConfirmation ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
memoryStore . SetName ( new PersonalMemoryTenantScope ( "acct-a" , "loop-a" , "device-a" ) , "alex" ) ;
var service = CreateService ( memoryStore ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "yes" ,
NormalizedTranscript = "yes" ,
DeviceId = "device-a" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a" ,
[PersonalReportStateKey] = "awaiting_opt_in"
}
} ) ;
Assert . Equal ( "personal_report_verify_user" , decision . IntentName ) ;
Assert . Equal ( "I think this is alex. Is that right?" , decision . ReplyText ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "awaiting_identity_confirmation" , decision . ContextUpdates ! [ PersonalReportStateKey ] ) ;
Assert . Equal ( "alex" , decision . ContextUpdates [ PersonalReportUserNameKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_VerifiedIdentity_DeliversReportAndResetsState ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Boston, U.S." , "light rain" , 61 , 65 , 54 , "rain" , false )
2026-05-06 14:50:37 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "yes" ,
NormalizedTranscript = "yes" ,
Attributes = new Dictionary < string , object? >
{
[PersonalReportStateKey] = "awaiting_identity_confirmation" ,
[PersonalReportUserNameKey] = "alex"
}
} ) ;
Assert . Equal ( "personal_report_delivered" , decision . IntentName ) ;
Assert . Contains ( "Great, alex. Here is your personal report." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-05-11 19:59:15 -05:00
Assert . Contains ( "Right now in Boston, U.S., it is light rain and 61 degrees Fahrenheit." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-05-06 14:50:37 -05:00
Assert . Contains ( "That is your personal report." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "idle" , decision . ContextUpdates ! [ PersonalReportStateKey ] ) ;
Assert . Equal ( true , decision . ContextUpdates [ PersonalReportUserVerifiedKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_NoMatchRetriesThenDeclines ( )
{
var service = CreateService ( ) ;
var firstDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "maybe" ,
NormalizedTranscript = "maybe" ,
Attributes = new Dictionary < string , object? >
{
[PersonalReportStateKey] = "awaiting_opt_in" ,
[PersonalReportNoMatchCountKey] = 0
}
} ) ;
Assert . Equal ( "personal_report_no_match" , firstDecision . IntentName ) ;
Assert . NotNull ( firstDecision . ContextUpdates ) ;
Assert . Equal ( 1 , firstDecision . ContextUpdates ! [ PersonalReportNoMatchCountKey ] ) ;
var secondDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "maybe" ,
NormalizedTranscript = "maybe" ,
Attributes = new Dictionary < string , object? >
{
[PersonalReportStateKey] = "awaiting_opt_in" ,
[PersonalReportNoMatchCountKey] = 1
}
} ) ;
Assert . Equal ( "personal_report_declined" , secondDecision . IntentName ) ;
Assert . NotNull ( secondDecision . ContextUpdates ) ;
Assert . Equal ( "idle" , secondDecision . ContextUpdates ! [ PersonalReportStateKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_StartCanApplyToggleHints ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "personal report without weather and no news" ,
NormalizedTranscript = "personal report without weather and no news"
} ) ;
Assert . Equal ( "personal_report_opt_in" , decision . IntentName ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( false , decision . ContextUpdates ! [ PersonalReportWeatherEnabledKey ] ) ;
Assert . Equal ( false , decision . ContextUpdates [ PersonalReportNewsEnabledKey ] ) ;
Assert . Equal ( true , decision . ContextUpdates [ PersonalReportCalendarEnabledKey ] ) ;
Assert . Equal ( true , decision . ContextUpdates [ PersonalReportCommuteEnabledKey ] ) ;
}
2026-05-14 07:44:46 -05:00
[Theory]
[InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping")]
[InlineData("to do list", "todo_list_prompt", "What should I add to your to-do list?", "todo")]
public async Task BuildDecisionAsync_ListStart_PromptsForFollowUpItems (
string transcript ,
string expectedIntent ,
string expectedReply ,
string expectedListType )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = transcript ,
NormalizedTranscript = transcript
} ) ;
Assert . Equal ( expectedIntent , decision . IntentName ) ;
Assert . Equal ( expectedReply , decision . ReplyText ) ;
Assert . NotNull ( decision . ContextUpdates ) ;
Assert . Equal ( "awaiting_item" , decision . ContextUpdates ! [ HouseholdListStateKey ] ) ;
Assert . Equal ( expectedListType , decision . ContextUpdates [ HouseholdListTypeKey ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_ShoppingList_FollowUpFlow_AddsItemsAndRecallsThem ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var tenantAttributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ;
var promptDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "shopping list" ,
NormalizedTranscript = "shopping list" ,
DeviceId = "device-a" ,
Attributes = new Dictionary < string , object? > ( tenantAttributes )
} ) ;
Assert . Equal ( "shopping_list_prompt" , promptDecision . IntentName ) ;
Assert . Equal ( "awaiting_item" , promptDecision . ContextUpdates ! [ HouseholdListStateKey ] ) ;
Assert . Equal ( "shopping" , promptDecision . ContextUpdates [ HouseholdListTypeKey ] ) ;
var addDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "milk" ,
NormalizedTranscript = "milk" ,
DeviceId = "device-a" ,
Attributes = new Dictionary < string , object? > ( tenantAttributes )
{
[HouseholdListStateKey] = promptDecision . ContextUpdates [ HouseholdListStateKey ] ,
[HouseholdListTypeKey] = promptDecision . ContextUpdates [ HouseholdListTypeKey ]
}
} ) ;
Assert . Equal ( "shopping_list_add" , addDecision . IntentName ) ;
Assert . Contains ( "Added milk to your shopping list." , addDecision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Contains ( "What else should I add?" , addDecision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Equal ( "awaiting_item" , addDecision . ContextUpdates ! [ HouseholdListStateKey ] ) ;
Assert . Equal ( "shopping" , addDecision . ContextUpdates [ HouseholdListTypeKey ] ) ;
Assert . Equal ( [ "milk" ] , memoryStore . GetListItems ( new PersonalMemoryTenantScope ( "acct-a" , "loop-a" , "device-a" ) , "shopping" ) ) ;
var doneDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "that's it" ,
NormalizedTranscript = "that's it" ,
DeviceId = "device-a" ,
Attributes = new Dictionary < string , object? > ( tenantAttributes )
{
[HouseholdListStateKey] = addDecision . ContextUpdates [ HouseholdListStateKey ] ,
[HouseholdListTypeKey] = addDecision . ContextUpdates [ HouseholdListTypeKey ]
}
} ) ;
Assert . Equal ( "shopping_list_done" , doneDecision . IntentName ) ;
Assert . Contains ( "Okay. Your shopping list has milk." , doneDecision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Equal ( "idle" , doneDecision . ContextUpdates ! [ HouseholdListStateKey ] ) ;
var recallDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's on my shopping list" ,
NormalizedTranscript = "what's on my shopping list" ,
DeviceId = "device-a" ,
Attributes = new Dictionary < string , object? > ( tenantAttributes )
} ) ;
Assert . Equal ( "shopping_list_recall" , recallDecision . IntentName ) ;
Assert . Contains ( "milk" , recallDecision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
}
[Fact]
public async Task BuildDecisionAsync_TodoList_FollowUpFlow_AddsItemAndCanBeCompleted ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var service = CreateService ( memoryStore ) ;
var tenantAttributes = new Dictionary < string , object? >
{
["accountId"] = "acct-b" ,
["loopId"] = "loop-b"
} ;
var promptDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "to do list" ,
NormalizedTranscript = "to do list" ,
DeviceId = "device-b" ,
Attributes = new Dictionary < string , object? > ( tenantAttributes )
} ) ;
Assert . Equal ( "todo_list_prompt" , promptDecision . IntentName ) ;
Assert . Equal ( "awaiting_item" , promptDecision . ContextUpdates ! [ HouseholdListStateKey ] ) ;
Assert . Equal ( "todo" , promptDecision . ContextUpdates [ HouseholdListTypeKey ] ) ;
var addDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "call mom" ,
NormalizedTranscript = "call mom" ,
DeviceId = "device-b" ,
Attributes = new Dictionary < string , object? > ( tenantAttributes )
{
[HouseholdListStateKey] = promptDecision . ContextUpdates [ HouseholdListStateKey ] ,
[HouseholdListTypeKey] = promptDecision . ContextUpdates [ HouseholdListTypeKey ]
}
} ) ;
Assert . Equal ( "todo_list_add" , addDecision . IntentName ) ;
Assert . Contains ( "call mom" , addDecision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Contains ( "What else should I add?" , addDecision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Equal ( [ "call mom" ] , memoryStore . GetListItems ( new PersonalMemoryTenantScope ( "acct-b" , "loop-b" , "device-b" ) , "todo" ) ) ;
var doneDecision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "finished" ,
NormalizedTranscript = "finished" ,
DeviceId = "device-b" ,
Attributes = new Dictionary < string , object? > ( tenantAttributes )
{
[HouseholdListStateKey] = addDecision . ContextUpdates ! [ HouseholdListStateKey ] ,
[HouseholdListTypeKey] = addDecision . ContextUpdates [ HouseholdListTypeKey ]
}
} ) ;
Assert . Equal ( "todo_list_done" , doneDecision . IntentName ) ;
Assert . Contains ( "Okay. Your to-do list has call mom." , doneDecision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Equal ( "idle" , doneDecision . ContextUpdates ! [ HouseholdListStateKey ] ) ;
}
2026-05-06 08:13:26 -05:00
[Fact]
2026-05-06 10:49:24 -05:00
public async Task BuildDecisionAsync_WeatherQuery_WithoutProvider_UsesSpokenFallback ( )
2026-05-06 08:13:26 -05:00
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "how is the weather" ,
NormalizedTranscript = "how is the weather"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
2026-05-06 10:49:24 -05:00
Assert . Null ( decision . SkillName ) ;
Assert . Null ( decision . SkillPayload ) ;
Assert . Equal ( "I can check weather once my weather service is connected." , decision . ReplyText ) ;
2026-05-06 08:13:26 -05:00
}
[Fact]
2026-05-06 10:49:24 -05:00
public async Task BuildDecisionAsync_WeatherTomorrowQuery_WithoutProvider_StillReturnsFallback ( )
2026-05-06 08:13:26 -05:00
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather tomorrow" ,
NormalizedTranscript = "what's the weather tomorrow"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
2026-05-06 10:49:24 -05:00
Assert . Equal ( "I can check weather once my weather service is connected." , decision . ReplyText ) ;
2026-05-06 08:13:26 -05:00
}
[Fact]
2026-05-06 10:49:24 -05:00
public async Task BuildDecisionAsync_WeatherConditionQuery_WithoutProvider_StillReturnsFallback ( )
2026-05-06 08:13:26 -05:00
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "will it rain tomorrow" ,
NormalizedTranscript = "will it rain tomorrow"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
2026-05-06 10:49:24 -05:00
Assert . Equal ( "I can check weather once my weather service is connected." , decision . ReplyText ) ;
2026-05-06 08:13:26 -05:00
}
2026-05-07 07:22:33 -05:00
[Fact]
public async Task BuildDecisionAsync_WeatherTodaysForecastQuery_WithoutProvider_StillReturnsFallback ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's today's weather look like" ,
NormalizedTranscript = "what's today's weather look like"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "I can check weather once my weather service is connected." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_WeatherConditionForecastQuery_WithoutProvider_StillReturnsFallback ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "will it be sunny tomorrow" ,
NormalizedTranscript = "will it be sunny tomorrow"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "I can check weather once my weather service is connected." , decision . ReplyText ) ;
}
2026-05-06 08:13:26 -05:00
[Fact]
2026-05-06 10:49:24 -05:00
public async Task BuildDecisionAsync_ClientNluRequestWeatherPR_WithoutProvider_StillReturnsFallback ( )
2026-05-06 08:13:26 -05:00
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "requestWeatherPR" ,
NormalizedTranscript = "requestWeatherPR" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "requestWeatherPR"
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
2026-05-06 10:49:24 -05:00
Assert . Equal ( "I can check weather once my weather service is connected." , decision . ReplyText ) ;
2026-05-06 08:13:26 -05:00
}
[Fact]
public async Task BuildDecisionAsync_WeatherQuery_WithProvider_UsesProviderSummary ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Boston, U.S." , "light rain" , 61 , 65 , 54 , "rain" , false )
2026-05-06 08:13:26 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "how is the weather" ,
NormalizedTranscript = "how is the weather"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
2026-05-11 07:26:56 -05:00
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
2026-05-07 07:22:33 -05:00
Assert . NotNull ( decision . SkillPayload ) ;
Assert . Contains ( "cat='weather'" , decision . SkillPayload ! [ "esml" ] ? . ToString ( ) , StringComparison . OrdinalIgnoreCase ) ;
Assert . Contains ( "meta='rain'" , decision . SkillPayload [ "esml" ] ? . ToString ( ) , StringComparison . OrdinalIgnoreCase ) ;
2026-05-10 23:08:06 -05:00
Assert . Equal ( "report-skill" , decision . SkillPayload [ "skillId" ] ) ;
2026-05-07 07:22:33 -05:00
Assert . Equal ( "WeatherCommentRain" , decision . SkillPayload [ "mim_id" ] ) ;
2026-05-07 07:48:51 -05:00
Assert . Equal ( true , decision . SkillPayload [ "weather_view_enabled" ] ) ;
Assert . Equal ( "weatherHiLo" , decision . SkillPayload [ "weather_view_kind" ] ) ;
Assert . Equal ( "rain" , decision . SkillPayload [ "weather_icon" ] ) ;
Assert . Equal ( 65 , decision . SkillPayload [ "weather_high" ] ) ;
Assert . Equal ( 54 , decision . SkillPayload [ "weather_low" ] ) ;
Assert . Equal ( "F" , decision . SkillPayload [ "weather_unit" ] ) ;
Assert . Equal ( "Normal" , decision . SkillPayload [ "weather_theme" ] ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "Right now in Boston, U.S., it is light rain and 61 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-06 10:49:24 -05:00
Assert . NotNull ( provider . LastRequest ) ;
Assert . False ( provider . LastRequest ! . IsTomorrow ) ;
2026-05-09 09:21:45 -05:00
Assert . Equal ( 0 , provider . LastRequest . ForecastDayOffset ) ;
2026-05-06 08:13:26 -05:00
}
[Fact]
public async Task BuildDecisionAsync_WeatherLocationTomorrow_WithProvider_PassesLocationAndTomorrowRequest ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Chicago, U.S." , "mostly cloudy" , 72 , 74 , 60 , "cloudy" , false )
2026-05-06 08:13:26 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather in chicago tomorrow" ,
NormalizedTranscript = "what's the weather in chicago tomorrow"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Chicago" , provider . LastRequest ? . LocationQuery ) ;
Assert . True ( provider . LastRequest ? . IsTomorrow ) ;
2026-05-09 09:21:45 -05:00
Assert . Equal ( 1 , provider . LastRequest ? . ForecastDayOffset ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "Tomorrow in Chicago, U.S., expect mostly cloudy with a high near 74 degrees Fahrenheit and a low around 60 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-06 08:13:26 -05:00
}
2026-05-07 07:22:33 -05:00
[Fact]
public async Task BuildDecisionAsync_WeatherLocationForToday_WithProvider_PassesLocation ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Seattle, U.S." , "light rain" , 58 , 61 , 52 , "rain" , false )
2026-05-07 07:22:33 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather for seattle today" ,
NormalizedTranscript = "what's the weather for seattle today"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Seattle" , provider . LastRequest ? . LocationQuery ) ;
Assert . False ( provider . LastRequest ? . IsTomorrow ) ;
2026-05-09 09:21:45 -05:00
Assert . Equal ( 0 , provider . LastRequest ? . ForecastDayOffset ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "Right now in Seattle, U.S., it is light rain and 58 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-07 07:22:33 -05:00
}
[Fact]
public async Task BuildDecisionAsync_WeatherLocationWithWeekendSuffix_WithProvider_PassesLocation ( )
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot ( "Paris, FR" , "overcast clouds" , 66 , 70 , 60 , "cloudy" , false )
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather in paris this weekend" ,
NormalizedTranscript = "what's the weather in paris this weekend"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Paris" , provider . LastRequest ? . LocationQuery ) ;
Assert . False ( provider . LastRequest ? . IsTomorrow ) ;
2026-05-09 09:21:45 -05:00
Assert . Equal ( 0 , provider . LastRequest ? . ForecastDayOffset ) ;
2026-05-07 07:22:33 -05:00
Assert . Equal ( "Right now in Paris, FR, it is overcast clouds and 66 degrees Fahrenheit." , decision . ReplyText ) ;
}
2026-05-09 09:21:45 -05:00
[Fact]
public async Task BuildDecisionAsync_TemperatureLocationQuery_WithProvider_MapsToWeatherIntent ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Redmond, U.S." , "clear sky" , 63 , 66 , 52 , "sunny" , false )
2026-05-09 09:21:45 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what is the temperature in redmond oregon" ,
NormalizedTranscript = "what is the temperature in redmond oregon"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Redmond Oregon" , provider . LastRequest ? . LocationQuery ) ;
Assert . False ( provider . LastRequest ? . IsTomorrow ) ;
Assert . Equal ( 0 , provider . LastRequest ? . ForecastDayOffset ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "Right now in Redmond, U.S., it is clear sky and 63 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-09 09:21:45 -05:00
}
[Fact]
public async Task BuildDecisionAsync_ForecastLocationQuery_WithProvider_MapsToWeatherIntent ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "New York, U.S." , "partly cloudy" , 71 , 76 , 61 , "cloudy" , false )
2026-05-09 09:21:45 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "forecast for new york city" ,
NormalizedTranscript = "forecast for new york city"
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "New York City" , provider . LastRequest ? . LocationQuery ) ;
2026-05-09 21:46:03 -05:00
Assert . True ( provider . LastRequest ? . IsTomorrow ) ;
Assert . Equal ( 1 , provider . LastRequest ? . ForecastDayOffset ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "Tomorrow in New York, U.S., expect partly cloudy with a high near 76 degrees Fahrenheit and a low around 61 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-09 21:46:03 -05:00
}
[Fact]
2026-05-12 20:36:43 -05:00
public async Task BuildDecisionAsync_ForecastWithoutDate_WithProvider_ReturnsFiveDaySummary ( )
2026-05-09 21:46:03 -05:00
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Kansas City, U.S." , "clear sky" , 72 , 79 , 63 , "sunny" , false )
2026-05-09 21:46:03 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the forecast" ,
2026-05-12 20:36:43 -05:00
NormalizedTranscript = "what's the forecast" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
2026-05-09 21:46:03 -05:00
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Null ( provider . LastRequest ? . LocationQuery ) ;
2026-05-12 20:36:43 -05:00
Assert . False ( provider . LastRequest ? . IsTomorrow ) ;
Assert . Equal ( 5 , provider . LastRequest ? . ForecastDayOffset ) ;
Assert . Equal ( 5 , provider . Requests . Count ) ;
Assert . Contains ( "next five-day forecast" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Contains ( "Tuesday: clear sky, high 79, low 63." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Contains ( "Saturday: clear sky, high 79, low 63." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-05-09 21:46:03 -05:00
}
[Fact]
public async Task BuildDecisionAsync_WeatherLocationQuery_IgnoresRuntimeCoordinates ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Chicago, U.S." , "mostly cloudy" , 70 , 75 , 62 , "cloudy" , false )
2026-05-09 21:46:03 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather in chicago" ,
NormalizedTranscript = "what's the weather in chicago" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" lat ":39.0997," lng ":-94.5786," iso ":" 2026 - 05 - 09 T09 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Chicago" , provider . LastRequest ? . LocationQuery ) ;
Assert . Null ( provider . LastRequest ? . Latitude ) ;
Assert . Null ( provider . LastRequest ? . Longitude ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "Right now in Chicago, U.S., it is mostly cloudy and 70 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-09 21:46:03 -05:00
}
2026-05-12 07:52:38 -05:00
[Fact]
public async Task BuildDecisionAsync_WeatherLocationQuery_WithClientDateEntity_PrefersTranscriptCurrentWeather ( )
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot ( "Chicago, U.S." , "mostly cloudy" , 70 , 75 , 62 , "cloudy" , false )
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather in chicago" ,
NormalizedTranscript = "what's the weather in chicago" ,
Attributes = new Dictionary < string , object? >
{
["clientEntities"] = new Dictionary < string , object? >
{
["date"] = "2026-05-18"
} ,
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 05 - 12 T07 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Chicago" , provider . LastRequest ? . LocationQuery ) ;
Assert . Equal ( 0 , provider . LastRequest ? . ForecastDayOffset ) ;
Assert . False ( provider . LastRequest ? . IsTomorrow ) ;
Assert . Equal ( "Right now in Chicago, U.S., it is mostly cloudy and 70 degrees Fahrenheit." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_ForecastLocationQuery_WithClientDateEntity_DefaultsToTomorrow ( )
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot ( "Chicago, U.S." , "mostly cloudy" , 70 , 75 , 62 , "cloudy" , false )
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the forecast in chicago" ,
NormalizedTranscript = "what's the forecast in chicago" ,
Attributes = new Dictionary < string , object? >
{
["clientEntities"] = new Dictionary < string , object? >
{
["date"] = "2026-05-18"
} ,
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 05 - 12 T07 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Chicago" , provider . LastRequest ? . LocationQuery ) ;
Assert . Equal ( 1 , provider . LastRequest ? . ForecastDayOffset ) ;
Assert . True ( provider . LastRequest ? . IsTomorrow ) ;
Assert . Equal ( "Tomorrow in Chicago, U.S., expect mostly cloudy with a high near 75 degrees Fahrenheit and a low around 62 degrees Fahrenheit." , decision . ReplyText ) ;
}
2026-05-09 21:46:03 -05:00
[Theory]
[InlineData("how is the weather", null, 0, false)]
2026-05-12 20:36:43 -05:00
[InlineData("what's the forecast", null, 5, false)]
2026-05-09 21:46:03 -05:00
[InlineData("forecast for new york city", "New York City", 1, true)]
[InlineData("what's today's forecast", null, 0, false)]
[InlineData("what's the weather in chicago", "Chicago", 0, false)]
[InlineData("what's the weather in chicago tomorrow", "Chicago", 1, true)]
[InlineData("what is the temperature in redmond oregon", "Redmond Oregon", 0, false)]
[InlineData("will it rain tomorrow", null, 1, true)]
public async Task BuildDecisionAsync_WeatherPromptRegression_MatchesExpectedRouting (
string transcript ,
string? expectedLocationQuery ,
int expectedForecastOffset ,
bool expectedIsTomorrow )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Test City, U.S." , "light rain" , 62 , 66 , 55 , "rain" , false )
2026-05-09 21:46:03 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = transcript ,
NormalizedTranscript = transcript
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . NotNull ( provider . LastRequest ) ;
Assert . Equal ( expectedLocationQuery , provider . LastRequest ! . LocationQuery ) ;
Assert . Equal ( expectedForecastOffset , provider . LastRequest . ForecastDayOffset ) ;
Assert . Equal ( expectedIsTomorrow , provider . LastRequest . IsTomorrow ) ;
2026-05-11 07:26:56 -05:00
Assert . Equal ( "chitchat-skill" , decision . SkillName ) ;
2026-05-09 21:46:03 -05:00
Assert . Equal ( true , decision . SkillPayload ? [ "weather_view_enabled" ] ) ;
2026-05-12 20:36:43 -05:00
if ( string . Equals ( transcript , "what's the forecast" , StringComparison . Ordinal ) )
{
Assert . Equal ( 5 , provider . Requests . Count ) ;
}
2026-05-09 09:21:45 -05:00
}
[Fact]
public async Task BuildDecisionAsync_WeatherQueryWithClientDateEntity_UsesForecastDayOffset ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Portland, U.S." , "scattered clouds" , 64 , 68 , 53 , "cloudy" , false )
2026-05-09 09:21:45 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather" ,
NormalizedTranscript = "what's the weather" ,
Attributes = new Dictionary < string , object? >
{
["clientEntities"] = new Dictionary < string , object? >
{
["date"] = "2026-05-11"
} ,
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 05 - 09 T09 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( 2 , provider . LastRequest ? . ForecastDayOffset ) ;
Assert . False ( provider . LastRequest ? . IsTomorrow ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "On Monday in Portland, U.S., expect scattered clouds with a high near 68 degrees Fahrenheit and a low around 53 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-09 09:21:45 -05:00
}
[Fact]
public async Task BuildDecisionAsync_WeatherQueryWithWeekday_UsesForecastDayOffset ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Chicago, U.S." , "light rain" , 59 , 63 , 51 , "rain" , false )
2026-05-09 09:21:45 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather in chicago on tuesday" ,
NormalizedTranscript = "what's the weather in chicago on tuesday" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Chicago" , provider . LastRequest ? . LocationQuery ) ;
Assert . Equal ( 1 , provider . LastRequest ? . ForecastDayOffset ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "On Tuesday in Chicago, U.S., expect light rain with a high near 63 degrees Fahrenheit and a low around 51 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-09 09:21:45 -05:00
}
[Fact]
public async Task BuildDecisionAsync_WeatherQueryBeyondSupportedForecastRange_ReturnsGuardrailMessage ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Chicago, U.S." , "light rain" , 59 , 63 , 51 , "rain" , false )
2026-05-09 09:21:45 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather next saturday" ,
NormalizedTranscript = "what's the weather next saturday" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "I can forecast up to 5 days ahead. Try tomorrow or another day this week." , decision . ReplyText ) ;
Assert . Null ( provider . LastRequest ) ;
}
[Fact]
public async Task BuildDecisionAsync_WeatherThisWeekend_WithContext_UsesWeekendOffset ( )
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot ( "Paris, FR" , "overcast clouds" , 66 , 70 , 60 , "cloudy" , false )
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather in paris this weekend" ,
NormalizedTranscript = "what's the weather in paris this weekend" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Paris" , provider . LastRequest ? . LocationQuery ) ;
Assert . Equal ( 5 , provider . LastRequest ? . ForecastDayOffset ) ;
Assert . Equal ( "This weekend in Paris, FR, expect overcast clouds with a high near 70 degrees Fahrenheit and a low around 60 degrees Fahrenheit." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_WeatherThisWeek_WithContext_UsesRangeOffset ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Seattle, U.S." , "light rain" , 58 , 61 , 52 , "rain" , false )
2026-05-09 09:21:45 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "forecast for seattle this week" ,
NormalizedTranscript = "forecast for seattle this week" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Seattle" , provider . LastRequest ? . LocationQuery ) ;
2026-05-11 07:15:11 -05:00
Assert . Equal ( 5 , provider . LastRequest ? . ForecastDayOffset ) ;
Assert . Equal ( 5 , provider . Requests . Count ) ;
Assert . Contains ( "rest of this week's forecast" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Contains ( "Tuesday: light rain, high 61, low 52." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Contains ( "Saturday: light rain, high 61, low 52." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . Contains ( "Temperatures are in Fahrenheit." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-05-11 22:44:56 -05:00
Assert . NotNull ( decision . SkillPayload ) ;
Assert . True ( decision . SkillPayload ! . TryGetValue ( "weather_weekly_cards" , out var weeklyCardsValue ) ) ;
var weeklyCards = Assert . IsAssignableFrom < IReadOnlyList < IDictionary < string , object? > > > ( weeklyCardsValue ) ;
Assert . Equal ( 5 , weeklyCards . Count ) ;
var firstCard = weeklyCards [ 0 ] ;
Assert . Equal ( "Tuesday" , firstCard [ "weather_day" ] ) ;
Assert . Equal ( 61 , firstCard [ "weather_high" ] ) ;
Assert . Equal ( 52 , firstCard [ "weather_low" ] ) ;
2026-05-09 09:21:45 -05:00
}
[Fact]
2026-05-10 20:31:07 -05:00
public async Task BuildDecisionAsync_WeatherNextWeek_WithContext_ReturnsFiveDaySummary ( )
2026-05-09 09:21:45 -05:00
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Seattle, U.S." , "light rain" , 58 , 61 , 52 , "rain" , false )
2026-05-09 09:21:45 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "forecast for seattle next week" ,
NormalizedTranscript = "forecast for seattle next week" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
2026-05-10 20:31:07 -05:00
Assert . Contains ( "next five-day forecast" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-05-11 19:59:15 -05:00
Assert . Contains ( "Seattle, U.S." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-05-10 20:31:07 -05:00
Assert . Contains ( "Temperatures are in Fahrenheit." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . NotNull ( provider . LastRequest ) ;
Assert . Equal ( "Seattle" , provider . LastRequest ! . LocationQuery ) ;
Assert . Equal ( 5 , provider . LastRequest . ForecastDayOffset ) ;
2026-05-11 22:44:56 -05:00
Assert . NotNull ( decision . SkillPayload ) ;
Assert . True ( decision . SkillPayload ! . TryGetValue ( "weather_weekly_cards" , out var weeklyCardsValue ) ) ;
var weeklyCards = Assert . IsAssignableFrom < IReadOnlyList < IDictionary < string , object? > > > ( weeklyCardsValue ) ;
Assert . Equal ( 5 , weeklyCards . Count ) ;
2026-05-09 09:21:45 -05:00
}
2026-05-10 23:08:06 -05:00
[Fact]
public async Task BuildDecisionAsync_WeatherForecastNextPhrase_WithContext_ReturnsFiveDaySummary ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Seattle, U.S." , "light rain" , 58 , 61 , 52 , "rain" , false )
2026-05-10 23:08:06 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the forecast next" ,
NormalizedTranscript = "what's the forecast next" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Contains ( "next five-day forecast" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-05-11 19:59:15 -05:00
Assert . Contains ( "Seattle, U.S." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-05-10 23:08:06 -05:00
Assert . Contains ( "Temperatures are in Fahrenheit." , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . NotNull ( provider . LastRequest ) ;
Assert . Null ( provider . LastRequest ! . LocationQuery ) ;
Assert . Equal ( 5 , provider . LastRequest . ForecastDayOffset ) ;
}
2026-05-09 09:21:45 -05:00
[Fact]
public async Task BuildDecisionAsync_WeatherDayAfterTomorrow_WithContext_PassesDayOffsetAndLocation ( )
{
var provider = new CapturingWeatherReportProvider
{
2026-05-11 19:59:15 -05:00
Snapshot = new WeatherReportSnapshot ( "Chicago, U.S." , "mostly cloudy" , 72 , 74 , 60 , "cloudy" , false )
2026-05-09 09:21:45 -05:00
} ;
var service = CreateService ( weatherReportProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the weather in chicago day after tomorrow" ,
NormalizedTranscript = "what's the weather in chicago day after tomorrow" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "weather" , decision . IntentName ) ;
Assert . Equal ( "Chicago" , provider . LastRequest ? . LocationQuery ) ;
Assert . Equal ( 2 , provider . LastRequest ? . ForecastDayOffset ) ;
2026-05-11 19:59:15 -05:00
Assert . Equal ( "The day after tomorrow in Chicago, U.S., expect mostly cloudy with a high near 74 degrees Fahrenheit and a low around 60 degrees Fahrenheit." , decision . ReplyText ) ;
2026-05-09 09:21:45 -05:00
}
2026-04-17 17:49:43 -05:00
[Fact]
public async Task BuildDecisionAsync_ClientNluAskForDate_MapsToDateIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "askForDate"
}
} ) ;
Assert . Equal ( "date" , decision . IntentName ) ;
2026-04-21 21:28:15 -05:00
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "askForDate" , decision . SkillPayload ! [ "clockIntent" ] ) ;
2026-04-17 17:49:43 -05:00
}
2026-05-05 22:27:28 -05:00
[Fact]
public async Task BuildDecisionAsync_ClientNluAskForDate_WithBirthdayTranscript_PrefersRobotBirthdayIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's your birthday" ,
NormalizedTranscript = "what's your birthday" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "askForDate"
}
} ) ;
Assert . Equal ( "robot_birthday" , decision . IntentName ) ;
Assert . Equal ( "My birthday is March 22, 2026." , decision . ReplyText ) ;
}
2026-05-06 09:51:36 -05:00
[Fact]
public async Task BuildDecisionAsync_ClientNluAskForDate_WithPrefixBirthdayTranscript_PrefersRobotBirthdayIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "so what's your birthday" ,
NormalizedTranscript = "so what's your birthday" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "askForDate"
}
} ) ;
Assert . Equal ( "robot_birthday" , decision . IntentName ) ;
Assert . Equal ( "My birthday is March 22, 2026." , decision . ReplyText ) ;
}
2026-04-18 16:29:27 -05:00
[Fact]
public async Task BuildDecisionAsync_YesNoFollowUp_MapsShortAffirmationToYesIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "yeah" ,
NormalizedTranscript = "yeah" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "create/is_it_a_keeper" ]
2026-04-18 16:29:27 -05:00
}
} ) ;
Assert . Equal ( "yes" , decision . IntentName ) ;
Assert . Equal ( "Yes." , decision . ReplyText ) ;
}
2026-04-19 21:44:04 -05:00
[Fact]
public async Task BuildDecisionAsync_YesNoFollowUp_FromAsrHints_MapsShortDenialToNoIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "no" ,
NormalizedTranscript = "no" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "surprises-ota/want_to_download_now" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ]
2026-04-19 21:44:04 -05:00
}
} ) ;
Assert . Equal ( "no" , decision . IntentName ) ;
Assert . Equal ( "No." , decision . ReplyText ) ;
}
2026-04-23 07:35:15 -05:00
[Fact]
public async Task BuildDecisionAsync_SharedYesNoPrompt_MapsShortAffirmationToYesIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "yes" ,
NormalizedTranscript = "yes" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "shared/yes_no" , "globals/gui_nav" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ]
2026-04-23 07:35:15 -05:00
}
} ) ;
Assert . Equal ( "yes" , decision . IntentName ) ;
Assert . Equal ( "Yes." , decision . ReplyText ) ;
}
2026-04-26 08:32:19 -05:00
[Fact]
public async Task BuildDecisionAsync_AlarmTimerChangePrompt_MapsShortAffirmationToYesIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "yes" ,
NormalizedTranscript = "yes" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "clock/alarm_timer_change" , "globals/gui_nav" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ]
2026-04-26 08:32:19 -05:00
}
} ) ;
Assert . Equal ( "yes" , decision . IntentName ) ;
Assert . Equal ( "Yes." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_AlarmTimerNoneSetPrompt_MapsShortDenialToNoIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "no" ,
NormalizedTranscript = "no" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "clock/alarm_timer_none_set" , "globals/global_commands_launch" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ]
2026-04-26 08:32:19 -05:00
}
} ) ;
Assert . Equal ( "no" , decision . IntentName ) ;
Assert . Equal ( "No." , decision . ReplyText ) ;
}
2026-05-10 21:22:25 -05:00
[Fact]
public async Task BuildDecisionAsync_SharedYesNoPrompt_MapsAffirmativeWordToYesIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "affirmative" ,
NormalizedTranscript = "affirmative" ,
Attributes = new Dictionary < string , object? >
{
["listenRules"] = ( string [ ] ) [ "shared/yes_no" , "globals/gui_nav" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ]
}
} ) ;
Assert . Equal ( "yes" , decision . IntentName ) ;
Assert . Equal ( "Yes." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_SharedYesNoPrompt_MapsNegativeWordToNoIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "negative" ,
NormalizedTranscript = "negative" ,
Attributes = new Dictionary < string , object? >
{
["listenRules"] = ( string [ ] ) [ "shared/yes_no" , "globals/gui_nav" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ]
}
} ) ;
Assert . Equal ( "no" , decision . IntentName ) ;
Assert . Equal ( "No." , decision . ReplyText ) ;
}
2026-05-12 20:36:43 -05:00
[Fact]
public async Task BuildDecisionAsync_SurprisesDateOfferPrompt_WithNoisyAffirmation_MapsToSurpriseIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "- Thank you. - Yes." ,
NormalizedTranscript = "- Thank you. - Yes." ,
Attributes = new Dictionary < string , object? >
{
["listenRules"] = ( string [ ] ) [ "surprises-date/offer_date_fact" , "globals/gui_nav" , "globals/global_commands_launch" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ] ,
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 20 T08 : 00 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "proactive_offer_pizza_fact" , decision . IntentName ) ;
Assert . Equal ( "Do you want to hear a fun pizza fact?" , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayOfferPrompt_WithNoisyAffirmation_MapsToWordOfDayLaunch ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "- Me. - Yes." ,
NormalizedTranscript = "- Me. - Yes." ,
Attributes = new Dictionary < string , object? >
{
["listenRules"] = ( string [ ] ) [ "word-of-the-day/surprise" , "globals/gui_nav" , "globals/global_commands_launch" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ]
}
} ) ;
Assert . Equal ( "word_of_the_day" , decision . IntentName ) ;
Assert . Equal ( "Starting word of the day." , decision . ReplyText ) ;
Assert . Equal ( "@be/word-of-the-day" , decision . SkillName ) ;
}
2026-04-19 22:03:41 -05:00
[Fact]
public async Task BuildDecisionAsync_SettingsDownloadPrompt_MapsShortDenialToNoIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "No." ,
NormalizedTranscript = "No." ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "settings/download_now_later" , "globals/global_commands_launch" ]
2026-04-19 22:03:41 -05:00
}
} ) ;
Assert . Equal ( "no" , decision . IntentName ) ;
Assert . Equal ( "No." , decision . ReplyText ) ;
}
2026-04-20 21:45:55 -05:00
[Fact]
2026-05-12 20:36:43 -05:00
public async Task BuildDecisionAsync_SurprisesDateOfferPrompt_MapsShortAffirmationToSurpriseFlow ( )
2026-04-20 21:45:55 -05:00
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "Yes!" ,
NormalizedTranscript = "Yes!" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "surprises-date/offer_date_fact" , "globals/global_commands_launch" ] ,
["listenAsrHints"] = ( string [ ] ) [ "$YESNO" ]
2026-04-20 21:45:55 -05:00
}
} ) ;
2026-05-12 20:36:43 -05:00
Assert . Equal ( "proactive_offer_pizza_fact" , decision . IntentName ) ;
Assert . Equal ( "Do you want to hear a fun pizza fact?" , decision . ReplyText ) ;
2026-04-20 21:45:55 -05:00
}
2026-04-18 16:29:27 -05:00
[Fact]
public async Task BuildDecisionAsync_SkillPhraseVariant_MapsToKnownIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "make me laugh" ,
NormalizedTranscript = "make me laugh"
} ) ;
Assert . Equal ( "joke" , decision . IntentName ) ;
}
2026-04-20 20:55:49 -05:00
[Fact]
public async Task BuildDecisionAsync_OpenTheRadio_MapsToRadioLaunchIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "open the radio" ,
NormalizedTranscript = "open the radio"
} ) ;
Assert . Equal ( "radio" , decision . IntentName ) ;
Assert . Equal ( "@be/radio" , decision . SkillName ) ;
Assert . Equal ( "@be/radio" , decision . SkillPayload ! [ "skillId" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_PlayCountryMusic_MapsToRadioGenreLaunchIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "play country music" ,
NormalizedTranscript = "play country music"
} ) ;
Assert . Equal ( "radio_genre" , decision . IntentName ) ;
Assert . Equal ( "@be/radio" , decision . SkillName ) ;
Assert . Equal ( "Country" , decision . SkillPayload ! [ "station" ] ) ;
}
2026-04-26 20:19:16 -05:00
[Fact]
public async Task BuildDecisionAsync_StopThat_MapsToIdleStopCommand ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "stop that" ,
NormalizedTranscript = "stop that"
} ) ;
Assert . Equal ( "stop" , decision . IntentName ) ;
Assert . Equal ( "@be/idle" , decision . SkillName ) ;
Assert . Equal ( "stop" , decision . SkillPayload ! [ "globalIntent" ] ) ;
Assert . Equal ( "global_commands" , decision . SkillPayload [ "nluDomain" ] ) ;
}
2026-04-27 22:13:59 -05:00
[Fact]
public async Task BuildDecisionAsync_NeverMindWithPunctuation_MapsToIdleStopCommand ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "Never mind." ,
NormalizedTranscript = "Never mind."
} ) ;
Assert . Equal ( "stop" , decision . IntentName ) ;
Assert . Equal ( "@be/idle" , decision . SkillName ) ;
}
2026-04-26 20:19:16 -05:00
[Fact]
public async Task BuildDecisionAsync_TurnItUp_MapsToGlobalVolumeUpCommand ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "turn it up" ,
NormalizedTranscript = "turn it up"
} ) ;
Assert . Equal ( "volume_up" , decision . IntentName ) ;
Assert . Equal ( "global_commands" , decision . SkillName ) ;
Assert . Equal ( "volumeUp" , decision . SkillPayload ! [ "globalIntent" ] ) ;
Assert . Equal ( "null" , decision . SkillPayload [ "volumeLevel" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_SetVolumeToSix_MapsToGlobalVolumeToValueCommand ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set volume to six" ,
NormalizedTranscript = "set volume to six"
} ) ;
Assert . Equal ( "volume_to_value" , decision . IntentName ) ;
Assert . Equal ( "global_commands" , decision . SkillName ) ;
Assert . Equal ( "volumeToValue" , decision . SkillPayload ! [ "globalIntent" ] ) ;
Assert . Equal ( "6" , decision . SkillPayload [ "volumeLevel" ] ) ;
}
2026-04-27 22:13:59 -05:00
[Fact]
public async Task BuildDecisionAsync_SetVolumeTwoSix_UsesTrailingHomophoneLevel ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "Set Volume 2-6." ,
NormalizedTranscript = "Set Volume 2-6."
} ) ;
Assert . Equal ( "volume_to_value" , decision . IntentName ) ;
Assert . Equal ( "volumeToValue" , decision . SkillPayload ! [ "globalIntent" ] ) ;
Assert . Equal ( "6" , decision . SkillPayload [ "volumeLevel" ] ) ;
}
2026-04-26 20:19:16 -05:00
[Fact]
public async Task BuildDecisionAsync_ShowVolumeControls_MapsToSettingsVolumeQuery ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "show volume controls" ,
NormalizedTranscript = "show volume controls"
} ) ;
Assert . Equal ( "volume_query" , decision . IntentName ) ;
Assert . Equal ( "@be/settings" , decision . SkillName ) ;
Assert . Equal ( "volumeQuery" , decision . SkillPayload ! [ "localIntent" ] ) ;
}
2026-04-27 22:13:59 -05:00
[Fact]
public async Task BuildDecisionAsync_OpenPhotogal_MapsToGalleryLaunch ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "open the photogal" ,
NormalizedTranscript = "open the photogal"
} ) ;
Assert . Equal ( "photo_gallery" , decision . IntentName ) ;
Assert . Equal ( "@be/gallery" , decision . SkillName ) ;
}
2026-04-20 22:25:08 -05:00
[Fact]
public async Task BuildDecisionAsync_OpenTimer_MapsToLocalClockTimerMenu ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "open timer" ,
NormalizedTranscript = "open timer"
} ) ;
Assert . Equal ( "timer_menu" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "timer" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "menu" , decision . SkillPayload [ "clockIntent" ] ) ;
}
2026-04-21 21:28:15 -05:00
[Fact]
public async Task BuildDecisionAsync_OpenClock_MapsToDirectClockView ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "open the clock" ,
NormalizedTranscript = "open the clock"
} ) ;
Assert . Equal ( "clock_open" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "clock" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "askForTime" , decision . SkillPayload [ "clockIntent" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_WhatTimeIsIt_MapsToLocalClockTimeIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what time is it" ,
NormalizedTranscript = "what time is it"
} ) ;
Assert . Equal ( "time" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "askForTime" , decision . SkillPayload ! [ "clockIntent" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_TodaysDate_MapsToLocalClockDateIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's today's date" ,
NormalizedTranscript = "what's today's date"
} ) ;
Assert . Equal ( "date" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "askForDate" , decision . SkillPayload ! [ "clockIntent" ] ) ;
}
2026-04-20 22:25:08 -05:00
[Fact]
2026-04-21 22:53:14 -05:00
public async Task BuildDecisionAsync_SetTimerForFiveMinutes_MapsToClockStartIntent ( )
2026-04-20 22:25:08 -05:00
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set a timer for five minutes" ,
NormalizedTranscript = "set a timer for five minutes"
} ) ;
Assert . Equal ( "timer_value" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "timer" , decision . SkillPayload ! [ "domain" ] ) ;
2026-04-21 22:53:14 -05:00
Assert . Equal ( "start" , decision . SkillPayload [ "clockIntent" ] ) ;
2026-04-20 22:25:08 -05:00
Assert . Equal ( "0" , decision . SkillPayload [ "hours" ] ) ;
Assert . Equal ( "5" , decision . SkillPayload [ "minutes" ] ) ;
Assert . Equal ( "null" , decision . SkillPayload [ "seconds" ] ) ;
}
[Fact]
2026-04-21 22:53:14 -05:00
public async Task BuildDecisionAsync_SetAlarmForSevenThirtyAm_MapsToClockStartIntent ( )
2026-04-20 22:25:08 -05:00
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm for 7:30 am" ,
NormalizedTranscript = "set an alarm for 7:30 am"
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "alarm" , decision . SkillPayload ! [ "domain" ] ) ;
2026-04-21 22:53:14 -05:00
Assert . Equal ( "start" , decision . SkillPayload [ "clockIntent" ] ) ;
2026-04-20 22:25:08 -05:00
Assert . Equal ( "7:30" , decision . SkillPayload [ "time" ] ) ;
Assert . Equal ( "am" , decision . SkillPayload [ "ampm" ] ) ;
}
2026-04-21 21:28:15 -05:00
[Fact]
public async Task BuildDecisionAsync_SetAlarmForEightThirty_ParsesCompactTime ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm for 830" ,
NormalizedTranscript = "set an alarm for 830"
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "8:30" , decision . SkillPayload ! [ "time" ] ) ;
Assert . Equal ( "am" , decision . SkillPayload [ "ampm" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForEightThirtySpokenDigits_ParsesSplitTime ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm for 8 30" ,
NormalizedTranscript = "set an alarm for 8 30"
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "8:30" , decision . SkillPayload ! [ "time" ] ) ;
Assert . Equal ( "am" , decision . SkillPayload [ "ampm" ] ) ;
2026-04-21 22:53:14 -05:00
}
2026-04-21 23:24:56 -05:00
[Fact]
public async Task BuildDecisionAsync_SetAlarmForTenTwentyFiveWithHyphen_ParsesSplitTime ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm for 10-25" ,
NormalizedTranscript = "set an alarm for 10-25"
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "10:25" , decision . SkillPayload ! [ "time" ] ) ;
Assert . Equal ( "am" , decision . SkillPayload [ "ampm" ] ) ;
}
2026-04-21 23:27:18 -05:00
[Fact]
public async Task BuildDecisionAsync_SetAlarmForTenTwentyFivePm_ParsesPmSuffix ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm for 10:25 pm" ,
NormalizedTranscript = "set an alarm for 10:25 pm"
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "10:25" , decision . SkillPayload ! [ "time" ] ) ;
Assert . Equal ( "pm" , decision . SkillPayload [ "ampm" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForTenTwentyFiveSpacedPm_ParsesPmSuffix ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm for 10 25 p m" ,
NormalizedTranscript = "set an alarm for 10 25 p m"
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "10:25" , decision . SkillPayload ! [ "time" ] ) ;
Assert . Equal ( "pm" , decision . SkillPayload [ "ampm" ] ) ;
}
2026-04-22 07:48:36 -05:00
[Fact]
public async Task BuildDecisionAsync_SetAlarmForSevenEighteen_UsesNextOccurrenceFromContext ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm for 7:18" ,
NormalizedTranscript = "set an alarm for 7:18" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 22 T07 : 15 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "7:18" , decision . SkillPayload ! [ "time" ] ) ;
Assert . Equal ( "am" , decision . SkillPayload [ "ampm" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForSevenTen_UsesNextOccurrenceFromContext ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm for 7:10" ,
NormalizedTranscript = "set an alarm for 7:10" ,
Attributes = new Dictionary < string , object? >
{
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 22 T07 : 15 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "7:10" , decision . SkillPayload ! [ "time" ] ) ;
Assert . Equal ( "pm" , decision . SkillPayload [ "ampm" ] ) ;
}
2026-04-21 22:53:14 -05:00
[Fact]
public async Task BuildDecisionAsync_TimerValueFollowUp_ParsesBareDuration ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "twenty five minutes" ,
NormalizedTranscript = "twenty five minutes" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "clock/timer_set_value" ]
2026-04-21 22:53:14 -05:00
}
} ) ;
Assert . Equal ( "timer_value" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "start" , decision . SkillPayload ! [ "clockIntent" ] ) ;
Assert . Equal ( "25" , decision . SkillPayload [ "minutes" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_AlarmValueFollowUp_ParsesBareSpokenTime ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "ten twenty five" ,
NormalizedTranscript = "ten twenty five" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "clock/alarm_set_value" ]
2026-04-21 22:53:14 -05:00
}
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "start" , decision . SkillPayload ! [ "clockIntent" ] ) ;
Assert . Equal ( "10:25" , decision . SkillPayload [ "time" ] ) ;
Assert . Equal ( "am" , decision . SkillPayload [ "ampm" ] ) ;
2026-04-21 21:28:15 -05:00
}
2026-04-26 08:32:19 -05:00
[Fact]
public async Task BuildDecisionAsync_AlarmValueFollowUp_ParsesCommaSeparatedSpokenDigits ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "7, 44" ,
NormalizedTranscript = "7, 44" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "clock/alarm_set_value" ] ,
2026-04-26 08:32:19 -05:00
["context"] = "" "{" runtime ":{" location ":{" iso ":" 2026 - 04 - 26 T07 : 43 : 00 - 05 : 00 "}}}" ""
}
} ) ;
Assert . Equal ( "alarm_value" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "start" , decision . SkillPayload ! [ "clockIntent" ] ) ;
Assert . Equal ( "7:44" , decision . SkillPayload [ "time" ] ) ;
Assert . Equal ( "am" , decision . SkillPayload [ "ampm" ] ) ;
}
2026-04-21 21:28:15 -05:00
[Fact]
public async Task BuildDecisionAsync_SetAlarmWithoutTime_AsksForClarification ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set an alarm" ,
NormalizedTranscript = "set an alarm"
} ) ;
Assert . Equal ( "alarm_clarify" , decision . IntentName ) ;
2026-04-25 22:44:57 -05:00
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "alarm" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "set" , decision . SkillPayload [ "clockIntent" ] ) ;
2026-04-21 21:28:15 -05:00
Assert . Equal ( "What time should I set the alarm for?" , decision . ReplyText ) ;
}
2026-04-23 07:35:15 -05:00
[Fact]
public async Task BuildDecisionAsync_CancelAlarm_MapsToClockDeleteIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "cancel alarm" ,
NormalizedTranscript = "cancel alarm"
} ) ;
Assert . Equal ( "alarm_delete" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "alarm" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "delete" , decision . SkillPayload [ "clockIntent" ] ) ;
}
2026-04-28 08:01:35 -05:00
[Theory]
[InlineData("delete the alarm")]
[InlineData("so, delete the alarm")]
[InlineData("delete along")]
[InlineData("so, delete the along")]
public async Task BuildDecisionAsync_DeleteAlarmVariants_MapsToClockDeleteIntent ( string transcript )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = transcript ,
NormalizedTranscript = transcript
} ) ;
Assert . Equal ( "alarm_delete" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "alarm" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "delete" , decision . SkillPayload [ "clockIntent" ] ) ;
}
2026-04-24 06:55:15 -05:00
[Fact]
public async Task BuildDecisionAsync_ClientNluSetAlarmWithoutTime_AsksForClarification ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set" ,
NormalizedTranscript = "set" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "set" ,
["clientEntities"] = new Dictionary < string , object? >
{
["domain"] = "alarm"
} ,
2026-05-03 22:42:41 -05:00
["clientRules"] = ( string [ ] ) [ "clock/clock_menu" ]
2026-04-24 06:55:15 -05:00
}
} ) ;
Assert . Equal ( "alarm_clarify" , decision . IntentName ) ;
2026-04-25 22:44:57 -05:00
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "alarm" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "set" , decision . SkillPayload [ "clockIntent" ] ) ;
2026-04-24 06:55:15 -05:00
}
[Fact]
public async Task BuildDecisionAsync_ClientNluCancelFromAlarmQueryMenu_UsesLastClockDomain ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "cancel" ,
NormalizedTranscript = "cancel" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "cancel" ,
2026-05-03 22:42:41 -05:00
["clientRules"] = ( string [ ] ) [ "clock/alarm_timer_query_menu" ] ,
2026-04-24 06:55:15 -05:00
["lastClockDomain"] = "alarm"
}
} ) ;
Assert . Equal ( "alarm_delete" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "alarm" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "delete" , decision . SkillPayload [ "clockIntent" ] ) ;
}
2026-04-26 11:20:00 -05:00
[Fact]
public async Task BuildDecisionAsync_ClientNluCancelFromAlarmValuePrompt_MapsToClockCancelIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "cancel" ,
NormalizedTranscript = "cancel" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "cancel" ,
2026-05-03 22:42:41 -05:00
["clientRules"] = ( string [ ] ) [ "clock/alarm_set_value" ]
2026-04-26 11:20:00 -05:00
}
} ) ;
Assert . Equal ( "alarm_cancel" , decision . IntentName ) ;
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "alarm" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "cancel" , decision . SkillPayload [ "clockIntent" ] ) ;
}
2026-04-21 21:28:15 -05:00
[Fact]
public async Task BuildDecisionAsync_SetTimerWithoutDuration_AsksForClarification ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "set a timer" ,
NormalizedTranscript = "set a timer"
} ) ;
Assert . Equal ( "timer_clarify" , decision . IntentName ) ;
2026-04-25 22:44:57 -05:00
Assert . Equal ( "@be/clock" , decision . SkillName ) ;
Assert . Equal ( "timer" , decision . SkillPayload ! [ "domain" ] ) ;
Assert . Equal ( "set" , decision . SkillPayload [ "clockIntent" ] ) ;
2026-04-21 21:28:15 -05:00
Assert . Equal ( "How long should I set the timer for?" , decision . ReplyText ) ;
}
2026-04-20 22:55:42 -05:00
[Fact]
public async Task BuildDecisionAsync_OpenPhotoGallery_MapsToGalleryLaunch ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "open photo gallery" ,
NormalizedTranscript = "open photo gallery"
} ) ;
Assert . Equal ( "photo_gallery" , decision . IntentName ) ;
Assert . Equal ( "@be/gallery" , decision . SkillName ) ;
Assert . Equal ( "menu" , decision . SkillPayload ! [ "localIntent" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_SnapAPicture_MapsToCreateOnePhoto ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "snap a picture" ,
NormalizedTranscript = "snap a picture"
} ) ;
Assert . Equal ( "snapshot" , decision . IntentName ) ;
Assert . Equal ( "@be/create" , decision . SkillName ) ;
Assert . Equal ( "createOnePhoto" , decision . SkillPayload ! [ "localIntent" ] ) ;
}
[Fact]
public async Task BuildDecisionAsync_OpenPhotobooth_MapsToCreateSomePhotos ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "open photobooth" ,
NormalizedTranscript = "open photobooth"
} ) ;
Assert . Equal ( "photobooth" , decision . IntentName ) ;
Assert . Equal ( "@be/create" , decision . SkillName ) ;
Assert . Equal ( "createSomePhotos" , decision . SkillPayload ! [ "localIntent" ] ) ;
}
2026-04-20 22:09:23 -05:00
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_UsesNimbusCloudSkillPath ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "tell me the news" ,
NormalizedTranscript = "tell me the news"
} ) ;
Assert . Equal ( "news" , decision . IntentName ) ;
Assert . Equal ( "news" , decision . SkillName ) ;
Assert . Equal ( "news" , decision . SkillPayload ! [ "skillId" ] ) ;
Assert . Equal ( "news" , decision . SkillPayload [ "cloudSkill" ] ) ;
Assert . Equal ( "runtime-news" , decision . SkillPayload [ "mim_id" ] ) ;
2026-05-10 21:22:25 -05:00
Assert . Equal ( "provider_unavailable" , decision . SkillPayload [ "news_provider_status" ] ) ;
2026-04-20 22:09:23 -05:00
Assert . DoesNotContain ( "future cloud integration" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
}
2026-05-10 00:30:31 -05:00
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_WithProvider_UsesProviderHeadlines ( )
{
var provider = new CapturingNewsBriefingProvider
{
Snapshot = new NewsBriefingSnapshot (
[
new NewsHeadline ( "Local robotics team unveils weather-ready helper" ) ,
new NewsHeadline ( "Community makerspace hosts weekend AI expo" )
] ,
"NewsAPI" )
} ;
var service = CreateService ( newsBriefingProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "tell me the news" ,
NormalizedTranscript = "tell me the news"
} ) ;
Assert . Equal ( "news" , decision . IntentName ) ;
Assert . Equal ( "news" , decision . SkillName ) ;
Assert . Equal ( "news" , decision . SkillPayload ! [ "skillId" ] ) ;
Assert . Equal ( "news" , decision . SkillPayload [ "cloudSkill" ] ) ;
Assert . Equal ( "runtime-news" , decision . SkillPayload [ "mim_id" ] ) ;
2026-05-10 20:31:07 -05:00
Assert . Contains ( "news-stinger" , decision . SkillPayload [ "esml" ] ? . ToString ( ) , StringComparison . OrdinalIgnoreCase ) ;
2026-05-10 00:30:31 -05:00
Assert . Equal ( "NewsAPI" , decision . SkillPayload [ "news_source" ] ) ;
Assert . Equal ( 2 , decision . SkillPayload [ "news_headline_count" ] ) ;
2026-05-10 21:22:25 -05:00
Assert . Equal ( "provider_success" , decision . SkillPayload [ "news_provider_status" ] ) ;
Assert . Equal ( 3 , decision . SkillPayload [ "news_provider_requested_headlines" ] ) ;
Assert . Equal ( 2 , decision . SkillPayload [ "news_provider_resolved_headlines" ] ) ;
2026-05-10 00:30:31 -05:00
Assert . Contains ( "Local robotics team unveils weather-ready helper" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
Assert . NotNull ( provider . LastRequest ) ;
Assert . Equal ( 3 , provider . LastRequest ! . MaxHeadlines ) ;
}
2026-05-10 20:31:07 -05:00
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_WithAIAlias_UsesTechnologyCategory ( )
{
var provider = new CapturingNewsBriefingProvider
{
Snapshot = new NewsBriefingSnapshot (
[
new NewsHeadline ( "AI labs unveil new home companion behaviors" )
] ,
"NewsAPI" )
} ;
var service = CreateService ( newsBriefingProvider : provider ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "tell me the a i news" ,
NormalizedTranscript = "tell me the a i news"
} ) ;
Assert . Equal ( "news" , decision . IntentName ) ;
Assert . NotNull ( provider . LastRequest ) ;
Assert . Contains ( "technology" , provider . LastRequest ! . PreferredCategories , StringComparer . OrdinalIgnoreCase ) ;
2026-05-10 21:22:25 -05:00
Assert . Contains ( "artificial intelligence" , decision . SkillPayload ? [ "esml" ] ? . ToString ( ) , StringComparison . OrdinalIgnoreCase ) ;
2026-05-10 20:31:07 -05:00
}
2026-05-10 00:30:31 -05:00
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_WithMemoryPreference_UsesCategoryHints ( )
{
var memoryStore = new InMemoryPersonalMemoryStore ( ) ;
var provider = new CapturingNewsBriefingProvider
{
Snapshot = new NewsBriefingSnapshot (
[
new NewsHeadline ( "City soccer clubs prepare for summer playoffs" )
] ,
"NewsAPI" )
} ;
var service = CreateService ( memoryStore , newsBriefingProvider : provider ) ;
await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "i like sports" ,
NormalizedTranscript = "i like sports" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "tell me the news" ,
NormalizedTranscript = "tell me the news" ,
Attributes = new Dictionary < string , object? >
{
["accountId"] = "acct-a" ,
["loopId"] = "loop-a"
} ,
DeviceId = "device-a"
} ) ;
Assert . Equal ( "news" , decision . IntentName ) ;
Assert . NotNull ( provider . LastRequest ) ;
Assert . Contains ( "sports" , provider . LastRequest ! . PreferredCategories , StringComparer . OrdinalIgnoreCase ) ;
}
2026-04-20 22:13:37 -05:00
[Fact]
public async Task BuildDecisionAsync_CloudVersion_UsesSharedBuildInfo ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "what's the cloud version" ,
NormalizedTranscript = "what's the cloud version"
} ) ;
Assert . Equal ( "cloud_version" , decision . IntentName ) ;
Assert . Equal ( OpenJiboCloudBuildInfo . SpokenVersion , decision . ReplyText ) ;
2026-04-30 00:21:33 -05:00
Assert . DoesNotContain ( "Jibo" , decision . ReplyText , StringComparison . OrdinalIgnoreCase ) ;
2026-04-20 22:13:37 -05:00
}
2026-04-18 16:43:38 -05:00
[Fact]
public async Task BuildDecisionAsync_WordOfDayGuess_UsesStructuredClientNluGuess ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "guess" ,
NormalizedTranscript = "guess" ,
Attributes = new Dictionary < string , object? >
{
["clientIntent"] = "guess" ,
2026-05-03 22:42:41 -05:00
["clientRules"] = ( string [ ] ) [ "word-of-the-day/puzzle" ] ,
2026-04-18 16:43:38 -05:00
["clientEntities"] = JsonDocument . Parse ( "" "{" guess ":" pastoral "}" "" ) . RootElement . Clone ( )
}
} ) ;
Assert . Equal ( "word_of_the_day_guess" , decision . IntentName ) ;
Assert . Equal ( "I heard pastoral." , decision . ReplyText ) ;
}
2026-04-18 16:57:18 -05:00
[Fact]
public async Task BuildDecisionAsync_WordOfDayGuess_UsesSpokenTranscriptDuringPuzzleTurn ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "pastoral" ,
NormalizedTranscript = "pastoral" ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "word-of-the-day/puzzle" ]
2026-04-18 16:57:18 -05:00
}
} ) ;
Assert . Equal ( "word_of_the_day_guess" , decision . IntentName ) ;
Assert . Equal ( "I heard pastoral." , decision . ReplyText ) ;
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayStartPhrase_MapsToSkillIntent ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "start word of the day" ,
NormalizedTranscript = "start word of the day"
} ) ;
Assert . Equal ( "word_of_the_day" , decision . IntentName ) ;
2026-04-18 17:15:49 -05:00
Assert . Equal ( "Starting word of the day." , decision . ReplyText ) ;
2026-04-18 22:14:28 -05:00
Assert . Equal ( "@be/word-of-the-day" , decision . SkillName ) ;
2026-04-19 08:17:28 -05:00
Assert . Equal ( "word-of-the-day" , decision . SkillPayload ! [ "domain" ] ) ;
2026-04-18 22:27:46 -05:00
Assert . Equal ( "@be/word-of-the-day" , decision . SkillPayload [ "skillId" ] ) ;
2026-04-18 17:15:49 -05:00
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayGuess_LineNumberUsesListenHints ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "Two." ,
NormalizedTranscript = "Two." ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "word-of-the-day/puzzle" ] ,
["listenAsrHints"] = ( string [ ] ) [ "doodad" , "pastoral" , "escarpment" ]
2026-04-18 17:15:49 -05:00
}
} ) ;
Assert . Equal ( "word_of_the_day_guess" , decision . IntentName ) ;
Assert . Equal ( "I heard pastoral." , decision . ReplyText ) ;
2026-04-19 07:34:54 -05:00
Assert . Equal ( "@be/word-of-the-day" , decision . SkillName ) ;
2026-04-18 17:15:49 -05:00
Assert . Equal ( "pastoral" , decision . SkillPayload ! [ "guess" ] ) ;
2026-04-19 07:34:54 -05:00
Assert . Equal ( "@be/word-of-the-day" , decision . SkillPayload [ "skillId" ] ) ;
Assert . Equal ( "completion_only" , decision . SkillPayload [ "cloudResponseMode" ] ) ;
2026-04-18 16:57:18 -05:00
}
2026-04-19 21:44:04 -05:00
[Fact]
public async Task BuildDecisionAsync_WordOfDayGuess_FuzzyMatchesClosestHint ( )
{
var service = CreateService ( ) ;
var decision = await service . BuildDecisionAsync ( new TurnContext
{
RawTranscript = "Haglet." ,
NormalizedTranscript = "Haglet." ,
Attributes = new Dictionary < string , object? >
{
2026-05-03 22:42:41 -05:00
["listenRules"] = ( string [ ] ) [ "word-of-the-day/puzzle" ] ,
["listenAsrHints"] = ( string [ ] ) [ "aglet" , "hovel" , "wisenheimer" ]
2026-04-19 21:44:04 -05:00
}
} ) ;
Assert . Equal ( "word_of_the_day_guess" , decision . IntentName ) ;
Assert . Equal ( "I heard aglet." , decision . ReplyText ) ;
Assert . Equal ( "aglet" , decision . SkillPayload ! [ "guess" ] ) ;
}
2026-05-06 08:13:26 -05:00
private static JiboInteractionService CreateService (
IPersonalMemoryStore ? personalMemoryStore = null ,
2026-05-10 00:30:31 -05:00
IWeatherReportProvider ? weatherReportProvider = null ,
2026-05-14 07:44:46 -05:00
INewsBriefingProvider ? newsBriefingProvider = null ,
IJiboExperienceContentRepository ? contentRepository = null )
2026-04-17 17:49:43 -05:00
{
return new JiboInteractionService (
2026-05-14 07:44:46 -05:00
new JiboExperienceContentCache ( contentRepository ? ? new InMemoryJiboExperienceContentRepository ( ) ) ,
2026-05-05 22:40:11 -05:00
new FirstItemRandomizer ( ) ,
2026-05-06 08:13:26 -05:00
personalMemoryStore ? ? new InMemoryPersonalMemoryStore ( ) ,
2026-05-10 00:30:31 -05:00
weatherReportProvider ,
newsBriefingProvider ) ;
2026-04-17 17:49:43 -05:00
}
private sealed class FirstItemRandomizer : IJiboRandomizer
{
public T Choose < T > ( IReadOnlyList < T > items )
{
return items [ 0 ] ;
}
}
2026-05-06 08:13:26 -05:00
private sealed class CapturingWeatherReportProvider : IWeatherReportProvider
{
public WeatherReportRequest ? LastRequest { get ; private set ; }
2026-05-11 07:15:11 -05:00
public List < WeatherReportRequest > Requests { get ; } = [ ] ;
2026-05-06 08:13:26 -05:00
public WeatherReportSnapshot ? Snapshot { get ; init ; }
public Task < WeatherReportSnapshot ? > GetReportAsync (
WeatherReportRequest request ,
CancellationToken cancellationToken = default )
{
LastRequest = request ;
2026-05-11 07:15:11 -05:00
Requests . Add ( request ) ;
2026-05-06 08:13:26 -05:00
return Task . FromResult ( Snapshot ) ;
}
}
2026-05-10 00:30:31 -05:00
private sealed class CapturingNewsBriefingProvider : INewsBriefingProvider
{
public NewsBriefingRequest ? LastRequest { get ; private set ; }
public NewsBriefingSnapshot ? Snapshot { get ; init ; }
public Task < NewsBriefingSnapshot ? > GetBriefingAsync (
NewsBriefingRequest request ,
CancellationToken cancellationToken = default )
{
LastRequest = request ;
return Task . FromResult ( Snapshot ) ;
}
}
2026-05-14 07:44:46 -05:00
private sealed class StaticCatalogRepository ( JiboExperienceCatalog catalog ) : IJiboExperienceContentRepository
{
public Task < JiboExperienceCatalog > GetCatalogAsync ( CancellationToken cancellationToken = default )
{
return Task . FromResult ( catalog ) ;
}
}
2026-04-17 17:49:43 -05:00
}