dc0d7653b9
Replace the deleted equipment-base-prefix {{equip}} mechanism (impossible now
that equipment is reference-only) with per-reference resolution:
{{equip}}/<RefName> resolves through the owning equipment's UnsTagReference rows
(by effective name) to the backing raw tag's RawPath. Every unresolved <RefName>
is a clear deploy error + authoring rejection + Monaco diagnostic — preserving
"editor accepts <=> publish accepts".
Commons:
- EquipmentScriptPaths: delete DeriveEquipmentBase; SubstituteEquipmentToken now
takes the equipment's reference map (effectiveName -> RawPath) and substitutes
{{equip}}/<RefName> inside path literals (unresolved left intact, never throws);
add ExtractEquipReferenceNames (path-literal scoped) +
ExtractEquipReferenceNamesFromText (message templates). Slash syntax replaces
the v2 dot joint.
- New EquipmentReferenceMap: shared, input-shape-agnostic builder (entity + JSON
sides) over RawPathResolver — the single authority both compose seams + the
validator use, so resolved RawPaths agree byte-for-byte.
Compose seams (byte-parity kept):
- AddressSpaceComposer.Compose + DeploymentArtifact.ParseComposition both build
the per-equipment reference map from UnsTagReferences + Tags + raw topology and
substitute VirtualTag AND ScriptedAlarm-predicate sources before dependency
extraction (runtime dep refs = resolved RawPaths).
Deploy gate + authoring + editor:
- DraftValidator.ValidateEquipReferenceResolution: EquipReferenceUnresolved error
for unresolved {{equip}}/<RefName> in VirtualTag/ScriptedAlarm scripts + alarm
message-template tokens (naming script, equipment, missing ref).
- UnsTreeService.ValidateEquipTokenAsync (Wave-A M1): reference-relative authoring
rejection, replacing the stale DeriveEquipmentBase(empty)->reject-all logic.
- ScriptAnalysisService: {{equip}}/ completion offers the equipment's reference
effective names; DiagnoseAsync flags unresolved refs (OTSCRIPT_EQUIPREF) same as
the deploy gate. Requests carry optional EquipmentId (threaded MonacoEditor ->
monaco-init.js -> fetch bodies). IScriptTagCatalog.GetEquipmentRelativeLeavesAsync
repurposed to GetEquipmentReferenceNamesAsync(equipmentId, filter).
Tests: EquipmentScriptPaths substitution/extraction (slash pinned); composer<->
artifact reference-resolution byte-parity; DraftValidator unresolved-ref;
ScriptAnalysis completion+diagnostic; M1 authoring gate. Build clean (0/0); all
five affected suites green.
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
57 lines
2.5 KiB
C#
57 lines
2.5 KiB
C#
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;
|
|
|
|
public sealed class CtxCompletionGuardTests
|
|
{
|
|
private sealed class FakeCatalog : IScriptTagCatalog
|
|
{
|
|
public Task<IReadOnlyList<string>> GetPathsAsync(string? f, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<string>>(new[] { "Line1.Speed" });
|
|
public Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct)
|
|
=> Task.FromResult<ScriptTagInfo?>(new ScriptTagInfo(path, "Virtual tag", "Double", null));
|
|
public Task<IReadOnlyList<string>> GetEquipmentReferenceNamesAsync(string equipmentId, string? f, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<string>>(System.Array.Empty<string>());
|
|
}
|
|
|
|
private static readonly ScriptAnalysisService Svc = new(new FakeCatalog());
|
|
|
|
[Fact] public async Task Completion_on_ctx_GetTag_literal_offers_catalog_paths()
|
|
{
|
|
// "return ctx.GetTag(" + '""' + ')' — col 20 lands on the closing quote of the empty string literal.
|
|
var res = await Svc.CompleteAsync(new CompletionsRequest("return ctx.GetTag(\"\")", 1, 20));
|
|
res.Items.ShouldContain(i => i.InsertText == "Line1.Speed");
|
|
}
|
|
|
|
[Fact] public async Task Completion_on_non_ctx_receiver_does_NOT_offer_catalog_paths()
|
|
{
|
|
// Same shape but receiver is "foo" — must NOT offer catalog paths.
|
|
var res = await Svc.CompleteAsync(new CompletionsRequest("return foo.GetTag(\"\")", 1, 20));
|
|
res.Items.ShouldNotContain(i => i.InsertText == "Line1.Speed");
|
|
}
|
|
|
|
[Fact] public async Task Hover_on_ctx_SetVirtualTag_literal_warns_write_is_dropped()
|
|
{
|
|
var md = (await Svc.Hover(new HoverRequest("return ctx.SetVirtualTag(\"V\", 1);", 1, 27))).Markdown;
|
|
md.ShouldNotBeNull();
|
|
md!.ShouldContain("single-tag mode");
|
|
}
|
|
|
|
[Fact] public async Task Hover_on_ctx_GetTag_literal_has_no_dropped_write_note()
|
|
{
|
|
var md = (await Svc.Hover(new HoverRequest("return ctx.GetTag(\"V\").Value;", 1, 20))).Markdown;
|
|
md.ShouldNotBeNull();
|
|
md!.ShouldNotContain("single-tag mode");
|
|
}
|
|
|
|
[Fact] public async Task Hover_on_non_ctx_receiver_literal_is_not_treated_as_a_tag_path()
|
|
{
|
|
var md = (await Svc.Hover(new HoverRequest("return bar.GetTag(\"V\");", 1, 20))).Markdown;
|
|
(md is null || !md.Contains("Tag path")).ShouldBeTrue();
|
|
}
|
|
}
|