test(app): slice 7 fix round B item 15 — build every mosstank*.xml against a real MossTankPanel

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 <list>'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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 13:58:18 +02:00
parent b0fee80e6f
commit 7ca9785665
6 changed files with 140 additions and 0 deletions

View file

@ -11,6 +11,14 @@
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="AcDream.Plugins.MossTank.Tests" />
<!-- Fix round B item 15: AcDream.App.Tests constructs a real
MossTankPanel directly (MossTankMarkupBuildOverRealFilesTests) so
MarkupDocument.Build validates every mosstank*.xml's actual
attribute/binding grammar, not just reflection-visible property
names/types. A test-only exception to the plugin/host compile-time
boundary (AcDream.App itself never links this assembly — see
AcDream.App.csproj's ReferenceOutputAssembly=false copy target). -->
<InternalsVisibleTo Include="AcDream.App.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AcDream.Plugin.Abstractions\AcDream.Plugin.Abstractions.csproj">

View file

@ -471,6 +471,14 @@ internal sealed partial class MossTankPanel
public IReadOnlyList<string> AdvancedOptionCategoryNames { get; } =
VtankOptionCatalog.CategoryBits.Select(bit => $"0x{bit:X}").ToArray();
public IReadOnlyList<bool> AdvancedOptionCategoryEnabled => _advancedOptionCategoryEnabled;
// Fix round B item 15's own build-over-real-files test caught this:
// <list> 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<int> ToggleAdvancedOptionCategoryAt => index =>
{
if ((uint)index >= (uint)_advancedOptionCategoryEnabled.Length)

View file

@ -81,7 +81,12 @@
<column type="text" width="187" items="{AdvancedOptionNames}" onclick="{SelectAdvancedOption}" />
<column type="text" width="69" items="{AdvancedOptionValueColumn}" onclick="{SelectAdvancedOption}" />
</list>
<!-- Fix round B item 15's own build-over-real-files test caught a real
bug here: <list> markup requires a "selected" int binding even for a
pure checklist with no real selection concept — see
SelectedAdvancedOptionCategoryIndex's own doc comment. -->
<list x="268" y="4" w="120" h="180" rowheight="18"
selected="{SelectedAdvancedOptionCategoryIndex}"
tooltip="Uncheck a category to hide its settings from the list on the left.">
<column type="check" width="20" values="{AdvancedOptionCategoryEnabled}" onchange="{ToggleAdvancedOptionCategoryAt}" />
<column type="text" width="*" items="{AdvancedOptionCategoryNames}" onclick="{ToggleAdvancedOptionCategoryAt}" />