using PlotLine.Models; using PlotLine.Data; using System.Reflection; using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using PlotLine.Services; using PlotLine.ViewModels; internal static class LocationSignificanceTests { private static void Check(bool condition, string message) { if (!condition) throw new Exception(message); } private static LocationSignificanceEvidence Evidence(int scene, int chapter, bool setting = false, int eventScore = 0, int context = 0) => new(scene, chapter, 1, $"Chapter {chapter}, Scene {scene}", setting, eventScore, context, [], [], []); private static StoryIntelligenceLocationReviewCandidateViewModel Candidate(string key, int score, string? parent = null, string confidence = "High", string status = "Pending") => new() { Key = key, LocationName = key, Category = "Named location", ProposedParentCandidateKey = parent, Confidence = confidence, DecisionStatus = status, Significance = new() { Score = score, Classification = score >= 40 ? "Significant Location" : "Incidental Location" } }; public static void ConsequenceOutranksFrequency() { var hallway = LocationSignificanceService.Assess("Room / interior area", Enumerable.Range(1, 100).Select(i => Evidence(i, i, true, context: 18))); var observatory = LocationSignificanceService.Assess("Named location", [Evidence(200, 1, true, 50, 18)]); Check(observatory.Score > hallway.Score && observatory.IsSignificant && !hallway.IsSignificant, "One consequential event must outrank 100 routine setting scenes."); Check(hallway.Score == 26 && hallway.Classification == "Recurring Location", "Frequency must be capped at eight and cannot alone create a primary setting."); var duplicates = LocationSignificanceService.Assess("Named location", Enumerable.Repeat(Evidence(200, 1, true, 50, 18), 100)); Check(duplicates.Score == observatory.Score, "Repeated observations in the same scene must not inflate score."); Check(LocationSignificanceService.Assess("Named location", [Evidence(1, 1)]).Classification == "Incidental Location", "A distinctive name alone is incidental."); Check(LocationSignificanceService.Assess("Named location", Enumerable.Range(1, 6).Select(i => Evidence(i, i, true, 50, 18))).Classification == "Primary Setting", "Consequential multi-chapter settings should be primary."); } public static void SceneContextBelongsToSetting() { var parsed = new SceneIntelligenceScene { ScenePurpose = new() { ObservedFunction = "A betrayal and rescue transform the investigation." }, PointOfView = new() { CharacterName = "Leena" }, Metrics = new() { ["tension"] = new() { Score = 9 } }, KnowledgeChanges = [new() { KnowledgeItem = "The captain is the informant" }], NarrativeArcs = [new() { Title = "The missing expedition", Significance = "high" }] }; var scene = new Scene { SceneID = 7, ChapterID = 3, BookID = 1 }; var site = LocationSignificanceService.Observe(scene, parsed, "Chapter 3, Scene 7", true, true, false, null); var mention = LocationSignificanceService.Observe(scene, parsed, "Chapter 3, Scene 7", false, false, true, "They mention the pantry."); Check(site.EventScore == 50 && site.ContextScore > 18 && site.Characters.Contains("Leena") && site.PlotLines.Contains("The missing expedition"), "Reuse structured scene purpose, metrics, knowledge, plot and POV evidence."); Check(mention.EventScore == 0 && mention.ContextScore == 0 && mention.Characters.Count == 0, "An incidental mention cannot inherit the scene's consequential context."); var hypothetical = LocationSignificanceService.Observe(scene, parsed, "scene", false, false, true, "They might discover a body at the beach."); Check(hypothetical.EventScore == 0, "Hypothetical consequential vocabulary must not create an actual event site."); var routine = new SceneIntelligenceScene { ScenePurpose = new() { ObservedFunction = "The conversation reveals the employee staffing schedule." }, NarrativeArcs = [new() { Title = "Work routines", Significance = "minor" }], KnowledgeChanges = [new() { KnowledgeItem = "The secretary works on Tuesdays." }] }; var routineScore = LocationSignificanceService.Assess("Named location", [LocationSignificanceService.Observe(scene, routine, "scene", true, true, false, null)]); Check(!routineScore.IsSignificant, "Ordinary exposition and trivial knowledge must not be mistaken for major revelations."); var explicitSite = LocationSignificanceService.Observe(scene, parsed, "scene", false, false, true, "The abduction occurred at the lighthouse."); Check(explicitSite.EventScore == 50, "Location-specific evidence can support a consequential site even when off-stage."); } public static void PersistedAnalysisKeepsContextAndParentEvidence() { Scene[] scenes = [ new() { SceneID = 1, ChapterID = 1, SceneNumber = 1, POVCharacterName = "Maggie", StructuralSetting = "Maggie's bedroom" }, new() { SceneID = 2, ChapterID = 1, SceneNumber = 2, POVCharacterName = "Beth", StructuralSetting = "Beth's bedroom" }, new() { SceneID = 3, ChapterID = 1, SceneNumber = 3 } ]; var ordinary = new SceneIntelligenceScene { SchemaVersion = "1", Setting = new() { LocationName = "bedroom", GenericRoomType = "bedroom" } }; var major = new SceneIntelligenceScene { SchemaVersion = "1", Setting = new() { LocationName = "library", GenericRoomType = "library", ParentLocationHint = "Westmere House" }, ScenePurpose = new() { ObservedFunction = "A rescue follows the abduction." } }; IReadOnlyList results = scenes.Select(x => new StoryIntelligenceSavedSceneResult { SceneID = x.SceneID, ParsedJson = JsonSerializer.Serialize(x.SceneID == 3 ? major : ordinary, new JsonSerializerOptions(JsonSerializerDefaults.Web)) }).ToList(); var runs = Proxy((method, _) => method switch { "GetRunAsync" => Task.FromResult(new() { Status = "Completed" }), "ListSceneResultsAsync" => Task.FromResult(results), _ => throw new Exception($"Unexpected run operation: {method}") }); var locations = Proxy((method, _) => method == "ListByProjectAsync" ? Task.FromResult>([]) : throw new Exception($"Unexpected location write/read: {method}")); var sceneRepository = Proxy((method, _) => method == "ListByChapterAsync" ? Task.FromResult>(scenes) : throw new Exception($"Unexpected scene write/read: {method}")); var importer = new StoryIntelligenceLocationImportService(runs, locations, sceneRepository, null!, NullLogger.Instance); var result = importer.BuildSignificanceAsync(new() { ProjectID = 1, BookID = 1, Items = [new() { RunID = 1, ChapterID = 1 }] }).GetAwaiter().GetResult(); Check(result.Keys.Any(x => x.Contains("maggie", StringComparison.OrdinalIgnoreCase)) && result.Keys.Any(x => x.Contains("beth", StringComparison.OrdinalIgnoreCase)), "Assessment preparation must preserve separate contextual bedrooms."); Check(result.Any(x => x.Key.Contains("westmere", StringComparison.OrdinalIgnoreCase) && x.Value.IsSignificant), "A house must retain the significance of consequential scenes within it."); Check(result.Values.SelectMany(x => x.KeyScenes).All(x => x.BookID == 1), "Stored assessment evidence must retain canonical book/scene provenance."); } private static T Proxy(Func handler) where T : class { var proxy = DispatchProxy.Create(); ((StoryPointProxy)(object)proxy).Handler = handler; return proxy; } public static void HierarchyAndUncertainty() { var parent = Candidate("Estate", 8); var high = Candidate("Tower", 90, "Estate"); var low = Candidate("Kitchen", 3, "Estate"); var medium = Candidate("Library", 60, "Estate"); var unrelated = Candidate("Harbour", 70); var unsure = Candidate("Unresolved room", 0, confidence: "Medium"); var reviewed = Candidate("Reviewed ambiguity", 50, confidence: "Low", status: "Reviewed"); var input = new[] { low, unrelated, medium, unsure, parent, high, reviewed }; var all = LocationSignificanceService.OrderHierarchy(input, "all"); Check(all.Take(4).Select(x => x.Key).SequenceEqual(new[] { "Estate", "Tower", "Library", "Kitchen" }), "Descendants must remain grouped; significant siblings first, significant subtrees first."); Check(high.HierarchyDepth == 1 && parent.HierarchyDepth == 0, "Hierarchy depth must be retained."); var significant = LocationSignificanceService.OrderHierarchy(input, "significant"); Check(significant.Contains(parent) && significant.Contains(high) && !significant.Contains(low) && significant.Contains(reviewed), "Significant filter must include ancestors and keep already reviewed evidence accessible."); var uncertain = LocationSignificanceService.OrderHierarchy(input, "uncertain"); Check(uncertain.Count == 1 && uncertain[0] == unsure, "Only genuine unresolved ambiguity belongs in the uncertainty filter, independently of significance."); Check(LocationSignificanceService.OrderHierarchy([Candidate("A", 50, "B"), Candidate("B", 0, "A")], "significant").Count == 2, "Legacy cyclic parent hints must terminate and retain all candidates."); var page = new StoryIntelligenceLocationReviewViewModel { FilteredCount = 52, PendingCandidateCount = 2, PageNumber = 3, PageSize = 25, Candidates = [parent, high] }; Check(page.PageCount == 3 && page.ShowingStart == 51 && page.ShowingEnd == 52 && !page.HasNextPage, "Browse pagination must use filter totals, independently of pending decision counts."); } }