fix(calculation): read cache carries real input status + timestamps

This commit is contained in:
Joseph Doherty
2026-08-20 10:40:15 -04:00
parent 48efdac600
commit 60e5b40228
6 changed files with 163 additions and 25 deletions
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
namespace ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests;
@@ -230,6 +231,74 @@ public sealed class CalculationEvaluatorTests
return (int)countProp.GetValue(cache)!;
}
// ── input quality reaches the script (archreview 05/STAB-19) ────────────────────────────
/// <summary>
/// A script reading <c>ctx.GetTag(path).StatusCode</c> sees the input's <b>real</b> status.
/// The read cache used to stamp every dependency Good, so a script that checked quality was lied
/// to — and, since the driver now holds on Bad, an Uncertain input is precisely the case where a
/// script can still make a qualified decision and needs the truth to do it.
/// </summary>
[Fact]
public void The_read_cache_carries_the_inputs_real_status_code()
{
const uint uncertain = 0x40000000u;
using var sut = new CalculationEvaluator(NullLogger<CalculationEvaluator>.Instance, NoOpScriptRoot());
var sourceStamp = new DateTime(2026, 8, 20, 9, 0, 0, DateTimeKind.Utc);
var result = sut.Evaluate(
calcTagId: "calc-quality",
scriptSource: "return (long)ctx.GetTag(\"a\").StatusCode;",
dependencies: new Dictionary<string, DataValueSnapshot>
{
["a"] = new(7.0, uncertain, sourceStamp, sourceStamp),
});
result.Success.ShouldBeTrue(result.Reason);
result.Value.ShouldBe((long)uncertain);
}
/// <summary>
/// The dependency's own source timestamp reaches the script too. The old cache stamped
/// evaluation time on every input, so a script comparing input ages compared its own clock with
/// itself.
/// </summary>
[Fact]
public void The_read_cache_carries_the_inputs_real_source_timestamp()
{
using var sut = new CalculationEvaluator(NullLogger<CalculationEvaluator>.Instance, NoOpScriptRoot());
var sourceStamp = new DateTime(2026, 8, 20, 9, 0, 0, DateTimeKind.Utc);
var result = sut.Evaluate(
calcTagId: "calc-stamp",
scriptSource: "return ctx.GetTag(\"a\").SourceTimestampUtc;",
dependencies: new Dictionary<string, DataValueSnapshot>
{
["a"] = new(7.0, 0u, sourceStamp, DateTime.UtcNow),
});
result.Success.ShouldBeTrue(result.Reason);
result.Value.ShouldBe(sourceStamp);
}
/// <summary>The passthrough fast path takes the same snapshot input and returns its value.</summary>
[Fact]
public void The_passthrough_fast_path_accepts_snapshots()
{
using var sut = new CalculationEvaluator(NullLogger<CalculationEvaluator>.Instance, NoOpScriptRoot());
var result = sut.Evaluate(
calcTagId: "calc-mirror",
scriptSource: "return ctx.GetTag(\"a\").Value;",
dependencies: new Dictionary<string, DataValueSnapshot>
{
["a"] = new(42, 0u, DateTime.UtcNow, DateTime.UtcNow),
});
result.Success.ShouldBeTrue(result.Reason);
result.Value.ShouldBe(42);
}
/// <summary>Minimal <see cref="ILogger{T}"/> that records emitted entries so the SetVirtualTag-drop
/// test can assert the Debug "dropped" line was logged.</summary>
private sealed class CapturingLogger : ILogger<CalculationEvaluator>