amend POS895 plan: merge discovery tasks, drop network-dependent test

Pre-flight scan found two things the plan mandated that a reviewer would
rightly flag: Task 1 committed a DiscoverPrintersAsync that threw
NotImplementedException until Task 2 replaced it, and a unit test asserted
an empty result from a real UDP broadcast, which passes on a laptop and
fails on any network with a POS895 on it.

Tasks 1 and 2 are now one task delivering the complete service, and the
network-dependent test is gone. The deterministic bind-failure test still
covers the degradation path. Six tasks now.

Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
This commit is contained in:
EugeneTes
2026-09-03 11:46:25 +02:00
parent 84b94e8c1a
commit 2f26a933cb

View File

@@ -19,9 +19,9 @@
---
### Task 1: Test project, `DiscoveredPrinter.Serial`, and SPRT protocol parsing
### Task 1: `SprtDiscoveryService`, test project, and `DiscoveredPrinter.Serial`
Creates the test project the later tasks need, adds the one new DTO field, and implements the pure functions of the SPRT protocol so they can be tested without a network.
Delivers the complete SPRT discovery service: the test project the later tasks need, the one new DTO field, the protocol's pure functions, and the network path.
**Files:**
- Create: `Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
@@ -32,8 +32,8 @@ Creates the test project the later tasks need, adds the one new DTO field, and i
- Modify: `Inspectron.Epson.slnx`
**Interfaces:**
- Consumes: `DiscoveredPrinter` (existing class, `Inspectron.Epson`).
- Produces: `SprtDiscoveryService.BuildProbe()` returning `byte[]`; `SprtDiscoveryService.TryParseReply(byte[] payload, string ipAddress, out DiscoveredPrinter? printer)` returning `bool`. Task 2 uses both. `DiscoveredPrinter.Serial` (`string?`) used by Tasks 1-2.
- Consumes: `DiscoveredPrinter`, `IDiscoveryService` (existing, `Inspectron.Epson`).
- Produces: `SprtDiscoveryService(ILogger? logger = null)`; `SprtDiscoveryService.BuildProbe()` returning `byte[]`; `SprtDiscoveryService.TryParseReply(byte[] payload, string ipAddress, out DiscoveredPrinter? printer)` returning `bool`; a working `DiscoverPrintersAsync`. Task 6 constructs it for DI. `DiscoveredPrinter.Serial` (`string?`).
- [ ] **Step 1: Create the test project**
@@ -160,9 +160,27 @@ public class SprtDiscoveryServiceTests
Assert.True(ok);
Assert.Equal("", printer!.Serial);
}
[Fact]
public async Task DiscoverPrintersAsync_returns_empty_when_port_9100_is_taken()
{
// Hold UDP 9100 so the service cannot bind it. It must degrade, not throw:
// ENPC discovery has to keep working when this one cannot start.
using var blocker = new System.Net.Sockets.UdpClient();
blocker.Client.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Any, 9100));
var service = new SprtDiscoveryService();
var result = await service.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(200));
Assert.Empty(result);
}
}
```
Note there is deliberately no "nothing answers" test. Asserting an empty list from
a real broadcast would pass on a developer laptop and fail on any network with a
POS895 on it, including the print server's own.
- [ ] **Step 3: Run the tests to verify they fail**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
@@ -192,7 +210,10 @@ In `Inspectron.Epson/PreconfiguredDiscoveryService.cs`, inside the object initia
Create `Inspectron.Epson/SprtDiscoveryService.cs`:
```csharp
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging;
namespace Inspectron.Epson;
@@ -214,11 +235,20 @@ public class SprtDiscoveryService : IDiscoveryService
{
private const string ProbeMagic = "SPRT-Infor";
private const int MacLength = 17;
private const int DiscoveryPort = 3289;
private const int ReplyPort = 9100;
/// <summary>Model reported for anything answering SPRT. The reply carries no
/// model name, and a POS896 is indistinguishable from a POS895 here.</summary>
public const string SprtModelName = "POS895";
private readonly ILogger? _logger;
public SprtDiscoveryService(ILogger? logger = null)
{
_logger = logger;
}
/// <summary>The 13-byte discovery query, byte-identical to EasySet's.</summary>
public static byte[] BuildProbe()
{
@@ -265,102 +295,6 @@ public class SprtDiscoveryService : IDiscoveryService
return true;
}
public Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
TimeSpan? timeout = null,
Action<DiscoveredPrinter>? onPrinterDiscovered = null)
{
throw new NotImplementedException("Task 2");
}
}
```
- [ ] **Step 6: Run the tests to verify they pass**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 6 tests.
- [ ] **Step 7: Commit**
```bash
git add Inspectron.Epson.Tests Inspectron.Epson.slnx Inspectron.Epson/SprtDiscoveryService.cs Inspectron.Epson/DiscoveredPrinter.cs Inspectron.Epson/PreconfiguredDiscoveryService.cs
git commit -m "add SPRT discovery protocol parsing and test project"
```
---
### Task 2: `SprtDiscoveryService` network path
Broadcasts the probe and collects replies. Binding UDP 9100 can fail when another process holds it; discovery must degrade to empty rather than crash, so ENPC discovery still runs.
**Files:**
- Modify: `Inspectron.Epson/SprtDiscoveryService.cs`
- Modify: `Inspectron.Epson.Tests/SprtDiscoveryServiceTests.cs`
**Interfaces:**
- Consumes: `BuildProbe()`, `TryParseReply(...)` from Task 1.
- Produces: `SprtDiscoveryService(ILogger? logger = null)` constructor; a working `DiscoverPrintersAsync`. Task 6 constructs it for DI.
- [ ] **Step 1: Write the failing tests**
Append to `Inspectron.Epson.Tests/SprtDiscoveryServiceTests.cs`:
```csharp
[Fact]
public async Task DiscoverPrintersAsync_returns_empty_when_port_9100_is_taken()
{
// Hold UDP 9100 so the service cannot bind it. It must degrade, not throw:
// ENPC discovery has to keep working when this one cannot start.
using var blocker = new System.Net.Sockets.UdpClient();
blocker.Client.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Any, 9100));
var service = new SprtDiscoveryService();
var result = await service.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(200));
Assert.Empty(result);
}
[Fact]
public async Task DiscoverPrintersAsync_returns_empty_when_nothing_answers()
{
var service = new SprtDiscoveryService();
var result = await service.DiscoverPrintersAsync(TimeSpan.FromMilliseconds(300));
Assert.Empty(result);
}
```
- [ ] **Step 2: Run the tests to verify they fail**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj --filter DiscoverPrintersAsync`
Expected: FAIL with `NotImplementedException: Task 2`.
- [ ] **Step 3: Write the implementation**
In `Inspectron.Epson/SprtDiscoveryService.cs`, add these usings at the top:
```csharp
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
```
Add a logger field and constructor above `BuildProbe`:
```csharp
private const int DiscoveryPort = 3289;
private const int ReplyPort = 9100;
private readonly ILogger? _logger;
public SprtDiscoveryService(ILogger? logger = null)
{
_logger = logger;
}
```
Replace the `NotImplementedException` body of `DiscoverPrintersAsync` with:
```csharp
public async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
TimeSpan? timeout = null,
Action<DiscoveredPrinter>? onPrinterDiscovered = null)
@@ -442,23 +376,24 @@ Replace the `NotImplementedException` body of `DiscoverPrintersAsync` with:
return discovered;
}
}
```
- [ ] **Step 4: Run the tests to verify they pass**
- [ ] **Step 6: Run the tests to verify they pass**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 8 tests.
Expected: PASS, 7 tests.
- [ ] **Step 5: Commit**
- [ ] **Step 7: Commit**
```bash
git add Inspectron.Epson/SprtDiscoveryService.cs Inspectron.Epson.Tests/SprtDiscoveryServiceTests.cs
git commit -m "implement SPRT discovery broadcast and reply collection"
git add Inspectron.Epson.Tests Inspectron.Epson.slnx Inspectron.Epson/SprtDiscoveryService.cs Inspectron.Epson/DiscoveredPrinter.cs Inspectron.Epson/PreconfiguredDiscoveryService.cs
git commit -m "add SPRT discovery service for POS895 printers"
```
---
### Task 3: `CompositeDiscoveryService`
### Task 2: `CompositeDiscoveryService`
Runs several discovery services concurrently and merges their results, so ENPC and SPRT printers appear in one list.
@@ -674,7 +609,7 @@ Note the children are called **without** the `onPrinterDiscovered` callback. The
- [ ] **Step 4: Run the tests to verify they pass**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 13 tests.
Expected: PASS, 12 tests.
- [ ] **Step 5: Commit**
@@ -685,7 +620,7 @@ git commit -m "add CompositeDiscoveryService merging ENPC and SPRT results"
---
### Task 4: `POS895` printer adapter
### Task 3: `POS895` printer adapter
The adapter differs from `TM_T30IIITranslated` in exactly three ways, each confirmed on hardware. A recording fake asserts the two that would otherwise fail silently on paper.
@@ -696,7 +631,7 @@ The adapter differs from `TM_T30IIITranslated` in exactly three ways, each confi
**Interfaces:**
- Consumes: `IPrinter` (`Inspectron.Epson.PrintServer.PrintServices`), `IEpsonPrinter` (`Inspectron.Epson`), `PrintCommand` (`Inspectron.Epson.PrintServer.Printers.Utils`).
- Produces: `POS895(IEpsonPrinter printer)` with a `bool SkipDelays { get; set; }`. Task 5 constructs it.
- Produces: `POS895(IEpsonPrinter printer)` with a `bool SkipDelays { get; set; }`. Task 4 constructs it.
- [ ] **Step 1: Write the recording test double**
@@ -1026,7 +961,7 @@ Note `PrintAsync` feeds 10 lines at the end **and** 10 on an `IsCut` command, ma
- [ ] **Step 5: Run the tests to verify they pass**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 20 tests.
Expected: PASS, 19 tests.
- [ ] **Step 6: Commit**
@@ -1037,7 +972,7 @@ git commit -m "add POS895 printer adapter with LF feed and ESC * logo"
---
### Task 5: Register printer ID `0x20` in the three switches
### Task 4: Register printer ID `0x20` in the three switches
Three one-line additions in three files. The third is the one that fails silently if missed.
@@ -1048,7 +983,7 @@ Three one-line additions in three files. The third is the one that fails silentl
- Create: `Inspectron.Epson.Tests/PrinterRegistrationTests.cs`
**Interfaces:**
- Consumes: `POS895` from Task 4.
- Consumes: `POS895` from Task 3.
- Produces: nothing new; wires `0x20` into existing dispatch.
- [ ] **Step 1: Write the failing tests**
@@ -1184,7 +1119,7 @@ Leave `EReceiptType.NextCourse` at `33, 20`. It is hardcoded for every printer a
- [ ] **Step 6: Run the tests to verify they pass**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 25 tests.
Expected: PASS, 24 tests.
- [ ] **Step 7: Commit**
@@ -1195,7 +1130,7 @@ git commit -m "register printer id 0x20 as POS895 across the three dispatch poin
---
### Task 6: Guard the nullable printer ID
### Task 5: Guard the nullable printer ID
`printerId` is a `byte?` dereferenced with `.Value` twice. A printer that does not answer `GS I 1` throws `NullReferenceException` into the generic catch, reporting `PrintErrorType.Other` and burning three `PrinterQueue` retries with no usable log line.
@@ -1230,7 +1165,7 @@ Expected: `0 Error(s)`, and no new warnings attributable to `EpsonPrintService.c
- [ ] **Step 3: Run the full test suite**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 25 tests.
Expected: PASS, 24 tests.
- [ ] **Step 4: Commit**
@@ -1241,7 +1176,7 @@ git commit -m "fail cleanly when a printer does not answer GS I 1"
---
### Task 7: DI wiring and emulation entry
### Task 6: DI wiring and emulation entry
Binds the composite discovery in production and adds a 576px emulated POS895 so receipt layouts can be checked without hardware.
@@ -1249,7 +1184,7 @@ Binds the composite discovery in production and adds a 576px emulated POS895 so
- Modify: `EpsonPrintService/PrintServerBootstrapper.cs:52-78`
**Interfaces:**
- Consumes: `SprtDiscoveryService` (Task 2), `CompositeDiscoveryService` (Task 3).
- Consumes: `SprtDiscoveryService` (Task 1), `CompositeDiscoveryService` (Task 2).
- Produces: nothing new.
- [ ] **Step 1: Add the emulated POS895**
@@ -1313,7 +1248,7 @@ Expected: `0 Error(s)`.
- [ ] **Step 4: Run the full test suite**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 25 tests.
Expected: PASS, 24 tests.
- [ ] **Step 5: Verify discovery against the real printer**