fix(templateengine+centralui): resolve follow-ups #3 (derived-template collisions) and #7 (sandbox batch/wait surface)

#3 — CollisionDetector counted a derived template's IsInherited placeholder
rows as a distinct origin from the parent members the inheritance walk
re-adds, reporting a spurious "Naming collision" for every inherited row and
blocking any attribute/composition add to a derived template. CollectDirectMembers
now skips IsInherited rows on the direct-template and inherited-parent walks;
it keeps them for the composed-module walk, where placeholders are the sole
representation of a derived module's inherited members (that walk does not
climb the composed template's parent chain).

#7 — SandboxAttributeAccessor (Central UI Test-Run host) omitted
WriteBatchAndWaitAsync / WaitAsync / WaitForAsync, so the editor false-flagged
valid instance scripts with CS1061 even though `template validate` and the
deploy gate accept them. Added the five overloads mirroring the runtime
AttributeAccessor; they throw a labelled ScriptSandboxException if run in
Test Run (the central sandbox has no device-batch / event-waiter transport).

Tests: +3 CollisionDetector unit + 1 end-to-end TemplateService (derived add
now succeeds); +2 ScriptAnalysisService diagnose-clean. Each new test verified
to fail without its fix with the exact user-facing symptom. Full suites green
(TemplateEngine.Tests 438, CentralUI.Tests 866).

Docs: Component-TemplateEngine.md (inherited-placeholder collision rule),
Component-ScriptAnalysis.md (third sandbox surface + its compile-clean guard),
known-issues tracker #3/#7 marked resolved and the minor note promoted to #8.
This commit is contained in:
Joseph Doherty
2026-06-24 15:03:27 -04:00
parent 1a647cf1c4
commit b3f6833b36
8 changed files with 254 additions and 12 deletions
@@ -313,6 +313,59 @@ public class SandboxAttributeAccessor
_ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
return Task.CompletedTask;
}
// Batch-write/wait helpers. These mirror the runtime AttributeAccessor
// (SiteRuntime/Scripts/ScopeAccessors.cs) and the deploy-gate
// ScriptCompileSurface member-for-member so instance scripts using them COMPILE
// in the editor and pass Test Run analysis (follow-up #7) — previously the
// sandbox omitted them and the editor false-flagged valid scripts with CS1061.
// Execution needs the site's DCL batch path + event-driven attribute waiter,
// for which the central Test Run sandbox has no transport, so each throws a
// clearly-labelled ScriptSandboxException; the same code validates/deploys/runs
// unchanged at a site.
/// <summary>
/// Sandbox stand-in for <c>AttributeAccessor.WriteBatchAndWaitAsync</c>: present
/// for editor/compile parity, throws <see cref="ScriptSandboxException"/> when run
/// in Test Run (no device batch-write transport here).
/// </summary>
public Task<bool> WriteBatchAndWaitAsync(
IReadOnlyDictionary<string, object?> values, string flagKey, object? flagValue,
string responseKey, object? responseValue, TimeSpan timeout)
=> throw NotInSandbox(nameof(WriteBatchAndWaitAsync));
/// <summary>
/// Sandbox stand-in for <c>AttributeAccessor.WaitAsync</c> (value-equality form);
/// see <see cref="WriteBatchAndWaitAsync"/>.
/// </summary>
public Task<bool> WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
=> throw NotInSandbox(nameof(WaitAsync));
/// <summary>
/// Sandbox stand-in for <c>AttributeAccessor.WaitAsync</c> (predicate form);
/// see <see cref="WriteBatchAndWaitAsync"/>.
/// </summary>
public Task<bool> WaitAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
=> throw NotInSandbox(nameof(WaitAsync));
/// <summary>
/// Sandbox stand-in for <c>AttributeAccessor.WaitForAsync</c> (value-equality form);
/// see <see cref="WriteBatchAndWaitAsync"/>.
/// </summary>
public Task<WaitResult> WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
=> throw NotInSandbox(nameof(WaitForAsync));
/// <summary>
/// Sandbox stand-in for <c>AttributeAccessor.WaitForAsync</c> (predicate form);
/// see <see cref="WriteBatchAndWaitAsync"/>.
/// </summary>
public Task<WaitResult> WaitForAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
=> throw NotInSandbox(nameof(WaitForAsync));
private static ScriptSandboxException NotInSandbox(string member) =>
new($"{member}(...) drives live device tags and the site's event-driven " +
"attribute waiter, which aren't available in the central Test Run sandbox — " +
"deploy to a site to exercise batch-write/wait handshakes.");
}
/// <summary>