diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 7edc3332..fd6ca881 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "main", - "commit": "1d052c3919ca1fdf043747d858dab9b6c384e827", - "sourcePullRequest": 87, - "purpose": "Pins the ARIEC61850 engine used by ARSAS. PR #76 preserves unresolved static DataSet members, PR #77 canonicalizes cross-logical-device SCL references, PR #78 keeps one descriptor per static DataSet member while separating resolved runtime primary leaves from the original FCDA/FCD identity, PR #79 projects generic Boolean status structures to scalar stVal while preserving quality/timestamp, PR #80 normalizes validated DataRef-enabled InformationReport ordering, PR #81 accepts valid zero OptFlds reports while quarantining unmapped canonical report metadata so OptFlds/inclusion/reason fields can never leak into process values, PR #84 routes exact PrimaryValue residuals through dynamic reporting before MMS polling while preventing quality/timestamp descriptors from hijacking the primary-value fallback, PR #85 evaluates negotiated MMS write/DefineNamedVariableList/DeleteNamedVariableList support plus fresh per-RCB DatSet/RptEna/TrgOps/OptFlds/GI/IntgPd/reservation/Owner evidence before automatic dynamic reporting, PR #86 requires explicit per-signal dynamic-attempt/skip evidence before final polling while returning runtime dynamic-attempt failure reasons and best-effort rollback evidence for failed temporary DataSet/RCB activation, and PR #87 restores baseline-safe static precedence by keeping freshly verified populated static RCBs visible to the stable planner while capability qualification remains an additional gate only for empty RCB slots that require dynamic DataSet/RCB mutation." + "commit": "d4612fb7fd5d499a70df17110f6191c452340e0c", + "sourcePullRequest": 88, + "purpose": "Pins the ARIEC61850 engine used by ARSAS. PR #76 preserves unresolved static DataSet members, PR #77 canonicalizes cross-logical-device SCL references, PR #78 keeps one descriptor per static DataSet member while separating resolved runtime primary leaves from the original FCDA/FCD identity, PR #79 projects generic Boolean status structures to scalar stVal while preserving quality/timestamp, PR #80 normalizes validated DataRef-enabled InformationReport ordering, PR #81 accepts valid zero OptFlds reports while quarantining unmapped canonical report metadata so OptFlds/inclusion/reason fields can never leak into process values, PR #84 routes exact PrimaryValue residuals through dynamic reporting before MMS polling while preventing quality/timestamp descriptors from hijacking the primary-value fallback, PR #85 evaluates negotiated MMS write/DefineNamedVariableList/DeleteNamedVariableList support plus fresh per-RCB DatSet/RptEna/TrgOps/OptFlds/GI/IntgPd/reservation/Owner evidence before automatic dynamic reporting, PR #86 requires explicit per-signal dynamic-attempt/skip evidence before final polling while returning runtime dynamic-attempt failure reasons and best-effort rollback evidence for failed temporary DataSet/RCB activation, PR #87 restores baseline-safe static precedence by keeping freshly verified populated static RCBs visible to the stable planner while capability qualification remains an additional gate only for empty RCB slots that require dynamic DataSet/RCB mutation, and PR #88 adds a fail-closed single-member DefineNamedVariableList/GetNamedVariableListAttributes/DeleteNamedVariableList probation before any dynamic RCB mutation while preserving exact invokeID, request/response BER, routing, member, DataSet, association-state, and cleanup evidence for field root-cause analysis." } diff --git a/tests/ARSAS.Tests/HybridReportAssociationCapabilityP3RegressionTests.cs b/tests/ARSAS.Tests/HybridReportAssociationCapabilityP3RegressionTests.cs index a5704e78..8e45e112 100644 --- a/tests/ARSAS.Tests/HybridReportAssociationCapabilityP3RegressionTests.cs +++ b/tests/ARSAS.Tests/HybridReportAssociationCapabilityP3RegressionTests.cs @@ -18,18 +18,20 @@ public void HybridPlanning_UsesAssociationCapabilityForInitialPlanAndFreshRevali } [Fact] - public void EngineLock_PinsP61StaticPreservingAttemptAwareEngine() + public void EngineLock_PinsP62ProbeEngineWhilePreservingP61StaticSafetyHistory() { var source = Read("engines/ARIEC61850.lock.json"); - Assert.Contains("1d052c3919ca1fdf043747d858dab9b6c384e827", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("\"sourcePullRequest\": 87", source, StringComparison.Ordinal); + Assert.Contains("d4612fb7fd5d499a70df17110f6191c452340e0c", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("\"sourcePullRequest\": 88", source, StringComparison.Ordinal); + Assert.Contains("PR #87", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("baseline-safe static precedence", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("DefineNamedVariableList", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("GetNamedVariableListAttributes", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("DeleteNamedVariableList", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("dynamic-attempt", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("rollback", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("baseline-safe static precedence", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("dynamic DataSet/RCB mutation", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("single-member", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("before any dynamic RCB mutation", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("cleanup evidence", source, StringComparison.OrdinalIgnoreCase); } private static int Count(string source, string value) diff --git a/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs b/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs index dcfeb889..599ed46d 100644 --- a/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs +++ b/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs @@ -69,25 +69,29 @@ public void SignalSelectionRecovery_RunsAgainAfterLegacyConstructorDisplayPrepar } [Fact] - public void EngineLock_PreservesReportProjectionHistoryAndPinsP61StaticSafetyEngine() + public void EngineLock_PreservesReportProjectionHistoryAndPinsP62ProbeEngine() { var source = File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json")); using var document = JsonDocument.Parse(source); var root = document.RootElement; - Assert.Equal("1d052c3919ca1fdf043747d858dab9b6c384e827", root.GetProperty("commit").GetString()); - Assert.Equal(87, root.GetProperty("sourcePullRequest").GetInt32()); + Assert.Equal("d4612fb7fd5d499a70df17110f6191c452340e0c", root.GetProperty("commit").GetString()); + Assert.Equal(88, root.GetProperty("sourcePullRequest").GetInt32()); Assert.Contains("one descriptor per static DataSet member", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("generic Boolean status structures", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("DataRef-enabled InformationReport ordering", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("zero OptFlds", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("quarantining unmapped canonical report metadata", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("dynamic reporting before MMS polling", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("DefineNamedVariableList", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("TrgOps", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("dynamic-attempt", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("rollback", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("PR #87", source, StringComparison.OrdinalIgnoreCase); Assert.Contains("baseline-safe static precedence", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("DefineNamedVariableList", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("GetNamedVariableListAttributes", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("DeleteNamedVariableList", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("single-member", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("cleanup evidence", source, StringComparison.OrdinalIgnoreCase); } [Fact] diff --git a/tests/ARSAS.Tests/P62DynamicNvlProbeRegressionTests.cs b/tests/ARSAS.Tests/P62DynamicNvlProbeRegressionTests.cs new file mode 100644 index 00000000..7b158a99 --- /dev/null +++ b/tests/ARSAS.Tests/P62DynamicNvlProbeRegressionTests.cs @@ -0,0 +1,70 @@ +using AR.Iec61850.Mms; + +namespace ARSAS.Tests; + +public sealed class P62DynamicNvlProbeRegressionTests +{ + [Fact] + public void PinnedEngine_ExposesExactSingleMemberProbeEvidenceContract() + { + var evidence = new MmsDynamicDataSetProbeServiceEvidence + { + Service = "DefineNamedVariableList", + Attempted = true, + IsSuccess = false, + InvokeId = 41, + DataSetReference = "IEDLD0/LLN0.AR_HYB_01", + MemberReference = "IEDLD0/GGIO1$ST$Ind1$stVal", + RequestHex = "AA BB CC", + ResponseHex = string.Empty, + StateBefore = MmsAssociationState.MmsInitiated, + StateAfter = MmsAssociationState.MmsInitiateFailed, + ReceiveRoutingSummary = "transport closed", + Message = "probe transport fault" + }; + + Assert.True(evidence.Attempted); + Assert.Equal(41, evidence.InvokeId); + Assert.Equal("IEDLD0/LLN0.AR_HYB_01", evidence.DataSetReference); + Assert.Equal("IEDLD0/GGIO1$ST$Ind1$stVal", evidence.MemberReference); + Assert.Equal(MmsAssociationState.MmsInitiated, evidence.StateBefore); + Assert.Equal(MmsAssociationState.MmsInitiateFailed, evidence.StateAfter); + Assert.Contains("DefineNamedVariableList", evidence.Summary, StringComparison.Ordinal); + Assert.Contains("IEDLD0/LLN0.AR_HYB_01", evidence.Summary, StringComparison.Ordinal); + } + + [Theory] + [InlineData(MmsReportActivationFailureReason.DynamicDataSetProbeDefineFailed)] + [InlineData(MmsReportActivationFailureReason.DynamicDataSetProbeVerificationFailed)] + [InlineData(MmsReportActivationFailureReason.DynamicDataSetProbeDeleteFailed)] + public void PinnedEngine_ExposesProbeFailureStages(MmsReportActivationFailureReason reason) + { + Assert.Contains("Probe", reason.ToString(), StringComparison.Ordinal); + } + + [Fact] + public void RuntimeDiagnostics_SurfaceProbeEvidenceWithoutAddingAnotherLiveGrid() + { + var runtime = ReadRepoFile("Services/Iec61850MonitorRuntime.cs"); + var diagnostics = ReadRepoFile("MainWindow.HybridAcquisitionDiagnostics.cs"); + + Assert.Contains("result.Warnings.Take(3)", runtime, StringComparison.Ordinal); + Assert.Contains("Report plan unavailable", runtime, StringComparison.Ordinal); + Assert.DoesNotContain("DispatcherTimer", diagnostics, StringComparison.Ordinal); + Assert.DoesNotContain("DataGrid", diagnostics, StringComparison.Ordinal); + } + + private static string ReadRepoFile(string relativePath) + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + return File.ReadAllText(candidate).Replace("\r\n", "\n", StringComparison.Ordinal); + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } +}