fix(adminui): bound the diagnostics marker payload on both the endpoint and the circuit (D-a review r1)

I-1 - a non-C# paste produced thousands of Roslyn markers. The Problems-list report sent
them all as one JS->.NET message, which could exceed the 1 MB hub limit and kill the
circuit. The JS now sends at most MaxReportedMarkers (200) markers, errors first, each
message cut to 300 chars, plus the total. ScriptProblemReport.FromEditor clamps again
on arrival, and the Problems list shows "showing N of Total" / "+N more". The diagnostics
endpoint also bounds its response: Distinct, errors first, MaxDiagnosticMarkers (1000),
messages cut to 1000 chars. An error is always kept, so editor-accepts == publish-accepts.

M-1 - drop the per-instance compilation lineage; analyses derive from BaseCompilation.
M-2 - the VirtualTag modal clears its problems (and editor ref) on close and on a script
reload.
M-5 - an over-limit text is announced at blur (OnSyncRefused), not only at Save.

Refs #745

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 17:51:35 -04:00
parent 17c7d05696
commit a9e8f5abfe
10 changed files with 305 additions and 34 deletions
@@ -62,10 +62,34 @@ public sealed class MonacoInteropSourceTests
[Fact]
public void Markers_cross_to_dotnet_only_for_a_subscribed_problems_list()
{
Js().ShouldContain("if (e.reportMarkers) dotNetRef.invokeMethodAsync(\"OnMarkersChanged\"");
Js().ShouldContain("if (e.reportMarkers) {");
Read("Components", "Shared", "MonacoEditor.razor").ShouldContain("reportMarkers = MarkersChanged.HasDelegate");
}
[Fact]
public void The_markers_report_is_capped_before_it_crosses_the_circuit()
{
// Review I-1: the only OnMarkersChanged invoke sends the capped report, never the raw marker array.
var js = Js();
Regex.Matches(js, "\"OnMarkersChanged\"").Count.ShouldBe(1);
js.ShouldContain("dotNetRef.invokeMethodAsync(\"OnMarkersChanged\", report.markers, report.total)");
js.ShouldContain("const report = markerReport(e, markers);");
js.ShouldContain(".slice(0, max)");
js.ShouldContain("m.message.substring(0, maxChars - 1)");
// The caps come from the C# constants, so the JS and ScriptProblemReport.FromEditor agree.
var razor = Read("Components", "Shared", "MonacoEditor.razor");
razor.ShouldContain("maxReportedMarkers = ScriptEditorLimits.MaxReportedMarkers");
razor.ShouldContain("maxReportedMessageChars = ScriptEditorLimits.MaxReportedMessageChars");
razor.ShouldContain("ScriptProblemReport.FromEditor(markers, total)");
}
[Fact]
public void An_oversize_text_is_announced_at_blur_not_only_at_save()
{
Js().ShouldContain("invokeMethodAsync(\"OnSyncRefused\", serializedBytes(v))");
Read("Components", "Shared", "MonacoEditor.razor").ShouldContain("public void OnSyncRefused(int bytes)");
}
[Theory]
[InlineData("Components", "Pages", "ScriptEdit.razor")]
[InlineData("Components", "Shared", "Uns", "VirtualTagModal.razor")]