Files
Wiki/test/Wiki.Application.Tests/SplitArticleUseCaseTests.cs
T

223 lines
9.4 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Wiki.Application.Encyclopedia;
using Wiki.Core.Model;
namespace Wiki.Application.Tests;
public class SplitArticleUseCaseTests
{
private static readonly EncyclopediaSettings Settings = new(16000, 10, 300_000, [], null) { MaxSourcesPerArticle = 4 };
private static SplitArticleUseCase UseCase(FakeTextModel model) =>
new(model, Settings, NullLogger<SplitArticleUseCase>.Instance);
private static PlannedArticle Article(string slug, params string[] sources) =>
new(slug, "Titel " + slug, "Title " + slug, ArticleCategory.System, "Scope " + slug, sources);
private static Dictionary<string, Document> Notes(params string[] slugs) =>
slugs.ToDictionary(s => s, s => TestNotes.Note(s));
private static ArticlePlan PlanWith(params PlannedArticle[] articles)
{
var plan = ArticlePlan.Empty();
foreach (var a in articles) plan.AddArticle(a);
return plan;
}
private const string Valid = """
{"overview":{"scope":"Allgemein.","sources":["a"]},
"children":[{"slug":"x","titleDe":"X","titleEn":"X","category":"device","scope":"sx","sources":["b"]},
{"slug":"y","titleDe":"Y","titleEn":"Y","category":"topic","scope":"sy","sources":["c","d"]}]}
""";
[Fact]
public async Task Split_Valid()
{
var article = Article("heating", "a", "b", "c", "d");
var model = new FakeTextModel().Reply(Valid);
var outcome = await UseCase(model).SplitAsync(article, Notes("a", "b", "c", "d"), PlanWith(article), CancellationToken.None);
Assert.NotNull(outcome);
Assert.Equal("heating", outcome.Overview.Slug);
Assert.Equal("Titel heating", outcome.Overview.TitleDe);
Assert.Equal(ArticleCategory.System, outcome.Overview.Category);
Assert.Equal("Allgemein.", outcome.Overview.Scope);
Assert.Equal(["a"], outcome.Overview.Sources);
Assert.Equal(["x", "y"], outcome.Children.Select(c => c.Slug));
Assert.Equal(ArticleCategory.Device, outcome.Children[0].Category);
Assert.Equal(["c", "d"], outcome.Children[1].Sources);
Assert.Equal(Settings.SplitMaxTokens, model.Calls[0].MaxTokens);
Assert.StartsWith("[split]", model.Calls[0].System);
}
[Fact]
public async Task Split_LostSource_IsNull()
{
var article = Article("heating", "a", "b", "c", "d");
var model = new FakeTextModel().Reply("""
{"overview":{"scope":"s","sources":["a"]},
"children":[{"slug":"x","titleDe":"X","titleEn":"X","category":"device","scope":"sx","sources":["b"]},
{"slug":"y","titleDe":"Y","titleEn":"Y","category":"topic","scope":"sy","sources":["c"]}]}
""");
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b", "c", "d"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_OneChild_IsNull()
{
var article = Article("heating", "a", "b");
var model = new FakeTextModel().Reply("""
{"overview":{"scope":"s","sources":["a"]},
"children":[{"slug":"x","titleDe":"X","titleEn":"X","category":"device","scope":"sx","sources":["b"]}]}
""");
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_SevenChildren_IsNull()
{
var article = Article("heating", "a", "b");
var children = string.Join(",", Enumerable.Range(0, 7).Select(i =>
$$"""{"slug":"c{{i}}","titleDe":"X","titleEn":"X","category":"device","scope":"s","sources":["b"]}"""));
var model = new FakeTextModel().Reply($$"""{"overview":{"scope":"s","sources":["a"]},"children":[{{children}}]}""");
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_ChildSlugTaken_IsNull()
{
var article = Article("heating", "a", "b", "c", "d");
var model = new FakeTextModel().Reply(Valid);
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b", "c", "d"), PlanWith(article, Article("x", "z")), CancellationToken.None));
}
[Fact]
public async Task Split_ChildSlugDuplicate_IsNull()
{
var article = Article("heating", "a", "b");
var model = new FakeTextModel().Reply("""
{"overview":{"scope":"s","sources":["a"]},
"children":[{"slug":"x","titleDe":"X","titleEn":"X","category":"device","scope":"sx","sources":["b"]},
{"slug":"x","titleDe":"Y","titleEn":"Y","category":"topic","scope":"sy","sources":["b"]}]}
""");
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_ChildWithoutSources_IsNull()
{
var article = Article("heating", "a", "b");
var model = new FakeTextModel().Reply("""
{"overview":{"scope":"s","sources":["a","b"]},
"children":[{"slug":"x","titleDe":"X","titleEn":"X","category":"device","scope":"sx","sources":["b"]},
{"slug":"y","titleDe":"Y","titleEn":"Y","category":"topic","scope":"sy","sources":[]}]}
""");
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_SourceOutsideInput_IsNull()
{
var article = Article("heating", "a", "b");
var model = new FakeTextModel().Reply("""
{"overview":{"scope":"s","sources":["a"]},
"children":[{"slug":"x","titleDe":"X","titleEn":"X","category":"device","scope":"sx","sources":["b","ghost"]},
{"slug":"y","titleDe":"Y","titleEn":"Y","category":"topic","scope":"sy","sources":["b"]}]}
""");
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_Truncated_IsNull()
{
var article = Article("heating", "a", "b", "c", "d");
var model = new FakeTextModel().Reply(Valid, truncated: true);
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b", "c", "d"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_NotJson_IsNull()
{
var article = Article("heating", "a", "b");
var model = new FakeTextModel().Reply("sorry");
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_BadRequest_IsNull()
{
var article = Article("heating", "a", "b");
var model = new FakeTextModel().Throw(new HttpRequestException("bad", null, System.Net.HttpStatusCode.BadRequest));
Assert.Null(await UseCase(model).SplitAsync(article, Notes("a", "b"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_ModelUnavailable_Propagates()
{
var article = Article("heating", "a", "b");
var model = new FakeTextModel().Throw(new ModelUnavailableException("down"));
await Assert.ThrowsAsync<ModelUnavailableException>(() =>
UseCase(model).SplitAsync(article, Notes("a", "b"), PlanWith(article), CancellationToken.None));
}
[Fact]
public async Task Split_InputCappedAtMaxSources_RestStaysWithOverview()
{
var article = Article("heating", "a", "b", "c", "d", "e", "f");
var model = new FakeTextModel().Reply(Valid);
var outcome = await UseCase(model).SplitAsync(article, Notes("a", "b", "c", "d", "e", "f"), PlanWith(article), CancellationToken.None);
Assert.NotNull(outcome);
Assert.Equal(["a", "e", "f"], outcome.Overview.Sources);
var prompt = model.Calls[0].User;
Assert.Contains("\nd | ", prompt);
Assert.DoesNotContain("\ne | ", prompt);
Assert.DoesNotContain("\nf | ", prompt);
}
[Fact]
public async Task Split_PromptFlattensTitles()
{
var article = Article("heating", "a", "b", "c", "d");
var notes = Notes("a", "b", "c", "d");
notes["a"] = TestNotes.Note("a", title: "one | two\nthree");
var model = new FakeTextModel().Reply(Valid);
await UseCase(model).SplitAsync(article, notes, PlanWith(article), CancellationToken.None);
Assert.Contains("\na | one two three | ", model.Calls[0].User);
}
[Fact]
public void NeedsSplit_ByCharsAndByCount()
{
var s = new EncyclopediaSettings(16000, 10, 1000, [], null) { MaxSourcesPerArticle = 3 };
var notes = new Dictionary<string, Document>
{
["a"] = TestNotes.Note("a", 600),
["b"] = TestNotes.Note("b", 600),
["c"] = TestNotes.Note("c", 100),
["d"] = TestNotes.Note("d", 1),
};
Assert.True(SplitArticleUseCase.NeedsSplit(Article("x", "a", "b"), notes, s));
Assert.False(SplitArticleUseCase.NeedsSplit(Article("x", "a", "c"), notes, s));
Assert.False(SplitArticleUseCase.NeedsSplit(Article("x", "a", "ghost"), notes, s));
Assert.True(SplitArticleUseCase.NeedsSplit(Article("x", "c", "d", "c2", "d2"), notes, s));
Assert.False(SplitArticleUseCase.NeedsSplit(Article("x", "c", "d", "ghost"), notes, s));
}
}