mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-13 10:07:21 +00:00
Fixed line endings in the reflection folder and tests on Windows (#1192)
This commit is contained in:
1 parent
6f187f1bd8
commit
b1c1f13993
7 files changed
+157
-22
No files matched your search
@@ -13,6 +13,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IERI/@EntryIndexedValue">IERI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IMIME/@EntryIndexedValue">IMIME</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IONOS/@EntryIndexedValue">IONOS</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LF/@EntryIndexedValue">LF</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
||||
|
||||
@@ -2,6 +2,8 @@ using System.Text;
|
||||
|
||||
using AIStudio.Chat;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace AIStudio.Assistants.BeyondFactsLab;
|
||||
|
||||
/// <summary>
|
||||
@@ -12,7 +14,7 @@ namespace AIStudio.Assistants.BeyondFactsLab;
|
||||
/// No model takes part. The person confirmed every work state word for word, so the folder repeats them word for word
|
||||
/// instead of having a model rewrite them. A step without a confirmed work state is named, not filled. Personal
|
||||
/// disclosures stay out unless the person includes them, and so does the conversation of a step which may contain
|
||||
/// some: the folder is meant to be passed on.
|
||||
/// some: the folder is meant to be passed on. Every line ends with LF, on every system.
|
||||
/// </remarks>
|
||||
public static class BflReflectionFolder
|
||||
{
|
||||
@@ -33,7 +35,7 @@ public static class BflReflectionFolder
|
||||
public static string Build(BflReflectionFolderInput input)
|
||||
{
|
||||
var text = new StringBuilder();
|
||||
text.Append("# ").Append(Markdown.EscapeInlineText(BflResultKind.REFLECTION_FOLDER.Title)).Append(": ").AppendLine(Markdown.EscapeInlineText(input.ProjectName));
|
||||
text.Append("# ").Append(Markdown.EscapeInlineText(BflResultKind.REFLECTION_FOLDER.Title)).Append(": ").AppendLineLF(Markdown.EscapeInlineText(input.ProjectName));
|
||||
|
||||
foreach (var step in BflStep.All)
|
||||
AppendStep(text, step, input);
|
||||
@@ -78,15 +80,15 @@ public static class BflReflectionFolder
|
||||
/// <param name="input">What the folder is put together from.</param>
|
||||
private static void AppendStep(StringBuilder text, BflStep step, BflReflectionFolderInput input)
|
||||
{
|
||||
text.AppendLine().Append("## ").AppendLine(Markdown.EscapeInlineText(step.Title)).AppendLine();
|
||||
text.AppendLineLF().Append("## ").AppendLineLF(Markdown.EscapeInlineText(step.Title)).AppendLineLF();
|
||||
if (input.WorkStates.GetValueOrDefault(step)?.Confirmed is not { } confirmed)
|
||||
{
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(TB("No confirmed work state."))).AppendLine("_");
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(TB("No confirmed work state."))).AppendLineLF("_");
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmedAt = confirmed.ConfirmedAtUtc.ToLocalTime().ToString("d", input.Culture);
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(string.Format(input.Culture, TB("Version {0}, confirmed on {1}."), confirmed.Revision, confirmedAt))).AppendLine("_");
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(string.Format(input.Culture, TB("Version {0}, confirmed on {1}."), confirmed.Revision, confirmedAt))).AppendLineLF("_");
|
||||
var notice = BflStaleness.Of(step, input.WorkStates, input.Forms) switch
|
||||
{
|
||||
BflWorkStateStatus.OUTDATED => TB("An earlier step changed after this confirmation."),
|
||||
@@ -95,14 +97,14 @@ public static class BflReflectionFolder
|
||||
};
|
||||
|
||||
if (notice.Length > 0)
|
||||
text.AppendLine().Append('_').Append(Markdown.EscapeInlineText(notice)).AppendLine("_");
|
||||
text.AppendLineLF().Append('_').Append(Markdown.EscapeInlineText(notice)).AppendLineLF("_");
|
||||
|
||||
foreach (var section in BflWorkStateSections.ForStep(step))
|
||||
{
|
||||
if (section.Key is OPEN_POINTS_KEY || section.IsPersonalDisclosure && !input.IncludePersonalDisclosures || !confirmed.Sections.TryGetValue(section.Key, out var sectionText) || string.IsNullOrWhiteSpace(sectionText))
|
||||
continue;
|
||||
|
||||
text.AppendLine().Append("### ").AppendLine(Markdown.EscapeInlineText(section.Title)).AppendLine().AppendLine(sectionText.Trim());
|
||||
text.AppendLineLF().Append("### ").AppendLineLF(Markdown.EscapeInlineText(section.Title)).AppendLineLF().AppendLineLF(WithLineBreaks(sectionText));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +115,7 @@ public static class BflReflectionFolder
|
||||
/// <param name="input">What the folder is put together from.</param>
|
||||
private static void AppendOpenPoints(StringBuilder text, BflReflectionFolderInput input)
|
||||
{
|
||||
text.AppendLine().Append("## ").AppendLine(Markdown.EscapeInlineText(TB("Open questions"))).AppendLine();
|
||||
text.AppendLineLF().Append("## ").AppendLineLF(Markdown.EscapeInlineText(TB("Open questions"))).AppendLineLF();
|
||||
var any = false;
|
||||
foreach (var step in BflStep.All)
|
||||
{
|
||||
@@ -121,11 +123,11 @@ public static class BflReflectionFolder
|
||||
continue;
|
||||
|
||||
any = true;
|
||||
text.Append("### ").AppendLine(Markdown.EscapeInlineText(step.Title)).AppendLine().AppendLine(openPoints.Trim()).AppendLine();
|
||||
text.Append("### ").AppendLineLF(Markdown.EscapeInlineText(step.Title)).AppendLineLF().AppendLineLF(WithLineBreaks(openPoints)).AppendLineLF();
|
||||
}
|
||||
|
||||
if (!any)
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(TB("No confirmed work state names open questions."))).AppendLine("_");
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(TB("No confirmed work state names open questions."))).AppendLineLF("_");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,15 +137,15 @@ public static class BflReflectionFolder
|
||||
/// <param name="input">What the folder is put together from.</param>
|
||||
private static void AppendMaterials(StringBuilder text, BflReflectionFolderInput input)
|
||||
{
|
||||
text.AppendLine().Append("## ").AppendLine(Markdown.EscapeInlineText(TB("Material"))).AppendLine();
|
||||
text.AppendLineLF().Append("## ").AppendLineLF(Markdown.EscapeInlineText(TB("Material"))).AppendLineLF();
|
||||
if (input.Materials.Count is 0)
|
||||
{
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(TB("No material was added."))).AppendLine("_");
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(TB("No material was added."))).AppendLineLF("_");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var material in input.Materials.OrderBy(material => material.AddedAtUtc))
|
||||
text.Append("- ").Append(Markdown.EscapeInlineText(material.Name)).Append(" (").Append(Markdown.EscapeInlineText(KindName(material.Kind))).AppendLine(")");
|
||||
text.Append("- ").Append(Markdown.EscapeInlineText(material.Name)).Append(" (").Append(Markdown.EscapeInlineText(KindName(material.Kind))).AppendLineLF(")");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -153,27 +155,36 @@ public static class BflReflectionFolder
|
||||
/// <param name="input">What the folder is put together from.</param>
|
||||
private static void AppendConversations(StringBuilder text, BflReflectionFolderInput input)
|
||||
{
|
||||
text.AppendLine().Append("## ").AppendLine(Markdown.EscapeInlineText(TB("Appendix: the conversations"))).AppendLine();
|
||||
text.AppendLineLF().Append("## ").AppendLineLF(Markdown.EscapeInlineText(TB("Appendix: the conversations"))).AppendLineLF();
|
||||
foreach (var step in BflStep.All)
|
||||
{
|
||||
if (!input.Conversations.TryGetValue(step, out var messages) || messages.Count is 0)
|
||||
continue;
|
||||
|
||||
text.Append("### ").AppendLine(Markdown.EscapeInlineText(step.Title)).AppendLine();
|
||||
text.Append("### ").AppendLineLF(Markdown.EscapeInlineText(step.Title)).AppendLineLF();
|
||||
if (!input.IncludePersonalDisclosures && MayHoldPersonalDisclosures(step))
|
||||
{
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(TB("Left out, as it may contain personal disclosures."))).AppendLine("_").AppendLine();
|
||||
text.Append('_').Append(Markdown.EscapeInlineText(TB("Left out, as it may contain personal disclosures."))).AppendLineLF("_").AppendLineLF();
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
var speaker = message.IsFromPerson ? TA("You") : TB("Lab");
|
||||
text.Append("**").Append(Markdown.EscapeInlineText(speaker)).AppendLine(":**").AppendLine().AppendLine(message.Text).AppendLine();
|
||||
text.Append("**").Append(Markdown.EscapeInlineText(speaker)).AppendLineLF(":**").AppendLineLF().AppendLineLF(WithLineBreaks(message.Text)).AppendLineLF();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a text which the person or a model wrote, trimmed and with LF line breaks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A model may answer with CR LF; without this, the folder would mix two kinds of line breaks.
|
||||
/// </remarks>
|
||||
/// <param name="text">The text.</param>
|
||||
private static string WithLineBreaks(string text) => text.Trim().ReplaceLineEndings("\n");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of a kind of material.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedTools;
|
||||
|
||||
/// <summary>
|
||||
/// Appends lines with LF, the same line break on every system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// StringBuilder.AppendLine writes Environment.NewLine, which is CR LF on Windows: a text built with it differs between
|
||||
/// the systems, and it mixes two kinds of line breaks as soon as one of its parts comes with LF. These members carry a
|
||||
/// name of their own, because C# always prefers the instance method AppendLine over an extension of the same name.
|
||||
/// </remarks>
|
||||
public static class StringBuilderExtensions
|
||||
{
|
||||
private const char LINE_FEED = '\n';
|
||||
|
||||
/// <param name="builder">The builder to append to.</param>
|
||||
extension(StringBuilder builder)
|
||||
{
|
||||
/// <summary>
|
||||
/// Appends a line break, LF.
|
||||
/// </summary>
|
||||
/// <returns>The builder, to append more.</returns>
|
||||
public StringBuilder AppendLineLF() => builder.Append(LINE_FEED);
|
||||
|
||||
/// <summary>
|
||||
/// Appends a text and a line break, LF.
|
||||
/// </summary>
|
||||
/// <param name="value">The text; for null, only the line break.</param>
|
||||
/// <returns>The builder, to append more.</returns>
|
||||
public StringBuilder AppendLineLF(string? value) => builder.Append(value).Append(LINE_FEED);
|
||||
|
||||
/// <summary>
|
||||
/// Appends an interpolated text and a line break, LF.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Like StringBuilder.AppendLine, the handler writes the text right into the builder, without a string in
|
||||
/// between; afterward, only the line break is missing.
|
||||
/// </remarks>
|
||||
/// <param name="handler">The handler which the compiler creates for the interpolated text.</param>
|
||||
/// <returns>The builder, to append more.</returns>
|
||||
public StringBuilder AppendLineLF([InterpolatedStringHandlerArgument("builder")] ref StringBuilder.AppendInterpolatedStringHandler handler) => builder.Append(LINE_FEED);
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ public sealed class BflPromptComposerTests
|
||||
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());
|
||||
Assert.That(prompt, Does.Contain(WithLf(parts.Step)), step.ToString());
|
||||
Assert.That(prompt, Does.Contain(WithLf(parts.Knowledge)[..200]), step.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ public sealed class BflPromptComposerTests
|
||||
{
|
||||
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.");
|
||||
Assert.That(prompt, Does.Not.Contain(WithLf(knowledge)[..200]), "The onboarding stays short, so it leaves out the guide.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ public sealed class BflPromptComposerTests
|
||||
|
||||
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(prompt.IndexOf(WithLf(parts.Step), StringComparison.Ordinal), Is.GreaterThanOrEqualTo(0).And.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)));
|
||||
}
|
||||
}
|
||||
@@ -330,4 +330,14 @@ public sealed class BflPromptComposerTests
|
||||
}
|
||||
|
||||
private static BflMaterialExcerpt Excerpt(string name, string text, int textLength) => new(new(new BflMaterial { MaterialId = Guid.NewGuid(), Name = name, TextLength = textLength }, textLength), text);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a file of the lab's prompts as the composer inserts it: trimmed and with LF line breaks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The files keep the line endings of the checkout, CR LF on Windows with core.autocrlf. Compared as they are, a
|
||||
/// check for a part fails there, and a check for its absence passes without checking anything.
|
||||
/// </remarks>
|
||||
/// <param name="text">The text of the file.</param>
|
||||
private static string WithLf(string text) => text.ReplaceLineEndings("\n").Trim();
|
||||
}
|
||||
@@ -174,6 +174,27 @@ public sealed class BflReflectionFolderTests
|
||||
Assert.That(folder.Split('\n')[0], Does.StartWith("# ").And.Contain("Panel").And.Contain("Kopf"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryLineEndsWithLfAlsoWhenATextComesWithCrLf()
|
||||
{
|
||||
var workStates = new Dictionary<BflStep, BflWorkState>();
|
||||
Confirm(workStates, BflStep.KNOWLEDGE, new() { ["ESTABLISHED"] = "GESICHERT-1\r\nGESICHERT-2", ["OPEN_POINTS"] = "OFFEN-1\r\nOFFEN-2" });
|
||||
var conversations = new Dictionary<BflStep, IReadOnlyList<BflConversationMessage>>
|
||||
{
|
||||
[BflStep.STRATEGY] = [new(false, "ANTWORT-1\r\n\r\nANTWORT-2")],
|
||||
};
|
||||
|
||||
var folder = Build(workStates, conversations: conversations);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(folder, Does.Not.Contain("\r"), "The folder must never mix two kinds of line breaks, also not on Windows.");
|
||||
Assert.That(folder, Does.Contain("GESICHERT-1\nGESICHERT-2\n"));
|
||||
Assert.That(folder, Does.Contain("OFFEN-1\nOFFEN-2\n"));
|
||||
Assert.That(folder, Does.Contain("ANTWORT-1\n\nANTWORT-2\n"));
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
@@ -101,7 +101,8 @@ public sealed class I18NSourceScannerTests
|
||||
|
||||
var texts = I18NSourceScanner.FindTexts(SOURCE, "Notice.cs", []);
|
||||
|
||||
Assert.That(texts.Select(text => text.Text), Is.EqualTo(new[] { "## Welcome!\n\nThe first paragraph." }));
|
||||
// The scanner keeps the line endings of the checkout, as the compiler does, and the collector makes them equal:
|
||||
Assert.That(texts.Select(text => I18NKeyHash.NormalizeLineEndings(text.Text)), Is.EqualTo(new[] { "## Welcome!\n\nThe first paragraph." }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that AppendLineLF ends every line with LF, whatever the system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// StringBuilder.AppendLine writes CR LF on Windows. These tests would show it there, while on macOS and Linux they
|
||||
/// pass with both.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class StringBuilderExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void EveryKindOfLineEndsWithLf()
|
||||
{
|
||||
var number = 4711;
|
||||
var text = new StringBuilder()
|
||||
.AppendLineLF("Erste Zeile")
|
||||
.AppendLineLF()
|
||||
.AppendLineLF($"Nummer {number}")
|
||||
.AppendLineLF(null)
|
||||
.ToString();
|
||||
|
||||
Assert.That(text, Is.EqualTo("Erste Zeile\n\nNummer 4711\n\n"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnInterpolatedTextGoesIntoTheSameBuilderInItsPlace()
|
||||
{
|
||||
var value = 1.5;
|
||||
var builder = new StringBuilder("Anfang ");
|
||||
|
||||
var returned = builder.AppendLineLF($"{value.ToString(CultureInfo.InvariantCulture)} und {"Text"}").Append("Ende");
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(returned, Is.SameAs(builder));
|
||||
Assert.That(builder.ToString(), Is.EqualTo("Anfang 1.5 und Text\nEnde"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user