using Microsoft.Extensions.Logging.Abstractions; using Wiki.Application.Encyclopedia; using Wiki.Core.Model; namespace Wiki.Application.Tests; public class AssignNotesUseCaseTests { private static readonly EncyclopediaSettings Settings = new(16000, 10, 300_000, [], null); private static AssignNotesUseCase UseCase(FakeTextModel model) => new(model, Settings, NullLogger.Instance); private static PlannedArticle Article(string slug, params string[] sources) => new(slug, "Titel " + slug, "Title " + slug, ArticleCategory.System, "Scope " + slug, sources); private static ArticlePlan PlanWith(params PlannedArticle[] articles) { var plan = ArticlePlan.Empty(); foreach (var a in articles) plan.AddArticle(a); return plan; } private static Document[] Batch(params string[] slugs) => slugs.Select(s => TestNotes.Note(s)).ToArray(); [Fact] public void Prompt_RoomNotesAlsoFeedTheRoomArticle() { Assert.Contains("additionally feeds that room's article (category room)", Prompts.Assign); Assert.Contains("\"EG Schlafzimmer\"", Prompts.Assign); } [Fact] public async Task Assign_ExistingAndNew() { var heating = Article("heating", "old"); var model = new FakeTextModel().Reply(""" {"new":[{"slug":"garden","titleDe":"Garten","titleEn":"Garden","category":"room","scope":"Der Garten."}], "notes":[{"note":"a","articles":["heating","garden"]},{"note":"b","articles":["garden"]},{"note":"c","skip":true}]} """); var outcome = await UseCase(model).AssignAsync(Batch("a", "b", "c"), [heating], PlanWith(heating), CancellationToken.None); Assert.False(outcome.Unusable); var garden = Assert.Single(outcome.New); Assert.Equal("garden", garden.Slug); Assert.Equal(ArticleCategory.Room, garden.Category); Assert.Equal(["a", "b"], garden.Sources); Assert.Equal(["heating", "garden"], outcome.Assignments["a"]); Assert.Empty(outcome.Assignments["c"]); } [Fact] public async Task Assign_NoteOutsideBatch_Dropped() { var heating = Article("heating"); var model = new FakeTextModel().Reply("""{"new":[],"notes":[{"note":"a","articles":["heating"]},{"note":"zzz","articles":["heating"]}]}"""); var outcome = await UseCase(model).AssignAsync(Batch("a"), [heating], PlanWith(heating), CancellationToken.None); Assert.Equal(["a"], outcome.Assignments.Keys); } [Fact] public async Task Assign_ArticleOutsideCandidates_Dropped() { var heating = Article("heating"); var other = Article("other"); var model = new FakeTextModel().Reply("""{"new":[],"notes":[{"note":"a","articles":["heating","other"]},{"note":"b","articles":["other"]}]}"""); var outcome = await UseCase(model).AssignAsync(Batch("a", "b"), [heating], PlanWith(heating, other), CancellationToken.None); Assert.Equal(["heating"], outcome.Assignments["a"]); Assert.False(outcome.Assignments.ContainsKey("b")); } [Fact] public async Task Assign_NewWithExistingNonCandidateSlug_MergedIntoExistingArticle() { var heating = Article("heating"); var plan = PlanWith(heating, Article("garden")); var model = new FakeTextModel().Reply(""" {"new":[{"slug":"garden","titleDe":"G","titleEn":"G","category":"room","scope":"s"}], "notes":[{"note":"a","articles":["garden"]},{"note":"b","articles":["heating"]}]} """); var outcome = await UseCase(model).AssignAsync(Batch("a", "b"), [heating], plan, CancellationToken.None); Assert.False(outcome.Unusable); Assert.Empty(outcome.New); Assert.Equal(["garden"], outcome.Assignments["a"]); Assert.Equal(["heating"], outcome.Assignments["b"]); } [Fact] public async Task Assign_NewWithRedirectedSlug_DroppedAndItsNotesUnassigned() { var heating = Article("heating"); var plan = PlanWith(heating, Article("garden"), Article("yard")); plan.Fold("yard", "garden", isPublic: false); var model = new FakeTextModel().Reply(""" {"new":[{"slug":"yard","titleDe":"G","titleEn":"G","category":"room","scope":"s"}], "notes":[{"note":"a","articles":["yard"]},{"note":"b","articles":["heating"]}]} """); var outcome = await UseCase(model).AssignAsync(Batch("a", "b"), [heating], plan, CancellationToken.None); Assert.Empty(outcome.New); Assert.False(outcome.Assignments.ContainsKey("a")); Assert.Equal(["heating"], outcome.Assignments["b"]); } [Fact] public async Task Assign_NewWithRetiredSlug_DroppedAndItsNotesUnassigned() { var heating = Article("heating"); var plan = PlanWith(heating, Article("garden", "gone")); plan.RemoveNote("gone"); var model = new FakeTextModel().Reply(""" {"new":[{"slug":"garden","titleDe":"G","titleEn":"G","category":"room","scope":"s"}], "notes":[{"note":"a","articles":["garden"]},{"note":"b","articles":["heating"]}]} """); var outcome = await UseCase(model).AssignAsync(Batch("a", "b"), [heating], plan, CancellationToken.None); Assert.Empty(outcome.New); Assert.False(outcome.Assignments.ContainsKey("a")); Assert.Equal(["heating"], outcome.Assignments["b"]); } [Fact] public async Task Assign_UnreferencedNew_Dropped() { var heating = Article("heating"); var model = new FakeTextModel().Reply(""" {"new":[{"slug":"garden","titleDe":"G","titleEn":"G","category":"room","scope":"s"}], "notes":[{"note":"a","articles":["heating"]}]} """); var outcome = await UseCase(model).AssignAsync(Batch("a"), [heating], PlanWith(heating), CancellationToken.None); Assert.False(outcome.Unusable); Assert.Empty(outcome.New); } [Fact] public async Task Assign_Truncated_IsUnusable() { var model = new FakeTextModel().Reply("""{"new":[],"notes":[{"note":"a","skip":true}]}""", truncated: true); var outcome = await UseCase(model).AssignAsync(Batch("a"), [], ArticlePlan.Empty(), CancellationToken.None); Assert.True(outcome.Unusable); Assert.Empty(outcome.Assignments); } [Fact] public async Task Assign_PromptIsBounded() { var articles = Enumerable.Range(0, 500).Select(i => Article($"article-{i}", "n")).ToArray(); var plan = PlanWith(articles); var batch = Batch(Enumerable.Range(0, 20).Select(i => $"note-{i:00}").ToArray()); var model = new FakeTextModel().Reply("""{"new":[],"notes":[]}"""); await UseCase(model).AssignAsync(batch, articles.Take(100).ToArray(), plan, CancellationToken.None); var user = model.Calls[0].User; Assert.Equal(20, user.Split('\n').Count(l => l.StartsWith("note-"))); Assert.Equal(20, user.Split('\n').Count(l => l.StartsWith("article-"))); Assert.DoesNotContain("article-20 ", user); } [Fact] public async Task Assign_FirstParagraph_SkipsHeadingsAndBlankLines() { var note = TestNotes.Note("a") with { Body = "# T\n\n \nfirst line\ncontinued\n\nsecond para" }; var model = new FakeTextModel().Reply("""{"new":[],"notes":[]}"""); await UseCase(model).AssignAsync([note], [], ArticlePlan.Empty(), CancellationToken.None); var line = model.Calls[0].User.Split('\n').Single(l => l.StartsWith("a | ")); Assert.EndsWith(" | first line continued", line); } [Fact] public async Task Assign_FirstParagraph_CutTo300() { var note = TestNotes.Note("a") with { Body = new string('y', 500) }; var model = new FakeTextModel().Reply("""{"new":[],"notes":[]}"""); await UseCase(model).AssignAsync([note], [], ArticlePlan.Empty(), CancellationToken.None); var line = model.Calls[0].User.Split('\n').Single(l => l.StartsWith("a | ")); Assert.EndsWith(" | " + new string('y', 300), line); } [Fact] public async Task Assign_NewWithoutTitle_Dropped() { var heating = Article("heating"); var model = new FakeTextModel().Reply(""" {"new":[{"slug":"garden","titleDe":"G","category":"room","scope":"s"}], "notes":[{"note":"a","articles":["garden","heating"]}]} """); var outcome = await UseCase(model).AssignAsync(Batch("a"), [heating], PlanWith(heating), CancellationToken.None); Assert.Empty(outcome.New); Assert.Equal(["heating"], outcome.Assignments["a"]); } [Fact] public async Task Assign_ModelUnavailable_Propagates() { var model = new FakeTextModel().Throw(new ModelUnavailableException("down")); await Assert.ThrowsAsync(() => UseCase(model).AssignAsync(Batch("a"), [], ArticlePlan.Empty(), CancellationToken.None)); } }