using AIStudio.Assistants.BeyondFactsLab; using AIStudio.Chat; using Microsoft.Extensions.Logging.Abstractions; namespace AIStudio.Tests.BeyondFactsLab; /// /// Checks how the BeyondFacts Lab stores the chats of the steps. /// [TestFixture] public sealed class BeyondFactsLabStoreThreadTests { private string dataDirectory = string.Empty; private string RootDirectory => Path.Combine(this.dataDirectory, BeyondFactsLabStore.ROOT_DIRECTORY_NAME); [SetUp] public void CreateDataDirectory() { this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-beyondfacts-lab-threads-{Guid.NewGuid():N}"); Directory.CreateDirectory(this.dataDirectory); } [TearDown] public void DeleteDataDirectory() { if (Directory.Exists(this.dataDirectory)) Directory.Delete(this.dataDirectory, true); } [Test] public async Task AStepWithoutAChatHasNone() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var thread = await store.LoadThreadAsync(new BflStepThreadLocation(created.ProjectId, BflStep.FRAMEWORK)); Assert.That(thread, Is.Null); } [Test] public async Task AStoredChatIsLoadedWithItsMessages() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var location = new BflStepThreadLocation(created.ProjectId, BflStep.KNOWLEDGE); var thread = WithMessage(BflThreadFactory.Create(location), "Worauf beruht meine Expertise?"); await store.SaveThreadAsync(location, thread); var changed = await store.LoadAsync(created.ProjectId); var loaded = await this.CreateStore().LoadThreadAsync(location); using (Assert.EnterMultipleScope()) { Assert.That(loaded, Is.Not.Null); Assert.That(loaded!.ChatId, Is.EqualTo(location.ChatId)); Assert.That(loaded.Blocks, Has.Count.EqualTo(1)); Assert.That(((ContentText)loaded.Blocks[0].Content!).Text, Is.EqualTo("Worauf beruht meine Expertise?")); Assert.That(changed!.ModifiedAtUtc, Is.GreaterThan(created.ModifiedAtUtc), "A conversation is work on the preparation."); Assert.That(File.Exists(Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"), "steps", "02-knowledge", "thread.json")), Is.True); } } [Test] public async Task AChatOfAnotherStepIsNotStored() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var thread = BflThreadFactory.Create(new BflStepThreadLocation(created.ProjectId, BflStep.STRATEGY)); await Assert.ThrowsAsync(async () => await store.SaveThreadAsync(new BflStepThreadLocation(created.ProjectId, BflStep.FRAMEWORK), thread)); } [Test] public async Task AChatOfADeletedPreparationIsNotStored() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var location = new BflStepThreadLocation(created.ProjectId, BflStep.FRAMEWORK); await store.DeleteAsync(created.ProjectId); await Assert.ThrowsAsync(async () => await store.SaveThreadAsync(location, BflThreadFactory.Create(location))); Assert.That(Directory.Exists(Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"))), Is.False, "An answer which ends after the deletion must not bring the preparation back."); } [Test] public async Task AChatCopiedIntoAnotherStepIsNotOpened() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var strategy = new BflStepThreadLocation(created.ProjectId, BflStep.STRATEGY); var framework = new BflStepThreadLocation(created.ProjectId, BflStep.FRAMEWORK); await store.SaveThreadAsync(strategy, BflThreadFactory.Create(strategy)); var stepsDirectory = Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"), "steps"); Directory.CreateDirectory(Path.Combine(stepsDirectory, "01-framework")); File.Copy(Path.Combine(stepsDirectory, "05-strategy", "thread.json"), Path.Combine(stepsDirectory, "01-framework", "thread.json")); using (Assert.EnterMultipleScope()) { await Assert.ThrowsAsync(async () => await store.LoadThreadAsync(framework)); await Assert.ThrowsAsync(async () => await store.SaveThreadAsync(framework, BflThreadFactory.Create(framework))); } } [Test] public async Task ADamagedChatIsNeitherOpenedNorOverwritten() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var location = new BflStepThreadLocation(created.ProjectId, BflStep.FRAMEWORK); var path = Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"), "steps", "01-framework", "thread.json"); Directory.CreateDirectory(Path.GetDirectoryName(path)!); await File.WriteAllTextAsync(path, "{ damaged"); using (Assert.EnterMultipleScope()) { await Assert.ThrowsAsync(async () => await store.LoadThreadAsync(location)); await Assert.ThrowsAsync(async () => await store.SaveThreadAsync(location, BflThreadFactory.Create(location))); Assert.That(await File.ReadAllTextAsync(path), Is.EqualTo("{ damaged")); } } [Test] public async Task StartingOverDeletesTheChat() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var location = new BflStepThreadLocation(created.ProjectId, BflStep.FRAMEWORK); await store.SaveThreadAsync(location, WithMessage(BflThreadFactory.Create(location), "Hallo")); await store.DeleteThreadAsync(location); Assert.That(await store.LoadThreadAsync(location), Is.Null); } [Test] public async Task EveryChatOnTheDiskIsFoundByItsId() { var store = this.CreateStore(); var first = await store.CreateAsync("Panel"); var second = await store.CreateAsync("Interview"); var firstStrategy = new BflStepThreadLocation(first.ProjectId, BflStep.STRATEGY); var secondFramework = new BflStepThreadLocation(second.ProjectId, BflStep.FRAMEWORK); await store.SaveThreadAsync(firstStrategy, BflThreadFactory.Create(firstStrategy)); await store.SaveThreadAsync(secondFramework, BflThreadFactory.Create(secondFramework)); // A preparation this version cannot open keeps its chats all the same: await File.WriteAllTextAsync(Path.Combine(this.RootDirectory, second.ProjectId.ToString("D"), "project.json"), "{ damaged"); var locations = this.CreateStore().ReadAllThreadLocations(); using (Assert.EnterMultipleScope()) { Assert.That(locations, Has.Count.EqualTo(2)); Assert.That(locations[firstStrategy.ChatId], Is.EqualTo(firstStrategy)); Assert.That(locations[secondFramework.ChatId], Is.EqualTo(secondFramework)); Assert.That(store.GetThreadWrittenAt(firstStrategy), Is.Not.Null); Assert.That(store.GetThreadWrittenAt(new BflStepThreadLocation(first.ProjectId, BflStep.FRAMEWORK)), Is.Null); } } [Test] public async Task TheOnboardingIsStoredApartFromThePreparations() { var store = this.CreateStore(); var location = BflOnboardingThreadLocation.INSTANCE; await store.SaveThreadAsync(location, WithMessage(BflThreadFactory.Create(location), "Passt das Lab zu meinem Interview?")); var otherStore = this.CreateStore(); var loaded = await otherStore.LoadThreadAsync(location); using (Assert.EnterMultipleScope()) { Assert.That(loaded, Is.Not.Null); Assert.That(loaded!.ChatId, Is.EqualTo(BflThreadId.ONBOARDING)); Assert.That(((ContentText)loaded.Blocks[0].Content!).Text, Is.EqualTo("Passt das Lab zu meinem Interview?")); Assert.That(File.Exists(Path.Combine(this.RootDirectory, "onboarding", "thread.json")), Is.True); Assert.That(await otherStore.ListProjectsAsync(), Is.Empty, "The folder of the onboarding is no preparation."); Assert.That(otherStore.ReadAllThreadLocations()[BflThreadId.ONBOARDING], Is.EqualTo(location)); Assert.That(otherStore.GetThreadWrittenAt(location), Is.Not.Null); } } [Test] public async Task StartingTheOnboardingOverDeletesIt() { var store = this.CreateStore(); var location = BflOnboardingThreadLocation.INSTANCE; await store.SaveThreadAsync(location, WithMessage(BflThreadFactory.Create(location), "Hallo")); await store.DeleteThreadAsync(location); using (Assert.EnterMultipleScope()) { Assert.That(await store.LoadThreadAsync(location), Is.Null); Assert.That(store.ReadAllThreadLocations(), Does.Not.ContainKey(BflThreadId.ONBOARDING)); } } [Test] public async Task AChatOfAStepIsNotStoredAsTheOnboarding() { var store = this.CreateStore(); var created = await store.CreateAsync("Panel"); var thread = BflThreadFactory.Create(new BflStepThreadLocation(created.ProjectId, BflStep.FRAMEWORK)); await Assert.ThrowsAsync(async () => await store.SaveThreadAsync(BflOnboardingThreadLocation.INSTANCE, thread)); } private static ChatThread WithMessage(ChatThread thread, string text) { thread.Blocks.Add(new ContentBlock { Time = DateTimeOffset.Now, ContentType = ContentType.TEXT, Role = ChatRole.USER, Content = new ContentText { Text = text } }); return thread; } private BeyondFactsLabStore CreateStore() => new(NullLogger.Instance, new BflStorageOptions { DataDirectory = this.dataDirectory }); }