using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis; /// /// v3 {{equip}}/<RefName> editor niceties: hover on a path literal containing the token shows an /// "equipment-relative" note (not the "not a known configured tag path" warning); completion after /// {{equip}}/ offers the owning equipment's reference effective names; and the diagnostic flags an /// unresolved {{equip}}/<RefName> exactly as the deploy gate does (editor accepts ⇔ publish /// accepts). /// public sealed class EquipTokenEditorTests { /// A fake catalog whose equipment has two references — "Speed" and "Temp" — so those are the /// resolvable {{equip}}/<RefName> names for the completion + diagnostic. private sealed class FakeCatalog : IScriptTagCatalog { public Task> GetPathsAsync(string? filter, CancellationToken ct) => Task.FromResult>(new[] { "Cell1/Modbus/Dev1/Speed" }); public Task GetTagInfoAsync(string path, CancellationToken ct) => Task.FromResult(null); public Task> GetEquipmentReferenceNamesAsync(string equipmentId, string? filter, CancellationToken ct) { IEnumerable names = new[] { "Speed", "Temp" }; if (!string.IsNullOrWhiteSpace(filter)) names = names.Where(n => n.StartsWith(filter, StringComparison.OrdinalIgnoreCase)); return Task.FromResult>(names.ToList()); } } private static readonly ScriptAnalysisService Svc = new(new FakeCatalog()); private static async Task> Items(string code, int line, int col, string? equipmentId) => (await Svc.CompleteAsync(new CompletionsRequest(code, line, col, equipmentId))).Items; [Fact] public async Task Completion_after_equip_slash_offers_reference_names() { // caret right after the slash of "{{equip}}/" — column 30 sits between the trailing slash and the // closing quote of ctx.GetTag("{{equip}}/"). var items = await Items("""return ctx.GetTag("{{equip}}/").Value;""", 1, 30, "EQ-1"); items.Select(i => i.Label).ShouldContain("{{equip}}/Speed"); items.Select(i => i.Label).ShouldContain("{{equip}}/Temp"); items.ShouldAllBe(i => i.Detail == "tag path"); } [Fact] public async Task Completion_after_equip_slash_inserts_the_full_token_qualified_ref() { var items = await Items("""return ctx.GetTag("{{equip}}/").Value;""", 1, 30, "EQ-1"); var speed = items.FirstOrDefault(i => i.Label == "{{equip}}/Speed"); speed.ShouldNotBeNull(); speed!.InsertText.ShouldBe("{{equip}}/Speed"); } [Fact] public async Task Completion_after_equip_slash_is_inert_without_equipment_context() { // Shared ScriptEdit page (no EquipmentId) can't resolve references → no equip completions. var items = await Items("""return ctx.GetTag("{{equip}}/").Value;""", 1, 30, null); items.ShouldBeEmpty(); } [Fact] public async Task Hover_on_equip_token_literal_notes_equipment_relative() { // caret inside ctx.GetTag("{{equip}}/Speed") var md = (await Svc.Hover(new HoverRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", 1, 24))).Markdown; md.ShouldNotBeNull(); md!.ShouldContain("Equipment-relative"); // the {{equip}} token must render literally (non-interpolated markdown segment) md.ShouldContain("{{equip}}"); md.ShouldNotContain("Not a known"); } [Fact] public async Task Diagnostic_flags_unresolved_equip_reference() { // "Missing" is not one of the equipment's references (Speed/Temp) → a diagnostic, matching the // deploy gate. Uses the equipment context via the request's EquipmentId. var resp = await Svc.DiagnoseAsync( new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;""", "EQ-1")); resp.Markers.ShouldContain(m => m.Code == "OTSCRIPT_EQUIPREF" && m.Message.Contains("Missing")); } [Fact] public async Task Diagnostic_clean_for_resolved_equip_reference() { var resp = await Svc.DiagnoseAsync( new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", "EQ-1")); resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF"); } [Fact] public async Task Diagnostic_equipref_inert_without_equipment_context() { // No EquipmentId (shared ScriptEdit page) → the equip-ref check is inert (base markers only), // matching the deploy gate which only resolves per owning equipment. var resp = await Svc.DiagnoseAsync( new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;""")); resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF"); } }