diff --git a/src/Wiki.Application/Encyclopedia/PlanParsing.cs b/src/Wiki.Application/Encyclopedia/PlanParsing.cs index 34a2e8d..32e7333 100644 --- a/src/Wiki.Application/Encyclopedia/PlanParsing.cs +++ b/src/Wiki.Application/Encyclopedia/PlanParsing.cs @@ -26,12 +26,12 @@ internal static partial class PlanParsing return text is not null && text.All(char.IsAsciiLetter) && Enum.TryParse(text, ignoreCase: true, out category); } - /// The JSON object in a reply, which may be wrapped in a code fence; null when it is not one. + /// The JSON object in a reply, which may be wrapped in a code fence; null when it is not one or repeats a property. public static JsonObject? Object(string reply) { try { - return JsonNode.Parse(StripFence(reply)) as JsonObject; + return JsonNode.Parse(StripFence(reply), documentOptions: new JsonDocumentOptions { AllowDuplicateProperties = false }) as JsonObject; } catch (JsonException) { diff --git a/test/Wiki.Application.Tests/AssignNotesUseCaseTests.cs b/test/Wiki.Application.Tests/AssignNotesUseCaseTests.cs index 87da135..90b7ea8 100644 --- a/test/Wiki.Application.Tests/AssignNotesUseCaseTests.cs +++ b/test/Wiki.Application.Tests/AssignNotesUseCaseTests.cs @@ -170,6 +170,20 @@ public class AssignNotesUseCaseTests Assert.Empty(outcome.Assignments); } + [Fact] + public async Task Assign_DuplicatePropertyInReply_IsUnusable() + { + var model = new FakeTextModel().Reply(""" + {"new":[{"slug":"garden","titleDe":"G","titleEn":"G","titleEn":"H","category":"room","scope":"s"}], + "notes":[{"note":"a","articles":["garden"]}]} + """); + + 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() {