2026-04-17 17:49:43 -05:00
|
|
|
using Jibo.Cloud.Application.Services;
|
|
|
|
|
using Jibo.Cloud.Infrastructure.Content;
|
|
|
|
|
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
|
|
|
|
|
{
|
|
|
|
|
[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);
|
|
|
|
|
Assert.Equal("Okay. Watch this.", decision.ReplyText);
|
|
|
|
|
Assert.Equal("<speak>Okay.<break size='0.2'/> Watch this.<anim cat='dance' filter='music, rom-upbeat' /></speak>", decision.SkillPayload!["esml"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
Assert.Contains("Today is", decision.ReplyText, StringComparison.Ordinal);
|
|
|
|
|
}
|
|
|
|
|
|
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?>
|
|
|
|
|
{
|
|
|
|
|
["listenRules"] = new[] { "create/is_it_a_keeper" }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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?>
|
|
|
|
|
{
|
|
|
|
|
["listenRules"] = new[] { "surprises-ota/want_to_download_now" },
|
|
|
|
|
["listenAsrHints"] = new[] { "$YESNO" }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Assert.Equal("no", decision.IntentName);
|
|
|
|
|
Assert.Equal("No.", decision.ReplyText);
|
|
|
|
|
}
|
|
|
|
|
|
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?>
|
|
|
|
|
{
|
|
|
|
|
["listenRules"] = new[] { "settings/download_now_later", "globals/global_commands_launch" }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Assert.Equal("no", decision.IntentName);
|
|
|
|
|
Assert.Equal("No.", decision.ReplyText);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 21:45:55 -05:00
|
|
|
[Fact]
|
|
|
|
|
public async Task BuildDecisionAsync_SurprisesDateOfferPrompt_MapsShortAffirmationToYesIntent()
|
|
|
|
|
{
|
|
|
|
|
var service = CreateService();
|
|
|
|
|
|
|
|
|
|
var decision = await service.BuildDecisionAsync(new TurnContext
|
|
|
|
|
{
|
|
|
|
|
RawTranscript = "Yes!",
|
|
|
|
|
NormalizedTranscript = "Yes!",
|
|
|
|
|
Attributes = new Dictionary<string, object?>
|
|
|
|
|
{
|
|
|
|
|
["listenRules"] = new[] { "surprises-date/offer_date_fact", "globals/global_commands_launch" },
|
|
|
|
|
["listenAsrHints"] = new[] { "$YESNO" }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Assert.Equal("yes", decision.IntentName);
|
|
|
|
|
Assert.Equal("Yes.", decision.ReplyText);
|
|
|
|
|
}
|
|
|
|
|
|
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-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"]);
|
|
|
|
|
Assert.DoesNotContain("future cloud integration", decision.ReplyText, StringComparison.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-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",
|
|
|
|
|
["clientRules"] = new[] { "word-of-the-day/puzzle" },
|
|
|
|
|
["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?>
|
|
|
|
|
{
|
|
|
|
|
["listenRules"] = new[] { "word-of-the-day/puzzle" }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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?>
|
|
|
|
|
{
|
|
|
|
|
["listenRules"] = new[] { "word-of-the-day/puzzle" },
|
|
|
|
|
["listenAsrHints"] = new[] { "doodad", "pastoral", "escarpment" }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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?>
|
|
|
|
|
{
|
|
|
|
|
["listenRules"] = new[] { "word-of-the-day/puzzle" },
|
|
|
|
|
["listenAsrHints"] = new[] { "aglet", "hovel", "wisenheimer" }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Assert.Equal("word_of_the_day_guess", decision.IntentName);
|
|
|
|
|
Assert.Equal("I heard aglet.", decision.ReplyText);
|
|
|
|
|
Assert.Equal("aglet", decision.SkillPayload!["guess"]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 17:49:43 -05:00
|
|
|
private static JiboInteractionService CreateService()
|
|
|
|
|
{
|
|
|
|
|
return new JiboInteractionService(
|
|
|
|
|
new JiboExperienceContentCache(new InMemoryJiboExperienceContentRepository()),
|
|
|
|
|
new FirstItemRandomizer());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class FirstItemRandomizer : IJiboRandomizer
|
|
|
|
|
{
|
|
|
|
|
public T Choose<T>(IReadOnlyList<T> items)
|
|
|
|
|
{
|
|
|
|
|
return items[0];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|