using AcDream.Launcher.Core.Profiles; namespace AcDream.Launcher.Core.Tests.Profiles; /// /// The roster-merge matrix (Campaign LA plan §LA3 acceptance): a new /// character, an existing character keeping its user settings, and a /// character absent from a later roster snapshot being retained /// (possibly pending-delete). /// public sealed class RosterMergeTests { private static LauncherProfileStore NewStoreWithServerAndAccount() { var store = new LauncherProfileStore( Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".json")); store.Load(); store.AddServer("Local ACE", "127.0.0.1", 9000); store.AddAccount("Local ACE", "testaccount", "testpassword"); return store; } [Fact] public void FirstMergeAddsNewCharactersWithDefaultLaunchModeGuiSelect() { LauncherProfileStore store = NewStoreWithServerAndAccount(); store.MergeRoster( "Local ACE", "testaccount", [ new CharacterRosterEntry(0x5000000A, "+Acdream", 0), new CharacterRosterEntry(0x5000000B, "+Second", 0), ]); List characters = store.Document.Servers.Single().Accounts.Single().Characters; Assert.Equal(2, characters.Count); CharacterProfile first = characters.Single(c => c.Name == "+Acdream"); Assert.Equal("0x5000000A", first.Id); Assert.Equal(LaunchMode.GuiSelect, first.LaunchMode); Assert.Empty(first.Plugins); Assert.Empty(first.LoginCommands); CharacterProfile second = characters.Single(c => c.Name == "+Second"); Assert.Equal("0x5000000B", second.Id); Assert.Equal(LaunchMode.GuiSelect, second.LaunchMode); } [Fact] public void SecondMergePreservesUserSettingsOnAnExistingCharacter() { LauncherProfileStore store = NewStoreWithServerAndAccount(); store.MergeRoster( "Local ACE", "testaccount", [new CharacterRosterEntry(0x5000000A, "+Acdream", 0)]); store.EditCharacter( "Local ACE", "testaccount", "+Acdream", launchMode: LaunchMode.Headless, plugins: ["ExamplePlugin"], loginCommands: ["/vt start"]); // A later probe reports the same character again (same id), with // a renamed display — settings must survive untouched. store.MergeRoster( "Local ACE", "testaccount", [new CharacterRosterEntry(0x5000000A, "+Acdream", 0)]); CharacterProfile character = Assert.Single( store.Document.Servers.Single().Accounts.Single().Characters); Assert.Equal(LaunchMode.Headless, character.LaunchMode); Assert.Equal(["ExamplePlugin"], character.Plugins); Assert.Equal(["/vt start"], character.LoginCommands); } [Fact] public void MergeUpdatesNameWhenIdMatchesButDisplayNameChanged() { LauncherProfileStore store = NewStoreWithServerAndAccount(); store.MergeRoster( "Local ACE", "testaccount", [new CharacterRosterEntry(0x5000000A, "+OldName", 0)]); store.MergeRoster( "Local ACE", "testaccount", [new CharacterRosterEntry(0x5000000A, "+NewName", 0)]); CharacterProfile character = Assert.Single( store.Document.Servers.Single().Accounts.Single().Characters); Assert.Equal("+NewName", character.Name); Assert.Equal("0x5000000A", character.Id); } [Fact] public void CharacterAbsentFromALaterRosterSnapshotIsRetained() { LauncherProfileStore store = NewStoreWithServerAndAccount(); store.MergeRoster( "Local ACE", "testaccount", [ new CharacterRosterEntry(0x5000000A, "+Acdream", 0), new CharacterRosterEntry(0x5000000B, "+PendingDelete", 1), ]); // A later probe's roster only reports one of the two — e.g. the // other was deleted and is now in ACE's grace window / a // partial snapshot. The store never removes rows on the // caller's behalf. store.MergeRoster( "Local ACE", "testaccount", [new CharacterRosterEntry(0x5000000A, "+Acdream", 0)]); List characters = store.Document.Servers.Single().Accounts.Single().Characters; Assert.Equal(2, characters.Count); Assert.Contains(characters, c => c.Name == "+Acdream"); Assert.Contains(characters, c => c.Name == "+PendingDelete"); } [Fact] public void MergeNormalizesAnUnprefixedHexIdInsteadOfCreatingADuplicateRow() { // Review finding F10: a hand-edited row can carry an id without // the "0x" prefix (e.g. copy-pasted from somewhere that dropped // it). CharacterIdFormat.TryParse now REJECTS that string // outright (it no longer guesses hex-without-a-prefix), so the // old raw string-equality comparison against the roster's // canonical "0x..." form would never match and would add a // second row forever. The name-fallback match must still // recognize this as the SAME character and self-heal its id. LauncherProfileStore store = NewStoreWithServerAndAccount(); store.Document.Servers.Single().Accounts.Single().Characters.Add( new CharacterProfile { Id = "5000000A", Name = "+Acdream" }); store.MergeRoster( "Local ACE", "testaccount", [new CharacterRosterEntry(0x5000000A, "+Acdream", 0)]); CharacterProfile character = Assert.Single( store.Document.Servers.Single().Accounts.Single().Characters); Assert.Equal("0x5000000A", character.Id); Assert.Equal("+Acdream", character.Name); } [Fact] public void MergeThrowsForUnknownServerOrAccount() { LauncherProfileStore store = NewStoreWithServerAndAccount(); Assert.Throws( () => store.MergeRoster( "Nope", "testaccount", [new CharacterRosterEntry(1, "x", 0)])); Assert.Throws( () => store.MergeRoster( "Local ACE", "nope", [new CharacterRosterEntry(1, "x", 0)])); } }