diff --git a/docs/plans/2026-10-07-native-ab-transport.md.tasks.json b/docs/plans/2026-10-07-native-ab-transport.md.tasks.json index a9429bda..11d2e909 100644 --- a/docs/plans/2026-10-07-native-ab-transport.md.tasks.json +++ b/docs/plans/2026-10-07-native-ab-transport.md.tasks.json @@ -166,7 +166,7 @@ { "id": 18, "subject": "Task 18: Differential harness (libplctag vs native, same tar", - "status": "pending", + "status": "completed", "description": "**Goal:** A `LiveIntegration` test that reads the same tags through both transports against one endpoint and asserts equal raw buffers and equal decoded values. It skips cleanly when the endpoint env var is absent.\n\n**Files:**\n- Create: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/TransportDifferentialTests.cs`\n- Create: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/DifferentialTarget.cs`\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs` and `NativeAbCipTagRuntime.cs` (add `internal ReadOnlyMemory RawBuffer`)\n- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj` (`InternalsVisibleTo` for `ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests`)\n\n**Acceptance Criteria:**\n- [ ] **Env gating:**\n - `AB_DIFF_ENDPOINT` (`host:port`), `AB_DIFF_PATH` (default `1,0`), `AB_DIFF_FAMILY` (default `CompactLogix`).\n - `AB_DIFF_TAGS`: an optional comma list. Absent \u21d2 every atomic scalar, the first 20 arrays and the first 20 UDT members from browse.\n - `AB_DIFF_MAX_TAGS` (default 200).\n - Absent `AB_DIFF_ENDPOINT` \u21d2 every test reports **skipped**, with the reason naming the variable.\n- [ ] **The suite is read-only by construction.** It never calls `WriteAsync` or `EncodeValue`. A guard test reflects over `TransportDifferentialTests` and fails if any method body references `WriteAsync` (scan its source file).\n- [ ] **Browse parity:** `@tags` plus every program listing through both enumerators decode to identical `AbCipDiscoveredTag` sets (order-insensitive). Every `TemplateInstanceId` seen decodes to an identical `AbCipUdtShape` through both template readers.\n- [ ] **Read parity:** for each selected tag, under both transports:\n - Run `InitializeAsync` + `ReadAsync`.\n - Assert `GetStatus()` is equal, **except** that native `TypeMismatch` where libplctag says `Ok` is reported as a distinct *expected divergence* (spec \u00a75), listed in the output and not failed.\n - Assert the raw buffers are equal. To expose the raw bytes, add `internal ReadOnlyMemory RawBuffer` to both runtimes: libplctag's via `_tag.GetBuffer()`, native's via its stored data. Add `InternalsVisibleTo` for the integration test assembly in the AbCip csproj.\n - Assert the decoded values are equal.\n- [ ] **Connection discipline (review finding 9):**\n - Before any native work, the harness acquires one `DeviceSessionPool.Shared.Acquire(key, options)` **anchor lease** for the target, and holds it until the run ends. Per-tag native runtimes therefore share that one connection: the pool never reaches refcount 0, so it never sends Forward Close and re-opens between tags.\n - libplctag runtimes and native runtimes are never alive at the same time. All libplctag reads run first, and their handles are disposed. Then all native reads run. So the press sees at most two sessions over the run, never concurrently.\n - With `AB_DIFF_COUNT_OPENS=1`, the native side asserts it made exactly **one** Forward Open over the whole run, using `CipConnection.ForwardOpenCount` from Task 9 (reset it at the start of the run).\n- [ ] Output: a markdown table (tag, type, libplctag status, native status, equal?) written to `TestContext.Current.TestOutputHelper`.\n- [ ] Results are appended under the gate-results doc's `## Differential vs ab_server (T18)` heading only.\n- [ ] Run against `ab_server` (`controllogix` profile) with `AB_DIFF_ENDPOINT=10.100.0.35:44818` \u21d2 zero unexpected divergences. Append the results to the gate-results doc.\n- [ ] **Ordering:** blocked by Task 17, so any transport fixes found by the ab_server suites land before the differential run.\n\n**Verify:** `AB_DIFF_ENDPOINT=10.100.0.35:44818 dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests --filter \"FullyQualifiedName~Differential\"` \u2192 pass with 0 unexpected divergences; without the env var \u2192 all skipped.\n\nSteps + code: docs/plans/2026-10-07-native-ab-transport.md \u00a7 Task 18.\n\n```json:metadata\n{\"files\": [\"ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs\", \"src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj\", \"tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/DifferentialTarget.cs\", \"tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Differential/TransportDifferentialTests.cs\"], \"verifyCommand\": \"AB_DIFF_ENDPOINT=10.100.0.35:44818 dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests --filter \\\"FullyQualifiedName~Differential\\\"\", \"modelTier\": \"frontier\", \"model\": \"opus\", \"acceptanceCriteriaCount\": 9}\n```", "blockedBy": [ 17