diff --git a/CLAUDE.md b/CLAUDE.md index 1aec5946..72e3783d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -319,11 +319,12 @@ When the section is disabled the applier binds the no-op `NullHistorianProvision run. Every dispatch logs a tally (`dispatched/requested/ensured/skipped/failed`); `dispatched=N` with `requested=0` flags the dormant no-op. The API key must carry `historian:tags:write` for `EnsureTags`. (PR #423 shipped `GatewayTagProvisioner` but left this wiring out, so provisioning was dormant; restored 2026-06-27.) -**0.2.0 DataType delta (archreview 06/U-7):** `EnsureTags` is an **upsert** (create-or-update, not -skip-if-exists), and the 0.2.0 `HistorianTagDefinition.DataType` proto3-optional change means Boolean tags now -provision as **Int1** (0.1.0 silently defaulted them to Float). A pre-bump Float Boolean is asked to retype -on its next deploy; the AVEVA-side retype policy is pinned by the live retype probe — see the "0.2.0 DataType -provisioning delta" section in `docs/Historian.md`. +**DataType mapping (issue #441):** `EnsureTags` is an **upsert** (create-or-update, not skip-if-exists). +`HistorianTypeMapper` maps **`Boolean → Int2`** (Boolean historizes as a small 0/1 integer) because the +historian's `Int1` analog-tag creation path is **server-degenerate** — `EnsureTags(Int1)` stores an unusable +stub (live-probed; gateway-side resolution in HistorianGateway #11 / PR #16). Both Float and Int2 are +supported analog types, so a pre-existing Float Boolean tag retypes cleanly to Int2 on its next deploy — see +the "Boolean historization maps to Int2" section in `docs/Historian.md`. ### Gateway-side prerequisites @@ -358,7 +359,7 @@ export HISTGW_GATEWAY_ENDPOINT=https://wonder-sql-vd03:5222 # absolute gateway export HISTGW_GATEWAY_APIKEY=histgw__ # must carry historian:read + historian:write (+ tags:write) scopes export HISTGW_TEST_TAG= # read round-trip export HISTGW_WRITE_SANDBOX_TAG= # e.g. HistGW.LiveTest.Sandbox — write round-trip (EnsureTags + write) -export HISTGW_BOOL_SANDBOX_TAG= # e.g. HistGW.LiveTest.BoolSandbox — 0.2.0 Boolean EnsureTags leg + retype probe (06/U-7) +export HISTGW_BOOL_SANDBOX_TAG= # e.g. HistGW.LiveTest.BoolSandbox — Boolean→Int2 EnsureTags leg + retype probe (issue #441) export HISTGW_ALARM_SOURCE= # alarm SendEvent → ReadEvents round-trip dotnet test --filter "Category=LiveIntegration" ``` diff --git a/archreview/plans/STATUS.md b/archreview/plans/STATUS.md index 87ba8247..1d9581c4 100644 --- a/archreview/plans/STATUS.md +++ b/archreview/plans/STATUS.md @@ -327,24 +327,31 @@ convention) + a regression unit test + `StorageRateMs` on the live-suite inline - ✅ `Write_then_read_on_sandbox_tag` — `EnsureTags(Float)` + `WriteLiveValues` (SQL) + read-back. - ✅ `Alarm_SendEvent_is_acked` + ✅ `Alarm_SendEvent_then_ReadEvents` — alarm `SendEvent` + SQL `ReadEvents`. - ✅ `EnsureTags_type_change_outcome_is_observable` — now observes a **real** retype outcome (not a storage-rate error). -- ❌ `EnsureTags_boolean_sandbox_provisions_as_Int1` — **Int1 (Boolean) tag creation is not supported by the - gateway's histsdk on this historian**: `EnsureTags(Int1)` returns `ProtocolEvidenceMissingException` (the vendored - client's "no capture evidence for this type" guard → gRPC `Unimplemented`), both fresh and as a retype target. - A **type sweep** confirmed it is Int1-specific: **Int2/UInt2/Int4/UInt4/Float/Double all provision `success=True`; - only Int1 fails.** Root cause: `histsdk` `HistorianTagWriteProtocol.GetAnalogDataTypeCode` has no captured - CTagMetadata wire code for Int1. This is a **gateway/histsdk type-support gap**, not an OtOpcUa defect — **filed as - [historiangw#11](https://gitea.dohertylan.com/dohertj2/historiangw/issues/11)**. +- ⚠️ `EnsureTags_boolean_sandbox_provisions_as_Int1` (as originally written) — **Int1 (Boolean) tag creation is not + supported by the gateway's histsdk on this historian**: `EnsureTags(Int1)` returns `ProtocolEvidenceMissingException` + (the vendored client's "no capture evidence for this type" guard → gRPC `Unimplemented`), both fresh and as a retype + target. A **type sweep** confirmed it is Int1-specific: **Int2/UInt2/Int4/UInt4/Float/Double all provision + `success=True`; only Int1 fails.** Root cause: `histsdk` `HistorianTagWriteProtocol.GetAnalogDataTypeCode` has no + captured CTagMetadata wire code for Int1 — a **gateway/histsdk type-support gap**, not an OtOpcUa defect + (**filed as [historiangw#11](https://gitea.dohertylan.com/dohertj2/historiangw/issues/11)**, which routed the + decision back as **[issue #441](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/441)**). + + **✅ RESOLVED (issue #441, 2026-07-14):** `HistorianTypeMapper` now maps **`Boolean → Int2`** (0/1 integer) instead + of the degenerate `Int1`. Int2 is a proven-creatable analog type on this historian, and the value-write path already + sends every value as a `double` on `WriteLiveValues` regardless of tag type, so Boolean `0.0/1.0` round-trips + cleanly. The live gate test is renamed `EnsureTags_boolean_sandbox_provisions_as_Int2` and now passes → **6/6**. Unit + pins updated (`HistorianTypeMapperTests`, `GatewayTagProvisionerTests.Boolean_definition_carries_explicit_Int2_presence`); + gateway driver unit `103/103`. **✅ U-7 retype policy — ANSWERED (via `grpcurl` against the real historian):** - `EnsureTags` is a **real create-or-update upsert** — re-`EnsureTags` of an existing tag at the same type → `success`. - An **in-place retype between SUPPORTED types is ACCEPTED** — `Float→Double` on an existing tag returns `success` (of the three 06/U-7 hypotheses: **accepted-retyped**, not rejected). -- **But the specific 0.2.0 Boolean case (Float→Int1) can't retype** — Int1 isn't a creatable type here, so a - pre-0.2.0 **Float Boolean tag asked to become Int1 fails the provision** with `ProtocolEvidenceMissingException` - — it is **neither silently retyped nor data-lost**; the provision just fails (and is metered `failed=N`). That is - the practically-relevant U-7 answer for Boolean tags on this deployment. +- **The specific `Int1` case can't retype** — Int1 isn't a creatable type here. Rather than accept a broken Boolean + path, OtOpcUa **remapped `Boolean → Int2`** (issue #441, above), which retypes cleanly from a pre-existing Float + Boolean tag (both are supported analog types) and never hits the Int1 gap. -The R2-06 **code** deliverables (S-11 fail-fast + `ServerHistorianOptionsValidator`; U-7 Boolean→Int1 mapping + +The R2-06 **code** deliverables (S-11 fail-fast + `ServerHistorianOptionsValidator`; Boolean provisioning mapping + upsert; C-7 doc) were already unit-covered + merged; this live gate additionally **found + fixed the StorageRateMs -provisioning bug** and **answered the U-7 retype policy**. The one remaining red test is a documented gateway -histsdk Int1-support gap. +provisioning bug**, **answered the U-7 retype policy**, and surfaced the Int1 histsdk gap that drove the +**Boolean→Int2 remap (issue #441)** — closing the last red test to reach **6/6**. diff --git a/docs/Historian.md b/docs/Historian.md index ab252622..566f82c7 100644 --- a/docs/Historian.md +++ b/docs/Historian.md @@ -141,48 +141,44 @@ for this node), which is the signal to check that `ServerHistorian:Enabled=true` > so a HistoryRead on it runs to the full `CallTimeout` (~30 s) before erroring. Auto-provisioning on deploy > keeps freshly-historized tags from hitting that slow path. See the `CallTimeout` row above. -### 0.2.0 DataType provisioning delta (archreview 06/U-7) +### Boolean historization maps to Int2 (issue #441) -`EnsureTags` is contract-documented (HistorianGateway 0.2.0) as an **upsert** — *"creates or updates +`EnsureTags` is contract-documented (HistorianGateway 0.2.0+) as an **upsert** — *"creates or updates historian tags according to the given definitions"* — that maps per-tag onto AVEVA's `HistorianClient.EnsureTagAsync(definition) -> bool`. It is **not idempotent** (not routed through the idempotent retry pipeline) and **not skip-if-exists**. -The 0.1.0→0.2.0 client bump made `HistorianTagDefinition.DataType` **proto3-optional** (explicit presence). -Under 0.1.0, the provisioner's `Boolean → Int1` mapping sent wire value `0`, indistinguishable from unset, so -the gateway defaulted such tags to the SDK-default **Float**. Under 0.2.0 the same call carries explicit -presence, so Boolean tags now provision as **Int1** (the pre-existing intent of `HistorianTypeMapper` — no -mapper code change is needed or wanted). Consequences: +`HistorianTypeMapper` maps **`DriverDataType.Boolean → HistorianDataType.Int2`** and Boolean values +historize as a small **0/1 integer**. Int2 is used rather than the semantically-exact `Int1` because the +historian's **`Int1` analog-tag creation path is server-degenerate** — `EnsureTags(Int1)` stores an +unusable stub, so a Boolean tag mapped to `Int1` never becomes historizable. This is an evidence-based +decision from a live probe against the deployed 2023 R2 historian; the root cause and gateway-side +resolution are tracked in [issue #441](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/441) and +HistorianGateway [#11](https://gitea.dohertylan.com/dohertj2/historiangw/issues/11) / PR #16 (which +documents `Int1` as an explicit, evidence-based exclusion). The value-write path is unaffected: the +recorder sends every value as a `double` on `WriteLiveValues` regardless of tag type, so a Boolean +`0.0/1.0` round-trips cleanly into the Int2 tag. -1. **New Boolean tags provision correctly as Int1.** This is the fix arriving as a silent, zero-diff - semantic change from the bump. -2. **Pre-bump Boolean tags exist historian-side as Float.** On the next deploy that dispatches provisioning - for such a tag, the gateway is asked (via the upsert) to update it to Int1. +**Observed live (2026-07-13, real 0.2.0 gateway → real AVEVA historian on `wonder-sql-vd03`, probed via +`grpcurl` — archreview R2-06 T12):** +- `EnsureTags` is a **real create-or-update upsert** — re-`EnsureTags` of an existing tag at the same type + returns `Success=true`. +- An **in-place retype between SUPPORTED types is ACCEPTED** — `Float→Double` on an existing tag returns + `Success=true`. +- A type sweep confirmed **`Int2/UInt2/Int4/UInt4/Float/Double` all provision `Success=true`; only `Int1` + fails** (`Success=false, Error="ProtocolEvidenceMissingException"` — the vendored client's "no capture + evidence for this type" guard → gRPC `Unimplemented`), both for a fresh tag and as a retype target. Mapping + Boolean to `Int2` sidesteps that gap entirely. - **Observed live (2026-07-13, real 0.2.0 gateway → real AVEVA historian on `wonder-sql-vd03`, probed via - `grpcurl` — archreview R2-06 T12):** - - `EnsureTags` is a **real create-or-update upsert** — re-`EnsureTags` of an existing tag at the same type - returns `Success=true`. - - An **in-place retype between SUPPORTED types is ACCEPTED** — `Float→Double` on an existing tag returns - `Success=true` (hypothesis **(ii) accepted as a plain retype**, not rejected). - - **BUT `Int1` — the 0.2.0 Boolean target — is *not a creatable type* on this historian's histsdk:** - `EnsureTags(Int1)` returns `Success=false, Error="ProtocolEvidenceMissingException"` (the vendored client's - "no capture evidence for this type" guard → gRPC `Unimplemented`), both for a fresh tag and as a retype - target. A type sweep confirmed it is **Int1-specific**: `Int2/UInt2/Int4/UInt4/Float/Double` all provision - `Success=true`; only `Int1` fails. - - **Net for a pre-bump Float Boolean tag asked to become Int1:** the provision **fails** (per-tag - `Success=false` → `failed=N` tally). It is **neither silently retyped nor data-lost** — the tag stays Float - and remains readable; the Int1 provision simply doesn't land until the gateway's histsdk gains `Int1` - create-evidence — filed as - [historiangw#11](https://gitea.dohertylan.com/dohertj2/historiangw/issues/11). (Prerequisite for reaching this - outcome at all: the provisioner must stamp a **positive - `StorageRateMs`** — a `0` made `EnsureTags` throw `ArgumentOutOfRangeException` and fail *every* provision; - fixed in `GatewayTagProvisioner`, PR #439.) -3. **The failure mode is bounded and observable either way.** Provisioning is fire-and-forget and never - blocks/fails a deploy; a per-tag rejection surfaces only as the tally's `failed` count. Values written to - a still-Float tag by the recorder remain readable (Float supersets Int1's 0/1 range); nothing is lost - pending a reconcile. A bulk retype sweep is **not** done in code — assess via the probe first, then decide - whether reconciliation is a one-time operator action. +> **Prerequisite for provisioning to land at all:** the provisioner must stamp a **positive `StorageRateMs`** +> — a `0` made `EnsureTags` throw `ArgumentOutOfRangeException` and fail *every* provision (fixed in +> `GatewayTagProvisioner`, PR #439). + +**Migration note for pre-existing Boolean tags.** A Boolean tag historian-side as Float (e.g. from the +0.1.0-era default) is asked, on its next deploy, to upsert to Int2. Both are supported analog types, so the +retype is accepted. Provisioning is fire-and-forget and never blocks/fails a deploy; a per-tag rejection (if +any) surfaces only as the tally's `failed` count, and values written to a still-Float tag remain readable +(Float supersets the 0/1 range) pending reconcile. A bulk retype sweep is **not** done in code. --- @@ -407,11 +403,11 @@ The gateway-backed driver also ships an env-gated live suite (`Category=LiveInte `HISTGW_GATEWAY_ENDPOINT` / `HISTGW_GATEWAY_APIKEY` / `HISTGW_TEST_TAG` / `HISTGW_WRITE_SANDBOX_TAG` / `HISTGW_BOOL_SANDBOX_TAG` / `HISTGW_ALARM_SOURCE` env vars (it skips cleanly when they are absent). `HISTGW_BOOL_SANDBOX_TAG` (e.g. `HistGW.LiveTest.BoolSandbox`, a **dedicated** writable Boolean tag — -never the shared Float write sandbox) drives the 0.2.0 Boolean `EnsureTags` leg -(`EnsureTags_boolean_sandbox_provisions_as_Int1`) and the retype probe -(`EnsureTags_type_change_outcome_is_observable`) that resolve the -[0.2.0 DataType provisioning delta](#020-datatype-provisioning-delta-archreview-06u-7) open question. Running -the full suite against a **0.2.0** gateway (all six tests, none skipped) also discharges 06/U-2's +never the shared Float write sandbox) drives the Boolean historization leg +(`EnsureTags_boolean_sandbox_provisions_as_Int2`) and the retype probe +(`EnsureTags_type_change_outcome_is_observable`) that back the +[Boolean historization maps to Int2](#boolean-historization-maps-to-int2-issue-441) decision. Running +the full suite against the gateway (all six tests, none skipped) also discharges 06/U-2's "full documented run" for the historian suite. See [AlarmHistorian.md](AlarmHistorian.md) for the alarm write path and diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs index 57b7b76d..fdd8909c 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs @@ -9,11 +9,14 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; /// /// /// Only the nine numeric types are historizable on the gateway's analog write path. Two of them -/// fall back to a wider historian type because the narrower one's write path is deferred upstream: -/// maps to (the historian's -/// UInt2 write path is not proven). String / DateTime / Reference are not historized in v1 -/// and throw ; callers that want to skip them without catching an -/// exception should consult first. +/// fall back to a wider/adjacent historian type because the exact one's write path is not usable +/// upstream: maps to (the +/// historian's UInt2 write path is not proven), and maps +/// to — the historian's Int1 analog-tag creation path is +/// server-degenerate (issue #441 / HistorianGateway #11: EnsureTags(Int1) stores an unusable +/// stub), so Boolean historizes as a small 0/1 integer instead. String / DateTime / Reference are not +/// historized in v1 and throw ; callers that want to skip them +/// without catching an exception should consult first. /// internal static class HistorianTypeMapper { @@ -25,7 +28,8 @@ internal static class HistorianTypeMapper /// public static HistorianDataType ToHistorianDataType(DriverDataType dataType) => dataType switch { - DriverDataType.Boolean => HistorianDataType.Int1, + DriverDataType.Boolean => HistorianDataType.Int2, // Int1 analog-tag creation is server-degenerate (issue #441) → store 0/1 + DriverDataType.Int16 => HistorianDataType.Int2, DriverDataType.Int32 => HistorianDataType.Int4, DriverDataType.Int64 => HistorianDataType.Int8, diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs index 698d766c..b03a9af9 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs @@ -26,24 +26,23 @@ public sealed class GatewayTagProvisionerTests Assert.NotNull(fake.LastEnsureDefinitions); Assert.Equal(2, fake.LastEnsureDefinitions!.Count); Assert.Equal(HistorianDataType.Float, fake.LastEnsureDefinitions[0].DataType); - Assert.Equal(HistorianDataType.Int1, fake.LastEnsureDefinitions[1].DataType); + Assert.Equal(HistorianDataType.Int2, fake.LastEnsureDefinitions[1].DataType); Assert.Equal(2, result.Requested); Assert.Equal(0, result.Skipped); } /// - /// archreview 06/U-7 — pins the 0.2.0 Boolean→Int1 provisioning semantics at TWO levels. The - /// 0.1.0→0.2.0 client bump made HistorianTagDefinition.DataType proto3-optional (explicit - /// presence): under 0.1.0 the provisioner's Int1 (wire 0) was indistinguishable from unset, - /// so the gateway defaulted Boolean tags to Float; under 0.2.0 the same call carries explicit - /// presence and provisions Int1 (the intended behavior). Asserting BOTH the mapped type AND - /// HasDataType compile-pins the package floor (the HasDataType/ClearDataType - /// members exist only on the 0.2.0 proto3-optional contract — a downgrade breaks the build) and - /// assert-pins that the provisioner always sends explicit presence (a future contract change that - /// stopped sending it would break the assert). This is a pin, not a fix — it passes on current code. + /// Pins the Boolean→Int2 provisioning semantics at TWO levels. Boolean maps to Int2 because + /// the historian's Int1 analog-tag creation path is server-degenerate — EnsureTags(Int1) + /// stores an unusable stub (issue #441 / HistorianGateway #11) — so Boolean historizes as a small 0/1 + /// integer. Asserting BOTH the mapped type AND HasDataType compile-pins the package floor (the + /// HasDataType/ClearDataType members exist only on the 0.2.0+ proto3-optional contract — + /// a downgrade breaks the build) and assert-pins that the provisioner always sends explicit presence + /// (a future contract change that stopped sending it would default the tag to the SDK's Float and + /// break this assert). This is a pin, not a fix — it passes on current code. /// [Fact] - public async Task Boolean_definition_carries_explicit_Int1_presence() + public async Task Boolean_definition_carries_explicit_Int2_presence() { var fake = new FakeHistorianGatewayClient { EnsureTagsResult = new TagOperationResults() }; var p = Provisioner(fake); @@ -54,9 +53,9 @@ public sealed class GatewayTagProvisionerTests var defs = fake.LastEnsureDefinitions!; Assert.Single(defs); - Assert.Equal(HistorianDataType.Int1, defs[0].DataType); - // 0.2.0 proto3-optional presence witness — explicit presence is what flips the gateway from its - // SDK-default Float to the requested Int1. Without this, a Boolean tag would provision as Float. + Assert.Equal(HistorianDataType.Int2, defs[0].DataType); + // proto3-optional presence witness — explicit presence is what flips the gateway from its + // SDK-default Float to the requested Int2. Without this, a Boolean tag would provision as Float. Assert.True(defs[0].HasDataType); } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs index 484687d2..009f8dfd 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs @@ -255,25 +255,26 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC } /// - /// archreview 06/U-7 — Boolean provisioning leg on the 0.2.0 contract. Through the real + /// issue #441 — Boolean historization leg. Through the real /// , EnsureTags a - /// request for the dedicated Boolean sandbox tag (mapped to Int1 with explicit proto3 - /// presence), assert it is ensured (not failed), then WriteLiveValues a 1.0 and read - /// it back over a wide window. This retires "the live EnsureTags leg has never run on 0.2.0" for - /// the Boolean path. The ±12h read window + timezone caveat mirror . + /// request for the dedicated Boolean sandbox tag (mapped to Int2 — the historian's + /// Int1 analog-tag creation path is server-degenerate, see issue #441 / HistorianGateway + /// #11, so Boolean historizes as a small 0/1 integer), assert it is ensured (not failed), then + /// WriteLiveValues a 1.0 and read it back over a wide window. The ±12h read window + /// + timezone caveat mirror . /// [Fact] [Trait("Category", "LiveIntegration")] - public async Task EnsureTags_boolean_sandbox_provisions_as_Int1() + public async Task EnsureTags_boolean_sandbox_provisions_as_Int2() { if (_fx.NotConfigured) Assert.Skip(_fx.SkipReason!); if (_fx.BoolSandboxTag is null) - Assert.Skip("Skipped: set HISTGW_BOOL_SANDBOX_TAG to a writable Boolean sandbox tag (e.g. HistGW.LiveTest.BoolSandbox) to run the 0.2.0 Boolean EnsureTags leg."); + Assert.Skip("Skipped: set HISTGW_BOOL_SANDBOX_TAG to a writable Boolean sandbox tag (e.g. HistGW.LiveTest.BoolSandbox) to run the Boolean historization leg."); var ct = TestContext.Current.CancellationToken; var tag = _fx.BoolSandboxTag; - // EnsureTags (Boolean → Int1) through the REAL provisioner seam (the same code path deploys use). + // EnsureTags (Boolean → Int2) through the REAL provisioner seam (the same code path deploys use). await using var writeClient = _fx.CreateClient(); var provisioner = new GatewayTagProvisioner(writeClient, NullLogger.Instance); var provision = await provisioner.EnsureTagsAsync( @@ -281,7 +282,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC ct); provision.Ensured.ShouldBe(1, - "the Boolean sandbox tag must be ensured (needs the gateway on 0.2.0 contracts + an API key carrying historian:tags:write)."); + "the Boolean sandbox tag must be ensured as Int2 (needs the gateway on 0.2.0+ contracts + an API key carrying historian:tags:write)."); provision.Failed.ShouldBe(0); // WriteLiveValues a Boolean-as-1.0 and read it back (SQL live path can lag a flush cadence → poll). @@ -308,14 +309,16 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC hit.ShouldNotBeNull($"the written Boolean sample ({written}) should read back from '{tag}'."); TestContext.Current.SendDiagnosticMessage( - $"Boolean round-trip: EnsureTags(Int1) + WriteLiveValues '{tag}'={written} → read back at {hit!.SourceTimestampUtc:O}."); + $"Boolean round-trip: EnsureTags(Int2) + WriteLiveValues '{tag}'={written} → read back at {hit!.SourceTimestampUtc:O}."); } /// /// archreview 06/U-7 retype probe — answers the assessment note's open question: what does the /// deployed AVEVA Historian do when EnsureTags (an upsert) is asked to change an /// existing tag's type? Against the dedicated Boolean sandbox tag only (never the shared - /// Float write sandbox), EnsureTags first as Float then as Int1 and assert the second + /// Float write sandbox), EnsureTags first as Float then as Int2 (both supported + /// analog types — Int2 is the type Boolean now historizes as; the previously-probed + /// Int1 is a documented server-degenerate exclusion, issue #441) and assert the second /// call's per-tag outcome is deterministic and observable — either Success=true /// (retype / tag-versioning accepted) or Success=false with a non-empty Error (mapped /// to the provisioner's failed tally). The assertion accepts both because the contract @@ -336,7 +339,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC await using var client = _fx.CreateClient(); - // First establish the tag as Float, then ask EnsureTags to retype it to Int1 (Boolean). + // First establish the tag as Float, then ask EnsureTags to retype it to Int2. await client.EnsureTagsAsync( new[] { @@ -357,9 +360,9 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC new HistorianTagDefinition { TagName = tag, - DataType = HistorianDataType.Int1, + DataType = HistorianDataType.Int2, EngineeringUnit = string.Empty, - Description = "OtOpcUa live validation retype probe (stage 2: Int1)", + Description = "OtOpcUa live validation retype probe (stage 2: Int2)", StorageRateMs = 1000, // 0 makes the gateway's EnsureTags throw (storageRateMs must be > 0). }, }, @@ -372,7 +375,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC "or Success=false with a non-empty Error (mapped to the provisioner's failed tally)."); TestContext.Current.SendDiagnosticMessage( - $"retype probe '{tag}' Float→Int1: Success={outcome.Success}, Error='{outcome.Error}'. " + + $"retype probe '{tag}' Float→Int2: Success={outcome.Success}, Error='{outcome.Error}'. " + "(records the deployed historian's in-place analog-retype policy — feed this back into docs/Historian.md)."); } } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/HistorianTypeMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/HistorianTypeMapperTests.cs index 1dde2678..a5c581d8 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/HistorianTypeMapperTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/HistorianTypeMapperTests.cs @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping; public sealed class HistorianTypeMapperTests { [Theory] - [InlineData(DriverDataType.Boolean, HistorianDataType.Int1)] + [InlineData(DriverDataType.Boolean, HistorianDataType.Int2)] // Int1 analog-tag creation is server-degenerate (issue #441) → store 0/1 [InlineData(DriverDataType.Int16, HistorianDataType.Int2)] [InlineData(DriverDataType.Int32, HistorianDataType.Int4)] [InlineData(DriverDataType.Int64, HistorianDataType.Int8)]