From 834419def843a2172508d9e8457c127b2c7413c1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 8 Sep 2026 17:12:51 +0700 Subject: [PATCH 1/8] fix: use black IED fascia and restore FAT progress safely --- Models/IoTesting/IoFatSourceModels.cs | 74 ++++ Resources/ArvrelMiniIedFascia.xaml | 325 ++++++++---------- .../IoTesting/IoTestPerIedProgressIdentity.cs | 68 +++- .../IoTestWorkspaceBootstrapService.cs | 186 +++++++++- tests/ARSAS.Tests/BlackFasciaRuntimeTests.cs | 61 ++++ ...tEngineeringContinuationRegressionTests.cs | 216 ++++++++++++ 6 files changed, 720 insertions(+), 210 deletions(-) create mode 100644 tests/ARSAS.Tests/BlackFasciaRuntimeTests.cs create mode 100644 tests/ARSAS.Tests/FatEngineeringContinuationRegressionTests.cs diff --git a/Models/IoTesting/IoFatSourceModels.cs b/Models/IoTesting/IoFatSourceModels.cs index d2e0d8a27..1bbe713f9 100644 --- a/Models/IoTesting/IoFatSourceModels.cs +++ b/Models/IoTesting/IoFatSourceModels.cs @@ -93,6 +93,63 @@ public static string ProjectSourceFingerprint(IoTestProject project) return string.Empty; } + /// + /// Content-only fingerprint used exclusively for SCL workspace continuation. + /// File names and SourceId values are presentation/staging metadata and must not make + /// the same IEC 61850 source bytes look like a different FAT project after reopening + /// Engineering and switching back to FAT. + /// + /// Do not use this fingerprint for workbook identity or package integrity. Those paths + /// retain the strict source-set contract above. + /// + public static string ComputeContentFingerprint(IEnumerable sources) + { + ArgumentNullException.ThrowIfNull(sources); + var canonical = sources + .Select(Normalize) + .OrderBy(source => source.Kind, StringComparer.Ordinal) + .ThenBy(source => source.Sha256, StringComparer.Ordinal) + .Select(source => $"{source.Kind}\t{source.Sha256}") + .ToArray(); + if (canonical.Length == 0) + return string.Empty; + var bytes = Encoding.UTF8.GetBytes(string.Join("\n", canonical)); + return Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); + } + + public static bool SameSclContentSet( + IEnumerable left, + IEnumerable right) + { + ArgumentNullException.ThrowIfNull(left); + ArgumentNullException.ThrowIfNull(right); + + var leftSources = left.Select(Normalize).ToArray(); + var rightSources = right.Select(Normalize).ToArray(); + if (leftSources.Length == 0 || rightSources.Length == 0 || + leftSources.Length != rightSources.Length || + leftSources.Any(source => !source.Kind.Equals(IoFatSourceKinds.Scl, StringComparison.OrdinalIgnoreCase)) || + rightSources.Any(source => !source.Kind.Equals(IoFatSourceKinds.Scl, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + + return ComputeContentFingerprint(leftSources) + .Equals(ComputeContentFingerprint(rightSources), StringComparison.OrdinalIgnoreCase); + } + + public static bool CompatibleContinuationSchema(string? savedSchema, string? currentSchema) + { + var saved = (savedSchema ?? string.Empty).Trim(); + var current = (currentSchema ?? string.Empty).Trim(); + if (saved.Equals(current, StringComparison.OrdinalIgnoreCase)) + return true; + + return TryGetSclSchemaMajor(saved, out var savedMajor) && + TryGetSclSchemaMajor(current, out var currentMajor) && + savedMajor == currentMajor; + } + public static string ProjectStorageFingerprint(IoTestProject project) { ArgumentNullException.ThrowIfNull(project); @@ -204,6 +261,23 @@ private static string NormalizeHash(string sha256) return normalized; } + private static bool TryGetSclSchemaMajor(string schema, out int major) + { + major = 0; + const string prefix = "ARSAS-FAT-SCL-"; + if (!schema.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + return false; + + var version = schema[prefix.Length..].Trim(); + var separator = version.IndexOf('.'); + var majorText = separator >= 0 ? version[..separator] : version; + return int.TryParse( + majorText, + System.Globalization.NumberStyles.None, + System.Globalization.CultureInfo.InvariantCulture, + out major); + } + private static string SafeFileName(string? value, string fallback) { var name = Path.GetFileName(string.IsNullOrWhiteSpace(value) ? fallback : value.Trim()); diff --git a/Resources/ArvrelMiniIedFascia.xaml b/Resources/ArvrelMiniIedFascia.xaml index e9d3e2aa9..b86853a4a 100644 --- a/Resources/ArvrelMiniIedFascia.xaml +++ b/Resources/ArvrelMiniIedFascia.xaml @@ -3,87 +3,86 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> -