fix(vt): C separate exports/meta/ and exports/nav/ .af directories

Item C (slice-1 fix round). MossTankMetaProfileStore and
MossTankRouteProfileStore both wrote their .af exports into the same
flat "exports/" directory keyed only by profile name — a Meta profile
and a route (Navigation) profile sharing a name (e.g. both named
"Same") would silently clobber each other's .af file on save, with no
error and no warning.

- MossTankMetaProfileStore.WriteLegacyExport now writes to
  "exports/meta/{name}.af".
- MossTankRouteProfileStore.WriteLegacyExport now writes to
  "exports/nav/{name}.af".
- Both class docs updated to name the collision this avoids and point at
  the sibling store's subdirectory.
- MossTankPanelTests: existing NavCommandsImportAndExportExactVtankNavFiles
  and MetaCommandsImportAndExportExactVtankMetFiles updated to the new
  paths. New MetaAndRouteExportsWithTheSameNameDoNotCollide saves a Meta
  and a route profile both named "Same" and asserts both .af files exist
  with their own correct content — verified failing before the fix
  (asserted false on the meta file's existence once both paths were
  reverted to the flat "exports/" root, confirming the collision is real
  and this test catches it).

Full MossTank suite: 569/569 passing (568 -> 569, one new test).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 22:26:17 +02:00
parent e5bc5c6a3f
commit d0247dbb19
3 changed files with 49 additions and 7 deletions

View file

@ -195,7 +195,12 @@ internal sealed class MossTankMetaProfileStore
/// <c>.af</c> is the only VTank-compatible export format now (Campaign
/// VT slice 1 Part A — MossTank no longer authors the binary
/// <c>.met</c> format at all, matching <see cref="VtankMetaProfileSerializer"/>'s
/// demotion to a one-shot import).
/// demotion to a one-shot import). Exports live under
/// <c>exports/meta/</c> — a distinct subdirectory from
/// <see cref="MossTankRouteProfileStore"/>'s <c>exports/nav/</c> — so a
/// Meta profile and a route (Navigation) profile sharing the same name
/// cannot silently overwrite each other's <c>.af</c> file (both stores
/// used to write into the same flat <c>exports/</c> root).
/// </summary>
private void WriteLegacyExport(string name, MetaProfile profile)
{
@ -204,7 +209,7 @@ internal sealed class MossTankMetaProfileStore
try
{
_host.Storage.WriteText(
$"exports/{LegacyFileName(name)}.af",
$"exports/meta/{LegacyFileName(name)}.af",
MetafSerializer.SaveMeta(profile));
}
catch (Exception error)

View file

@ -264,7 +264,12 @@ internal sealed class MossTankRouteProfileStore
/// <c>.af</c> is the only VTank-compatible export format now (Campaign
/// VT slice 1 Part A — MossTank no longer authors the binary
/// <c>.nav</c> format at all, matching <see cref="VtankNavRouteSerializer"/>'s
/// demotion to a one-shot import).
/// demotion to a one-shot import). Exports live under
/// <c>exports/nav/</c> — a distinct subdirectory from
/// <see cref="MossTankMetaProfileStore"/>'s <c>exports/meta/</c> — so a
/// route (Navigation) profile and a Meta profile sharing the same name
/// cannot silently overwrite each other's <c>.af</c> file (both stores
/// used to write into the same flat <c>exports/</c> root).
/// </summary>
private void WriteLegacyExport(string name, NavigationSettings settings)
{
@ -273,7 +278,7 @@ internal sealed class MossTankRouteProfileStore
try
{
_host.Storage.WriteText(
$"exports/{LegacyFileName(name)}.af",
$"exports/nav/{LegacyFileName(name)}.af",
MetafSerializer.SaveNav(settings));
}
catch (Exception error)

View file

@ -678,7 +678,39 @@ public sealed class MossTankPanelTests
Command(panel, "nav save Exported.nav");
Assert.StartsWith(
"NAV: ",
storage.Text["exports/Exported.af"],
storage.Text["exports/nav/Exported.af"],
StringComparison.Ordinal);
}
// Item C (Campaign VT slice-1 fix round): a Meta profile and a route
// (Navigation) profile named identically used to write into the SAME
// flat "exports/" directory — "Same.af" from one silently clobbered
// "Same.af" from the other. exports/meta/ and exports/nav/ are
// separate subdirectories so both coexist.
[Fact]
public void MetaAndRouteExportsWithTheSameNameDoNotCollide()
{
var storage = new MemoryStorage();
storage.Text["imports/Same.met"] =
"1\r\nCondAct\r\n5\r\nCType\r\nAType\r\nCData\r\nAData\r\nState\r\n"
+ "n\r\nn\r\nn\r\nn\r\nn\r\n1\r\n"
+ "i\r\n1\r\ni\r\n2\r\ni\r\n0\r\ns\r\n/say imported\r\n"
+ "s\r\nDefault\r\n";
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
Command(panel, "meta load Same.met");
Command(panel, "meta save Same.met");
Command(panel, "nav save Same.nav");
Assert.True(storage.Text.ContainsKey("exports/meta/Same.af"));
Assert.True(storage.Text.ContainsKey("exports/nav/Same.af"));
Assert.StartsWith(
"STATE: ",
storage.Text["exports/meta/Same.af"],
StringComparison.Ordinal);
Assert.StartsWith(
"NAV: ",
storage.Text["exports/nav/Same.af"],
StringComparison.Ordinal);
}
@ -712,10 +744,10 @@ public sealed class MossTankPanelTests
Command(panel, "meta save Exported.met");
Assert.StartsWith(
"STATE: ",
storage.Text["exports/Exported.af"],
storage.Text["exports/meta/Exported.af"],
StringComparison.Ordinal);
Assert.True(MetafSerializer.TryLoadMeta(
storage.Text["exports/Exported.af"],
storage.Text["exports/meta/Exported.af"],
NoOpSpellCatalogForExport.Instance,
out MetaProfile exported,
out string error), error);