fix(virtual-tags): resolve High code-review finding (Core.VirtualTags-001)
OnScriptSetVirtualTag updated the value cache, notified observers, and recorded history for the written path but never scheduled a cascade for tags depending on that path. This contradicts docs/VirtualTags.md, which states ctx.SetVirtualTag writes "still participate in change-trigger cascades": a change-triggered virtual tag reading a script-written tag went stale until an unrelated trigger fired. OnScriptSetVirtualTag now launches a fire-and-forget CascadeAsync for the written path, mirroring OnUpstreamChange. The cascade is scheduled rather than invoked inline because the callback runs inside EvaluateInternalAsync while the non-reentrant _evalGate semaphore is held. Added regression test SetVirtualTag_within_script_cascades_to_dependents_of_the_written_tag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -275,6 +275,38 @@ public sealed class VirtualTagEngineTests
|
||||
engine.Read("Driver").Value.ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetVirtualTag_within_script_cascades_to_dependents_of_the_written_tag()
|
||||
{
|
||||
// Regression for Core.VirtualTags-001: a ctx.SetVirtualTag write must schedule
|
||||
// the change-trigger cascade for tags depending on the written path, exactly
|
||||
// like an upstream delta. "Dependent" reads "Target" and is ChangeTriggered, so
|
||||
// a write to Target via ctx.SetVirtualTag must re-evaluate Dependent.
|
||||
var up = new FakeUpstream();
|
||||
up.Set("In", 5);
|
||||
using var engine = Build(up);
|
||||
|
||||
engine.Load([
|
||||
new VirtualTagDefinition("Target", DriverDataType.Int32,
|
||||
"""return 0;""", ChangeTriggered: false), // operator-written via SetVirtualTag
|
||||
new VirtualTagDefinition("Dependent", DriverDataType.Int32,
|
||||
"""return (int)ctx.GetTag("Target").Value + 1;""", ChangeTriggered: true),
|
||||
new VirtualTagDefinition("Writer", DriverDataType.Int32,
|
||||
"""
|
||||
var v = (int)ctx.GetTag("In").Value;
|
||||
ctx.SetVirtualTag("Target", v * 100);
|
||||
return v;
|
||||
"""),
|
||||
]);
|
||||
await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Writer set Target = 500; the cascade must have re-evaluated Dependent to 501.
|
||||
engine.Read("Target").Value.ShouldBe(500);
|
||||
await WaitForConditionAsync(() => Equals(engine.Read("Dependent").Value, 501));
|
||||
engine.Read("Dependent").Value.ShouldBe(501,
|
||||
"a ctx.SetVirtualTag write must cascade to change-triggered dependents");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Type_coercion_from_script_double_to_config_int32()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user