b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask, SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass TestContext.Current.CancellationToken. Test counts unchanged. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
101 lines
5.1 KiB
C#
101 lines
5.1 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <c>{{equip}}/</c> offers the owning equipment's reference effective names; and the diagnostic flags an
|
|
/// unresolved <c>{{equip}}/<RefName></c> exactly as the deploy gate does (editor accepts ⇔ publish
|
|
/// accepts).
|
|
/// </summary>
|
|
public sealed class EquipTokenEditorTests
|
|
{
|
|
/// <summary>A fake catalog whose equipment has two references — "Speed" and "Temp" — so those are the
|
|
/// resolvable {{equip}}/<RefName> names for the completion + diagnostic.</summary>
|
|
private sealed class FakeCatalog : IScriptTagCatalog
|
|
{
|
|
public Task<IReadOnlyList<string>> GetPathsAsync(string? filter, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<string>>(new[] { "Cell1/Modbus/Dev1/Speed" });
|
|
|
|
public Task<ScriptTagInfo?> GetTagInfoAsync(string path, CancellationToken ct)
|
|
=> Task.FromResult<ScriptTagInfo?>(null);
|
|
|
|
public Task<IReadOnlyList<string>> GetEquipmentReferenceNamesAsync(string equipmentId, string? filter, CancellationToken ct)
|
|
{
|
|
IEnumerable<string> names = new[] { "Speed", "Temp" };
|
|
if (!string.IsNullOrWhiteSpace(filter))
|
|
names = names.Where(n => n.StartsWith(filter, StringComparison.OrdinalIgnoreCase));
|
|
return Task.FromResult<IReadOnlyList<string>>(names.ToList());
|
|
}
|
|
}
|
|
|
|
private static readonly ScriptAnalysisService Svc = new(new FakeCatalog());
|
|
|
|
private static async Task<IReadOnlyList<CompletionItem>> 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), TestContext.Current.CancellationToken)).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"), TestContext.Current.CancellationToken);
|
|
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"), TestContext.Current.CancellationToken);
|
|
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;"""), TestContext.Current.CancellationToken);
|
|
resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF");
|
|
}
|
|
}
|