diff --git a/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json b/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json
index 2bf733da..54febf58 100644
--- a/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json
+++ b/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json
@@ -57,7 +57,7 @@
{
"id": "T8",
"subject": "U-7 live leg: HISTGW_BOOL_SANDBOX_TAG in GatewayLiveFixture + EnsureTags_boolean_sandbox_provisions_as_Int1 + EnsureTags_type_change_outcome_is_observable retype probe (skip-clean offline; verify all-skip run)",
- "status": "pending",
+ "status": "completed",
"blockedBy": [
"T7"
]
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveFixture.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveFixture.cs
index eda48b62..25caa3e8 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveFixture.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveFixture.cs
@@ -22,6 +22,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Live;
/// - HISTGW_GATEWAY_APIKEY — the histgw_<id>_<secret> key (must carry historian:read + historian:write scopes). Required; absent ⇒ all tests skip.
/// - HISTGW_TEST_TAG — an existing Galaxy / historian tag for the read round-trip.
/// - HISTGW_WRITE_SANDBOX_TAG — a Float sandbox tag the write round-trip may EnsureTags + write (e.g. HistGW.LiveTest.Sandbox).
+/// - HISTGW_BOOL_SANDBOX_TAG — a writable Boolean sandbox tag (e.g. HistGW.LiveTest.BoolSandbox) for the 0.2.0 Boolean EnsureTags leg + the retype probe (archreview 06/U-7). Dedicated — never the shared write sandbox.
/// - HISTGW_ALARM_SOURCE — a source name for the alarm SendEvent → ReadEvents round-trip.
/// - HISTGW_GATEWAY_ALLOW_UNTRUSTED — true to accept a self-signed dev cert (optional).
///
@@ -44,6 +45,7 @@ public sealed class GatewayLiveFixture
private const string EnvApiKey = "HISTGW_GATEWAY_APIKEY";
private const string EnvTestTag = "HISTGW_TEST_TAG";
private const string EnvWriteSandboxTag = "HISTGW_WRITE_SANDBOX_TAG";
+ private const string EnvBoolSandboxTag = "HISTGW_BOOL_SANDBOX_TAG";
private const string EnvAlarmSource = "HISTGW_ALARM_SOURCE";
private const string EnvAllowUntrusted = "HISTGW_GATEWAY_ALLOW_UNTRUSTED";
@@ -68,6 +70,7 @@ public sealed class GatewayLiveFixture
_apiKey = Trimmed(EnvApiKey);
TestTag = Trimmed(EnvTestTag);
WriteSandboxTag = Trimmed(EnvWriteSandboxTag);
+ BoolSandboxTag = Trimmed(EnvBoolSandboxTag);
AlarmSource = Trimmed(EnvAlarmSource);
_allowUntrusted = string.Equals(
Trimmed(EnvAllowUntrusted), "true", StringComparison.OrdinalIgnoreCase);
@@ -105,6 +108,10 @@ public sealed class GatewayLiveFixture
/// The Float sandbox tag for the write round-trip (HISTGW_WRITE_SANDBOX_TAG); null when unset.
public string? WriteSandboxTag { get; }
+ /// The dedicated Boolean sandbox tag for the 0.2.0 Boolean EnsureTags leg + retype probe
+ /// (HISTGW_BOOL_SANDBOX_TAG, archreview 06/U-7); null when unset.
+ public string? BoolSandboxTag { get; }
+
/// The source name for the alarm round-trip (HISTGW_ALARM_SOURCE); null when unset.
public string? AlarmSource { get; }
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 8479aaa1..96260f1d 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
@@ -253,4 +253,124 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC
HistorianWriteOutcome.Ack,
"the alarm SendEvent must be acked (the AlarmEventMapper must NOT set the wire event Id — the gateway rejects a client-supplied id).");
}
+
+ ///
+ /// archreview 06/U-7 — Boolean provisioning leg on the 0.2.0 contract. 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 .
+ ///
+ [Fact]
+ [Trait("Category", "LiveIntegration")]
+ public async Task EnsureTags_boolean_sandbox_provisions_as_Int1()
+ {
+ 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.");
+
+ var ct = TestContext.Current.CancellationToken;
+ var tag = _fx.BoolSandboxTag;
+
+ // EnsureTags (Boolean → Int1) 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(
+ new[] { new HistorianTagProvisionRequest(tag, DriverDataType.Boolean, null, "OtOpcUa live validation Boolean sandbox") },
+ 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).");
+ provision.Failed.ShouldBe(0);
+
+ // WriteLiveValues a Boolean-as-1.0 and read it back (SQL live path can lag a flush cadence → poll).
+ const ushort goodQuality = 192; // OPC-DA "Good" floor.
+ var writeUtc = DateTime.UtcNow;
+ const double written = 1.0;
+ var valueWriter = new GatewayHistorianValueWriter(writeClient, NullLogger.Instance);
+ var acked = await valueWriter.WriteLiveValuesAsync(
+ tag, new[] { new HistorizationValue(writeUtc, written, goodQuality) }, ct);
+ acked.ShouldBeTrue("the Boolean live write must be acked (gateway RuntimeDb:Enabled=true + EnsureTags-provisioned tag).");
+
+ await using var dataSource = _fx.CreateDataSource();
+ DataValueSnapshot? hit = null;
+ var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(15);
+ do
+ {
+ var read = await dataSource.ReadRawAsync(
+ tag, writeUtc - TimeSpan.FromHours(12), writeUtc + TimeSpan.FromHours(12), maxValuesPerNode: 50_000, ct);
+ hit = read.Samples.FirstOrDefault(s => s.Value is double d && Math.Abs(d - written) < 0.5);
+ if (hit is not null) break;
+ await Task.Delay(TimeSpan.FromSeconds(1), ct);
+ }
+ while (DateTime.UtcNow < deadline);
+
+ 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}.");
+ }
+
+ ///
+ /// 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
+ /// 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
+ /// guarantees only the observability, not the policy; the observed behavior is emitted as a
+ /// diagnostic so the run documents which of accepted-versioned / accepted-retyped / rejected the
+ /// historian exhibits (fed back into docs/Historian.md after the first live run).
+ ///
+ [Fact]
+ [Trait("Category", "LiveIntegration")]
+ public async Task EnsureTags_type_change_outcome_is_observable()
+ {
+ if (_fx.NotConfigured) Assert.Skip(_fx.SkipReason!);
+ if (_fx.BoolSandboxTag is null)
+ Assert.Skip("Skipped: set HISTGW_BOOL_SANDBOX_TAG to a dedicated Boolean sandbox tag to run the retype probe.");
+
+ var ct = TestContext.Current.CancellationToken;
+ var tag = _fx.BoolSandboxTag;
+
+ await using var client = _fx.CreateClient();
+
+ // First establish the tag as Float, then ask EnsureTags to retype it to Int1 (Boolean).
+ await client.EnsureTagsAsync(
+ new[]
+ {
+ new HistorianTagDefinition
+ {
+ TagName = tag,
+ DataType = HistorianDataType.Float,
+ EngineeringUnit = string.Empty,
+ Description = "OtOpcUa live validation retype probe (stage 1: Float)",
+ },
+ },
+ ct);
+
+ var retype = await client.EnsureTagsAsync(
+ new[]
+ {
+ new HistorianTagDefinition
+ {
+ TagName = tag,
+ DataType = HistorianDataType.Int1,
+ EngineeringUnit = string.Empty,
+ Description = "OtOpcUa live validation retype probe (stage 2: Int1)",
+ },
+ },
+ ct);
+
+ var outcome = retype.Results.ShouldHaveSingleItem();
+ var observable = outcome.Success || !string.IsNullOrEmpty(outcome.Error);
+ observable.ShouldBeTrue(
+ "the retype outcome must be deterministic + observable: either Success=true (retype/version accepted) " +
+ "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}'. " +
+ "(records the deployed historian's in-place analog-retype policy — feed this back into docs/Historian.md).");
+ }
}