feat(scripts): self/child/parent attribute and script accessors
Phases 1+2 of the design at
docs/plans/2026-05-12-script-scope-access-design.md.
Adds ergonomic scope-aware accessors to compiled scripts. A script
on a composed TempSensor reads its own attribute via
Attributes["Temperature"]; reaches up to the parent via
Parent.Attributes["SpeedRPM"]; invokes a child script via
Children["TempSensor"].CallScript("Sample"). All resolve to the
existing flat Instance.GetAttribute / SetAttribute / CallScript
delegates by prepending the script's canonical path prefix.
Runtime types (SiteRuntime.Scripts.ScopeAccessors):
AttributeAccessor sync indexer + GetAsync / SetAsync
CompositionAccessor Attributes + CallScript
ChildrenAccessor Children["name"] => CompositionAccessor
ScriptGlobals gains Scope, Attributes, Children, Parent properties.
Sync indexer blocks on the Instance Actor Ask; explicit GetAsync /
SetAsync are also available for callers that want to await.
Plumbing:
- Commons.Types.Scripts.ScriptScope record (SelfPath / ParentPath).
- ResolvedScript.Scope (defaults to ScriptScope.Root for back-compat).
- FlatteningService emits new ScriptScope(prefix, "") for each
composed script so a script defined on TempSensor composed under
a parent gets SelfPath = "TempSensor".
- ScriptActor reads the Scope from its ResolvedScript and forwards
it through ScriptExecutionActor into ScriptGlobals on each call.
RevisionHashService not touched: the per-script canonical name
already encodes the composition path, so any structural change
already flips the hash.
10 new unit tests on the path arithmetic. Site/Template engine
suites stay green (129 + 199).
Editor surface (Phase 3: metadata fetch, Phase 4: completion +
SCADA006 / SCADA007 diagnostics) follows in the next commits.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using ScadaLink.Commons.Types.Scripts;
|
||||
using ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
namespace ScadaLink.SiteRuntime.Tests.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 1 of the script-scope rollout: verify path arithmetic for the new
|
||||
/// Attributes / Children / Parent accessors. The actor-mediated reads/writes
|
||||
/// are exercised end-to-end in Phase 2 once flattening carries scope info.
|
||||
/// </summary>
|
||||
public class ScopeAccessorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Root_SelfPath_Empty()
|
||||
{
|
||||
Assert.Equal("", ScriptScope.Root.SelfPath);
|
||||
Assert.Null(ScriptScope.Root.ParentPath);
|
||||
Assert.False(ScriptScope.Root.HasParent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompositionScope_HasParent()
|
||||
{
|
||||
var scope = new ScriptScope("TempSensor", "");
|
||||
Assert.True(scope.HasParent);
|
||||
Assert.Equal("", scope.ParentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeAccessor_RootScope_ResolvesBareKey()
|
||||
{
|
||||
var acc = new AttributeAccessor(null!, "");
|
||||
Assert.Equal("Temperature", acc.Resolve("Temperature"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeAccessor_ComposedScope_PrependsPath()
|
||||
{
|
||||
var acc = new AttributeAccessor(null!, "TempSensor");
|
||||
Assert.Equal("TempSensor.Temperature", acc.Resolve("Temperature"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeAccessor_NestedScope_ChainsPath()
|
||||
{
|
||||
var acc = new AttributeAccessor(null!, "Motor.TempSensor");
|
||||
Assert.Equal("Motor.TempSensor.Temperature", acc.Resolve("Temperature"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompositionAccessor_AttributesShareScope()
|
||||
{
|
||||
var comp = new CompositionAccessor(null!, "TempSensor");
|
||||
Assert.Equal("TempSensor", comp.Path);
|
||||
Assert.Equal("TempSensor", comp.Attributes.ScopePrefix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompositionAccessor_ResolveScript_PrependsPath()
|
||||
{
|
||||
var comp = new CompositionAccessor(null!, "TempSensor");
|
||||
Assert.Equal("TempSensor.Sample", comp.ResolveScript("Sample"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompositionAccessor_EmptyPath_LeavesScriptNameBare()
|
||||
{
|
||||
var comp = new CompositionAccessor(null!, "");
|
||||
Assert.Equal("Sample", comp.ResolveScript("Sample"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChildrenAccessor_FromRoot_GivesUnpathedChild()
|
||||
{
|
||||
var children = new ChildrenAccessor(null!, "");
|
||||
var temp = children["TempSensor"];
|
||||
Assert.Equal("TempSensor", temp.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChildrenAccessor_FromComposition_PrefixesChild()
|
||||
{
|
||||
var children = new ChildrenAccessor(null!, "Motor");
|
||||
var temp = children["TempSensor"];
|
||||
Assert.Equal("Motor.TempSensor", temp.Path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user