From bb1bcb20ff5e585b118378290a8a093d6b319017 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Mon, 17 Nov 2025 13:56:07 +0100 Subject: [PATCH 1/3] Create consistent data sync IDs for applicability switch --- .../AppliesSwitch/AppliesSwitchBlock.cs | 7 ++- .../Directives/ApplicabilitySwitchTests.cs | 47 +++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs index 600cd17eb..3034e9a35 100644 --- a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs @@ -4,6 +4,7 @@ using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Products; +using Elastic.Documentation.Extensions; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Helpers; @@ -74,8 +75,10 @@ public static string GenerateSyncKey(string appliesToDefinition, ProductsConfigu var applicableTo = YamlSerialization.Deserialize(appliesToDefinition, productsConfiguration); if (applicableTo != null) { - // Use the object's hash for a consistent, unique identifier - return $"applies-{System.Math.Abs(applicableTo.GetHashCode())}"; + // Use ShortId.Create for a stable, deterministic hash based on the normalized ToString() + // ToString() normalizes different YAML representations into a canonical form, + // ensuring semantically equivalent definitions get the same sync key + return $"applies-{ShortId.Create(applicableTo.ToString())}"; } } catch diff --git a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs index 0cbd76fc2..079851eb1 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs @@ -170,10 +170,7 @@ public void ParsesSyncKey() // Verify all sync keys have the expected hash-based format foreach (var item in items) - { item.SyncKey.Should().StartWith("applies-", "Sync key should start with 'applies-' prefix"); - item.SyncKey.Should().MatchRegex(@"^applies-\d+$", "Sync key should be in format 'applies-{hash}'"); - } // Verify that different applies_to definitions produce different sync keys items[0].SyncKey.Should().NotBe(items[1].SyncKey, "Different applies_to definitions should produce different sync keys"); @@ -220,7 +217,49 @@ public void GeneratesConsistentSyncKeysForYamlObjects() // Also verify the key has the expected format key1.Should().StartWith("applies-", "Sync key should start with 'applies-' prefix"); - key1.Should().MatchRegex(@"^applies-\d+$", "Sync key should be in format 'applies-{hash}'"); + key1.Should().MatchRegex(@"^applies-[0-9A-F]{8}$", "Sync key should be in format 'applies-{8 hex digits}'"); } } + + [Fact] + public void GeneratesDeterministicSyncKeysAcrossMultipleRuns() + { + // Test that the same input always produces the same sync key across multiple invocations + // This ensures the hash is deterministic and not affected by GetHashCode() instability + // The expected values are based on SHA256 hashing of the ToString() output of ApplicableTo + var testDefinitions = new[] + { + "stack: ga 9.1", + "stack: preview 9.0", + "ess: ga 8.11", + "deployment: { ece: ga 9.0, ess: ga 9.1 }", + "serverless: all", + }; + + foreach (var definition in testDefinitions) + { + // Generate the same key 10 times to prove determinism + var keys = Enumerable.Range(0, 10) + .Select(_ => AppliesItemBlock.GenerateSyncKey(definition, Block!.Build.ProductsConfiguration)) + .ToList(); + + // All keys should be identical + keys.Distinct().Should().HaveCount(1, + $"All 10 invocations for '{definition}' should produce the exact same deterministic key"); + + // The key should have the expected format + var key = keys[0]; + key.Should().StartWith("applies-", "Sync key should start with 'applies-' prefix"); + key.Should().MatchRegex(@"^applies-[0-9A-F]{8}$", + "Sync key should be in format 'applies-{8 uppercase hex digits}'"); + } + + // Also verify that different definitions produce different keys + var allKeys = testDefinitions + .Select(def => AppliesItemBlock.GenerateSyncKey(def, Block!.Build.ProductsConfiguration)) + .ToList(); + + allKeys.Distinct().Should().HaveCount(testDefinitions.Length, + "Different applies_to definitions should produce different sync keys"); + } } From 5e58af40484c933f03d62d17de5343abff1ebb84 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Mon, 17 Nov 2025 14:04:40 +0100 Subject: [PATCH 2/3] Simplify --- .../AppliesSwitch/AppliesSwitchBlock.cs | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs index 3034e9a35..ab34406a2 100644 --- a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs @@ -68,25 +68,10 @@ public override void FinalizeAndValidate(ParserContext context) public static string GenerateSyncKey(string appliesToDefinition, ProductsConfiguration productsConfiguration) { - // Parse the YAML to get the ApplicableTo object, then use its hash - // This ensures both simple syntax and YAML objects produce consistent sync keys - try - { - var applicableTo = YamlSerialization.Deserialize(appliesToDefinition, productsConfiguration); - if (applicableTo != null) - { - // Use ShortId.Create for a stable, deterministic hash based on the normalized ToString() - // ToString() normalizes different YAML representations into a canonical form, - // ensuring semantically equivalent definitions get the same sync key - return $"applies-{ShortId.Create(applicableTo.ToString())}"; - } - } - catch - { - // If parsing fails, fall back to the original definition - } - - // Fallback to original definition if parsing fails - return appliesToDefinition.Slugify().Replace(".", "-"); + var applicableTo = YamlSerialization.Deserialize(appliesToDefinition, productsConfiguration); + // Use ShortId.Create for a stable, deterministic hash based on the normalized ToString() + // ToString() normalizes different YAML representations into a canonical form, + // ensuring semantically equivalent definitions get the same sync key + return $"applies-{ShortId.Create(applicableTo.ToString())}"; } } From 6a59e010453fcbd1f9569b8ea8b7ae3ad105c7a6 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Mon, 17 Nov 2025 14:16:24 +0100 Subject: [PATCH 3/3] Check against hardcoded id --- .../Directives/ApplicabilitySwitchTests.cs | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs index 079851eb1..f0f372d2d 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs @@ -224,42 +224,36 @@ public void GeneratesConsistentSyncKeysForYamlObjects() [Fact] public void GeneratesDeterministicSyncKeysAcrossMultipleRuns() { - // Test that the same input always produces the same sync key across multiple invocations - // This ensures the hash is deterministic and not affected by GetHashCode() instability - // The expected values are based on SHA256 hashing of the ToString() output of ApplicableTo - var testDefinitions = new[] + var expectedKeys = new Dictionary { - "stack: ga 9.1", - "stack: preview 9.0", - "ess: ga 8.11", - "deployment: { ece: ga 9.0, ess: ga 9.1 }", - "serverless: all", + // These are the actual SHA256-based hashes that should never change + { "stack: ga 9.1", "applies-031B7112" }, + { "stack: preview 9.0", "applies-361F73DC" }, + { "ess: ga 8.11", "applies-32E204F7" }, + { "deployment: { ece: ga 9.0, ess: ga 9.1 }", "applies-D099CDEF" }, + { "serverless: all", "applies-A34B17C6" }, }; - foreach (var definition in testDefinitions) + foreach (var (definition, expectedKey) in expectedKeys) { - // Generate the same key 10 times to prove determinism - var keys = Enumerable.Range(0, 10) + var actualKey = AppliesItemBlock.GenerateSyncKey(definition, Block!.Build.ProductsConfiguration); + + actualKey.Should().Be(expectedKey, + $"The sync key for '{definition}' must match the expected value. " + + $"If this fails, the hash algorithm has changed and will break sync IDs across builds!"); + + // Also verify multiple invocations in this run produce the same key + var keys = Enumerable.Range(0, 5) .Select(_ => AppliesItemBlock.GenerateSyncKey(definition, Block!.Build.ProductsConfiguration)) .ToList(); - // All keys should be identical keys.Distinct().Should().HaveCount(1, - $"All 10 invocations for '{definition}' should produce the exact same deterministic key"); - - // The key should have the expected format - var key = keys[0]; - key.Should().StartWith("applies-", "Sync key should start with 'applies-' prefix"); - key.Should().MatchRegex(@"^applies-[0-9A-F]{8}$", - "Sync key should be in format 'applies-{8 uppercase hex digits}'"); + $"All invocations for '{definition}' should produce identical keys"); } - // Also verify that different definitions produce different keys - var allKeys = testDefinitions - .Select(def => AppliesItemBlock.GenerateSyncKey(def, Block!.Build.ProductsConfiguration)) - .ToList(); - - allKeys.Distinct().Should().HaveCount(testDefinitions.Length, - "Different applies_to definitions should produce different sync keys"); + // Verify that different definitions produce different keys + var allKeys = expectedKeys.Values.ToList(); + allKeys.Distinct().Should().HaveCount(expectedKeys.Count, + "Different applies_to definitions must produce different sync keys"); } }