test: migrate AdminUI/ControlPlane/Runtime/Host tests to xunit.v3 (#756)
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
This commit is contained in:
Joseph Doherty
2026-11-10 08:57:33 -05:00
parent 2a8e2981fe
commit b211911f98
191 changed files with 2161 additions and 2166 deletions
@@ -50,9 +50,9 @@ public sealed class AnalysisCompilationReuseTests
// A clean script stays clean and an error stays an error across interleaved analyses on one service
// — a stale tree leaking from the previous call would surface here.
var svc = new ScriptAnalysisService();
svc.Diagnose(new DiagnoseRequest("return ctx.Nope();")).Markers.ShouldContain(m => m.Severity == 8);
svc.Diagnose(new DiagnoseRequest("""return (double)ctx.GetTag("A").Value * 2.0;""")).Markers.ShouldBeEmpty();
svc.Diagnose(new DiagnoseRequest("var a = 1;\nreturn ctx.Nope();")).Markers
svc.Diagnose(new DiagnoseRequest("return ctx.Nope();"), TestContext.Current.CancellationToken).Markers.ShouldContain(m => m.Severity == 8);
svc.Diagnose(new DiagnoseRequest("""return (double)ctx.GetTag("A").Value * 2.0;"""), TestContext.Current.CancellationToken).Markers.ShouldBeEmpty();
svc.Diagnose(new DiagnoseRequest("var a = 1;\nreturn ctx.Nope();"), TestContext.Current.CancellationToken).Markers
.ShouldContain(m => m.Severity == 8 && m.StartLineNumber == 2);
}
}
@@ -23,34 +23,34 @@ public sealed class CtxCompletionGuardTests
[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));
var res = await Svc.CompleteAsync(new CompletionsRequest("return ctx.GetTag(\"\")", 1, 20), TestContext.Current.CancellationToken);
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));
var res = await Svc.CompleteAsync(new CompletionsRequest("return foo.GetTag(\"\")", 1, 20), TestContext.Current.CancellationToken);
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;
var md = (await Svc.Hover(new HoverRequest("return ctx.SetVirtualTag(\"V\", 1);", 1, 27), TestContext.Current.CancellationToken)).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;
var md = (await Svc.Hover(new HoverRequest("return ctx.GetTag(\"V\").Value;", 1, 20), TestContext.Current.CancellationToken)).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;
var md = (await Svc.Hover(new HoverRequest("return bar.GetTag(\"V\");", 1, 20), TestContext.Current.CancellationToken)).Markdown;
(md is null || !md.Contains("Tag path")).ShouldBeTrue();
}
}
@@ -65,7 +65,7 @@ public sealed class EquipTokenEditorTests
[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;
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)
@@ -78,14 +78,14 @@ public sealed class EquipTokenEditorTests
// "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"));
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"));
new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Speed").Value;""", "EQ-1"), TestContext.Current.CancellationToken);
resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF");
}
@@ -94,7 +94,7 @@ public sealed class EquipTokenEditorTests
// 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;"""));
new DiagnoseRequest("""return ctx.GetTag("{{equip}}/Missing").Value;"""), TestContext.Current.CancellationToken);
resp.Markers.ShouldNotContain(m => m.Code == "OTSCRIPT_EQUIPREF");
}
}
@@ -23,18 +23,18 @@ public sealed class HoverSignatureTests
[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;
var md = (await Svc.Hover(new HoverRequest("""return ctx.GetTag("A").Value;""", 1, 16), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("GetTag");
}
[Fact] public async Task Hover_on_nothing_returns_null()
=> (await Svc.Hover(new HoverRequest(" ", 1, 1))).Markdown.ShouldBeNull();
=> (await Svc.Hover(new HoverRequest(" ", 1, 1), TestContext.Current.CancellationToken)).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));
var sh = Svc.SignatureHelp(new SignatureHelpRequest("""return ctx.GetTag();""", 1, 19), TestContext.Current.CancellationToken);
sh.Label.ShouldNotBeNull();
sh.Label!.ShouldContain("GetTag");
sh.Parameters.ShouldNotBeNull();
@@ -44,7 +44,7 @@ public sealed class HoverSignatureTests
[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;
var md = (await Svc.Hover(new HoverRequest("var x = 1;\nreturn ctx.GetTag(\"A\").Value + x;", 2, 33), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("x");
md.ShouldNotContain("GetTag");
@@ -54,12 +54,12 @@ public sealed class HoverSignatureTests
{
// 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));
var sh = Svc.SignatureHelp(new SignatureHelpRequest("return ctx.GetTag();", 1, 19), TestContext.Current.CancellationToken);
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();
=> (await Svc.Hover(new HoverRequest("return 1;", 99, 99), TestContext.Current.CancellationToken)).Markdown.ShouldBeNull();
// ── tag-path hover (driven by the catalog) ─────────────────────────────────────────────────
@@ -67,7 +67,7 @@ public sealed class HoverSignatureTests
{
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;
var md = (await svc.Hover(new HoverRequest("""return ctx.GetTag("Line1.Speed").Value;""", 1, 24), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("Line1.Speed");
md.ShouldContain("Double");
@@ -79,7 +79,7 @@ public sealed class HoverSignatureTests
{
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;
var md = (await svc.Hover(new HoverRequest("""return ctx.GetTag("Unknownz").Value;""", 1, 22), TestContext.Current.CancellationToken)).Markdown;
md.ShouldNotBeNull();
md!.ShouldContain("Not a known");
}
@@ -26,7 +26,7 @@ public sealed class MarkerPayloadBoundTests
[Fact]
public void A_non_csharp_paste_returns_a_bounded_marker_set_that_still_holds_the_errors()
{
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest(NonCSharpPaste())).Markers;
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest(NonCSharpPaste()), TestContext.Current.CancellationToken).Markers;
markers.Count.ShouldBe(ScriptEditorLimits.MaxDiagnosticMarkers); // positive control: the cap was hit
markers.ShouldAllBe(m => m.Message.Length <= ScriptEditorLimits.MaxDiagnosticMessageChars);
@@ -39,7 +39,7 @@ public sealed class MarkerPayloadBoundTests
public void A_long_quoted_token_has_its_message_truncated()
{
var giant = new string('x', 20_000);
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest($"return {giant};")).Markers;
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest($"return {giant};"), TestContext.Current.CancellationToken).Markers;
markers.ShouldNotBeEmpty();
markers.ShouldAllBe(m => m.Message.Length <= ScriptEditorLimits.MaxDiagnosticMessageChars);
@@ -50,7 +50,7 @@ public sealed class MarkerPayloadBoundTests
public void A_small_script_is_not_truncated()
{
// Positive control for the cap: ordinary diagnostics pass through untouched.
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest("return ctx.Nope();")).Markers;
var markers = new ScriptAnalysisService().Diagnose(new DiagnoseRequest("return ctx.Nope();"), TestContext.Current.CancellationToken).Markers;
markers.ShouldContain(m => m.Severity == 8 && !m.Message.EndsWith("…"));
}
@@ -168,7 +168,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
var paths = await catalog.GetPathsAsync(null, default);
var paths = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
paths.ShouldContain("Plant/Modbus/dev1/Speed"); // directly under the device
paths.ShouldContain("Plant/Modbus/dev1/Motors/Torque"); // nested under a tag group
@@ -190,7 +190,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
var paths = await catalog.GetPathsAsync(null, default);
var paths = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
paths.Distinct(StringComparer.OrdinalIgnoreCase).Count().ShouldBe(paths.Count);
}
@@ -203,7 +203,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
var paths = await catalog.GetPathsAsync("Plant/Modbus/dev1/", default);
var paths = await catalog.GetPathsAsync("Plant/Modbus/dev1/", TestContext.Current.CancellationToken);
paths.ShouldContain("Plant/Modbus/dev1/Speed");
paths.ShouldContain("Plant/Modbus/dev1/Motors/Torque");
@@ -218,7 +218,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetPathsAsync("plant/modbus", default)).ShouldContain("Plant/Modbus/dev1/Speed");
(await catalog.GetPathsAsync("plant/modbus", TestContext.Current.CancellationToken)).ShouldContain("Plant/Modbus/dev1/Speed");
}
/// <summary>A tag whose ancestry chain is broken (device row missing) has no RawPath. The runtime could
@@ -302,7 +302,7 @@ public sealed class ScriptTagCatalogTests
db.SaveChanges();
}
(await catalog.GetPathsAsync(null, default)).ShouldContain("RootDrv/d0/Scalar");
(await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken)).ShouldContain("RootDrv/d0/Scalar");
}
/// <summary>An empty database yields an empty list rather than throwing.</summary>
@@ -311,7 +311,7 @@ public sealed class ScriptTagCatalogTests
{
var (catalog, _) = Fresh();
(await catalog.GetPathsAsync(null, default)).ShouldBeEmpty();
(await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken)).ShouldBeEmpty();
}
/// <summary>Hover: an absolute RawPath resolves to Tag-kind info carrying the configured DataType and —
@@ -322,7 +322,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
var info = await catalog.GetTagInfoAsync("Plant/Modbus/dev1/Motors/Torque", default);
var info = await catalog.GetTagInfoAsync("Plant/Modbus/dev1/Motors/Torque", TestContext.Current.CancellationToken);
info.ShouldNotBeNull();
info!.Path.ShouldBe("Plant/Modbus/dev1/Motors/Torque");
@@ -339,8 +339,8 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetTagInfoAsync("Computed", default)).ShouldBeNull();
var info = await catalog.GetTagInfoAsync("EQ-1/Computed", default);
(await catalog.GetTagInfoAsync("Computed", TestContext.Current.CancellationToken)).ShouldBeNull();
var info = await catalog.GetTagInfoAsync("EQ-1/Computed", TestContext.Current.CancellationToken);
info.ShouldNotBeNull();
info!.Kind.ShouldBe("Virtual tag");
@@ -356,7 +356,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetTagInfoAsync("Motor.Speed", default)).ShouldBeNull();
(await catalog.GetTagInfoAsync("Motor.Speed", TestContext.Current.CancellationToken)).ShouldBeNull();
}
/// <summary>An unknown path resolves to null.</summary>
@@ -366,7 +366,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetTagInfoAsync("Plant/Modbus/dev1/NoSuchTag", default)).ShouldBeNull();
(await catalog.GetTagInfoAsync("Plant/Modbus/dev1/NoSuchTag", TestContext.Current.CancellationToken)).ShouldBeNull();
}
/// <summary>The lookup is case-SENSITIVE (Ordinal), mirroring the runtime DependencyMuxActor's
@@ -378,8 +378,8 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts) = Fresh();
Seed(opts);
(await catalog.GetTagInfoAsync("plant/modbus/dev1/speed", default)).ShouldBeNull();
(await catalog.GetTagInfoAsync("Plant/Modbus/dev1/Speed", default)).ShouldNotBeNull();
(await catalog.GetTagInfoAsync("plant/modbus/dev1/speed", TestContext.Current.CancellationToken)).ShouldBeNull();
(await catalog.GetTagInfoAsync("Plant/Modbus/dev1/Speed", TestContext.Current.CancellationToken)).ShouldNotBeNull();
}
// -----------------------------------------------------------------------
@@ -396,8 +396,8 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts, factory, _) = FreshInstrumented();
Seed(opts);
var first = await catalog.GetPathsAsync(null, default);
var second = await catalog.GetPathsAsync(null, default);
var first = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
var second = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
factory.Created.ShouldBe(1);
second.ShouldBe(first);
@@ -412,7 +412,7 @@ public sealed class ScriptTagCatalogTests
var (catalog, opts, factory, time) = FreshInstrumented();
Seed(opts);
(await catalog.GetPathsAsync(null, default)).ShouldNotContain("Plant/Modbus/dev1/Pressure");
(await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken)).ShouldNotContain("Plant/Modbus/dev1/Pressure");
factory.Created.ShouldBe(1);
using (var db = new OtOpcUaConfigDbContext(opts))
@@ -427,7 +427,7 @@ public sealed class ScriptTagCatalogTests
time.Advance(TimeSpan.FromSeconds(6));
var after = await catalog.GetPathsAsync(null, default);
var after = await catalog.GetPathsAsync(null, TestContext.Current.CancellationToken);
factory.Created.ShouldBe(2);
after.ShouldContain("Plant/Modbus/dev1/Pressure");
@@ -445,19 +445,19 @@ public sealed class ScriptTagCatalogTests
factory.Gate = gate;
var first = Task.Run(() => catalog.GetPathsAsync(null, default));
await factory.FirstRequest.Task.WaitAsync(TimeSpan.FromSeconds(10));
await factory.FirstRequest.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
var second = Task.Run(() => catalog.GetPathsAsync(null, default));
// Give the second caller ample time to reach the cache. It must be parked on the rebuild gate,
// NOT scanning: a second scan here is exactly the duplicated work this test falsifies.
for (var i = 0; i < 50 && factory.Created < 2; i++) await Task.Delay(10);
for (var i = 0; i < 50 && factory.Created < 2; i++) await Task.Delay(10, TestContext.Current.CancellationToken);
factory.Created.ShouldBe(1);
gate.SetResult();
var a = await first.WaitAsync(TimeSpan.FromSeconds(10));
var b = await second.WaitAsync(TimeSpan.FromSeconds(10));
var a = await first.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
var b = await second.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
factory.Created.ShouldBe(1);
a.ShouldContain("Plant/Modbus/dev1/Speed");
@@ -473,14 +473,14 @@ public sealed class ScriptTagCatalogTests
var cache = new ScriptTagCatalogSnapshotCache(time);
IReadOnlyList<ScriptTagInfo> good = [new ScriptTagInfo("A/B", "Tag", "Float", "DRV-1")];
(await cache.GetOrBuildAsync(_ => Task.FromResult(good), default)).ShouldBe(good);
(await cache.GetOrBuildAsync(_ => Task.FromResult(good), TestContext.Current.CancellationToken)).ShouldBe(good);
time.Advance(TimeSpan.FromSeconds(6));
await Should.ThrowAsync<InvalidOperationException>(
cache.GetOrBuildAsync(_ => Task.FromException<IReadOnlyList<ScriptTagInfo>>(new InvalidOperationException("boom")), default));
cache.GetOrBuildAsync(_ => Task.FromException<IReadOnlyList<ScriptTagInfo>>(new InvalidOperationException("boom")), TestContext.Current.CancellationToken));
// The gate was released and the old snapshot survived, so the next build still runs and succeeds.
(await cache.GetOrBuildAsync(_ => Task.FromResult(good), default)).ShouldBe(good);
(await cache.GetOrBuildAsync(_ => Task.FromResult(good), TestContext.Current.CancellationToken)).ShouldBe(good);
}
// -----------------------------------------------------------------------
@@ -514,7 +514,7 @@ public sealed class ScriptTagCatalogTests
Seed(opts);
SeedReferences(opts);
var names = await catalog.GetEquipmentReferenceNamesAsync("EQ-1", null, default);
var names = await catalog.GetEquipmentReferenceNamesAsync("EQ-1", null, TestContext.Current.CancellationToken);
names.ShouldContain("Speed"); // TAG-EQ Name, no override
names.ShouldContain("Path"); // TAG-SP under the "Path" override
@@ -529,7 +529,7 @@ public sealed class ScriptTagCatalogTests
Seed(opts);
SeedReferences(opts);
var names = await catalog.GetEquipmentReferenceNamesAsync("EQ-1", "sp", default);
var names = await catalog.GetEquipmentReferenceNamesAsync("EQ-1", "sp", TestContext.Current.CancellationToken);
names.ShouldContain("Speed");
names.ShouldNotContain("Path");
@@ -543,7 +543,7 @@ public sealed class ScriptTagCatalogTests
Seed(opts);
SeedReferences(opts);
(await catalog.GetEquipmentReferenceNamesAsync("EQ-UNKNOWN", null, default)).ShouldBeEmpty();
(await catalog.GetEquipmentReferenceNamesAsync("", null, default)).ShouldBeEmpty();
(await catalog.GetEquipmentReferenceNamesAsync("EQ-UNKNOWN", null, TestContext.Current.CancellationToken)).ShouldBeEmpty();
(await catalog.GetEquipmentReferenceNamesAsync("", null, TestContext.Current.CancellationToken)).ShouldBeEmpty();
}
}