fix(vtank): slice 7 fix round C item D2 — /mt refresh now re-syncs the Monsters grid

EnsureDefaultMonsterRule() is a _combatSettings.Rules mutator (it appends
a DEFAULT row when the list is empty), but the /mt refresh command
handler never called RefreshMonsterEditor() afterward, unlike every other
Rules mutator in the plugin (AddMonsterRuleCore, DeleteMonsterRuleAtCore,
MoveMonsterRuleAtCore, UpdateMonsterActionsAt, the constructor, and
ResetProfileConsumers's own EnsureDefaultMonsterRule call).

Audited every _combatSettings.Rules mutation site in the plugin (grep for
"_combatSettings.Rules" across all of src/AcDream.Plugins.MossTank):
AddMonsterRuleCore, DeleteMonsterRuleAtCore, MoveMonsterRuleAtCore, and
UpdateMonsterActionsAt already call RefreshMonsterEditor right after
mutating; the constructor and ResetProfileConsumers already pair their
own EnsureDefaultMonsterRule call with one. The /mt refresh handler was
the only gap.

Mutation named: since Rules can never actually be observed empty through
the panel's own public surface (delete refuses removing DEFAULT, and
every profile-load path already re-adds it via ResetProfileConsumers
before this handler could see it), the new pin reaches the private
CombatSettings instance via reflection to clear Rules directly, and also
pokes the cached _monsterNameColumn field to a "STALE" sentinel first —
otherwise EnsureDefaultMonsterRule() re-adding "DEFAULT" would coincide
with the grid's already-cached construction-time value and the test
would pass even with the fix missing. Confirmed it fails (shows "STALE"
instead of "DEFAULT") with the RefreshMonsterEditor() call removed,
before restoring the fix.

MossTank suite 678 -> 679 (one new pin). Full solution build green; App
markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 15:27:33 +02:00
parent 23d4376cc3
commit f15667db5f
2 changed files with 53 additions and 0 deletions

View file

@ -172,7 +172,19 @@ internal sealed partial class MossTankPanel
WriteVtank($"Portal space toggle count: {_commandPortalCount}");
return;
case "refresh":
// Fix round C item D2: EnsureDefaultMonsterRule() mutates
// _combatSettings.Rules (adds the DEFAULT row when the list
// is empty) but this handler never re-materialized the
// Monsters grid's cached columns afterward — every OTHER
// _combatSettings.Rules mutator (AddMonsterRuleCore,
// DeleteMonsterRuleAtCore, MoveMonsterRuleAtCore,
// UpdateMonsterActionsAt) already calls RefreshMonsterEditor
// right after mutating, as do the constructor and
// ResetProfileConsumers's own EnsureDefaultMonsterRule call
// — this was the one remaining gap found by auditing every
// _combatSettings.Rules mutation site in the plugin.
EnsureDefaultMonsterRule();
RefreshMonsterEditor();
RefreshItemEditors();
RefreshLootEditor();
RefreshRouteEditor();

View file

@ -1,3 +1,4 @@
using System.Reflection;
using AcDream.Plugin.Abstractions;
namespace AcDream.Plugins.MossTank.Tests;
@ -2126,6 +2127,46 @@ public sealed class MossTankPanelTests
Assert.Equal(["DEFAULT", "mosswart"], panel.MonsterNameColumn);
}
[Fact]
public void VtankRefreshCommandRepopulatesTheMonstersGridAfterCreatingTheDefaultRule()
{
// Fix round C item D2: /mt refresh calls EnsureDefaultMonsterRule
// (a _combatSettings.Rules mutator) but used to skip
// RefreshMonsterEditor afterward — every OTHER Rules mutator in the
// panel (AddMonsterRuleCore, DeleteMonsterRuleAtCore,
// MoveMonsterRuleAtCore, UpdateMonsterActionsAt, the constructor,
// and ResetProfileConsumers's own EnsureDefaultMonsterRule call)
// already refreshes the grid right after mutating; this command
// handler was the one gap found by auditing every
// _combatSettings.Rules mutation site. Rules can never actually go
// empty through the panel's own public surface (delete refuses
// removing DEFAULT, and every profile-load path re-adds it via
// ResetProfileConsumers before this handler could ever observe it
// empty) — so this reaches the private CombatSettings instance
// directly to reproduce the corrupted/emptied-list precondition
// the command's own defensive EnsureDefaultMonsterRule() call
// exists to recover from. The cached _monsterNameColumn field is
// also poked to a sentinel value first: EnsureDefaultMonsterRule
// re-adding "DEFAULT" happens to reproduce the SAME content the
// grid already had from construction, so without also corrupting
// the cache this test would pass even with the refresh call
// missing (the stale cache and the freshly recomputed one would
// coincidentally agree).
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
Type panelType = typeof(MossTankPanel);
var combatSettings = (CombatSettings)panelType
.GetField("_combatSettings", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(panel)!;
combatSettings.Rules.Clear();
FieldInfo monsterNameColumnField = panelType
.GetField("_monsterNameColumn", BindingFlags.Instance | BindingFlags.NonPublic)!;
monsterNameColumnField.SetValue(panel, new[] { "STALE" });
panel.ExecuteVtankCommand(new PluginCommand("vt", "refresh", "/vt refresh"));
Assert.Equal(["DEFAULT"], panel.MonsterNameColumn);
}
[Fact]
public void MoveMonsterRuleAtReordersButNeverDisplacesDefault()
{