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:** **Files:**
- Create: `Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj` - 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` - Modify: `Inspectron.Epson.slnx`
**Interfaces:** **Interfaces:**
- Consumes: `DiscoveredPrinter` (existing class, `Inspectron.Epson`). - Consumes: `DiscoveredPrinter`, `IDiscoveryService` (existing, `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. - 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** - [ ] **Step 1: Create the test project**
@@ -160,9 +160,27 @@ public class SprtDiscoveryServiceTests
Assert.True(ok); Assert.True(ok);
Assert.Equal("", printer!.Serial); 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** - [ ] **Step 3: Run the tests to verify they fail**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj` 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`: Create `Inspectron.Epson/SprtDiscoveryService.cs`:
```csharp ```csharp
using System.Net;
using System.Net.Sockets;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging;
namespace Inspectron.Epson; namespace Inspectron.Epson;
@@ -214,11 +235,20 @@ public class SprtDiscoveryService : IDiscoveryService
{ {
private const string ProbeMagic = "SPRT-Infor"; private const string ProbeMagic = "SPRT-Infor";
private const int MacLength = 17; 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 /// <summary>Model reported for anything answering SPRT. The reply carries no
/// model name, and a POS896 is indistinguishable from a POS895 here.</summary> /// model name, and a POS896 is indistinguishable from a POS895 here.</summary>
public const string SprtModelName = "POS895"; 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> /// <summary>The 13-byte discovery query, byte-identical to EasySet's.</summary>
public static byte[] BuildProbe() public static byte[] BuildProbe()
{ {
@@ -265,102 +295,6 @@ public class SprtDiscoveryService : IDiscoveryService
return true; 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( public async Task<List<DiscoveredPrinter>> DiscoverPrintersAsync(
TimeSpan? timeout = null, TimeSpan? timeout = null,
Action<DiscoveredPrinter>? onPrinterDiscovered = null) Action<DiscoveredPrinter>? onPrinterDiscovered = null)
@@ -442,23 +376,24 @@ Replace the `NotImplementedException` body of `DiscoverPrintersAsync` with:
return discovered; 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` 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 ```bash
git add Inspectron.Epson/SprtDiscoveryService.cs Inspectron.Epson.Tests/SprtDiscoveryServiceTests.cs git add Inspectron.Epson.Tests Inspectron.Epson.slnx Inspectron.Epson/SprtDiscoveryService.cs Inspectron.Epson/DiscoveredPrinter.cs Inspectron.Epson/PreconfiguredDiscoveryService.cs
git commit -m "implement SPRT discovery broadcast and reply collection" 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. 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** - [ ] **Step 4: Run the tests to verify they pass**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj` Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 13 tests. Expected: PASS, 12 tests.
- [ ] **Step 5: Commit** - [ ] **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. 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:** **Interfaces:**
- Consumes: `IPrinter` (`Inspectron.Epson.PrintServer.PrintServices`), `IEpsonPrinter` (`Inspectron.Epson`), `PrintCommand` (`Inspectron.Epson.PrintServer.Printers.Utils`). - 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** - [ ] **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** - [ ] **Step 5: Run the tests to verify they pass**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj` Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 20 tests. Expected: PASS, 19 tests.
- [ ] **Step 6: Commit** - [ ] **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. 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` - Create: `Inspectron.Epson.Tests/PrinterRegistrationTests.cs`
**Interfaces:** **Interfaces:**
- Consumes: `POS895` from Task 4. - Consumes: `POS895` from Task 3.
- Produces: nothing new; wires `0x20` into existing dispatch. - Produces: nothing new; wires `0x20` into existing dispatch.
- [ ] **Step 1: Write the failing tests** - [ ] **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** - [ ] **Step 6: Run the tests to verify they pass**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj` Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 25 tests. Expected: PASS, 24 tests.
- [ ] **Step 7: Commit** - [ ] **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. `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** - [ ] **Step 3: Run the full test suite**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj` Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj`
Expected: PASS, 25 tests. Expected: PASS, 24 tests.
- [ ] **Step 4: Commit** - [ ] **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. 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` - Modify: `EpsonPrintService/PrintServerBootstrapper.cs:52-78`
**Interfaces:** **Interfaces:**
- Consumes: `SprtDiscoveryService` (Task 2), `CompositeDiscoveryService` (Task 3). - Consumes: `SprtDiscoveryService` (Task 1), `CompositeDiscoveryService` (Task 2).
- Produces: nothing new. - Produces: nothing new.
- [ ] **Step 1: Add the emulated POS895** - [ ] **Step 1: Add the emulated POS895**
@@ -1313,7 +1248,7 @@ Expected: `0 Error(s)`.
- [ ] **Step 4: Run the full test suite** - [ ] **Step 4: Run the full test suite**
Run: `dotnet test Inspectron.Epson.Tests/Inspectron.Epson.Tests.csproj` 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** - [ ] **Step 5: Verify discovery against the real printer**