fix #490 (part 2): plugin panels adopt an authored size change instead of keeping a stale saved size forever

MountPlugins registered every plugin window with authoredGeometryRevision
hard-coded to 0 (RetailUiRuntime.cs), so RetailWindowLayoutPersistence's
MigrateAuthoredGeometry -- gated on "saved revision >= authored revision"
-- never migrated a plugin window's saved size: 0 >= 0 forever. MossTank's
panel went 856x236 -> 984x271 and every user with a stored layout stayed
stuck at 856x236 with no way to see the new default.

Two changes:

1. MountPlugins now passes RetailWindowManager.ComputeAuthoredGeometryRevision
   (added previous commit) as the plugin window's authoredGeometryRevision,
   derived from the panel's own authored width/height/minw/minh/resizable.

2. MigrateAuthoredGeometry now compares revisions for INEQUALITY
   (saved.Revision == authored.Revision) instead of ordering
   (saved.Revision >= authored.Revision). A hash is not an incrementing
   counter -- two different authored sizes can hash in either order -- so
   "the authored size changed" has to mean "the value differs", not "the
   value went up". Built-in windows' hand-picked incrementing literals
   (chat: authoredGeometryRevision = 1) are unaffected: no existing saved
   revision is ever equal to a later, different literal either way.

Mutation shown to fail first: the two new PluginMarkupPanel_AuthoredSizeChanged_*
tests in RetailWindowLayoutPersistenceTests.cs reproduce the exact bug with
concrete literals (856x236/400/150/true -> 984x271/... and
200x100/100/80/true -> 220x110/...) chosen so ComputeAuthoredGeometryRevision's
OLD hash is >= the NEW hash for each pair -- confirmed via a throwaway probe
before writing the assertions, so the pre-fix run fails deterministically
rather than by chance of hash ordering. Both failed before this commit
(size stayed at the old authored extent) and pass after
(PluginMarkupPanel_AuthoredSizeUnchanged_KeepsUserResizedSize, unaffected
either way, is a regression-safety companion). Full RetailWindow/Markup/
PluginSidePanel filter: 249 passed (was 246), 0 failed, 0 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 17:51:46 +02:00
parent b2e68c2343
commit 05f22d46ff
3 changed files with 188 additions and 2 deletions

View file

@ -499,6 +499,143 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
Assert.Equal((200f, 150f), (panel.Width, panel.Height));
}
// ── #490 part 2: a plugin's authored panel size changes across a plugin
// update (MossTank went 856x236 -> 984x271) and every user's stored
// layout must adopt the new authored size rather than keep the old one
// forever. MountPlugins derives authoredGeometryRevision from the
// authored geometry tuple via RetailWindowManager.ComputeAuthoredGeometryRevision
// (#490 part 2) instead of a manual literal, so these tests register the
// way MountPlugins does: pass the SAME computed hash to RegisterWindow. ──
[Fact]
public void PluginMarkupPanel_AuthoredSizeChanged_ReplacesStoredSizeButKeepsPosition()
{
const string oldXml =
"<panel x=\"0\" y=\"0\" w=\"856\" h=\"236\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
const string newXml =
"<panel x=\"0\" y=\"0\" w=\"984\" h=\"271\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
var store = new SettingsStore(PathName);
// "Old session": the plugin's previous authored size registers and
// the user drags the window.
var oldPanel = MarkupDocument.Build(oldXml, new object(), _ => (1u, 32, 32));
var oldRoot = new UiRoot { Width = 1280, Height = 720 };
oldRoot.AddChild(oldPanel);
RetailWindowHandle oldHandle = oldRoot.RegisterWindow(
"moss-tank",
oldPanel,
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
oldPanel.Width, oldPanel.Height, oldPanel.MinWidth, oldPanel.MinHeight, oldPanel.Resizable));
using (var oldPersistence = new RetailWindowLayoutPersistence(
oldRoot.WindowManager, store, () => "Alice", () => (1280, 720)))
{
oldHandle.MoveTo(120f, 90f);
}
// "New session": the plugin ships its new authored 984x271 size.
var newPanel = MarkupDocument.Build(newXml, new object(), _ => (1u, 32, 32));
var newRoot = new UiRoot { Width = 1280, Height = 720 };
newRoot.AddChild(newPanel);
newRoot.RegisterWindow(
"moss-tank",
newPanel,
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
newPanel.Width, newPanel.Height, newPanel.MinWidth, newPanel.MinHeight, newPanel.Resizable));
using var persistence = new RetailWindowLayoutPersistence(
newRoot.WindowManager, store, () => "Alice", () => (1280, 720));
persistence.RestoreAll();
Assert.Equal((984f, 271f), (newPanel.Width, newPanel.Height));
Assert.Equal((120f, 90f), (newPanel.Left, newPanel.Top));
}
[Fact]
public void PluginMarkupPanel_AuthoredSizeUnchanged_KeepsUserResizedSize()
{
const string xml =
"<panel x=\"0\" y=\"0\" w=\"856\" h=\"236\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
var store = new SettingsStore(PathName);
var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
var root = new UiRoot { Width = 1280, Height = 720 };
root.AddChild(panel);
RetailWindowHandle handle = root.RegisterWindow(
"moss-tank",
panel,
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
panel.Width, panel.Height, panel.MinWidth, panel.MinHeight, panel.Resizable));
using (var persistence = new RetailWindowLayoutPersistence(
root.WindowManager, store, () => "Alice", () => (1280, 720)))
{
handle.MoveTo(50f, 50f);
handle.ResizeTo(900f, 300f);
}
// Fresh session: the SAME authored geometry (same markup) registers
// again — the derived revision is unchanged, so the user's own
// resize must survive.
var freshPanel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
var freshRoot = new UiRoot { Width = 1280, Height = 720 };
freshRoot.AddChild(freshPanel);
freshRoot.RegisterWindow(
"moss-tank",
freshPanel,
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
freshPanel.Width, freshPanel.Height, freshPanel.MinWidth, freshPanel.MinHeight, freshPanel.Resizable));
using var freshPersistence = new RetailWindowLayoutPersistence(
freshRoot.WindowManager, store, () => "Alice", () => (1280, 720));
freshPersistence.RestoreAll();
Assert.Equal((900f, 300f), (freshPanel.Width, freshPanel.Height));
}
[Fact]
public void PluginMarkupPanel_AuthoredSizeChanged_ClampsPositionToNewScreenBounds()
{
// The authored-size migration keeps the saved POSITION, but that
// position still runs through the ordinary restore clamp (Apply's
// MoveTo clamp) — a larger authored size can push a near-edge saved
// X/Y off the live screen.
const string oldXml =
"<panel x=\"0\" y=\"0\" w=\"200\" h=\"100\" resizable=\"true\" minw=\"100\" minh=\"80\"></panel>";
const string newXml =
"<panel x=\"0\" y=\"0\" w=\"220\" h=\"110\" resizable=\"true\" minw=\"100\" minh=\"80\"></panel>";
var store = new SettingsStore(PathName);
var oldPanel = MarkupDocument.Build(oldXml, new object(), _ => (1u, 32, 32));
var oldRoot = new UiRoot { Width = 800, Height = 600 };
oldRoot.AddChild(oldPanel);
RetailWindowHandle oldHandle = oldRoot.RegisterWindow(
"moss-tank",
oldPanel,
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
oldPanel.Width, oldPanel.Height, oldPanel.MinWidth, oldPanel.MinHeight, oldPanel.Resizable));
using (var oldPersistence = new RetailWindowLayoutPersistence(
oldRoot.WindowManager, store, () => "Alice", () => (800, 600)))
{
oldHandle.MoveTo(590f, 490f); // fits the OLD 200x100 size exactly
}
var newPanel = MarkupDocument.Build(newXml, new object(), _ => (1u, 32, 32));
var newRoot = new UiRoot { Width = 800, Height = 600 };
newRoot.AddChild(newPanel);
newRoot.RegisterWindow(
"moss-tank",
newPanel,
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
newPanel.Width, newPanel.Height, newPanel.MinWidth, newPanel.MinHeight, newPanel.Resizable));
using var persistence = new RetailWindowLayoutPersistence(
newRoot.WindowManager, store, () => "Alice", () => (800, 600));
persistence.RestoreAll();
Assert.Equal((220f, 110f), (newPanel.Width, newPanel.Height));
// maxX = 800 - 220 = 580 (590 clamps down); maxY = 600 - 110 = 490 (already in bounds).
Assert.Equal((580f, 490f), (newPanel.Left, newPanel.Top));
}
private static RetailWindowHandle Mount(
UiRoot root,
string name,