Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ScriptAnalysis/HoverSignatureTests.cs
T
Joseph Doherty dc0d7653b9 feat(v3-batch3): B3-WP4 — {{equip}}/<RefName> reference-relative resolution
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
2026-07-16 06:58:17 -04:00

87 lines
4.2 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;
public sealed class HoverSignatureTests
{
private static readonly ScriptAnalysisService Svc = new();
/// <summary>A fake catalog that resolves only "Line1.Speed" (Tag/Double/driver) and nothing else,
/// so the tag-path hover branch can be exercised without a database.</summary>
private sealed class FakeCatalog : IScriptTagCatalog
{
public Task<IReadOnlyList<string>> GetPathsAsync(string? f, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<string>>(System.Array.Empty<string>());
public Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct)
=> Task.FromResult(path == "Line1.Speed" ? new ScriptTagInfo("Line1.Speed", "Tag", "Double", "MAIN-modbus") : null);
public Task<IReadOnlyList<string>> GetEquipmentReferenceNamesAsync(string equipmentId, string? filter, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<string>>(new[] { "Speed" });
}
[Fact] public async Task Hover_on_GetTag_returns_member_markdown()
{
// hover over the "GetTag" identifier in ctx.GetTag("A")
var md = (await Svc.Hover(new HoverRequest("""return ctx.GetTag("A").Value;""", 1, 16))).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("GetTag");
}
[Fact] public async Task Hover_on_nothing_returns_null()
=> (await Svc.Hover(new HoverRequest(" ", 1, 1))).Markdown.ShouldBeNull();
[Fact] public void SignatureHelp_inside_GetTag_call_shows_the_signature()
{
// caret just after the open paren of ctx.GetTag(
var sh = Svc.SignatureHelp(new SignatureHelpRequest("""return ctx.GetTag();""", 1, 19));
sh.Label.ShouldNotBeNull();
sh.Label!.ShouldContain("GetTag");
sh.Parameters.ShouldNotBeNull();
sh.Parameters!.ShouldContain(p => p.Label.Contains("path") || p.Label.Contains("string"));
}
[Fact] public async Task Hover_on_a_plain_local_resolves_the_local_not_an_enclosing_member()
{
// hovering the local `x` (not a member) must resolve the LOCAL, and must NOT climb to GetTag/Value.
var md = (await Svc.Hover(new HoverRequest("var x = 1;\nreturn ctx.GetTag(\"A\").Value + x;", 2, 33))).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("x");
md.ShouldNotContain("GetTag");
}
[Fact] public void SignatureHelp_for_parameterless_call_does_not_throw_and_clamps_active_to_zero()
{
// ctx.Now is a property, not a call; use a parameterless method if one exists, else assert empty-safe behavior.
// Deadband is static on ScriptContext (3 params) — instead verify a 0-arg edge via an empty arg list doesn't throw:
var sh = Svc.SignatureHelp(new SignatureHelpRequest("return ctx.GetTag();", 1, 19));
sh.ActiveParameter.ShouldBe(0); // clamped; no exception
}
[Fact] public async Task Hover_with_out_of_range_position_returns_null_without_throwing()
=> (await Svc.Hover(new HoverRequest("return 1;", 99, 99))).Markdown.ShouldBeNull();
// ── tag-path hover (driven by the catalog) ─────────────────────────────────────────────────
[Fact] public async Task Hover_on_known_tag_path_literal_shows_kind_type_and_path()
{
var svc = new ScriptAnalysisService(new FakeCatalog());
// caret on the path literal inside ctx.GetTag("Line1.Speed")
var md = (await svc.Hover(new HoverRequest("""return ctx.GetTag("Line1.Speed").Value;""", 1, 24))).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("Line1.Speed");
md.ShouldContain("Double");
md.ShouldContain("Tag");
md.ShouldContain("MAIN-modbus");
}
[Fact] public async Task Hover_on_unknown_tag_path_literal_reports_not_known()
{
var svc = new ScriptAnalysisService(new FakeCatalog());
// caret on the path literal inside ctx.GetTag("Unknownz")
var md = (await svc.Hover(new HoverRequest("""return ctx.GetTag("Unknownz").Value;""", 1, 22))).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("Not a known");
}
}