94 lines
7.6 KiB
C#
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);
|
|
}
|