Added results and exports to the BeyondFacts Lab (#1184)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-10 13:54:10 +01:00
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));
}
}
+175
View File
@@ -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);
}
}
+2 -1
View File
@@ -61,11 +61,12 @@ public static partial class JsonTestRoots
yield return Case(new(typeof(IReadOnlyList<string>), SqliteProvenanceStoreClientImplementation.JSON_OPTIONS));
yield return Case(new(typeof(List<string>), SqliteProvenanceStoreClientImplementation.JSON_OPTIONS));
// The preparations of the BeyondFacts Lab, their forms, materials, and work states, and the preparation selected last:
// The preparations of the BeyondFacts Lab, their forms, materials, work states, and results, and the preparation selected last:
yield return Case(new(typeof(BflProject), BeyondFactsLabJson.PERSISTENCE));
yield return Case(new(typeof(BflStepFormData), BeyondFactsLabJson.PERSISTENCE));
yield return Case(new(typeof(BflMaterial), BeyondFactsLabJson.PERSISTENCE));
yield return Case(new(typeof(BflWorkState), BeyondFactsLabJson.PERSISTENCE));
yield return Case(new(typeof(BflResult), BeyondFactsLabJson.PERSISTENCE));
yield return Case(new(typeof(Guid?), BeyondFactsLabJson.PERSISTENCE), "selected preparation");
}