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>
363 lines
16 KiB
C#
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);
|
|
}
|
|
}
|