using System.Text.Json; using PlotLine.Models; using PlotLine.Services; var tests = new (string Name, Action Test)[] { ("Malformed confidence 0. is repaired to 0.0", RepairsTrailingDecimalConfidence), ("Markdown fences are stripped", StripsMarkdownFence), ("Unescaped backslash in evidence is repaired", RepairsUnescapedBackslash), ("Truncated JSON is rejected", RejectsTruncatedJson), ("Repaired JSON deserialises into SceneIntelligenceScene", RepairedJsonDeserialises), ("Chapter Structure confidence 0. repairs and deserialises", ChapterStructureConfidenceRepairs), ("Shared parser reports raw chapter output on unrecoverable JSON", SharedParserReportsRawOutput) }; foreach (var test in tests) { test.Test(); Console.WriteLine($"PASS {test.Name}"); } static void RepairsTrailingDecimalConfidence() { var result = StoryIntelligenceJsonRepair.Repair("""{"observations":[{"confidence":0. }]}"""); Assert(result.Success, result.ErrorMessage ?? "Repair failed."); Assert(result.RepairedJson.Contains("\"confidence\":0.0", StringComparison.Ordinal), result.RepairedJson); } static void StripsMarkdownFence() { var result = StoryIntelligenceJsonRepair.Repair(""" ```json {"schemaVersion":"1.0"} ``` """); Assert(result.Success, result.ErrorMessage ?? "Repair failed."); Assert(result.RepairedJson == """{"schemaVersion":"1.0"}""", result.RepairedJson); } static void RepairsUnescapedBackslash() { var result = StoryIntelligenceJsonRepair.Repair("""{"evidence":"Mara reads C:\qnotes"}"""); Assert(result.Success, result.ErrorMessage ?? "Repair failed."); using var document = JsonDocument.Parse(result.RepairedJson); var evidence = document.RootElement.GetProperty("evidence").GetString(); Assert(evidence == @"Mara reads C:\qnotes", evidence ?? "Evidence was null."); } static void RejectsTruncatedJson() { var result = StoryIntelligenceJsonRepair.Repair("""{"observations":[{"confidence":"""); Assert(!result.Success, "Truncated JSON should not be silently repaired."); } static void RepairedJsonDeserialises() { var result = StoryIntelligenceJsonRepair.Repair(""" { "schemaVersion": "1.0", "sceneReference": { "sceneId": null }, "summary": { "short": "A scene.", "detailed": "A scene.", "confidence": 0. }, "observations": [] } """); Assert(result.Success, result.ErrorMessage ?? "Repair failed."); var parsed = JsonSerializer.Deserialize( result.RepairedJson, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }); Assert(parsed is not null, "Scene JSON did not deserialise."); } static void ChapterStructureConfidenceRepairs() { var result = StoryIntelligenceAiJsonParser.Parse( """ { "schemaVersion": "1.0", "chapterSummary": "A chapter.", "sceneBoundaries": [ { "sceneNumber": 1, "startParagraph": 1, "endParagraph": 2, "confidence": 0. , "reason": "Opening scene." } ] } """, JsonOptions()); Assert(result.Success, result.ErrorMessage ?? "Chapter Structure parse failed."); Assert(result.Parsed?.SceneBoundaries?[0].Confidence == 0.0m, "Confidence was not repaired to 0.0."); Assert(result.Warnings.Any(warning => warning.Contains("$.sceneBoundaries[0].confidence", StringComparison.Ordinal)), "Repair warning did not include JSON path."); } static void SharedParserReportsRawOutput() { const string raw = """{"schemaVersion":"1.0","sceneBoundaries":["""; var result = StoryIntelligenceAiJsonParser.Parse(raw, JsonOptions()); Assert(!result.Success, "Unrecoverable JSON should fail."); Assert(result.RawJson == raw, "Raw malformed output was not retained."); } static JsonSerializerOptions JsonOptions() => new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; static void Assert(bool condition, string message) { if (!condition) { throw new InvalidOperationException(message); } }