From 7ca9785665f2cf491f80133098e4ba38bfe99cf6 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 13:58:18 +0200 Subject: [PATCH] =?UTF-8?q?test(app):=20slice=207=20fix=20round=20B=20item?= =?UTF-8?q?=2015=20=E2=80=94=20build=20every=20mosstank*.xml=20against=20a?= =?UTF-8?q?=20real=20MossTankPanel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every existing MossTank markup pin (MossTankMarkupContractTests) validates mosstank*.xml against MossTankPanel through reflection alone — "does a public property with this name and this CLR type exist" — never through MarkupDocument.Build itself, the code that actually mounts a plugin panel at runtime. Build has its own validation a reflection-only check can't see (attribute-format exceptions, delegate-shape mismatches, numeric- attribute parsing, column-type dispatch). Before this test, a markup bug of that shape would throw inside RetailUiRuntime.MountPlugins's own try/catch and the panel would simply not appear — no test failure, no visible error short of a live client screenshot. New MossTankMarkupBuildOverRealFilesTests (AcDream.App.Tests) builds every real mosstank*.xml file against a REAL MossTankPanel with a stub IPluginHost (same shape as MossTankMarkupContractTests.StubHost) and a fake sprite resolver. Required: a real compile-time ProjectReference from AcDream.App.Tests to AcDream.Plugins.MossTank (test-only exception to the plugin/host boundary — AcDream.App itself never links this assembly, see its own ReferenceOutputAssembly=false copy target) plus InternalsVisibleTo("AcDream.App.Tests") on the plugin project (MossTankPanel is internal). The test project also globs mosstank*.xml from the plugin's source directory into its own output, same pattern item 13 established. This test caught a REAL bug on its first run: mosstank-advanced.xml's new lFilterList (item 9's category checklist) had no selected="..." attribute. MarkupDocument.BindRequiredIntReader throws "list selected must be an int binding" when a 's selected attribute is missing at all — a pure checklist with no real "selected row" concept still needs one to satisfy the markup grammar. Fixed by adding SelectedAdvancedOptionCategoryIndex (always -1; item 10 already removed row-selection-band rendering for every column grid, so this drives no visible highlight) and wiring selected="{SelectedAdvancedOptionCategoryIndex}" onto that list. tests/AcDream.Plugins.MossTank.Tests: 675/675 (unchanged — this item's scope is entirely App-side plus one real bug fix invisible to MossTank's own reflection-only pins). tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 283/3 skipped/286 (was 278/3/281, +5: the new Theory's 5 file cases). Co-Authored-By: Claude Fable 5.1 --- .../AcDream.Plugins.MossTank.csproj | 8 ++ src/AcDream.Plugins.MossTank/MossTankPanel.cs | 8 ++ .../mosstank-advanced.xml | 5 + .../AcDream.App.Tests.csproj | 14 +++ .../MossTankMarkupBuildOverRealFilesTests.cs | 99 +++++++++++++++++++ .../packages.neutral.lock.json | 6 ++ 6 files changed, 140 insertions(+) create mode 100644 tests/AcDream.App.Tests/UI/MossTankMarkupBuildOverRealFilesTests.cs diff --git a/src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj b/src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj index 957c01406..1bb753011 100644 --- a/src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj +++ b/src/AcDream.Plugins.MossTank/AcDream.Plugins.MossTank.csproj @@ -11,6 +11,14 @@ + + diff --git a/src/AcDream.Plugins.MossTank/MossTankPanel.cs b/src/AcDream.Plugins.MossTank/MossTankPanel.cs index 11f67c764..1da42204f 100644 --- a/src/AcDream.Plugins.MossTank/MossTankPanel.cs +++ b/src/AcDream.Plugins.MossTank/MossTankPanel.cs @@ -471,6 +471,14 @@ internal sealed partial class MossTankPanel public IReadOnlyList AdvancedOptionCategoryNames { get; } = VtankOptionCatalog.CategoryBits.Select(bit => $"0x{bit:X}").ToArray(); public IReadOnlyList AdvancedOptionCategoryEnabled => _advancedOptionCategoryEnabled; + // Fix round B item 15's own build-over-real-files test caught this: + // markup REQUIRES a "selected" int binding (MarkupDocument. + // BindRequiredIntReader throws "must be an int binding" when the + // attribute is missing) even for a pure checklist with no real + // selection concept — item 10 already removed the row-selection band + // rendering for every column grid, so this value drives no visible + // highlight; it exists purely to satisfy the markup grammar. + public int SelectedAdvancedOptionCategoryIndex => -1; public Action ToggleAdvancedOptionCategoryAt => index => { if ((uint)index >= (uint)_advancedOptionCategoryEnabled.Length) diff --git a/src/AcDream.Plugins.MossTank/mosstank-advanced.xml b/src/AcDream.Plugins.MossTank/mosstank-advanced.xml index 91dab2938..a99cf30fd 100644 --- a/src/AcDream.Plugins.MossTank/mosstank-advanced.xml +++ b/src/AcDream.Plugins.MossTank/mosstank-advanced.xml @@ -81,7 +81,12 @@ + diff --git a/tests/AcDream.App.Tests/AcDream.App.Tests.csproj b/tests/AcDream.App.Tests/AcDream.App.Tests.csproj index b8dc39980..9caeae5d2 100644 --- a/tests/AcDream.App.Tests/AcDream.App.Tests.csproj +++ b/tests/AcDream.App.Tests/AcDream.App.Tests.csproj @@ -24,6 +24,15 @@ + + @@ -52,6 +61,11 @@ PreserveNewest + + diff --git a/tests/AcDream.App.Tests/UI/MossTankMarkupBuildOverRealFilesTests.cs b/tests/AcDream.App.Tests/UI/MossTankMarkupBuildOverRealFilesTests.cs new file mode 100644 index 000000000..ca2faac54 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/MossTankMarkupBuildOverRealFilesTests.cs @@ -0,0 +1,99 @@ +using AcDream.App.UI; +using AcDream.Plugin.Abstractions; +using AcDream.Plugins.MossTank; +using Xunit; + +namespace AcDream.App.Tests.UI; + +/// +/// Fix round B item 15. Every existing MossTank markup pin +/// (MossTankMarkupContractTests) validates mosstank*.xml +/// against through reflection alone — "does a +/// public property with this name and this CLR type exist" — never through +/// itself, the code that actually mounts +/// a plugin panel at runtime. MarkupDocument.Build has its own +/// validation a reflection-only check can't see (attribute-format +/// exceptions like ValidateArtStyle's "must be plain or retail", +/// delegate-shape mismatches surfaced as thrown s +/// rather than a missing property, numeric-attribute parsing, column-type +/// dispatch). Before this test, a markup bug of that shape would throw +/// inside RetailUiRuntime.MountPlugins's own try/catch and the panel +/// would simply not appear — no test failure, no visible error short of a +/// live client screenshot. This builds every real mosstank*.xml file +/// against a REAL (a stub , +/// same shape as MossTankMarkupContractTests.StubHost) so a bad +/// attribute fails a test instead of dropping the panel silently at mount. +/// +public sealed class MossTankMarkupBuildOverRealFilesTests +{ + private static string MossTankMarkupDirectory => + Path.Combine(AppContext.BaseDirectory, "MossTank"); + + public static IEnumerable MossTankMarkupFiles() => + Directory.GetFiles(MossTankMarkupDirectory, "mosstank*.xml") + .OrderBy(static path => path, StringComparer.Ordinal) + .Select(static path => new object[] { path }); + + [Theory] + [MemberData(nameof(MossTankMarkupFiles))] + public void EveryMossTankPanelFileBuildsAgainstARealPanelWithNoException(string path) + { + string xml = File.ReadAllText(path); + var panel = new MossTankPanel(new StubHost()); + + UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32)); + + Assert.NotNull(built); + Assert.NotEmpty(built.Children); + } + + private sealed class StubHost : IPluginHost + { + public bool HasUi => false; + public IPluginLogger Log { get; } = new StubLogger(); + public IGameState State { get; } = new StubState(); + public IEvents Events { get; } = new StubEvents(); + public ISelectionService Selection { get; } = new StubSelection(); + public IUiRegistry Ui => NoOpUiRegistry.Instance; + public IAutomationSurface Automation => NoOpAutomationSurface.Instance; + } + + private sealed class StubLogger : IPluginLogger + { + public void Info(string message) { } + public void Warn(string message) { } + public void Error(string message, Exception? exception = null) { } + } + + private sealed class StubState : IGameState + { + public IReadOnlyList Entities => []; + } + + private sealed class StubEvents : IEvents + { + public event Action EntitySpawned + { + add { } + remove { } + } + public event Action Tick + { + add { } + remove { } + } + } + + private sealed class StubSelection : ISelectionService + { + public uint? SelectedObjectId => null; + public uint? PreviousObjectId => null; + public event Action Changed + { + add { } + remove { } + } + public bool Select(uint objectId) => false; + public bool Clear() => false; + } +} diff --git a/tests/AcDream.App.Tests/packages.neutral.lock.json b/tests/AcDream.App.Tests/packages.neutral.lock.json index 352f60401..2d1df398d 100644 --- a/tests/AcDream.App.Tests/packages.neutral.lock.json +++ b/tests/AcDream.App.Tests/packages.neutral.lock.json @@ -448,6 +448,12 @@ "acdream.plugin.abstractions": { "type": "Project" }, + "acdream.plugins.mosstank": { + "type": "Project", + "dependencies": { + "AcDream.Plugin.Abstractions": "[1.0.0, )" + } + }, "acdream.runtime": { "type": "Project", "dependencies": {