Files
OpenNest/OpenNest.Server.Tests/SetReleaseEndToEndTests.cs
T
ajandClaude Opus 5.5 28db6ae807 feat(server): release several drawing sets at once, merge into drafts, and undo
A release can now cover several sets, each with its own kit count, so parts
from different kits share sheets. It can also merge into an existing draft
nest instead of always creating one. Schema v9 replaces the single-set
release tables: releases, their set lines, their nests (created or merged,
with the nest revision before and after) and the drawing quantities each set
added to each nest, so a kit stays traceable once kits share a nest. The v8
migration turns each existing release into one set line with created nests.

Undo runs on the server in one transaction: created nests are soft-deleted
and merged nests restored to their pre-merge revision, but only while each
still holds what the release wrote. Otherwise nothing changes and the
changed nests are listed, so the operator can skip them. A nest restored to
that revision counts as unchanged, which lets releases sharing a nest be
undone newest first.

The old /api/sets/{id}/releases and /api/nests/{id}/release routes are
replaced by /api/releases; no tagged desktop used them. The desktop still
releases one set at a time until the release dialog is reworked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:25:52 -04:00

128 lines
6.0 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.QueryReleasesAsync(setId: created.Id);
var release = Assert.Single(releases.Items);
var line = Assert.Single(release.Sets);
Assert.Equal((result.Release!.Id, 1, 2, "Synthetic release notes"),
(release.Id, line.SetRevision, line.Quantity, 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, SetReleaseNestKind.Created, 1), (n.Nest!.Status, n.Kind, n.RevisionAfter)));
Assert.Equal(CutType.Waterjet, release.Nests.Single(n => n.MaterialName == "Synthetic Aluminum").CutType);
Assert.Equal(new[] { (a.Id, 6), (b.Id, 2) },
release.Nests.Single(n => n.NestId == steel.Record!.Id).Items.Select(i => (i.DrawingId, i.Quantity)));
Assert.Equal(release.Id, (await sets.QueryReleasesAsync(nestId: steel.Record!.Id)).Items.Single().Id);
// Undo deletes every nest the release created.
var undone = await sets.UndoReleaseAsync(release.Id, new SetReleaseUndoRequest { UndoneBy = "Synthetic operator" });
Assert.All(undone.Nests, n => Assert.Equal(SetReleaseNestUndo.Deleted, n.Undo));
foreach (var group in result.Groups)
Assert.Null(await nests.GetMetadataAsync(group.Record!.Id));
}
}