2026-11-09 09:55:01 +01:00
using AIStudio.Assistants.BeyondFactsLab ;
using AIStudio.Settings.DataModel ;
namespace AIStudio.Tests.BeyondFactsLab ;
/// <summary>
/// Checks how the BeyondFacts Lab puts together the system prompt of a step.
/// </summary>
[TestFixture]
public sealed class BflPromptComposerTests
{
2026-11-09 20:00:01 +01:00
private const string CORE = "Sprache: {{RESPONSE_LANGUAGE}}\n{{ADDRESS_FORM}}\n{{KNOWLEDGE}}\n{{STEP}}\n{{MATERIALS}}\n{{DETAILS}}\n{{MISSING_DETAILS}}" ;
2026-11-09 09:55:01 +01:00
private static readonly BflPromptParts PARTS = new ( CORE , "Leitfaden" , "Schritt" , "Anrede" );
[Test]
public void APlaceholderWithoutAValueIsAnError ()
{
var exception = Assert . Throws < FormatException >(() => BflPromptTemplate . Fill ( "Hallo {{UNKNOWN}}" , new Dictionary < string , string >()));
Assert . That ( exception !. Message , Does . Contain ( "UNKNOWN" ));
}
[Test]
public void AMisspelledPlaceholderIsAnError ()
{
Assert . Throws < FormatException >(() => BflPromptTemplate . Fill ( "Hallo {{ step }}" , new Dictionary < string , string > { [ BflPromptTemplate . STEP ] = "Schritt" }));
}
[Test]
public void AValueIsInsertedAsItIs ()
{
var prompt = BflPromptTemplate . Fill ( "{{STEP}}" , new Dictionary < string , string > { [ BflPromptTemplate . STEP ] = "Die Person schrieb {{KNOWLEDGE}}." });
Assert . That ( prompt , Is . EqualTo ( "Die Person schrieb {{KNOWLEDGE}}." ), "What the person entered must never be read as a placeholder." );
}
[Test]
public async Task ThePromptOfEveryStepFillsEveryPlaceholder ()
{
foreach ( var step in BflStep . All )
{
var parts = await BflPromptLibrary . LoadPartsAsync ( step , AddressForm . FORMAL );
2026-11-09 20:00:01 +01:00
var prompt = BflPromptComposer . Compose ( parts , step , new Dictionary < BflStep , BflStepFormData >(), [], "Deutsch" );
2026-11-09 09:55:01 +01:00
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt , Does . Not . Contain ( "{{" ), step . ToString ());
Assert . That ( prompt , Does . Contain ( parts . Step . Trim ()), step . ToString ());
Assert . That ( prompt , Does . Contain ( parts . Knowledge . Trim ()[.. 200 ]), step . ToString ());
}
}
}
[Test]
public async Task TheFormOfAddressDecidesTheInstruction ()
{
2026-11-09 20:00:01 +01:00
var formal = BflPromptComposer . Compose ( await BflPromptLibrary . LoadPartsAsync ( BflStep . FRAMEWORK , AddressForm . FORMAL ), BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [], "Deutsch" );
var informal = BflPromptComposer . Compose ( await BflPromptLibrary . LoadPartsAsync ( BflStep . FRAMEWORK , AddressForm . INFORMAL ), BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [], "Deutsch" );
2026-11-09 09:55:01 +01:00
using ( Assert . EnterMultipleScope ())
{
Assert . That ( formal , Does . Contain ( "mit „Sie“" ));
Assert . That ( formal , Does . Not . Contain ( "mit „du“" ));
Assert . That ( informal , Does . Contain ( "mit „du“" ));
Assert . That ( informal , Does . Not . Contain ( "mit „Sie“" ));
}
}
2026-11-09 12:42:59 +01:00
[Test]
public async Task TheOnboardingFillsEveryPlaceholderWithoutTheGuide ()
{
var parts = await BflPromptLibrary . LoadOnboardingPartsAsync ( AddressForm . FORMAL );
var knowledge = await BflPromptLibrary . ReadAsync ( BflPromptLibrary . KNOWLEDGE_PATH );
var prompt = BflPromptComposer . ComposeOnboarding ( parts , "Deutsch" );
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt , Does . Not . Contain ( "{{" ));
Assert . That ( prompt , Does . Contain ( "Antworte auf Deutsch." ));
Assert . That ( prompt , Does . Not . Contain ( knowledge . Trim ()[.. 200 ]), "The onboarding stays short, so it leaves out the guide." );
}
}
[Test]
public async Task TheFormOfAddressDecidesTheInstructionOfTheOnboarding ()
{
var formal = BflPromptComposer . ComposeOnboarding ( await BflPromptLibrary . LoadOnboardingPartsAsync ( AddressForm . FORMAL ), "Deutsch" );
var informal = BflPromptComposer . ComposeOnboarding ( await BflPromptLibrary . LoadOnboardingPartsAsync ( AddressForm . INFORMAL ), "Deutsch" );
using ( Assert . EnterMultipleScope ())
{
Assert . That ( formal , Does . Contain ( "mit „Sie“" ));
Assert . That ( formal , Does . Not . Contain ( "mit „du“" ));
Assert . That ( informal , Does . Contain ( "mit „du“" ));
Assert . That ( informal , Does . Not . Contain ( "mit „Sie“" ));
}
}
[Test]
public void TheOnboardingIsTheSameOnEverySystem ()
{
var parts = new BflOnboardingPromptParts ( "Sprache: {{RESPONSE_LANGUAGE}}\r\n{{ADDRESS_FORM}}\r\nEnde" , " Anrede\r\n" );
var prompt = BflPromptComposer . ComposeOnboarding ( parts , "Deutsch" );
Assert . That ( prompt , Is . EqualTo ( "Sprache: Deutsch\nAnrede\nEnde" ));
}
[Test]
public void TheOnboardingKnowsNoDetailsOfAPreparation ()
{
var parts = new BflOnboardingPromptParts ( "{{DETAILS}}" , "Anrede" );
Assert . Throws < FormatException >(() => BflPromptComposer . ComposeOnboarding ( parts , "Deutsch" ));
}
2026-11-09 09:55:01 +01:00
[Test]
public void TheLanguageOfTheAnswersIsNamed ()
{
2026-11-09 20:00:01 +01:00
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [], "English (United States)" );
2026-11-09 09:55:01 +01:00
Assert . That ( prompt , Does . StartWith ( "Sprache: English (United States)\n" ));
}
[Test]
public void TheMissingKeyDetailsAreListed ()
{
2026-11-09 20:00:01 +01:00
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [], "Deutsch" );
2026-11-09 09:55:01 +01:00
foreach ( var field in BflFieldCatalog . FIELDS . Where ( field => field . IsKeyDetail ))
Assert . That ( prompt , Does . Contain ( field . Label ), field . Id );
}
[Test]
public void NothingIsMissingWhenEveryKeyDetailIsThere ()
{
var forms = BflStep . All . ToDictionary ( step => step , _ => new BflStepFormData ());
foreach ( var field in BflFieldCatalog . FIELDS . Where ( field => field . IsKeyDetail ))
forms [ field . Step ]. Set ( field . Id , "vorhanden" );
2026-11-09 20:00:01 +01:00
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , forms , [], "Deutsch" );
2026-11-09 09:55:01 +01:00
Assert . That ( prompt , Does . EndWith ( "Es fehlen keine wichtigen Angaben." ));
}
[Test]
public void TheDetailsShowTheLabelsAndTheChosenOptions ()
{
var framework = new BflStepFormData ();
framework . Set ( BflFieldCatalog . FORMAT , "Podiumsdiskussion" );
var goals = new BflStepFormData ();
var directions = BflFieldCatalog . FIELDS . Single ( field => field . Id == BflFieldCatalog . GOAL_DIRECTIONS );
goals . SetChoices ( directions , [ BflFieldCatalog . GOAL_INFORM , BflFieldCatalog . GOAL_SHOW_OPTIONS ]);
var forms = new Dictionary < BflStep , BflStepFormData > { [ BflStep . FRAMEWORK ] = framework , [ BflStep . GOALS_AND_ROLE ] = goals };
2026-11-09 20:00:01 +01:00
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , forms , [], "Deutsch" );
2026-11-09 09:55:01 +01:00
var inform = directions . Options . Single ( option => option . Id == BflFieldCatalog . GOAL_INFORM ). Label ;
var showOptions = directions . Options . Single ( option => option . Id == BflFieldCatalog . GOAL_SHOW_OPTIONS ). Label ;
var format = BflFieldCatalog . FIELDS . Single ( field => field . Id == BflFieldCatalog . FORMAT ). Label ;
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt , Does . Contain ( $"- {format}: Podiumsdiskussion" ));
Assert . That ( prompt , Does . Contain ( $"- {directions.Label}: {inform}, {showOptions}" ));
Assert . That ( prompt , Does . Contain ( $"## {BflStep.FRAMEWORK.Title} (dieser Schritt)" ));
Assert . That ( prompt , Does . Not . Contain ( BflFieldCatalog . GOAL_INFORM ), "The model reads the labels, not the IDs." );
}
}
[Test]
public void ALongTextKeepsItsLines ()
{
var knowledge = new BflStepFormData ();
knowledge . Set ( BflFieldCatalog . CORE_THESES , "Erste These\r\nZweite These" );
2026-11-09 20:00:01 +01:00
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . KNOWLEDGE , new Dictionary < BflStep , BflStepFormData > { [ BflStep . KNOWLEDGE ] = knowledge }, [], "Deutsch" );
2026-11-09 09:55:01 +01:00
Assert . That ( prompt , Does . Contain ( ": Erste These\n Zweite These" ));
}
[Test]
public void APersonalDetailReachesOnlyTheChatOfItsOwnStep ()
{
var values = new BflStepFormData ();
values . Set ( BflFieldCatalog . PERSONAL_CONCERN , "VERTRAULICH-4711" );
values . Set ( BflFieldCatalog . VALUES_AND_CONCERNS , "Offenheit" );
var forms = new Dictionary < BflStep , BflStepFormData > { [ BflStep . VALUES_AND_PERSPECTIVES ] = values };
foreach ( var step in BflStep . All )
{
2026-11-09 20:00:01 +01:00
var prompt = BflPromptComposer . Compose ( PARTS , step , forms , [], "Deutsch" );
2026-11-09 09:55:01 +01:00
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt . Contains ( "VERTRAULICH-4711" , StringComparison . Ordinal ), Is . EqualTo ( step is BflStep . VALUES_AND_PERSPECTIVES ), step . ToString ());
Assert . That ( prompt , Does . Contain ( "Offenheit" ), "Only what the person confided stays in its step." );
}
}
}
[Test]
public void TheSameFormsGiveTheSamePrompt ()
{
var framework = new BflStepFormData ();
framework . Set ( BflFieldCatalog . AUDIENCE , "Öffentlichkeit" );
framework . Set ( BflFieldCatalog . FORMAT , "Interview" );
var strategy = new BflStepFormData ();
strategy . Set ( BflFieldCatalog . COMMUNICATION_WORRIES , "Zeitdruck" );
2026-11-09 20:00:01 +01:00
var first = BflPromptComposer . Compose ( PARTS , BflStep . STRATEGY , new Dictionary < BflStep , BflStepFormData > { [ BflStep . FRAMEWORK ] = framework , [ BflStep . STRATEGY ] = strategy }, [], "Deutsch" );
var second = BflPromptComposer . Compose ( PARTS , BflStep . STRATEGY , new Dictionary < BflStep , BflStepFormData > { [ BflStep . STRATEGY ] = strategy , [ BflStep . FRAMEWORK ] = framework }, [], "Deutsch" );
2026-11-09 09:55:01 +01:00
using ( Assert . EnterMultipleScope ())
{
Assert . That ( second , Is . EqualTo ( first ));
Assert . That ( first , Does . Not . Contain ( "\r" ));
Assert . That ( first . IndexOf ( "Interview" , StringComparison . Ordinal ), Is . LessThan ( first . IndexOf ( "Öffentlichkeit" , StringComparison . Ordinal )), "The details follow the order of the forms." );
}
}
2026-11-09 20:00:01 +01:00
[Test]
public void APreparationWithoutMaterialSaysSo ()
{
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [], "Deutsch" );
Assert . That ( prompt , Does . Contain ( "Die Person hat kein Material hinzugefügt." ));
}
[Test]
public void TheMaterialComesWithItsNameAndText ()
{
var invitation = Excerpt ( "Einladung.docx" , "Podium am 10. Dezember\r\nim Audimax" , 10_000 );
var paper = Excerpt ( "Paper.pdf" , "Abstract" , 10_000 );
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [ invitation , paper ], "Deutsch" );
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt , Does . Contain ( "## Material 1: „Einladung.docx“\n\nPodium am 10. Dezember\nim Audimax" ));
Assert . That ( prompt , Does . Contain ( "## Material 2: „Paper.pdf“\n\nAbstract" ));
Assert . That ( prompt , Does . Not . Contain ( "Gekürzt" ));
}
}
[Test]
public void AShortenedMaterialSaysHowMuchOfItIsThere ()
{
var material = new BflMaterial { MaterialId = Guid . NewGuid (), Name = "Paper.pdf" , TextLength = 1_000 };
var shortened = new BflMaterialExcerpt ( new ( material , 400 ), "Der Anfang" );
var missing = new BflMaterialExcerpt ( new ( new BflMaterial { MaterialId = Guid . NewGuid (), Name = "Buch.pdf" , TextLength = 1_000 }, 0 ), string . Empty );
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [ shortened , missing ], "Deutsch" );
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt , Does . Contain ( "(Gekürzt: Hier steht nur der Anfang, etwa 40 % des Materials." ));
Assert . That ( prompt , Does . Contain ( "## Material 2: „Buch.pdf“\n(Für dieses Material reicht der Platz im Kontext des Modells nicht. Du kennst nur seinen Namen.)" ));
}
}
[Test]
public void APlaceholderInTheMaterialStaysText ()
{
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [ Excerpt ( "Notizen.md" , "Siehe {{KNOWLEDGE}}" , 100 )], "Deutsch" );
Assert . That ( prompt , Does . Contain ( "Siehe {{KNOWLEDGE}}" ));
}
[Test]
public async Task TheMaterialStandsBetweenTheStepAndTheDetails ()
{
var parts = await BflPromptLibrary . LoadPartsAsync ( BflStep . KNOWLEDGE , AddressForm . FORMAL );
var knowledge = new BflStepFormData ();
knowledge . Set ( BflFieldCatalog . CORE_THESES , "THESE-4711" );
var prompt = BflPromptComposer . Compose ( parts , BflStep . KNOWLEDGE , new Dictionary < BflStep , BflStepFormData > { [ BflStep . KNOWLEDGE ] = knowledge }, [ Excerpt ( "Paper.pdf" , "MATERIAL-4711" , 100 )], "Deutsch" );
var material = prompt . IndexOf ( "MATERIAL-4711" , StringComparison . Ordinal );
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt . IndexOf ( parts . Step . Trim (), StringComparison . Ordinal ), Is . LessThan ( material ), "The material changes less often than the details, so it comes first for the prompt caching." );
Assert . That ( material , Is . LessThan ( prompt . IndexOf ( "THESE-4711" , StringComparison . Ordinal )));
}
}
[TestCase(BflImageDelivery.SENT, "(Ein Bild. Es liegt der ersten Nachricht dieses Gesprächs bei.)")]
[TestCase(BflImageDelivery.MODEL_SEES_NO_IMAGES, "(Ein Bild. Das Modell kann keine Bilder sehen. Du kennst nur seinen Namen.)")]
[TestCase(BflImageDelivery.OVER_THE_LIMIT, "(Ein Bild. Das Modell nimmt nicht so viele Bilder auf einmal an. Du kennst nur seinen Namen.)")]
public void AnImageSaysWhetherItGoesAlong ( BflImageDelivery delivery , string expected )
{
var image = new BflMaterial { MaterialId = Guid . NewGuid (), Name = "Poster.png" , Kind = BflMaterialKind . IMAGE };
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [ new ( new ( image , 0 , delivery ), string . Empty )], "Deutsch" );
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt , Does . Contain ( $"## Material 1: „Poster.png“\n{expected}" ));
Assert . That ( prompt , Does . Not . Contain ( "reicht der Platz" ), "An image takes no room in the prompt." );
}
}
[Test]
public void ARecordingIsMarkedAsATranscript ()
{
var recording = new BflMaterial { MaterialId = Guid . NewGuid (), Name = "Interview.mp3" , Kind = BflMaterialKind . RECORDING , TextLength = 10 };
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [ new ( new ( recording , 10 ), "Guten Tag." )], "Deutsch" );
Assert . That ( prompt , Does . Contain ( "## Material 1: „Interview.mp3“\n(Das Transkript einer Aufnahme." ));
}
[Test]
public void AMaterialWhoseEndTheLabLeftOutSaysSo ()
{
var book = new BflMaterial { MaterialId = Guid . NewGuid (), Name = "Buch.pdf" , TextLength = 10 , IsShortened = true };
var prompt = BflPromptComposer . Compose ( PARTS , BflStep . FRAMEWORK , new Dictionary < BflStep , BflStepFormData >(), [ new ( new ( book , 10 ), "Kapitel 1." )], "Deutsch" );
using ( Assert . EnterMultipleScope ())
{
Assert . That ( prompt , Does . Contain ( "(Das Lab hat nur den Anfang dieses Materials übernommen. Den Rest kennst du nicht.)" ));
Assert . That ( prompt , Does . Not . Contain ( "Gekürzt:" ), "The whole stored text fits." );
}
}
[Test]
public void TheOnboardingKnowsNoMaterial ()
{
var parts = new BflOnboardingPromptParts ( "{{MATERIALS}}" , "Anrede" );
Assert . Throws < FormatException >(() => BflPromptComposer . ComposeOnboarding ( parts , "Deutsch" ));
}
private static BflMaterialExcerpt Excerpt ( string name , string text , int textLength ) => new ( new ( new BflMaterial { MaterialId = Guid . NewGuid (), Name = name , TextLength = textLength }, textLength ), text );
2026-11-09 09:55:01 +01:00
}