fix(vt): round 3 items 3-4 — hidden-prefix-first nav naming + disjoint pickers

Item 3: MossTankRouteProfileStore's per-character auto route file was named
nav_--Name_Server.af (NavPrefix + AutoCharacterFileName) — the "nav_" marker
came BEFORE the "--" hidden prefix, so the whole filename does not start
with "--" and defeats every StartsWith("--") hidden-file check in
VtankProfileDirectory, leaking another character's private route binding
into both the nav and meta pickers. Chose hidden-prefix-first naming
(--nav_Name_Server.af, matching VTank's own "--" convention with the nav_
kind marker second) via a new AutoCharacterFileName(name, server, ext,
marker) overload; applied only to the per-character auto file — named
routes keep their existing nav_Name.af (shared/visible) shape.

Item 4: ListNavigationProfiles and ListMetaProfiles shared the same flat
.af directory with no marker check at all, so each picker returned the
other's files too (a Meta profile appeared in the nav picker and vice
versa). ListNavigationProfiles now requires the nav_ marker;
ListMetaProfiles now excludes it.

Mutation: reverted VtankProfileDirectory.cs and MossTankRouteProfileStore.cs
to HEAD (keeping only the new/changed tests) — the build failed outright
(VtankProfileDirectory has no NavMarker/marker-overload for the new tests to
call), and the two pre-existing tests this round updated
(ListNavigationProfilesFiltersBothReservedPrefixes,
RouteStoreLeavesLegacyJsonUntouchedWhenAfCounterpartExists) independently
failed at runtime against their OLD un-marked/mis-ordered fixtures once
this round's marker/ordering requirement was pinned, confirming both are
exercising real, fixed behavior.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 01:18:54 +02:00
parent 145bd62503
commit 0398913538
4 changed files with 124 additions and 12 deletions

View file

@ -514,8 +514,9 @@ public sealed class NavigationTests
var storage = new MemoryStorage();
string legacyKey = LegacyRouteByCharacterKey("Barris");
storage.Text[legacyKey] = """{ "Mode": 1, "Waypoints": [] }""";
string realKey = "nav_" + VtankProfileDirectory.AutoCharacterFileName(
"Barris", string.Empty, "af");
// Round 3 item 3: hidden prefix first, nav_ marker second.
string realKey = VtankProfileDirectory.AutoCharacterFileName(
"Barris", string.Empty, "af", VtankProfileDirectory.NavMarker);
var real = new NavigationSettings { Mode = RouteMode.Circular };
real.Waypoints.Add(new RouteWaypoint
{

View file

@ -74,8 +74,10 @@ public sealed class VtankProfileDirectoryTests
public void ListNavigationProfilesFiltersBothReservedPrefixes()
{
var storage = new MemoryStorage();
storage.WriteText("Hunt.af", "1\r\n");
storage.WriteText("--Barris_Coldeve.af", "1\r\n");
// Round 3 item 4: a route .af must carry the nav_ marker to be
// listed at all — an un-marked file is a Meta profile, not a route.
storage.WriteText("nav_Hunt.af", "1\r\n");
storage.WriteText("--nav_Barris_Coldeve.af", "1\r\n");
storage.WriteText("~~backup.af", "1\r\n");
IReadOnlyList<VtankProfileDirectory.ProfileEntry> entries =
@ -83,11 +85,74 @@ public sealed class VtankProfileDirectoryTests
Assert.Equal(VtankProfileDirectory.NoneLabel, entries[0].DisplayName);
Assert.Equal(VtankProfileDirectory.ByCharacterLabel, entries[1].DisplayName);
Assert.Contains(entries, static e => e.DisplayName == "Hunt.af");
Assert.Contains(entries, static e => e.DisplayName == "nav_Hunt.af");
Assert.DoesNotContain(entries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
Assert.DoesNotContain(entries, static e => e.FileName.StartsWith("~~", StringComparison.Ordinal));
}
/// <summary>
/// Round 3 items 3/4: a Meta profile and a route share the exact same
/// flat directory and <c>.af</c> extension, so the nav_ marker (and the
/// hidden "--" prefix coming BEFORE it on a per-character auto file) is
/// the only thing that keeps the two pickers from showing each other's
/// files. This is the disambiguation test both pickers must pass
/// together against one mixed directory.
/// </summary>
[Fact]
public void NavigationAndMetaPickersPartitionTheSameMixedDirectory()
{
var storage = new MemoryStorage();
storage.WriteText("nav_Hunt.af", "1\r\n");
storage.WriteText("MyMeta.af", "1\r\n");
// Barris's own hidden per-character route: hidden prefix FIRST,
// marker SECOND (item 3's fix) — must be invisible to BOTH pickers,
// not just the nav one.
storage.WriteText("--nav_Barris_Coldeve.af", "1\r\n");
// Barris's own hidden per-character Meta file (unmarked, as
// MossTankMetaProfileStore has always named it) — must likewise be
// invisible to both.
storage.WriteText("--Barris_Coldeve.af", "1\r\n");
IReadOnlyList<VtankProfileDirectory.ProfileEntry> navEntries =
VtankProfileDirectory.ListNavigationProfiles(storage);
IReadOnlyList<VtankProfileDirectory.ProfileEntry> metaEntries =
VtankProfileDirectory.ListMetaProfiles(storage);
Assert.Contains(navEntries, static e => e.FileName == "nav_Hunt.af");
Assert.DoesNotContain(navEntries, static e => e.FileName == "MyMeta.af");
Assert.DoesNotContain(navEntries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
Assert.Contains(metaEntries, static e => e.FileName == "MyMeta.af");
Assert.DoesNotContain(metaEntries, static e => e.FileName == "nav_Hunt.af");
Assert.DoesNotContain(metaEntries, static e => e.FileName.StartsWith("--", StringComparison.Ordinal));
}
/// <summary>
/// Round 3 item 3 (BLOCKER-adjacent leak): before the fix, a hidden
/// per-character route file was named <c>nav_--Name_Server.af</c>
/// (marker first) — which does NOT start with "--" and so defeated the
/// hidden-file check in BOTH pickers, leaking another character's
/// private route/Meta binding.
/// </summary>
[Fact]
public void AnotherCharactersAutoRouteIsHiddenFromBothPickers()
{
var storage = new MemoryStorage();
// The CORRECT (post-fix) shape: hidden prefix first, marker second.
storage.WriteText(
VtankProfileDirectory.AutoCharacterFileName(
"Someone", "Coldeve", "af", VtankProfileDirectory.NavMarker),
"1\r\n");
IReadOnlyList<VtankProfileDirectory.ProfileEntry> navEntries =
VtankProfileDirectory.ListNavigationProfiles(storage);
IReadOnlyList<VtankProfileDirectory.ProfileEntry> metaEntries =
VtankProfileDirectory.ListMetaProfiles(storage);
Assert.DoesNotContain(navEntries, static e => e.FileName.Contains("Someone", StringComparison.Ordinal));
Assert.DoesNotContain(metaEntries, static e => e.FileName.Contains("Someone", StringComparison.Ordinal));
}
[Fact]
public void ListingsOnUnavailableStorageOnlySeedTheBuiltInEntries()
{