223 lines
9.4 KiB
C#
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));
|
|
}
|
|
}
|