PlotDirector/PlotLine.Tests/RelationshipDevelopmentTests.cs

94 lines
7.6 KiB
C#

using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using PlotLine.Data;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
internal static class RelationshipDevelopmentTests
{
public static void SceneReviewAndAcceptance() => Run().GetAwaiter().GetResult();
private static async Task Run()
{
foreach (var names in new[] { ("Rob", "Maggie"), ("Iris", "Jon") })
{
var accepted = new List<RelationshipDevelopmentAcceptance>();
var lookups = new CharacterLookupData
{
RelationshipTypes = [new() { RelationshipTypeID=1,TypeName="Other" },new() { RelationshipTypeID=2,TypeName="Sibling" }],
RelationshipStates = [new() { RelationshipStateID=1,StateName="Unknown" },new() { RelationshipStateID=2,StateName="Friendly" },new() { RelationshipStateID=3,StateName="Distrustful" },new() { RelationshipStateID=4,StateName="Romantic" }]
};
var characters = Stub<ICharacterRepository>((m,a) => m switch
{
"ListCharactersAsync" => Task.FromResult<IReadOnlyList<Character>>([new() { CharacterID=1, CharacterName=names.Item1 },new() { CharacterID=2,CharacterName=names.Item2 }]),
"ListAliasesAsync" => Task.FromResult<IReadOnlyList<CharacterAlias>>([]),
"ListRelationshipsByProjectAsync" => Task.FromResult<IReadOnlyList<CharacterRelationship>>([]),
"ListRelationshipEventsBySceneAsync" => Task.FromResult<IReadOnlyList<RelationshipEvent>>([]),
"GetLookupsAsync" => Task.FromResult(lookups),
"AcceptRelationshipDevelopmentAsync" => Save((RelationshipDevelopmentAcceptance)a![0]!),
_ => throw new Exception(m)
});
Task<int> Save(RelationshipDevelopmentAcceptance p) { accepted.Add(p); return Task.FromResult(77); }
SceneIntelligenceRelationship Proposal(string kind,string state,bool mutual,string evidence,int? intensity=null) => new()
{
CharacterA=names.Item1,CharacterB=names.Item2,ProposalKind=kind,RelationshipState=state,
IsReciprocal=mutual,IsKnownToOtherCharacter=false,Intensity=intensity,Confidence=.95m,
Development=evidence,Evidence=evidence,RelationshipSignal="affection",FactualRelationshipType=kind=="Factual"?"Sibling":null
};
var results = new List<StoryIntelligenceSavedSceneResult>
{
Result(30,[Proposal("Development","Romantic",false,"A privately develops feelings for B",3),Proposal("Factual","Unknown",true,"They are siblings")]),
Result(10,[Proposal("Development","Friendly",true,"They help each other escape",6),Proposal("Factual","Unknown",true,"They are siblings")]),
Result(20,[Proposal("Development","Distrustful",false,"A discovers B has lied"),new() { CharacterA=names.Item1,CharacterB=names.Item2,RelationshipSignal="trust",Evidence="They merely appear together with no interaction" }])
};
var runs=Stub<IStoryIntelligenceResultRepository>((m,a)=>m switch
{
"GetRunAsync"=>Task.FromResult<StoryIntelligenceSavedRun?>(new() { Status=StoryIntelligenceRunStatuses.Completed }),
"ListSceneResultsAsync"=>Task.FromResult<IReadOnlyList<StoryIntelligenceSavedSceneResult>>(results),
_=>throw new Exception(m)
});
var scenes=Stub<ISceneRepository>((m,a)=> Task.FromResult<IReadOnlyList<Scene>>([
new() { SceneID=30,SceneNumber=3,ChapterNumber=1 },new() { SceneID=10,SceneNumber=1,ChapterNumber=1 },new() { SceneID=20,SceneNumber=2,ChapterNumber=1 }]));
var pipeline=Stub<IStoryIntelligencePipelineStateService>((m,a)=>Task.FromResult<StoryIntelligenceBookPipelineState?>(null));
var service=new StoryIntelligenceRelationshipImportService(runs,characters,scenes,pipeline,NullLogger<StoryIntelligenceRelationshipImportService>.Instance);
var batch=new OnboardingStoryIntelligenceBatch { ProjectID=1,BookID=2,UserID=3,Items=[new() { RunID=1,ChapterID=1 }] };
var review=await service.BuildReviewAsync(batch);
Check(review.Candidates.Count==4,"Meaningful proposals must stay separate by scene, with co-presence excluded.");
Check(review.Candidates.Select(x=>x.FirstSceneID).SequenceEqual(new int?[]{10,10,20,30}),"Review must follow scene order.");
Check(review.Candidates.Last().IsReciprocal==false && review.Candidates.Last().RelationshipState=="Romantic","One-sided feelings must not become mutual.");
Check(review.Candidates.First(x=>x.ProposalKind=="Factual").RelationshipType=="Sibling","Factual ties must stay separate.");
var form=new StoryIntelligenceRelationshipImportForm { Relationships=review.Candidates.Select(x=>new StoryIntelligenceRelationshipImportChoiceForm
{ Key=x.Key,RelationshipTypeID=x.RelationshipTypeID,RelationshipStateID=x.RelationshipStateID,IsReciprocal=x.IsReciprocal,
IsKnownToOtherCharacter=x.IsKnownToOtherCharacter,Intensity=x.Intensity,Development=x.Development }).ToList() };
form.Relationships.Last().ReverseDirection=true;
var result=await service.ImportAsync(batch,form,false);
Check(result.Success && accepted.Count==4,"Acceptance must save all scene proposals.");
Check(accepted.Last().CharacterAID==2 && accepted.Last().CharacterBID==1 && !accepted.Last().IsReciprocal,"Direction correction must reach persistence.");
Check(accepted.Last().Intensity==3 && accepted.Any(x=>x.Intensity is null),"Confidence must never become intensity.");
Check(accepted.Single(x=>x.IsPermanent).RelationshipTypeID==2,"Only factual ties should be permanent.");
Check(form.Relationships.All(x=>x.AcceptedRelationshipID==77),"Review decisions must reference the canonical relationship ID, not the type ID.");
accepted.Clear();
form.Relationships.Last().RelationshipStateID=999;
Check(!(await service.ImportAsync(batch,form,false)).Success && accepted.Count==0,"Invalid later rows must prevent all writes.");
}
Check(Key(10,1,2,false)!=Key(10,2,1,false),"Directional keys must distinguish reverse feelings.");
Check(Key(10,1,2,true)==Key(10,2,1,true),"Mutual duplicate proposals must share identity.");
Check(Key(10,1,2,false)!=Key(20,1,2,false),"Later scenes must retain independent identity.");
}
private static string Key(int scene,int a,int b,bool reciprocal) => (string)typeof(StoryIntelligenceRelationshipImportService).GetMethod("DevelopmentKey",BindingFlags.NonPublic|BindingFlags.Static)!.Invoke(null,[scene,a,b,reciprocal,"Development","trust","Evidence"])!;
private static StoryIntelligenceSavedSceneResult Result(int scene,List<SceneIntelligenceRelationship> proposals) => new() { SceneID=scene,ParsedJson=JsonSerializer.Serialize(new SceneIntelligenceScene { SchemaVersion="1.0",Relationships=proposals }) };
private static void Check(bool value,string message) { if(!value)throw new Exception(message); }
private static T Stub<T>(Func<string,object?[]?,object?> handler) where T:class
{
var proxy=DispatchProxy.Create<T,RelationshipTestProxy>();
((RelationshipTestProxy)(object)proxy).Handler=handler;
return proxy;
}
}
public class RelationshipTestProxy:DispatchProxy
{
public Func<string,object?[]?,object?> Handler { get; set; }=null!;
protected override object? Invoke(MethodInfo? method,object?[]? args)=>Handler(method!.Name,args);
}