diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs index 600cd17eb..ab34406a2 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; @@ -67,23 +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 the object's hash for a consistent, unique identifier - return $"applies-{System.Math.Abs(applicableTo.GetHashCode())}"; - } - } - 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())}"; } } diff --git a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs index 0cbd76fc2..f0f372d2d 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,43 @@ 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() + { + var expectedKeys = new Dictionary + { + // 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, expectedKey) in expectedKeys) + { + 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(); + + keys.Distinct().Should().HaveCount(1, + $"All invocations for '{definition}' should produce identical 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"); + } }