perf(template-engine): read-only staleness/comparison paths skip Expression-trigger compiles — deploy gate remains the authoritative check (plan R2-05 T4)

This commit is contained in:
Joseph Doherty
2026-07-13 09:48:40 -04:00
parent d233ecbe8f
commit 31fa5eafa4
2 changed files with 76 additions and 15 deletions
@@ -462,4 +462,43 @@ public class ValidationServiceTests
Assert.DoesNotContain(strictResult.Errors, e => e.Category == ValidationCategory.AlarmTriggerReference);
Assert.DoesNotContain(strictResult.Warnings, w => w.Category == ValidationCategory.AlarmTriggerReference);
}
[Fact]
public void Validate_SkipCompilation_SkipsExpressionTriggerSyntaxCheck()
{
var config = ConfigWithExpressionTriggerScript("this is not C# ((");
var gated = new ValidationService().Validate(config, validateScriptCompilation: false);
Assert.DoesNotContain(gated.Errors, e => e.Message.Contains("failed validation")); // FAILS today
var full = new ValidationService().Validate(config);
Assert.Contains(full.Errors, e => e.Message.Contains("failed validation")); // deploy gate unchanged
}
[Fact]
public void Validate_SkipCompilation_StillChecksExpressionAttributeReferences()
{
// expression "Attributes[\"Ghost\"] != null" referencing a missing attribute:
// the reference scan is cheap string work and MUST survive the gate.
var config = ConfigWithExpressionTriggerScript("Attributes[\"Ghost\"] != null");
var gated = new ValidationService().Validate(config, validateScriptCompilation: false);
Assert.Contains(gated.Errors, e => e.Message.Contains("Ghost"));
}
private static FlattenedConfiguration ConfigWithExpressionTriggerScript(string expression)
{
var json = System.Text.Json.JsonSerializer.Serialize(new { expression });
return new FlattenedConfiguration
{
InstanceUniqueName = "Instance1",
Attributes = [new ResolvedAttribute { CanonicalName = "Temp", Value = "25", DataType = "Double" }],
Scripts =
[
new ResolvedScript
{
CanonicalName = "OnExpr",
TriggerType = "Expression",
TriggerConfiguration = json
}
]
};
}
}