mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-11 08:47:21 +00:00
Added results and exports to the BeyondFacts Lab (#1184)
This commit is contained in:
1 parent
8e1a66cb24
commit
e5c0931a60
42 files changed
+3034
-67
No files matched your search
@@ -0,0 +1,134 @@
|
||||
using AIStudio.Assistants.BeyondFactsLab;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.Tests.BeyondFactsLab;
|
||||
|
||||
/// <summary>
|
||||
/// Checks how the BeyondFacts Lab stores the results of a preparation.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class BeyondFactsLabStoreResultTests
|
||||
{
|
||||
private string dataDirectory = string.Empty;
|
||||
|
||||
[SetUp]
|
||||
public void CreateDataDirectory()
|
||||
{
|
||||
this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-beyondfacts-lab-{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 APreparationWithoutAResultGetsAnEmptyOne()
|
||||
{
|
||||
var store = this.CreateStore();
|
||||
var created = await store.CreateAsync("Panel");
|
||||
|
||||
var result = await store.LoadResultAsync(created.ProjectId, BflResultKind.POSITION_PAPER);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Draft, Is.Null);
|
||||
Assert.That(result.Confirmed, Is.Null);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AResultIsStoredWithItsDraftAndItsConfirmedVersion()
|
||||
{
|
||||
var store = this.CreateStore();
|
||||
var created = await store.CreateAsync("Panel");
|
||||
var confirmedAtUtc = new DateTimeOffset(2026, 11, 10, 9, 0, 0, TimeSpan.Zero);
|
||||
var result = new BflResult { Draft = new BflResultVersion { Text = "# Positionsblatt", WorkStateRevisions = { [BflStep.GOALS_AND_ROLE] = 3 } } };
|
||||
result.Confirm("# Positionsblatt\n\nBestätigt", confirmedAtUtc);
|
||||
result.Edit();
|
||||
result.Draft!.Text = "# Positionsblatt\n\nÜberarbeitet";
|
||||
|
||||
var changed = await store.SaveResultAsync(created.ProjectId, BflResultKind.POSITION_PAPER, result);
|
||||
var loaded = await this.CreateStore().LoadResultAsync(created.ProjectId, BflResultKind.POSITION_PAPER);
|
||||
var others = await this.CreateStore().LoadResultAsync(created.ProjectId, BflResultKind.CONVERSATION_GUIDE);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(changed.ModifiedAtUtc, Is.GreaterThanOrEqualTo(created.ModifiedAtUtc));
|
||||
Assert.That(loaded.Draft!.Text, Is.EqualTo("# Positionsblatt\n\nÜberarbeitet"));
|
||||
Assert.That(loaded.Confirmed!.Text, Is.EqualTo("# Positionsblatt\n\nBestätigt"));
|
||||
Assert.That(loaded.Confirmed.ChangedAtUtc, Is.EqualTo(confirmedAtUtc));
|
||||
Assert.That(loaded.Confirmed.WorkStateRevisions, Is.EqualTo(new Dictionary<BflStep, int> { [BflStep.GOALS_AND_ROLE] = 3 }));
|
||||
Assert.That(others.Confirmed, Is.Null, "Each kind of result has a file of its own.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ADamagedResultIsNeitherOpenedNorOverwritten()
|
||||
{
|
||||
const string DAMAGED = "{ \"draft\": ";
|
||||
var store = this.CreateStore();
|
||||
var created = await store.CreateAsync("Panel");
|
||||
var path = await this.WriteResultFileAsync(created.ProjectId, "open-points.json", DAMAGED);
|
||||
|
||||
var results = await store.LoadResultsAsync(created.ProjectId);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(results.ContainsKey(BflResultKind.OPEN_POINTS), Is.False);
|
||||
Assert.That(results.ContainsKey(BflResultKind.POSITION_PAPER), Is.True);
|
||||
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.LoadResultAsync(created.ProjectId, BflResultKind.OPEN_POINTS));
|
||||
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.SaveResultAsync(created.ProjectId, BflResultKind.OPEN_POINTS, new BflResult()));
|
||||
Assert.That(await File.ReadAllTextAsync(path), Is.EqualTo(DAMAGED));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AResultWithoutItsTextIsNotOpened()
|
||||
{
|
||||
const string WITHOUT_TEXT = """{ "schemaVersion": 1, "confirmed": { "text": null, "workStateRevisions": {} } }""";
|
||||
var store = this.CreateStore();
|
||||
var created = await store.CreateAsync("Panel");
|
||||
await this.WriteResultFileAsync(created.ProjectId, "position-paper.json", WITHOUT_TEXT);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.LoadResultAsync(created.ProjectId, BflResultKind.POSITION_PAPER));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AResultOfANewerVersionIsNotOverwritten()
|
||||
{
|
||||
const string NEWER = """{ "schemaVersion": 2 }""";
|
||||
var store = this.CreateStore();
|
||||
var created = await store.CreateAsync("Panel");
|
||||
var path = await this.WriteResultFileAsync(created.ProjectId, "reflection-folder.json", NEWER);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.SaveResultAsync(created.ProjectId, BflResultKind.REFLECTION_FOLDER, new BflResult()));
|
||||
Assert.That(await File.ReadAllTextAsync(path), Is.EqualTo(NEWER));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AResultOfAMissingPreparationIsNotWritten()
|
||||
{
|
||||
var store = this.CreateStore();
|
||||
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(async () => await store.SaveResultAsync(Guid.NewGuid(), BflResultKind.POSITION_PAPER, new BflResult()));
|
||||
}
|
||||
|
||||
private async Task<string> WriteResultFileAsync(Guid projectId, string fileName, string content)
|
||||
{
|
||||
var directory = Path.Combine(this.dataDirectory, BeyondFactsLabStore.ROOT_DIRECTORY_NAME, projectId.ToString("D"), "results");
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = Path.Combine(directory, fileName);
|
||||
await File.WriteAllTextAsync(path, content);
|
||||
return path;
|
||||
}
|
||||
|
||||
private BeyondFactsLabStore CreateStore() => new(NullLogger<BeyondFactsLabStore>.Instance, new BflStorageOptions { DataDirectory = this.dataDirectory });
|
||||
}
|
||||
@@ -68,12 +68,14 @@ public sealed class BflPromptLibraryTests
|
||||
var core = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.CORE_PATH).Select(name => $"{{{{{name}}}}}"));
|
||||
var onboarding = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.ONBOARDING_PATH).Select(name => $"{{{{{name}}}}}"));
|
||||
var workState = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.WORK_STATE_PATH).Select(name => $"{{{{{name}}}}}"));
|
||||
var result = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.ResultPath(BflResultKind.POSITION_PAPER)).Select(name => $"{{{{{name}}}}}"));
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(BflPromptComposer.Compose(new(core, "Leitfaden", "Schritt", "Anrede"), BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [], "Deutsch"), Does.Not.Contain("{{"));
|
||||
Assert.That(BflPromptComposer.ComposeOnboarding(new(onboarding, "Anrede"), "Deutsch"), Does.Not.Contain("{{"));
|
||||
Assert.That(BflPromptComposer.ComposeWorkStateRequest(workState, BflStep.FRAMEWORK, new BflStepFormData()), Does.Not.Contain("{{"));
|
||||
Assert.That(BflPromptComposer.ComposeResultRequest(result, new Dictionary<BflStep, BflWorkState>()), Does.Not.Contain("{{"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
using System.Globalization;
|
||||
|
||||
using AIStudio.Assistants.BeyondFactsLab;
|
||||
using AIStudio.Chat;
|
||||
|
||||
namespace AIStudio.Tests.BeyondFactsLab;
|
||||
|
||||
/// <summary>
|
||||
/// Checks how the BeyondFacts Lab puts the reflection folder together, without a model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The folder repeats what the person confirmed word for word, and it is meant to be passed on: personal disclosures
|
||||
/// stay out unless the person includes them.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class BflReflectionFolderTests
|
||||
{
|
||||
private static readonly DateTimeOffset NOW = new(2026, 11, 10, 9, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Test]
|
||||
public void AConfirmedWorkStateAppearsWordForWordUnderItsStep()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
Confirm(workStates, BflStep.KNOWLEDGE, new() { ["ESTABLISHED"] = "Gesichert-4711\n\n- mit Liste" });
|
||||
var established = BflWorkStateSections.ForStep(BflStep.KNOWLEDGE).Single(section => section.Key == "ESTABLISHED");
|
||||
|
||||
var folder = Build(workStates);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(folder, Does.Contain($"## {BflStep.KNOWLEDGE.Title}\n"));
|
||||
Assert.That(folder, Does.Contain($"### {established.Title}\n\nGesichert-4711\n\n- mit Liste\n"));
|
||||
Assert.That(folder.IndexOf("Gesichert-4711", StringComparison.Ordinal), Is.GreaterThan(folder.IndexOf($"## {BflStep.KNOWLEDGE.Title}", StringComparison.Ordinal)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ADraftNeverAppears()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>
|
||||
{
|
||||
[BflStep.KNOWLEDGE] = new() { Draft = new(StringComparer.Ordinal) { ["ESTABLISHED"] = "ENTWURF-4711" } },
|
||||
};
|
||||
|
||||
Assert.That(Build(workStates), Does.Not.Contain("ENTWURF-4711"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryStepAppearsInItsOrderAlsoWithoutAConfirmation()
|
||||
{
|
||||
var folder = Build(new Dictionary<BflStep, BflWorkState>());
|
||||
var positions = BflStep.All.Select(step => folder.IndexOf($"## {step.Title}\n", StringComparison.Ordinal)).ToList();
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(positions, Has.All.GreaterThanOrEqualTo(0));
|
||||
Assert.That(positions, Is.Ordered);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void APersonalSectionStaysOutUnlessThePersonIncludesIt()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
Confirm(workStates, BflStep.VALUES_AND_PERSPECTIVES, new() { ["VALUES"] = "Gerechtigkeit", ["PERSONAL_INVOLVEMENT"] = "PERSÖNLICH-4711" });
|
||||
|
||||
var without = Build(workStates);
|
||||
var with = Build(workStates, includePersonalDisclosures: true);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(without, Does.Not.Contain("PERSÖNLICH-4711"));
|
||||
Assert.That(without, Does.Contain("Gerechtigkeit"));
|
||||
Assert.That(with, Does.Contain("PERSÖNLICH-4711"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheOpenPointsOfAllStepsAreGatheredAfterTheSteps()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
Confirm(workStates, BflStep.FRAMEWORK, new() { ["FORMAT"] = "Podium", ["OPEN_POINTS"] = "OFFEN-1" });
|
||||
Confirm(workStates, BflStep.STRATEGY, new() { ["OPEN_POINTS"] = "OFFEN-5" });
|
||||
|
||||
var folder = Build(workStates);
|
||||
var lastStep = folder.IndexOf($"## {BflStep.RESULTS.Title}\n", StringComparison.Ordinal);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(folder.IndexOf("OFFEN-1", StringComparison.Ordinal), Is.GreaterThan(lastStep));
|
||||
Assert.That(folder.IndexOf("OFFEN-5", StringComparison.Ordinal), Is.GreaterThan(folder.IndexOf("OFFEN-1", StringComparison.Ordinal)));
|
||||
Assert.That(folder.IndexOf("OFFEN-1", StringComparison.Ordinal), Is.EqualTo(folder.LastIndexOf("OFFEN-1", StringComparison.Ordinal)), "The open points appear once.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheMaterialIsListedByName()
|
||||
{
|
||||
var materials = new List<BflMaterial>
|
||||
{
|
||||
new() { Name = "Studie_2026.pdf", Kind = BflMaterialKind.DOCUMENT, AddedAtUtc = NOW },
|
||||
new() { Name = "Interview.m4a", Kind = BflMaterialKind.RECORDING, AddedAtUtc = NOW.AddMinutes(1) },
|
||||
};
|
||||
|
||||
var folder = BflReflectionFolder.Build(new BflReflectionFolderInput("Panel", new Dictionary<BflStep, BflWorkState>(), Forms(), materials, new Dictionary<BflStep, IReadOnlyList<BflConversationMessage>>(), false, CultureInfo.InvariantCulture));
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(folder, Does.Contain("- Studie\\_2026\\.pdf ("));
|
||||
Assert.That(folder, Does.Contain("- Interview\\.m4a ("));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheConversationsAppearOnlyWhenThePersonAddsThem()
|
||||
{
|
||||
var conversations = new Dictionary<BflStep, IReadOnlyList<BflConversationMessage>>
|
||||
{
|
||||
[BflStep.STRATEGY] = [new(true, "FRAGE-4711"), new(false, "ANTWORT-4711")],
|
||||
};
|
||||
|
||||
var without = Build(new Dictionary<BflStep, BflWorkState>());
|
||||
var with = Build(new Dictionary<BflStep, BflWorkState>(), conversations: conversations);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(without, Does.Not.Contain("FRAGE-4711"));
|
||||
Assert.That(with, Does.Contain("FRAGE-4711"));
|
||||
Assert.That(with.IndexOf("ANTWORT-4711", StringComparison.Ordinal), Is.GreaterThan(with.IndexOf("FRAGE-4711", StringComparison.Ordinal)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheConversationOfAStepWithPersonalDisclosuresStaysOutUnlessThePersonIncludesThem()
|
||||
{
|
||||
var conversations = new Dictionary<BflStep, IReadOnlyList<BflConversationMessage>>
|
||||
{
|
||||
[BflStep.VALUES_AND_PERSPECTIVES] = [new(true, "BETROFFEN-4711")],
|
||||
[BflStep.STRATEGY] = [new(true, "STRATEGIE-4711")],
|
||||
};
|
||||
|
||||
var without = Build(new Dictionary<BflStep, BflWorkState>(), conversations: conversations);
|
||||
var with = Build(new Dictionary<BflStep, BflWorkState>(), includePersonalDisclosures: true, conversations: conversations);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(BflReflectionFolder.MayHoldPersonalDisclosures(BflStep.VALUES_AND_PERSPECTIVES), Is.True);
|
||||
Assert.That(BflReflectionFolder.MayHoldPersonalDisclosures(BflStep.STRATEGY), Is.False);
|
||||
Assert.That(without, Does.Not.Contain("BETROFFEN-4711"));
|
||||
Assert.That(without, Does.Contain("STRATEGIE-4711"));
|
||||
Assert.That(with, Does.Contain("BETROFFEN-4711"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheMessagesOfAConversationLeaveOutWhatThePersonNeverSaw()
|
||||
{
|
||||
var thread = BflThreadFactory.Create(new BflStepThreadLocation(Guid.NewGuid(), BflStep.STRATEGY));
|
||||
thread.Blocks.Add(Block(ChatRole.USER, "VERSTECKT-4711", hidden: true));
|
||||
thread.Blocks.Add(Block(ChatRole.AI, "Willkommen"));
|
||||
thread.Blocks.Add(Block(ChatRole.USER, " Meine Frage "));
|
||||
thread.Blocks.Add(Block(ChatRole.AI, " "));
|
||||
|
||||
var messages = BflReflectionFolder.MessagesOf(thread);
|
||||
|
||||
Assert.That(messages, Is.EqualTo(new[] { new BflConversationMessage(false, "Willkommen"), new BflConversationMessage(true, "Meine Frage") }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheNameOfThePreparationCannotBreakTheHeading()
|
||||
{
|
||||
var folder = BflReflectionFolder.Build(new BflReflectionFolderInput("Panel\n# Kopf", new Dictionary<BflStep, BflWorkState>(), Forms(), [], new Dictionary<BflStep, IReadOnlyList<BflConversationMessage>>(), false, CultureInfo.InvariantCulture));
|
||||
|
||||
Assert.That(folder.Split('\n')[0], Does.StartWith("# ").And.Contain("Panel").And.Contain("Kopf"));
|
||||
}
|
||||
|
||||
private static string Build(IReadOnlyDictionary<BflStep, BflWorkState> workStates, bool includePersonalDisclosures = false, IReadOnlyDictionary<BflStep, IReadOnlyList<BflConversationMessage>>? conversations = null) => BflReflectionFolder.Build(new BflReflectionFolderInput("Panel", workStates, Forms(), [], conversations ?? new Dictionary<BflStep, IReadOnlyList<BflConversationMessage>>(), includePersonalDisclosures, CultureInfo.InvariantCulture));
|
||||
|
||||
private static Dictionary<BflStep, BflStepFormData> Forms() => BflStep.All.ToDictionary(step => step, _ => new BflStepFormData());
|
||||
|
||||
private static ContentBlock Block(ChatRole role, string text, bool hidden = false) => new()
|
||||
{
|
||||
Time = NOW,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = role,
|
||||
Content = new ContentText { Text = text },
|
||||
HideFromUser = hidden,
|
||||
};
|
||||
|
||||
private static void Confirm(Dictionary<BflStep, BflWorkState> workStates, BflStep step, Dictionary<string, string> sections)
|
||||
{
|
||||
if (!workStates.TryGetValue(step, out var workState))
|
||||
{
|
||||
workState = new BflWorkState();
|
||||
workStates[step] = workState;
|
||||
}
|
||||
|
||||
workState.Confirm(sections, BflStaleness.UpstreamRevisionsOf(step, workStates), BflStaleness.FingerprintOf(new BflStepFormData()), NOW);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using AIStudio.Assistants.BeyondFactsLab;
|
||||
|
||||
namespace AIStudio.Tests.BeyondFactsLab;
|
||||
|
||||
/// <summary>
|
||||
/// Checks the requests which let the model write the results of the BeyondFacts Lab, and how the lab reads what the
|
||||
/// model wrote.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class BflResultRequestTests
|
||||
{
|
||||
private static readonly DateTimeOffset NOW = new(2026, 11, 10, 9, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static IEnumerable<BflResultKind> WrittenByModel => BflResultKind.All.Where(kind => kind.IsWrittenByModel);
|
||||
|
||||
[Test]
|
||||
public async Task TheRequestOfEveryResultNamesTheStepsWithoutAConfirmedWorkState()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
workStates[BflStep.FRAMEWORK] = new BflWorkState();
|
||||
workStates[BflStep.FRAMEWORK].Confirm(new Dictionary<string, string> { ["FORMAT"] = "Podium" }, new Dictionary<BflStep, int>(), string.Empty, NOW);
|
||||
|
||||
foreach (var kind in WrittenByModel)
|
||||
{
|
||||
var request = BflPromptComposer.ComposeResultRequest(await BflPromptLibrary.ReadAsync(BflPromptLibrary.ResultPath(kind)), workStates);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(request, Does.Not.Contain("{{"), kind.ToString());
|
||||
Assert.That(request, Does.Not.Contain($"„{BflStep.FRAMEWORK.Title}“"), kind.ToString());
|
||||
foreach (var step in BflStep.All.Where(step => step is not BflStep.FRAMEWORK))
|
||||
Assert.That(request, Does.Contain($"„{step.Title}“"), $"{kind}: {step}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ARequestSaysWhenEveryStepIsConfirmed()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
foreach (var step in BflStep.All)
|
||||
{
|
||||
workStates[step] = new BflWorkState();
|
||||
workStates[step].Confirm(new Dictionary<string, string> { ["OPEN_POINTS"] = "Punkt" }, new Dictionary<BflStep, int>(), string.Empty, NOW);
|
||||
}
|
||||
|
||||
Assert.That(BflPromptComposer.ComposeResultRequest("{{MISSING_WORK_STATES}}", workStates), Is.EqualTo("keine; alle Schritte sind bestätigt."));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ADraftOfAWorkStateCountsAsMissing()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState> { [BflStep.STRATEGY] = new() { Draft = new(StringComparer.Ordinal) { ["OPEN_POINTS"] = "Entwurf" } } };
|
||||
|
||||
Assert.That(BflPromptComposer.ComposeResultRequest("{{MISSING_WORK_STATES}}", workStates), Does.Contain($"„{BflStep.STRATEGY.Title}“"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryResultOfAModelHasARequestTheTestFolderMayReplace()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
foreach (var kind in WrittenByModel)
|
||||
Assert.That(BflPromptLibrary.AllPaths, Does.Contain(BflPromptLibrary.ResultPath(kind)), kind.ToString());
|
||||
|
||||
Assert.That(WrittenByModel.Select(BflPromptLibrary.ResultPath), Is.Unique);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => BflPromptLibrary.ResultPath(BflResultKind.REFLECTION_FOLDER));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("# Positionsblatt\n\nText", "# Positionsblatt\n\nText", TestName = "AResultInPlainMarkdownStaysAsItIs")]
|
||||
[TestCase("```markdown\n# Positionsblatt\n\nText\n```", "# Positionsblatt\n\nText", TestName = "ACodeBlockAroundTheWholeResultIsTakenOff")]
|
||||
[TestCase("~~~\n# Leitfaden\n~~~", "# Leitfaden", TestName = "ACodeBlockWithTildesAroundTheWholeResultIsTakenOff")]
|
||||
[TestCase("```\n# Fragen\n```\n\nText\n\n```\nCode\n```", "```\n# Fragen\n```\n\nText\n\n```\nCode\n```", TestName = "SeveralCodeBlocksStayAsTheyAre")]
|
||||
[TestCase(" # Liste\r\n\r\n- [ ] Punkt \r\n", "# Liste\n\n- [ ] Punkt", TestName = "LineEndingsAndSpacesAroundTheResultAreTidied")]
|
||||
[TestCase(" \n ", "", TestName = "AnAnswerWithoutTextGivesNoResult")]
|
||||
public void TheLabReadsTheResultFromTheAnswer(string answer, string expected)
|
||||
{
|
||||
Assert.That(BflResultText.Read(answer), Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using AIStudio.Assistants.BeyondFactsLab;
|
||||
|
||||
namespace AIStudio.Tests.BeyondFactsLab;
|
||||
|
||||
/// <summary>
|
||||
/// Checks the results of the BeyondFacts Lab: how a draft becomes the confirmed version, and when a result is
|
||||
/// outdated.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A result is outdated as soon as the work state of any step is confirmed for the first time, confirmed anew, or
|
||||
/// withdrawn after the lab wrote it.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class BflResultTests
|
||||
{
|
||||
private static readonly DateTimeOffset NOW = new(2026, 11, 10, 9, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static readonly Dictionary<string, string> SECTIONS = new(StringComparer.Ordinal) { ["GOAL"] = "Informieren" };
|
||||
|
||||
[Test]
|
||||
public void AConfirmationKeepsWhatTheDraftWasBuiltFrom()
|
||||
{
|
||||
var result = new BflResult { Draft = new BflResultVersion { Text = "Entwurf", WorkStateRevisions = { [BflStep.FRAMEWORK] = 2 } } };
|
||||
|
||||
result.Confirm("Bestätigt", NOW);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Draft, Is.Null);
|
||||
Assert.That(result.Confirmed!.Text, Is.EqualTo("Bestätigt"));
|
||||
Assert.That(result.Confirmed.WorkStateRevisions, Is.EqualTo(new Dictionary<BflStep, int> { [BflStep.FRAMEWORK] = 2 }));
|
||||
Assert.That(result.Confirmed.ChangedAtUtc, Is.EqualTo(NOW));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AResultWithoutADraftCannotBeConfirmed()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => new BflResult().Confirm("Text", NOW));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EditingAConfirmedResultStartsADraftAndKeepsTheConfirmedVersion()
|
||||
{
|
||||
var result = new BflResult { Confirmed = new BflResultVersion { Text = "Bestätigt", WorkStateRevisions = { [BflStep.FRAMEWORK] = 1 } } };
|
||||
|
||||
result.Edit();
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Draft!.Text, Is.EqualTo("Bestätigt"));
|
||||
Assert.That(result.Draft.WorkStateRevisions, Is.EqualTo(result.Confirmed.WorkStateRevisions));
|
||||
Assert.That(result.Draft.WorkStateRevisions, Is.Not.SameAs(result.Confirmed.WorkStateRevisions));
|
||||
Assert.That(result.Confirmed.Text, Is.EqualTo("Bestätigt"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EditingKeepsADraftWhichIsThereAlready()
|
||||
{
|
||||
var result = new BflResult
|
||||
{
|
||||
Draft = new BflResultVersion { Text = "Entwurf" },
|
||||
Confirmed = new BflResultVersion { Text = "Bestätigt" },
|
||||
};
|
||||
|
||||
result.Edit();
|
||||
|
||||
Assert.That(result.Draft!.Text, Is.EqualTo("Entwurf"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AResultFitsWhileTheWorkStatesStayAsTheyAre()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
Confirm(workStates, BflStep.FRAMEWORK);
|
||||
Confirm(workStates, BflStep.TEST_SITUATION);
|
||||
|
||||
var version = new BflResultVersion { WorkStateRevisions = new(BflStaleness.ConfirmedRevisionsOf(workStates)) };
|
||||
|
||||
Assert.That(BflStaleness.IsOutdated(version, workStates), Is.False);
|
||||
}
|
||||
|
||||
[TestCase(Change.CONFIRMED_FOR_THE_FIRST_TIME, TestName = "AStepConfirmedForTheFirstTimeOutdatesAResult")]
|
||||
[TestCase(Change.CONFIRMED_ANEW, TestName = "AStepConfirmedAnewOutdatesAResult")]
|
||||
[TestCase(Change.WITHDRAWN, TestName = "AStepWithdrawnOutdatesAResult")]
|
||||
[TestCase(Change.CONFIRMED_ANEW_IN_THE_LAST_STEP, TestName = "TheLastStepConfirmedAnewOutdatesAResult")]
|
||||
public void AChangedWorkStateOutdatesAResult(Change change)
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
Confirm(workStates, BflStep.FRAMEWORK);
|
||||
Confirm(workStates, BflStep.RESULTS);
|
||||
var version = new BflResultVersion { WorkStateRevisions = new(BflStaleness.ConfirmedRevisionsOf(workStates)) };
|
||||
|
||||
switch (change)
|
||||
{
|
||||
case Change.CONFIRMED_FOR_THE_FIRST_TIME:
|
||||
Confirm(workStates, BflStep.STRATEGY);
|
||||
break;
|
||||
|
||||
case Change.CONFIRMED_ANEW:
|
||||
Confirm(workStates, BflStep.FRAMEWORK);
|
||||
break;
|
||||
|
||||
case Change.WITHDRAWN:
|
||||
workStates[BflStep.FRAMEWORK].Withdraw();
|
||||
break;
|
||||
|
||||
case Change.CONFIRMED_ANEW_IN_THE_LAST_STEP:
|
||||
Confirm(workStates, BflStep.RESULTS);
|
||||
break;
|
||||
}
|
||||
|
||||
Assert.That(BflStaleness.IsOutdated(version, workStates), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ADraftOfAWorkStateDoesNotOutdateAResult()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
Confirm(workStates, BflStep.FRAMEWORK);
|
||||
var version = new BflResultVersion { WorkStateRevisions = new(BflStaleness.ConfirmedRevisionsOf(workStates)) };
|
||||
|
||||
workStates[BflStep.FRAMEWORK].Draft = new(SECTIONS);
|
||||
workStates[BflStep.KNOWLEDGE] = new BflWorkState { Draft = new(SECTIONS) };
|
||||
|
||||
Assert.That(BflStaleness.IsOutdated(version, workStates), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheConfirmedRevisionsNameEveryStepWithAConfirmedWorkState()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
Confirm(workStates, BflStep.KNOWLEDGE);
|
||||
Confirm(workStates, BflStep.RESULTS);
|
||||
Confirm(workStates, BflStep.RESULTS);
|
||||
workStates[BflStep.STRATEGY] = new BflWorkState { Draft = new(SECTIONS) };
|
||||
|
||||
Assert.That(BflStaleness.ConfirmedRevisionsOf(workStates), Is.EquivalentTo(new Dictionary<BflStep, int> { [BflStep.KNOWLEDGE] = 1, [BflStep.RESULTS] = 2 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryResultHasAFileOfItsOwn()
|
||||
{
|
||||
Assert.That(BflResultKind.All.Select(kind => kind.FileName), Is.Unique);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnlyTheReflectionFolderIsPutTogetherWithoutAModel()
|
||||
{
|
||||
Assert.That(BflResultKind.All.Where(kind => !kind.IsWrittenByModel), Is.EqualTo(new[] { BflResultKind.REFLECTION_FOLDER }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What happens to the work states after the lab wrote a result.
|
||||
/// </summary>
|
||||
public enum Change
|
||||
{
|
||||
CONFIRMED_FOR_THE_FIRST_TIME,
|
||||
CONFIRMED_ANEW,
|
||||
WITHDRAWN,
|
||||
CONFIRMED_ANEW_IN_THE_LAST_STEP,
|
||||
}
|
||||
|
||||
private static void Confirm(Dictionary<BflStep, BflWorkState> workStates, BflStep step)
|
||||
{
|
||||
if (!workStates.TryGetValue(step, out var workState))
|
||||
{
|
||||
workState = new BflWorkState();
|
||||
workStates[step] = workState;
|
||||
}
|
||||
|
||||
workState.Confirm(SECTIONS, BflStaleness.UpstreamRevisionsOf(step, workStates), BflStaleness.FingerprintOf(new BflStepFormData()), NOW);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user