diff --git a/docs/plans/2026-03-09-xunit-test-suite.md b/docs/plans/2026-03-09-xunit-test-suite.md
new file mode 100644
index 0000000..a8a2a16
--- /dev/null
+++ b/docs/plans/2026-03-09-xunit-test-suite.md
@@ -0,0 +1,417 @@
+# OpenNest xUnit Test Suite Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Convert the ad-hoc OpenNest.Test console harness into a proper xUnit test suite with test data hosted in a separate git repo.
+
+**Architecture:** Replace the console app with an xUnit test project. A `TestData` helper resolves the test data path from env var `OPENNEST_TEST_DATA` or fallback `../OpenNest.Test.Data/`. Tests skip with a message if data is missing. Test classes: `FillTests` (full plate fills), `RemnantFillTests` (filling remnant areas). All tests assert part count >= target and zero overlaps.
+
+**Tech Stack:** C# / .NET 8, xUnit, OpenNest.Core + Engine + IO
+
+---
+
+### Task 1: Set up test data repo and push fixture files
+
+**Step 1: Clone the empty repo next to OpenNest**
+
+```bash
+cd C:/Users/AJ/Desktop/Projects
+git clone https://git.thecozycat.net/aj/OpenNest.Test.git OpenNest.Test.Data
+```
+
+**Step 2: Copy fixture files into the repo**
+
+```bash
+cp ~/Desktop/"N0308-017.zip" OpenNest.Test.Data/
+cp ~/Desktop/"N0308-008.zip" OpenNest.Test.Data/
+cp ~/Desktop/"30pcs Fill.zip" OpenNest.Test.Data/
+```
+
+**Step 3: Commit and push**
+
+```bash
+cd OpenNest.Test.Data
+git add .
+git commit -m "feat: add initial test fixture nest files"
+git push
+```
+
+---
+
+### Task 2: Convert OpenNest.Test from console app to xUnit project
+
+**Files:**
+- Modify: `OpenNest.Test/OpenNest.Test.csproj`
+- Delete: `OpenNest.Test/Program.cs`
+- Create: `OpenNest.Test/TestData.cs`
+
+**Step 1: Replace the csproj with xUnit configuration**
+
+Overwrite `OpenNest.Test/OpenNest.Test.csproj`:
+
+```xml
+
+
+
+ net8.0-windows
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Step 2: Delete Program.cs**
+
+```bash
+rm OpenNest.Test/Program.cs
+```
+
+**Step 3: Create TestData helper**
+
+Create `OpenNest.Test/TestData.cs`:
+
+```csharp
+using OpenNest.IO;
+
+namespace OpenNest.Test;
+
+public static class TestData
+{
+ private static readonly string? BasePath = ResolveBasePath();
+
+ public static bool IsAvailable => BasePath != null;
+
+ public static string SkipReason =>
+ "Test data not found. Set OPENNEST_TEST_DATA env var or clone " +
+ "https://git.thecozycat.net/aj/OpenNest.Test.git to ../OpenNest.Test.Data/";
+
+ public static string GetPath(string filename)
+ {
+ if (BasePath == null)
+ throw new InvalidOperationException(SkipReason);
+
+ var path = Path.Combine(BasePath, filename);
+
+ if (!File.Exists(path))
+ throw new FileNotFoundException($"Test fixture not found: {path}");
+
+ return path;
+ }
+
+ public static Nest LoadNest(string filename)
+ {
+ var reader = new NestReader(GetPath(filename));
+ return reader.Read();
+ }
+
+ public static Plate CleanPlateFrom(Plate reference)
+ {
+ var plate = new Plate();
+ plate.Size = reference.Size;
+ plate.PartSpacing = reference.PartSpacing;
+ plate.EdgeSpacing = reference.EdgeSpacing;
+ plate.Quadrant = reference.Quadrant;
+ return plate;
+ }
+
+ private static string? ResolveBasePath()
+ {
+ // 1. Environment variable
+ var envPath = Environment.GetEnvironmentVariable("OPENNEST_TEST_DATA");
+
+ if (!string.IsNullOrEmpty(envPath) && Directory.Exists(envPath))
+ return envPath;
+
+ // 2. Sibling directory (../OpenNest.Test.Data/ relative to solution root)
+ var dir = AppContext.BaseDirectory;
+
+ // Walk up from bin/Debug/net8.0-windows to find the solution root.
+ for (var i = 0; i < 6; i++)
+ {
+ var parent = Directory.GetParent(dir);
+
+ if (parent == null)
+ break;
+
+ dir = parent.FullName;
+ var candidate = Path.Combine(dir, "OpenNest.Test.Data");
+
+ if (Directory.Exists(candidate))
+ return candidate;
+ }
+
+ return null;
+ }
+}
+```
+
+**Step 4: Build**
+
+Run: `dotnet build OpenNest.Test/OpenNest.Test.csproj`
+Expected: Build succeeds.
+
+**Step 5: Commit**
+
+```bash
+git add OpenNest.Test/
+git commit -m "refactor: convert OpenNest.Test to xUnit project with TestData helper"
+```
+
+---
+
+### Task 3: Add FillTests
+
+**Files:**
+- Create: `OpenNest.Test/FillTests.cs`
+
+**Step 1: Create FillTests.cs**
+
+```csharp
+using OpenNest.Geometry;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace OpenNest.Test;
+
+public class FillTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public FillTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ [Trait("Category", "Fill")]
+ public void N0308_008_HingePlate_FillsAtLeast75()
+ {
+ Skip.IfNot(TestData.IsAvailable, TestData.SkipReason);
+
+ var nest = TestData.LoadNest("N0308-008.zip");
+ var hinge = nest.Drawings.First(d => d.Name.Contains("HINGE PLATE #2"));
+ var plate = TestData.CleanPlateFrom(nest.Plates[0]);
+
+ var engine = new NestEngine(plate);
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ engine.Fill(new NestItem { Drawing = hinge, Quantity = 0 });
+ sw.Stop();
+
+ _output.WriteLine($"Parts: {plate.Parts.Count} | Time: {sw.ElapsedMilliseconds}ms");
+
+ Assert.True(plate.Parts.Count >= 75,
+ $"Expected >= 75 parts, got {plate.Parts.Count}");
+ AssertNoOverlaps(plate.Parts.ToList());
+ }
+
+ [Fact]
+ [Trait("Category", "Fill")]
+ public void RemainderStripRefill_30pcs_FillsAtLeast32()
+ {
+ Skip.IfNot(TestData.IsAvailable, TestData.SkipReason);
+
+ var nest = TestData.LoadNest("30pcs Fill.zip");
+ var drawing = nest.Drawings.First();
+ var plate = TestData.CleanPlateFrom(nest.Plates[0]);
+
+ var engine = new NestEngine(plate);
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ engine.Fill(new NestItem { Drawing = drawing, Quantity = 0 });
+ sw.Stop();
+
+ _output.WriteLine($"Parts: {plate.Parts.Count} | Time: {sw.ElapsedMilliseconds}ms");
+
+ Assert.True(plate.Parts.Count >= 32,
+ $"Expected >= 32 parts, got {plate.Parts.Count}");
+ AssertNoOverlaps(plate.Parts.ToList());
+ }
+
+ private void AssertNoOverlaps(List parts)
+ {
+ for (var i = 0; i < parts.Count; i++)
+ {
+ for (var j = i + 1; j < parts.Count; j++)
+ {
+ if (parts[i].Intersects(parts[j], out _))
+ Assert.Fail($"Overlap detected: part [{i}] vs [{j}]");
+ }
+ }
+ }
+}
+```
+
+**Step 2: Run tests**
+
+Run: `dotnet test OpenNest.Test/ -v normal`
+Expected: 2 tests pass (or skip if data missing).
+
+**Step 3: Commit**
+
+```bash
+git add OpenNest.Test/FillTests.cs
+git commit -m "test: add FillTests for full plate fill and remainder strip refill"
+```
+
+---
+
+### Task 4: Add RemnantFillTests
+
+**Files:**
+- Create: `OpenNest.Test/RemnantFillTests.cs`
+
+**Step 1: Create RemnantFillTests.cs**
+
+```csharp
+using OpenNest.Geometry;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace OpenNest.Test;
+
+public class RemnantFillTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public RemnantFillTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ [Trait("Category", "Remnant")]
+ public void N0308_017_PT02_RemnantFillsAtLeast10()
+ {
+ Skip.IfNot(TestData.IsAvailable, TestData.SkipReason);
+
+ var nest = TestData.LoadNest("N0308-017.zip");
+ var plate = nest.Plates[0];
+ var pt02 = nest.Drawings.First(d => d.Name.Contains("PT02"));
+ var remnant = plate.GetRemnants()[0];
+
+ _output.WriteLine($"Remnant: ({remnant.X:F2},{remnant.Y:F2}) {remnant.Width:F2}x{remnant.Height:F2}");
+
+ var countBefore = plate.Parts.Count;
+ var engine = new NestEngine(plate);
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ engine.Fill(new NestItem { Drawing = pt02, Quantity = 0 }, remnant);
+ sw.Stop();
+
+ var added = plate.Parts.Count - countBefore;
+ _output.WriteLine($"Added: {added} parts | Time: {sw.ElapsedMilliseconds}ms");
+
+ Assert.True(added >= 10, $"Expected >= 10 parts in remnant, got {added}");
+
+ var newParts = plate.Parts.Skip(countBefore).ToList();
+ AssertNoOverlaps(newParts);
+ AssertNoCrossOverlaps(plate.Parts.Take(countBefore).ToList(), newParts);
+ }
+
+ [Fact]
+ [Trait("Category", "Remnant")]
+ public void N0308_008_HingePlate_RemnantFillsAtLeast8()
+ {
+ Skip.IfNot(TestData.IsAvailable, TestData.SkipReason);
+
+ var nest = TestData.LoadNest("N0308-008.zip");
+ var plate = nest.Plates[0];
+ var hinge = nest.Drawings.First(d => d.Name.Contains("HINGE PLATE #2"));
+ var remnants = plate.GetRemnants();
+
+ _output.WriteLine($"Remnant 0: ({remnants[0].X:F2},{remnants[0].Y:F2}) {remnants[0].Width:F2}x{remnants[0].Height:F2}");
+
+ var countBefore = plate.Parts.Count;
+ var engine = new NestEngine(plate);
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ engine.Fill(new NestItem { Drawing = hinge, Quantity = 0 }, remnants[0]);
+ sw.Stop();
+
+ var added = plate.Parts.Count - countBefore;
+ _output.WriteLine($"Added: {added} parts | Time: {sw.ElapsedMilliseconds}ms");
+
+ Assert.True(added >= 8, $"Expected >= 8 parts in remnant, got {added}");
+
+ var newParts = plate.Parts.Skip(countBefore).ToList();
+ AssertNoOverlaps(newParts);
+ AssertNoCrossOverlaps(plate.Parts.Take(countBefore).ToList(), newParts);
+ }
+
+ private void AssertNoOverlaps(List parts)
+ {
+ for (var i = 0; i < parts.Count; i++)
+ {
+ for (var j = i + 1; j < parts.Count; j++)
+ {
+ if (parts[i].Intersects(parts[j], out _))
+ Assert.Fail($"Overlap detected: part [{i}] vs [{j}]");
+ }
+ }
+ }
+
+ private void AssertNoCrossOverlaps(List existing, List added)
+ {
+ for (var i = 0; i < existing.Count; i++)
+ {
+ for (var j = 0; j < added.Count; j++)
+ {
+ if (existing[i].Intersects(added[j], out _))
+ Assert.Fail($"Cross-overlap: existing [{i}] vs added [{j}]");
+ }
+ }
+ }
+}
+```
+
+**Step 2: Run all tests**
+
+Run: `dotnet test OpenNest.Test/ -v normal`
+Expected: 4 tests pass.
+
+**Step 3: Commit**
+
+```bash
+git add OpenNest.Test/RemnantFillTests.cs
+git commit -m "test: add RemnantFillTests for remnant area filling"
+```
+
+---
+
+### Task 5: Add test project to solution and final verification
+
+**Step 1: Add to solution**
+
+```bash
+cd C:/Users/AJ/Desktop/Projects/OpenNest
+dotnet sln OpenNest.sln add OpenNest.Test/OpenNest.Test.csproj
+```
+
+**Step 2: Build entire solution**
+
+Run: `dotnet build OpenNest.sln`
+Expected: All projects build, 0 errors.
+
+**Step 3: Run all tests**
+
+Run: `dotnet test OpenNest.sln -v normal`
+Expected: 4 tests pass, 0 failures.
+
+**Step 4: Commit**
+
+```bash
+git add OpenNest.sln
+git commit -m "chore: add OpenNest.Test to solution"
+```