diff --git a/tests/AcDream.App.Tests/UI/RetailWindowLayoutPersistenceTests.cs b/tests/AcDream.App.Tests/UI/RetailWindowLayoutPersistenceTests.cs
index 848f12eba..67c0abe1f 100644
--- a/tests/AcDream.App.Tests/UI/RetailWindowLayoutPersistenceTests.cs
+++ b/tests/AcDream.App.Tests/UI/RetailWindowLayoutPersistenceTests.cs
@@ -424,6 +424,81 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
Assert.Equal((77f, 88f), (saved.X, saved.Y));
}
+ // ── 2026-09-07: a resizable plugin markup panel's resized geometry
+ // persists and restores exactly like any other registered window — no
+ // persistence-layer change was needed for this, since RetailWindowLayoutPersistence
+ // already clamps against the frame's own MinWidth/MinHeight/MaxWidth/
+ // MaxHeight (Apply's ClampDimension) for every registered handle. ────────
+
+ [Fact]
+ public void ResizableMarkupPluginPanel_ResizedSize_RoundTripsThroughPersistence()
+ {
+ const string xml =
+ "";
+ var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
+ var store = new SettingsStore(PathName);
+ var root = new UiRoot { Width = 800, Height = 600 };
+ root.AddChild(panel);
+ RetailWindowHandle handle = root.RegisterWindow("plugin-resizable-panel", panel);
+ using var persistence = new RetailWindowLayoutPersistence(
+ root.WindowManager, store, () => "Alice", () => (800, 600));
+
+ handle.MoveTo(60f, 70f);
+ handle.ResizeTo(420f, 260f);
+
+ UiWindowLayout saved = Assert.IsType(
+ store.LoadWindowLayout("Alice", "800x600", "plugin-resizable-panel", default));
+ Assert.Equal((60f, 70f, 420f, 260f), (saved.X, saved.Y, saved.Width, saved.Height));
+
+ // Simulate a fresh session: rebuild the same markup (a fresh, un-resized
+ // panel) and restore — the saved 420x260 must come back, not the
+ // authored 300x200.
+ var freshPanel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
+ var freshRoot = new UiRoot { Width = 800, Height = 600 };
+ freshRoot.AddChild(freshPanel);
+ RetailWindowHandle freshHandle = freshRoot.RegisterWindow(
+ "plugin-resizable-panel", freshPanel);
+ using var freshPersistence = new RetailWindowLayoutPersistence(
+ freshRoot.WindowManager, store, () => "Alice", () => (800, 600));
+
+ freshPersistence.RestoreAll();
+
+ Assert.Equal((420f, 260f), (freshPanel.Width, freshPanel.Height));
+ Assert.Equal((60f, 70f), (freshPanel.Left, freshPanel.Top));
+ }
+
+ [Fact]
+ public void ResizableMarkupPluginPanel_RestoreClampsBelowFloorToAuthoredMin()
+ {
+ // A legacy save from before this feature's minw/minh existed (or one
+ // from a plugin update that raised its floor) can carry a size below
+ // the CURRENT authored minimum. Restore must clamp up to that floor —
+ // this is what RetailWindowLayoutPersistence.Apply's ClampDimension
+ // already does against frame.MinWidth/MinHeight for every registered
+ // window; it only bites here because MarkupDocument now sets those
+ // fields from minw/minh instead of leaving them at UiElement's
+ // generic 40x40 default.
+ const string xml =
+ "";
+ var store = new SettingsStore(PathName);
+ store.SaveWindowLayout(
+ "Alice",
+ "800x600",
+ "plugin-resizable-floor",
+ new UiWindowLayout(60f, 70f, 80f, 60f, true, false, false));
+
+ var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
+ var root = new UiRoot { Width = 800, Height = 600 };
+ root.AddChild(panel);
+ root.RegisterWindow("plugin-resizable-floor", panel);
+ using var persistence = new RetailWindowLayoutPersistence(
+ root.WindowManager, store, () => "Alice", () => (800, 600));
+
+ persistence.RestoreAll();
+
+ Assert.Equal((200f, 150f), (panel.Width, panel.Height));
+ }
+
private static RetailWindowHandle Mount(
UiRoot root,
string name,
diff --git a/tests/AcDream.App.Tests/UI/RetailWindowManagerTests.cs b/tests/AcDream.App.Tests/UI/RetailWindowManagerTests.cs
index 9c538bc21..9a245023c 100644
--- a/tests/AcDream.App.Tests/UI/RetailWindowManagerTests.cs
+++ b/tests/AcDream.App.Tests/UI/RetailWindowManagerTests.cs
@@ -250,6 +250,47 @@ public sealed class RetailWindowManagerTests
transitions);
}
+ // ── 2026-09-07: plugin markup's resizable="true" panel through the real
+ // window-manager ResizeTo path (no host wiring beyond what MarkupDocument
+ // already sets on the panel — Resizable/ResizeX/ResizeY/MinWidth/MinHeight
+ // are ordinary UiElement properties RetailWindowManager.ResizeTo already
+ // respects for any registered window). ───────────────────────────────────
+
+ [Fact]
+ public void ResizableMarkupPluginWindow_AcceptsResizeWithinMinAndParentConstraints()
+ {
+ const string xml =
+ "";
+ var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
+ var root = new UiRoot { Width = 800, Height = 600 };
+ root.AddChild(panel);
+ RetailWindowHandle handle = root.RegisterWindow("plugin-resizable", panel);
+
+ Assert.True(handle.ResizeTo(500f, 400f));
+ Assert.Equal((500f, 400f), (panel.Width, panel.Height));
+
+ // Below the authored floor clamps to minw/minh rather than shrinking further.
+ Assert.True(handle.ResizeTo(50f, 50f));
+ Assert.Equal((250f, 150f), (panel.Width, panel.Height));
+ }
+
+ [Fact]
+ public void NonResizableMarkupPluginWindow_RefusesResize()
+ {
+ const string xml = "";
+ var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
+ var root = new UiRoot { Width = 800, Height = 600 };
+ root.AddChild(panel);
+ RetailWindowHandle handle = root.RegisterWindow("plugin-fixed", panel);
+ int resized = 0;
+ handle.Resized += _ => resized++;
+
+ handle.ResizeTo(500f, 400f);
+
+ Assert.Equal((300f, 200f), (panel.Width, panel.Height));
+ Assert.Equal(0, resized);
+ }
+
private sealed class RecordingController : IRetainedPanelController
{
public int ShownCount { get; private set; }