109 lines
10 KiB
C#
109 lines
10 KiB
C#
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<StoryIntelligenceSavedSceneResult> 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<IStoryIntelligenceResultRepository>((method, _) => method switch {
|
|
"GetRunAsync" => Task.FromResult<StoryIntelligenceSavedRun?>(new() { Status = "Completed" }),
|
|
"ListSceneResultsAsync" => Task.FromResult(results),
|
|
_ => throw new Exception($"Unexpected run operation: {method}") });
|
|
var locations = Proxy<ILocationRepository>((method, _) => method == "ListByProjectAsync"
|
|
? Task.FromResult<IReadOnlyList<LocationItem>>([]) : throw new Exception($"Unexpected location write/read: {method}"));
|
|
var sceneRepository = Proxy<ISceneRepository>((method, _) => method == "ListByChapterAsync"
|
|
? Task.FromResult<IReadOnlyList<Scene>>(scenes) : throw new Exception($"Unexpected scene write/read: {method}"));
|
|
var importer = new StoryIntelligenceLocationImportService(runs, locations, sceneRepository, null!, NullLogger<StoryIntelligenceLocationImportService>.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<T>(Func<string, object?[]?, object?> handler) where T : class
|
|
{
|
|
var proxy = DispatchProxy.Create<T, StoryPointProxy>();
|
|
((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.");
|
|
}
|
|
}
|