From 2f26a933cb4d1becf92c6d93fbbb897481c6a33d Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Thu, 3 Sep 2026 11:46:25 +0200 Subject: [PATCH] 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 --- .../plans/2026-09-03-pos895-integration.md | 171 ++++++------------ 1 file changed, 53 insertions(+), 118 deletions(-) diff --git a/docs/superpowers/plans/2026-09-03-pos895-integration.md b/docs/superpowers/plans/2026-09-03-pos895-integration.md index 6e11508..39a39f8 100644 --- a/docs/superpowers/plans/2026-09-03-pos895-integration.md +++ b/docs/superpowers/plans/2026-09-03-pos895-integration.md @@ -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; /// Model reported for anything answering SPRT. The reply carries no /// model name, and a POS896 is indistinguishable from a POS895 here. public const string SprtModelName = "POS895"; + private readonly ILogger? _logger; + + public SprtDiscoveryService(ILogger? logger = null) + { + _logger = logger; + } + /// The 13-byte discovery query, byte-identical to EasySet's. public static byte[] BuildProbe() { @@ -265,102 +295,6 @@ public class SprtDiscoveryService : IDiscoveryService return true; } - public Task> DiscoverPrintersAsync( - TimeSpan? timeout = null, - Action? 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> DiscoverPrintersAsync( TimeSpan? timeout = null, Action? 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**