fix(r2-06): GREEN adapter guard — TryCreate + named InvalidOperationException on bad ServerHistorian:Endpoint (06/S-11)

This commit is contained in:
Joseph Doherty
2026-07-13 09:52:07 -04:00
parent 3a049a13a4
commit 3108abc13b
2 changed files with 52 additions and 14 deletions
@@ -4,43 +4,53 @@
"tasks": [
{
"id": "T1",
"subject": "RED: misconfig repro test HistorianGatewayClientAdapter.Create with empty/malformed Endpoint must throw a named InvalidOperationException (currently UriFormatException; test MUST fail on current code)",
"subject": "RED: misconfig repro test \u2014 HistorianGatewayClientAdapter.Create with empty/malformed Endpoint must throw a named InvalidOperationException (currently UriFormatException; test MUST fail on current code)",
"status": "completed",
"blockedBy": []
},
{
"id": "T2",
"subject": "GREEN: adapter guard Uri.TryCreate + InvalidOperationException naming ServerHistorian:Endpoint in HistorianGatewayClientAdapter.Create (defense in depth)",
"status": "pending",
"blockedBy": ["T1"]
"subject": "GREEN: adapter guard \u2014 Uri.TryCreate + InvalidOperationException naming ServerHistorian:Endpoint in HistorianGatewayClientAdapter.Create (defense in depth)",
"status": "completed",
"blockedBy": [
"T1"
]
},
{
"id": "T3",
"subject": "RED: ServerHistorianOptionsValidator unit tests (7 cases incl. the AlarmHistorian-enabled/ServerHistorian-disabled corner naming both sections)",
"status": "pending",
"blockedBy": ["T1"]
"blockedBy": [
"T1"
]
},
{
"id": "T4",
"subject": "GREEN: implement ServerHistorianOptionsValidator (OptionsValidatorBase, ctor IConfiguration, consumed = Enabled || AlarmHistorian:Enabled, fail on empty/non-absolute/non-http(s) Endpoint)",
"status": "pending",
"blockedBy": ["T3"]
"blockedBy": [
"T3"
]
},
{
"id": "T5",
"subject": "Wiring: AddValidatedOptions<ServerHistorianOptions, ServerHistorianOptionsValidator> in Program.cs hasDriver block + OptionsValidationException wiring test + residual alarm-only-mode startup warning",
"status": "pending",
"blockedBy": ["T4"]
"blockedBy": [
"T4"
]
},
{
"id": "T6",
"subject": "Warning parity: malformed-endpoint warning in ServerHistorianOptions.Validate() + Runtime.Tests case; commit 1 (S-11) after T1-T6 green",
"status": "pending",
"blockedBy": ["T4"]
"blockedBy": [
"T4"
]
},
{
"id": "T7",
"subject": "U-7 pin: GatewayTagProvisionerTests Boolean_definition_carries_explicit_Int1_presence (asserts HistorianDataType.Int1 AND HasDataType 0.2.0 proto3-optional presence witness; pin, no RED phase)",
"subject": "U-7 pin: GatewayTagProvisionerTests Boolean_definition_carries_explicit_Int1_presence (asserts HistorianDataType.Int1 AND HasDataType \u2014 0.2.0 proto3-optional presence witness; pin, no RED phase)",
"status": "pending",
"blockedBy": []
},
@@ -48,13 +58,17 @@
"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",
"blockedBy": ["T7"]
"blockedBy": [
"T7"
]
},
{
"id": "T9",
"subject": "U-7 docs: migration/assessment note (EnsureTags is upsert; pre-bump Float Booleans; three retype hypotheses; failed=N tally observability) in docs/Historian.md + CLAUDE.md HISTGW_BOOL_SANDBOX_TAG recipe update; commit 2 (U-7) after T7-T9",
"status": "pending",
"blockedBy": ["T8"]
"blockedBy": [
"T8"
]
},
{
"id": "T10",
@@ -66,13 +80,20 @@
"id": "T11",
"subject": "Bookkeeping: update archreview/plans/STATUS.md + 06-gateway-integrations.md prior-finding rows for S-11/U-7/C-7 when the work lands",
"status": "pending",
"blockedBy": ["T6", "T9", "T10"]
"blockedBy": [
"T6",
"T9",
"T10"
]
},
{
"id": "T12",
"subject": "OPERATOR GATE: run Category=LiveIntegration on the VPN against a 0.2.0 gateway (all 6 tests, incl. Boolean EnsureTags + retype probe); record the observed retype policy back into docs/Historian.md + STATUS.md; also discharges 06/U-2's full documented run",
"status": "pending",
"blockedBy": ["T9", "T11"]
"blockedBy": [
"T9",
"T11"
]
}
]
}
@@ -35,14 +35,31 @@ public sealed class HistorianGatewayClientAdapter : IHistorianGatewayClient, IDi
/// <param name="options">The bound <c>ServerHistorian</c> configuration (endpoint, key, TLS posture).</param>
/// <param name="loggerFactory">Logger factory threaded into the package client's channel diagnostics.</param>
/// <returns>A ready-to-use adapter whose underlying channel has not yet dialed the gateway.</returns>
/// <exception cref="InvalidOperationException">
/// <see cref="ServerHistorianOptions.Endpoint"/> is empty or not an absolute <c>http(s)</c> URI.
/// Defense in depth (archreview 06/S-11): the Host's <c>ServerHistorianOptionsValidator</c> fails
/// these configs at startup, but any caller that bypasses <c>ValidateOnStart</c> (tests, the live
/// fixture, future non-Host wiring) still gets a named, config-key-carrying error here instead of
/// the raw <see cref="UriFormatException"/> the bare <c>new Uri(...)</c> ctor throws.
/// </exception>
public static HistorianGatewayClientAdapter Create(ServerHistorianOptions options, ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(loggerFactory);
// Fail fast with a named, actionable error rather than letting `new Uri("")` throw a raw
// UriFormatException deep in the factory (archreview 06/S-11). The Endpoint is not a secret,
// so it is safe to echo; the ApiKey is never surfaced.
if (!Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var endpointUri)
|| (endpointUri.Scheme != Uri.UriSchemeHttp && endpointUri.Scheme != Uri.UriSchemeHttps))
{
throw new InvalidOperationException(
$"ServerHistorian:Endpoint must be an absolute http(s) URI (e.g. https://host:5222); got '{options.Endpoint}'.");
}
var clientOptions = new HistorianGatewayClientOptions
{
Endpoint = new Uri(options.Endpoint),
Endpoint = endpointUri,
ApiKey = options.ApiKey,
UseTls = options.UseTls,
CaCertificatePath = options.CaCertificatePath,