acdream/tests/AcDream.Plugins.MossTank.Tests/VtankProfileDirectoryTests.cs
Erik 2566dad1d3 test(vt): round 3 item 8 — nav byte-identity coverage + s-cell strip + .ast round-trip
The already-committed nav_briennecarlus.af/nav_empyrean.af/nav_lockandkeyjaw.af
fixtures (checkpoint/recall/portal2 node coverage, clean metaf headers)
were exercised by the model-equality proofs (parse, parse-write-parse,
binary-import-matches-af) but never by the REAL byte-identical writer-output
theory, which only ever covered nav_ab.af. Converted
WriterOutputMatchesMetafCanonicalEmissionNavOnly into a Theory over all
four nav-only fixtures.

Added two more coverage gaps this round's audit named:
- StringCellStripsEmbeddedNewlineAtConstructionNotJustOnWrite pins
  VtankCell.String's gy.cs:84 newline strip (construction-time, not just
  at WriteTo).
- RealAstFixturesRoundTripByteIdentical extends the existing schema-only
  owner-{a,b,c}.ast coverage to a real parse -> Render() byte-identity
  round trip, the same proof-4 rigor MetafSerializerTests already applies
  to .af fixtures.

No writer bug surfaced in the extended nav theory or the .ast round trip —
both already passed; these are coverage-closing additions, not fixes.
Verified each new test has teeth via a temporary synthetic mutation rather
than a real revert (there is no production fix to revert here): the
string-cell test was checked against a temporarily-unstripped VtankCell.String
(failed, restored), and the .ast round-trip was checked against a
temporarily LF-only VtankWriter.AppendLine (all three .ast cases failed,
the four nav cases were unaffected since MetafSerializer's .af writer is a
separate code path from VtankUsdDocument's "y" grammar writer — restored).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 01:38:56 +02:00

363 lines
16 KiB
C#

using AcDream.Plugin.Abstractions;
namespace AcDream.Plugins.MossTank.Tests;
public sealed class VtankProfileDirectoryTests
{
[Fact]
public void AutoCharacterFileNameMatchesRealInstalledConvention()
{
// Confirmed live: --Barris_Coldeve.usd is the auto per-character
// settings profile for character "Barris" on server "Coldeve".
Assert.Equal(
"--Barris_Coldeve.usd",
VtankProfileDirectory.AutoCharacterFileName("Barris", "Coldeve", "usd"));
Assert.Equal(
"--Barris_Coldeve.usd",
VtankProfileDirectory.AutoCharacterFileName("Barris", "Coldeve", ".usd"));
}
[Fact]
public void SubProfilesAreDisplayedAsCharBracketSuffix()
{
// Confirmed live: --Barris_Coldeve_Base.usd, --Barris_Coldeve_Blank.usd,
// --Barris_Coldeve_viridian.usd are Barris's own named sub-profiles,
// distinct from the single auto file --Barris_Coldeve.usd.
Assert.Equal(
"[Char] Base",
VtankProfileDirectory.TryDisplayName("--Barris_Coldeve_Base.usd", "Barris", "Coldeve"));
Assert.Equal(
"[Char] viridian",
VtankProfileDirectory.TryDisplayName("--Barris_Coldeve_viridian.usd", "Barris", "Coldeve"));
// The character's own single auto file has nothing after the
// trailing "_", so it is not a sub-profile.
Assert.Null(
VtankProfileDirectory.TryDisplayName("--Barris_Coldeve.usd", "Barris", "Coldeve"));
// A different character's file is not a sub-profile of this one.
Assert.Null(
VtankProfileDirectory.TryDisplayName("--Someone_Coldeve_Base.usd", "Barris", "Coldeve"));
}
[Fact]
public void OtherCharactersHiddenPrefixedFilesAreInvisible()
{
Assert.True(VtankProfileDirectory.IsHiddenFromOtherCharacters(
"--Someone_Coldeve.usd", "Barris", "Coldeve"));
Assert.False(VtankProfileDirectory.IsHiddenFromOtherCharacters(
"--Barris_Coldeve_Base.usd", "Barris", "Coldeve"));
Assert.False(VtankProfileDirectory.IsHiddenFromOtherCharacters(
"MyNamedProfile.usd", "Barris", "Coldeve"));
}
[Fact]
public void ListSettingsProfilesSeedsDefaultAndByCharFirst()
{
var storage = new MemoryStorage();
storage.WriteText("Shared.usd", "1\r\n");
storage.WriteText("--Barris_Coldeve.usd", "1\r\n");
storage.WriteText("--Barris_Coldeve_Base.usd", "1\r\n");
storage.WriteText("--Someone_Coldeve.usd", "1\r\n");
IReadOnlyList<VtankProfileDirectory.ProfileEntry> entries =
VtankProfileDirectory.ListSettingsProfiles(
storage, "Barris", "Coldeve", mineOnly: false);
Assert.Equal(VtankProfileDirectory.DefaultLabel, entries[0].DisplayName);
Assert.Equal(VtankProfileDirectory.ByCharacterLabel, entries[1].DisplayName);
Assert.Contains(entries, static e => e.DisplayName == "Shared.usd");
Assert.Contains(entries, static e => e.DisplayName == "[Char] Base");
Assert.DoesNotContain(entries, static e => e.FileName == "--Someone_Coldeve.usd");
Assert.DoesNotContain(entries, static e => e.FileName == "--Barris_Coldeve.usd");
}
[Fact]
public void ListNavigationProfilesFiltersBothReservedPrefixes()
{
var storage = new MemoryStorage();
// 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 =
VtankProfileDirectory.ListNavigationProfiles(storage);
Assert.Equal(VtankProfileDirectory.NoneLabel, entries[0].DisplayName);
Assert.Equal(VtankProfileDirectory.ByCharacterLabel, entries[1].DisplayName);
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()
{
IReadOnlyList<VtankProfileDirectory.ProfileEntry> entries =
VtankProfileDirectory.ListSettingsProfiles(
NoOpPluginStorage.Instance, "Barris", "Coldeve", mineOnly: false);
Assert.Equal(2, entries.Count);
}
// Item F (Campaign VT slice-1 fix round): a nested-path key (as a real
// IPluginStorage implementation might return for a sub-directory) must
// never surface as a bogus profile name — VTank's own profile
// directory is flat.
[Fact]
public void NestedPathKeysAreNotTreatedAsProfileFiles()
{
var storage = new MemoryStorage();
storage.WriteText("subdir/Nested.usd", "1\r\n");
storage.WriteText("Flat.usd", "1\r\n");
IReadOnlyList<VtankProfileDirectory.ProfileEntry> entries =
VtankProfileDirectory.ListSettingsProfiles(
storage, "Barris", "Coldeve", mineOnly: false);
Assert.Contains(entries, static e => e.FileName == "Flat.usd");
Assert.DoesNotContain(entries, static e => e.FileName.Contains('/'));
}
// Item G (Campaign VT slice-1 fix round): VTank's real "Mine only"
// predicate (uTank2/PluginCore.cs:7020-7024) is "!checked || file ==
// current", not "hide every shared file" — the profile actually
// assigned to the character must stay visible even under the
// checkbox, or the user's own current selection would vanish out of
// the list the moment they ticked it.
[Fact]
public void MineOnlyKeepsTheCurrentlySelectedSharedFileVisible()
{
var storage = new MemoryStorage();
storage.WriteText("Shared.usd", "1\r\n");
storage.WriteText("OtherShared.usd", "1\r\n");
IReadOnlyList<VtankProfileDirectory.ProfileEntry> withoutCurrent =
VtankProfileDirectory.ListSettingsProfiles(
storage, "Barris", "Coldeve", mineOnly: true);
Assert.DoesNotContain(withoutCurrent, static e => e.FileName == "Shared.usd");
Assert.DoesNotContain(withoutCurrent, static e => e.FileName == "OtherShared.usd");
IReadOnlyList<VtankProfileDirectory.ProfileEntry> withCurrent =
VtankProfileDirectory.ListSettingsProfiles(
storage, "Barris", "Coldeve", mineOnly: true, currentFileName: "Shared.usd");
Assert.Contains(withCurrent, static e => e.FileName == "Shared.usd");
Assert.DoesNotContain(withCurrent, static e => e.FileName == "OtherShared.usd");
}
[Fact]
public void MineOnlyUncheckedIgnoresCurrentFileAndKeepsEverything()
{
var storage = new MemoryStorage();
storage.WriteText("Shared.usd", "1\r\n");
IReadOnlyList<VtankProfileDirectory.ProfileEntry> entries =
VtankProfileDirectory.ListSettingsProfiles(
storage, "Barris", "Coldeve", mineOnly: false, currentFileName: null);
Assert.Contains(entries, static e => e.FileName == "Shared.usd");
}
[Fact]
public void AstFileNameHasNoHiddenPrefixAndNameServerOrder()
{
Assert.Equal(
"Barris_Coldeve.ast",
VtankProfileDirectory.AstFileName("Barris", "Coldeve"));
}
[Fact]
public void CdfFileNameUsesServerNameOrderReversedFromAst()
{
Assert.Equal(
"Coldeve_Barris.cdf",
VtankProfileDirectory.CdfFileName("Barris", "Coldeve"));
}
[Fact]
public void TryReadCharacterBindingParsesAllFourLines()
{
var storage = new MemoryStorage();
storage.WriteText(
"Coldeve_Barris.cdf",
"uTank2 CDF 1.0\r\n--Barris_Coldeve.usd\r\nLoot.utl\r\n--Barris_Coldeve.nav\r\nHunt.met\r\n");
VtankProfileDirectory.VtankCharacterBinding? binding =
VtankProfileDirectory.TryReadCharacterBinding(storage, "Barris", "Coldeve");
Assert.NotNull(binding);
Assert.Equal("--Barris_Coldeve.usd", binding!.Value.SettingsFileName);
Assert.Equal("Loot.utl", binding.Value.LootFileName);
Assert.Equal("--Barris_Coldeve.nav", binding.Value.NavFileName);
Assert.Equal("Hunt.met", binding.Value.MetaFileName);
}
[Fact]
public void TryReadCharacterBindingWithoutMetaLineLeavesMetaNull()
{
var storage = new MemoryStorage();
storage.WriteText(
"Coldeve_Barris.cdf",
"uTank2 CDF 1.0\r\n--Barris_Coldeve.usd\r\nLoot.utl\r\n--Barris_Coldeve.nav\r\n");
VtankProfileDirectory.VtankCharacterBinding? binding =
VtankProfileDirectory.TryReadCharacterBinding(storage, "Barris", "Coldeve");
Assert.NotNull(binding);
Assert.Null(binding!.Value.MetaFileName);
}
[Fact]
public void TryReadCharacterBindingRewritesLegacyUtsSettingsExtension()
{
var storage = new MemoryStorage();
storage.WriteText(
"Coldeve_Barris.cdf",
"uTank2 CDF 1.0\r\n--Barris_Coldeve.uts\r\nLoot.utl\r\n--Barris_Coldeve.nav\r\n");
VtankProfileDirectory.VtankCharacterBinding? binding =
VtankProfileDirectory.TryReadCharacterBinding(storage, "Barris", "Coldeve");
Assert.Equal("--Barris_Coldeve.usd", binding!.Value.SettingsFileName);
}
[Fact]
public void TryReadCharacterBindingReturnsNullOnHeaderMismatch()
{
var storage = new MemoryStorage();
storage.WriteText(
"Coldeve_Barris.cdf",
"uTank2 CDF 0.9\r\n--Barris_Coldeve.usd\r\nLoot.utl\r\n--Barris_Coldeve.nav\r\n");
Assert.Null(VtankProfileDirectory.TryReadCharacterBinding(storage, "Barris", "Coldeve"));
}
[Fact]
public void TryReadCharacterBindingReturnsNullWhenFileMissing()
{
Assert.Null(VtankProfileDirectory.TryReadCharacterBinding(
new MemoryStorage(), "Barris", "Coldeve"));
}
// Real owner-*.ast fixtures (Fixtures/vtank/owner-{a,b,c}.ast): same
// "y" database grammar as .usd, one "Spells" table, four columns
// (docs/research/vtank-kb/01-settings-and-profiles.md section 3,
// dm.cs:391 for the naming, live inspection of +Horan_sawato.ast for
// the schema).
[Theory]
[InlineData("owner-a.ast")]
[InlineData("owner-b.ast")]
[InlineData("owner-c.ast")]
public void RealAstFixturesParseAsTheSpellsTable(string fileName)
{
string text = File.ReadAllText(
Path.Combine(AppContext.BaseDirectory, "Fixtures", "vtank", fileName));
VtankDatabase database = VtankDatabase.Parse(text);
VtankTable? spells = database.Find("Spells");
Assert.NotNull(spells);
Assert.Equal(
["SpellID", "EndTime", "Target", "CastTime"],
spells!.ColumnNames);
}
/// <summary>
/// Round 3 item 8: the shared "y" database grammar's writer must be
/// REAL byte-identical against real installed data, not just the
/// hand-built synthetic fixtures in VtankUsdDocumentTests — the three
/// real owner-*.ast per-character spell-tracking caches round-trip
/// through parse -> Render() unchanged.
/// </summary>
[Theory]
[InlineData("owner-a.ast")]
[InlineData("owner-b.ast")]
[InlineData("owner-c.ast")]
public void RealAstFixturesRoundTripByteIdentical(string fileName)
{
string original = File.ReadAllText(
Path.Combine(AppContext.BaseDirectory, "Fixtures", "vtank", fileName));
VtankDatabase database = VtankDatabase.Parse(original);
string rewritten = database.Render();
Assert.Equal(original, rewritten);
}
private sealed class MemoryStorage : IPluginStorage
{
private readonly Dictionary<string, string> _text = new(StringComparer.Ordinal);
public bool IsAvailable => true;
public string? ReadText(string key) =>
_text.TryGetValue(key, out string? value) ? value : null;
public IReadOnlyList<string> List(string prefix) => _text.Keys
.Where(key => prefix.Length == 0
|| key.StartsWith(prefix + "/", StringComparison.Ordinal))
.OrderBy(static key => key, StringComparer.Ordinal)
.ToArray();
public void WriteText(string key, string content) => _text[key] = content;
public bool Delete(string key) => _text.Remove(key);
}
}