fix(scriptanalysis): memoise ScriptTrustValidator verdicts by code hash for the default reference set (review 2026-10-21 04/P3)

This commit is contained in:
Joseph Doherty
2026-10-22 06:18:15 -04:00
parent ae1282ed9d
commit 8c935480fa
4 changed files with 170 additions and 2 deletions
@@ -0,0 +1,66 @@
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests;
/// <summary>
/// Review 2026-10-21 04/P3: <see cref="ScriptTrustValidator.FindViolations(string, IEnumerable{Microsoft.CodeAnalysis.MetadataReference})"/>
/// is memoised by code hash for the default (policy) reference set, so the bundle
/// importer's preview and apply passes — which call it with no change at their call
/// sites — do not redo a full-framework semantic analysis per script. A cache hit
/// returns the very same verdict instance, which is what these tests observe.
/// </summary>
public class ScriptTrustValidatorCacheTests
{
/// <summary>Verifies that the same violating code analysed twice is served from the cache.</summary>
[Fact]
public void FindViolations_SameCodeTwice_SecondCallIsCacheHit()
{
var code = $"var x = System.IO.File.Exists(\"{Guid.NewGuid()}\");";
var first = ScriptTrustValidator.FindViolations(code);
var second = ScriptTrustValidator.FindViolations(code);
Assert.NotEmpty(second);
Assert.Same(first, second); // pre-fix: a fresh list per call
}
/// <summary>Verifies that a clean verdict is cached too.</summary>
[Fact]
public void FindViolations_CleanCodeTwice_SecondCallIsCacheHit()
{
var code = $"var x = \"{Guid.NewGuid()}\".Length;";
var first = ScriptTrustValidator.FindViolations(code);
var second = ScriptTrustValidator.FindViolations(code);
Assert.Empty(second);
Assert.Same(first, second);
}
/// <summary>
/// Verifies that a call with extra references is never served from (or stored in)
/// the cache — extra references can change symbol resolution, so the verdict is a
/// pure function of the code only for the default reference set.
/// </summary>
[Fact]
public void FindViolations_WithExtraReferences_BypassesCache()
{
var code = $"var x = System.IO.File.Exists(\"{Guid.NewGuid()}\");";
var extra = new[] { Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(typeof(ScriptCompileSurface).Assembly.Location) };
var a = ScriptTrustValidator.FindViolations(code, extra);
var b = ScriptTrustValidator.FindViolations(code, extra);
var cached = ScriptTrustValidator.FindViolations(code);
Assert.NotSame(a, b);
Assert.NotSame(a, cached);
Assert.NotSame(b, cached);
}
/// <summary>Verifies that the shared verdict cannot be mutated by a caller to poison the cache.</summary>
[Fact]
public void FindViolations_ReturnedList_IsReadOnly()
{
var code = $"var x = System.IO.File.Exists(\"{Guid.NewGuid()}\");";
var verdict = ScriptTrustValidator.FindViolations(code);
Assert.False(verdict is List<string>);
Assert.True(verdict is System.Collections.Generic.ICollection<string> { IsReadOnly: true });
}
}