feat(runtime): per-script execution timeout overriding the global default (#9)

Spec promised a per-script timeout but only the global ScriptExecutionTimeoutSeconds
existed. Add nullable TemplateScript.ExecutionTimeoutSeconds threaded through EF +
flattening (ResolvedScript) to ScriptExecutionActor/AlarmExecutionActor, which use
perScript ?? global for the execution CTS. Includes the EF migration for the new column.
This commit is contained in:
Joseph Doherty
2026-06-15 14:40:38 -04:00
parent 00304a26e6
commit 3edef09f51
22 changed files with 2094 additions and 17 deletions
@@ -371,6 +371,94 @@ public class FlatteningServiceTests
Assert.Equal("return base;", script.Code);
}
// ── M2.5 (#9): per-script execution timeout threads to ResolvedScript ───
[Fact]
public void Flatten_SingleTemplate_ScriptExecutionTimeoutSecondsThreadsThrough()
{
var template = CreateTemplate(1, "Base");
template.Scripts.Add(new TemplateScript("Slow", "// slow") { ExecutionTimeoutSeconds = 5 });
template.Scripts.Add(new TemplateScript("Default", "// default")); // null → use global
var instance = CreateInstance();
var result = _sut.Flatten(
instance,
[template],
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
new Dictionary<int, IReadOnlyList<Template>>(),
new Dictionary<int, DataConnection>());
Assert.True(result.IsSuccess);
var slow = result.Value.Scripts.First(s => s.CanonicalName == "Slow");
Assert.Equal(5, slow.ExecutionTimeoutSeconds);
var dflt = result.Value.Scripts.First(s => s.CanonicalName == "Default");
Assert.Null(dflt.ExecutionTimeoutSeconds);
}
[Fact]
public void Flatten_DerivedScriptOverride_ExecutionTimeoutFollowsWinningRow()
{
// Scripts inherit/override at whole-row granularity: an explicit override
// row on the derived template (IsInherited = false) fully replaces the
// base row, so its ExecutionTimeoutSeconds wins — exactly like the body.
var baseTemplate = CreateTemplate(2, "Sensor");
baseTemplate.Scripts.Add(new TemplateScript("Sample", "return base;")
{
ExecutionTimeoutSeconds = 10
});
var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
derived.Scripts.Add(new TemplateScript("Sample", "return derived;")
{
ExecutionTimeoutSeconds = 3
});
var instance = CreateInstance();
var result = _sut.Flatten(
instance,
[derived, baseTemplate],
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
new Dictionary<int, IReadOnlyList<Template>>(),
new Dictionary<int, DataConnection>());
Assert.True(result.IsSuccess);
var script = result.Value.Scripts.First(s => s.CanonicalName == "Sample");
Assert.Equal("return derived;", script.Code);
Assert.Equal(3, script.ExecutionTimeoutSeconds);
}
[Fact]
public void Flatten_InheritedScriptOnDerived_ExecutionTimeoutFollowsBaseRow()
{
// A stale inherited copy on the derived template (IsInherited = true) is
// ignored; the base row wins, carrying the base ExecutionTimeoutSeconds.
var baseTemplate = CreateTemplate(2, "Sensor");
baseTemplate.Scripts.Add(new TemplateScript("Sample", "return base;")
{
ExecutionTimeoutSeconds = 10
});
var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
derived.Scripts.Add(new TemplateScript("Sample", "stale code")
{
IsInherited = true,
ExecutionTimeoutSeconds = 3
});
var instance = CreateInstance();
var result = _sut.Flatten(
instance,
[derived, baseTemplate],
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
new Dictionary<int, IReadOnlyList<Template>>(),
new Dictionary<int, DataConnection>());
Assert.True(result.IsSuccess);
var script = result.Value.Scripts.First(s => s.CanonicalName == "Sample");
Assert.Equal("return base;", script.Code);
Assert.Equal(10, script.ExecutionTimeoutSeconds);
}
// ── TemplateEngine-002: per-slot alarm override ────────────────────────
[Fact]