diff --git a/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs b/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs
index fdac3af75..1c3ebd572 100644
--- a/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs
+++ b/tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs
@@ -458,6 +458,82 @@ public sealed class NavigationTests
Assert.Equal(-3, target.DoorLockpickExcessThreshold);
}
+ ///
+ /// Reproduces MossTankRouteProfileStore's pre-cutover by-character JSON
+ /// hash key (its own LegacyProfileKey is private; the format is
+ /// the migration contract itself, reproduced verbatim here).
+ ///
+ private static string LegacyRouteByCharacterKey(string characterName)
+ {
+ string identity = "char:" + characterName.Trim().ToUpperInvariant();
+ string hash = Convert.ToHexString(
+ System.Security.Cryptography.SHA256.HashData(
+ System.Text.Encoding.UTF8.GetBytes(identity)));
+ return $"profiles/route/{hash}.json";
+ }
+
+ [Fact]
+ public void RouteStoreMigratesLegacyJsonProfileToAfAndDeletesTheJsonKey()
+ {
+ var storage = new MemoryStorage();
+ string legacyKey = LegacyRouteByCharacterKey("Barris");
+ storage.Text[legacyKey] = """
+ {
+ "Mode": 1,
+ "Waypoints": [
+ { "Type": 0, "EastWest": 5.0, "NorthSouth": 6.0 }
+ ]
+ }
+ """;
+
+ var store = new MossTankRouteProfileStore(new FakeHost(new FakeAutomation(), storage));
+ Assert.True(store.BindCharacter("Barris"));
+ var target = new NavigationSettings();
+ Assert.True(store.LoadCurrent(target, MetafSerializer.NoOpSpells.Instance));
+
+ Assert.False(storage.Text.ContainsKey(legacyKey));
+ Assert.Equal(RouteMode.Linear, target.Mode);
+ RouteWaypoint waypoint = Assert.Single(target.Waypoints);
+ // metaf's "pnt" node format is bare x/y/z (three doubles) — no cell
+ // id at all, for any waypoint type — so only the coordinates
+ // round-trip.
+ Assert.Equal(5.0d, waypoint.Position.EastWest, precision: 3);
+ Assert.Equal(6.0d, waypoint.Position.NorthSouth, precision: 3);
+
+ // Idempotent second run: nothing left to migrate.
+ var reopened = new MossTankRouteProfileStore(new FakeHost(new FakeAutomation(), storage));
+ Assert.True(reopened.BindCharacter("Barris"));
+ var reloaded = new NavigationSettings();
+ Assert.True(reopened.LoadCurrent(reloaded, MetafSerializer.NoOpSpells.Instance));
+ Assert.Single(reloaded.Waypoints);
+ }
+
+ [Fact]
+ public void RouteStoreLeavesLegacyJsonUntouchedWhenAfCounterpartExists()
+ {
+ var storage = new MemoryStorage();
+ string legacyKey = LegacyRouteByCharacterKey("Barris");
+ storage.Text[legacyKey] = """{ "Mode": 1, "Waypoints": [] }""";
+ string realKey = "nav_" + VtankProfileDirectory.AutoCharacterFileName(
+ "Barris", string.Empty, "af");
+ var real = new NavigationSettings { Mode = RouteMode.Circular };
+ real.Waypoints.Add(new RouteWaypoint
+ {
+ Type = RouteWaypointType.Point,
+ Position = new PluginNavigationPosition(0x00010001u, 1d, 2d, 0d, 0f, true),
+ });
+ storage.Text[realKey] = MetafSerializer.SaveNav(real);
+
+ var store = new MossTankRouteProfileStore(new FakeHost(new FakeAutomation(), storage));
+ Assert.True(store.BindCharacter("Barris"));
+ var target = new NavigationSettings();
+ Assert.True(store.LoadCurrent(target, MetafSerializer.NoOpSpells.Instance));
+
+ Assert.True(storage.Text.ContainsKey(legacyKey));
+ Assert.Equal(RouteMode.Circular, target.Mode);
+ Assert.Single(target.Waypoints);
+ }
+
[Fact]
public void FollowModeRouteRoundTripsTheFollowTargetThroughAf()
{
@@ -619,6 +695,7 @@ public sealed class NavigationTests
private sealed class MemoryStorage : IPluginStorage
{
private readonly Dictionary _text = new(StringComparer.Ordinal);
+ public Dictionary Text => _text;
public bool IsAvailable => true;
public string? ReadText(string key) =>
_text.TryGetValue(key, out string? value) ? value : null;