Files
OpenNest/OpenNest.Server.Tests/SetReleaseEndToEndTests.cs
T
ajandClaude Opus 5.5 af6975b9f1 feat(data): add the drawing set client and release session
The desktop needs a cross-platform way to read and save sets and to turn
one into nests before any UI is built on it. RemoteDrawingSetStore covers
every /api/sets route with the library client's checks and conflict
handling. SetReleasePlan groups a set by material, grade, thickness, units
and cut type without I/O, so the release dialog can preview refusals
(duplicate names, drawings without material) before anything is created.

SetReleaseSession builds every group's nest through LibraryDrawingInsert
before uploading any, so a refusal leaves nothing on the server; uploads
then never cancel mid-flight, a lost reply is reconciled by the nest's
client-allocated id, and the release is posted only when every chosen
nest exists, with a same-id retry when that post fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:48:23 -04:00

119 lines
5.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Data;
using OpenNest.Geometry;
using OpenNest.IO;
using OpenNest.Shapes;
namespace OpenNest.Server.Tests;
/// <summary>
/// The whole release path: code-built drawings in two materials are packaged into the
/// library, collected into a set, released ×2 through the real clients, and the nests and
/// release record are read back from the production routes.
/// </summary>
public sealed class SetReleaseEndToEndTests
{
private static Drawing SyntheticDrawing(string name, double length)
{
var drawing = new RectangleShape { Name = name, Length = length, Width = 2 }.GetDrawing();
drawing.CutPins.Add(new ContourCutPin(new Vector(length, 2), new LineLeadIn { Length = 0.25 }, new NoLeadOut(),
new NormalTab { Size = 0.03 }));
return drawing;
}
private static async Task<DrawingLibraryRecord> Store(RemoteDrawingLibrary library, string name, double length,
string material, CutType cutType)
{
var record = new DrawingLibraryRecord
{
Id = Guid.NewGuid(),
Name = name,
Customer = "Synthetic customer",
MaterialName = material,
MaterialGrade = "Grade A",
Thickness = 0.25,
Units = Units.Inches,
CutType = cutType,
SavedBy = "Synthetic operator",
};
return await library.UploadAsync(record, DrawingPackage.Write(SyntheticDrawing(name, length), Units.Inches));
}
private static byte[] Write(Nest nest)
{
using var stream = new MemoryStream();
new NestWriter(nest).Write(stream);
return stream.ToArray();
}
private static Nest Read(byte[] archive)
{
using var stream = new MemoryStream(archive, writable: false);
return new NestReader(stream).Read();
}
[Fact]
public async Task Release_MakesOneDraftNestPerMaterialAndRecordsThem()
{
using var factory = new ServerFactory();
using var client = factory.CreateClient();
var url = client.BaseAddress!.ToString();
using var library = new RemoteDrawingLibrary(client, url);
using var sets = new RemoteDrawingSetStore(client, url);
using var nests = new RemoteNestRepository(client, url);
var a = await Store(library, "SYN-A", 4, "Synthetic Steel", CutType.Laser);
var b = await Store(library, "SYN-B", 5, "Synthetic Steel", CutType.Laser);
var c = await Store(library, "SYN-C", 6, "Synthetic Aluminum", CutType.Waterjet);
var created = await sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Customer = "Synthetic set customer",
Members = new[] { (a, 3), (b, 1), (c, 2) }
.Select(m => new DrawingSetMember { DrawingId = m.Item1.Id, Quantity = m.Item2 }).ToList(),
});
var set = await sets.GetAsync(created.Id);
var plan = SetReleasePlan.Build(set!, 2, includeArchived: false);
var number = 0;
var session = new SetReleaseSession(sets, library, nests, url,
bytes =>
{
var contents = DrawingPackage.Read(bytes);
return new LibraryPackage(bytes, contents.Drawing, contents.Units, contents.Warnings);
},
Write,
(group, _) => Task.FromResult(new Nest($"Synthetic release {++number}") { Units = group.Units }));
var result = await session.RunAsync(plan, plan.Groups, "Synthetic operator", "Synthetic release notes");
Assert.Equal("", result.ReleaseError);
Assert.Equal(2, result.Created.Count);
foreach (var group in result.Groups)
{
var stored = Read((await nests.GetFileAsync(group.Record!.Id))!);
Assert.Equal(NestStatus.Draft, (await nests.GetMetadataAsync(group.Record.Id))!.Status);
Assert.Equal((NestStatus.Draft, group.Group.MaterialName, 0.25, "Synthetic set customer"),
(stored.Status, stored.Material.Name, stored.Thickness, stored.Customer));
foreach (var item in group.Group.Items)
{
var drawing = stored.Drawings.Single(d => d.Name == item.Drawing.Name);
Assert.Equal(item.Member.Quantity * 2, drawing.Quantity.Required);
Assert.Equal((item.Drawing.Id, 1, url.TrimEnd('/')), (drawing.Library!.Id, drawing.Library.Revision, drawing.Library.ServerKey));
Assert.Single(drawing.CutPins);
}
}
var steel = result.Groups.Single(g => g.Group.MaterialName == "Synthetic Steel");
Assert.Equal(new[] { "SYN-A", "SYN-B" }, steel.Group.Items.Select(i => i.Drawing.Name));
var releases = await sets.GetReleasesAsync(created.Id);
var release = Assert.Single(releases!.Items);
Assert.Equal((result.Release!.Id, 1, 2, "Synthetic release notes"),
(release.Id, release.SetRevision, release.Multiplier, release.Notes));
Assert.Equal(result.Groups.Select(g => g.Record!.Id), release.Nests.Select(n => n.NestId));
Assert.All(release.Nests, n => Assert.Equal(NestStatus.Draft, n.Nest!.Status));
Assert.Equal(CutType.Waterjet, release.Nests.Single(n => n.MaterialName == "Synthetic Aluminum").CutType);
Assert.Equal(release.Id, (await sets.GetReleaseForNestAsync(steel.Record!.Id))!.Id);
}
}