fix(adminui): Monaco text syncs on demand, diagnostics are sequenced, Problems list (D1)

R04:P1 - the editor no longer ships its full text JS->.NET on every keystroke. It reports
at blur, and ScriptEdit / the VirtualTag modal read the authoritative value with
MonacoEditor.GetValueAsync at Save (validation runs after the pull). The Blazor hub's
MaximumReceiveMessageSize is raised from 32 KB to 1 MB. The JS refuses a value that
would not fit instead of sending an oversize frame, and Save surfaces that refusal
rather than persisting stale source.

R04:P9 - diagnostics carry a generation counter and an AbortController. Every edit aborts
the in-flight request, and only the response for the current text is applied.

R04:S2 - the always-empty inlay-hints provider, endpoint, service method and contracts
are removed.

R04:S3 - MarkersChanged is typed (DiagnosticMarker) and feeds a new ScriptProblemsList.
Clicking a problem calls RevealLineAsync. Markers cross to .NET only when a list
subscribes.

Refs #745

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 17:38:56 -04:00
parent 8e3bbf91fb
commit da2578833d
14 changed files with 514 additions and 61 deletions
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;
/// <summary>
/// R04:P1 / S2 — the Monaco editor no longer ships its text over the circuit per keystroke; the page reads it on
/// demand at blur / Save. That single read is one hub message, so the Blazor hub's receive limit is raised from
/// the 32 KB default to 1 MB, and the JS refuses (rather than sends) a value that would not fit.
/// </summary>
public sealed class MonacoSyncTests
{
private static HubOptions ComponentHubOptions(IServiceProvider sp)
{
// ComponentHub is internal to Microsoft.AspNetCore.Components.Server; resolve its options by type name.
var hubType = Type.GetType("Microsoft.AspNetCore.Components.Server.ComponentHub, Microsoft.AspNetCore.Components.Server", throwOnError: true)!;
var optionsType = typeof(IOptions<>).MakeGenericType(typeof(HubOptions<>).MakeGenericType(hubType));
var options = sp.GetRequiredService(optionsType);
return (HubOptions)optionsType.GetProperty("Value")!.GetValue(options)!;
}
[Fact]
public void The_blazor_hub_accepts_a_1_MB_message()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddAdminUI();
using var sp = services.BuildServiceProvider();
ComponentHubOptions(sp).MaximumReceiveMessageSize.ShouldBe(1024 * 1024);
ScriptEditorLimits.HubMaximumReceiveMessageSize.ShouldBe(1024 * 1024);
}
[Fact]
public void Without_AddAdminUI_the_default_limit_is_the_32_KB_that_truncated_large_scripts()
{
// Positive control for the test above: the 1 MB comes from AddAdminUI, not from a framework default.
var services = new ServiceCollection();
services.AddLogging();
services.AddRazorComponents().AddInteractiveServerComponents();
using var sp = services.BuildServiceProvider();
(ComponentHubOptions(sp).MaximumReceiveMessageSize ?? 32 * 1024).ShouldBe(32 * 1024);
}
[Fact]
public void The_editor_sync_budget_leaves_headroom_inside_the_hub_limit_and_fits_a_100_KB_script()
{
ScriptEditorLimits.MaxEditorSyncBytes.ShouldBeLessThan((int)ScriptEditorLimits.HubMaximumReceiveMessageSize);
ScriptEditorLimits.MaxEditorSyncBytes.ShouldBeGreaterThan(4 * 100 * 1024); // 100 KB even after escaping twice
}
}