using System.Text; using System.Text.Json; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis; /// /// Review D-a I-1 — the diagnostics markers cross the Blazor circuit (JS → .NET, for the Problems list) as ONE /// hub message. A non-C# paste yields several Roslyn errors per line; uncapped, a few thousand markers exceed /// the 1 MB hub limit and kill the circuit with every unsaved edit. Both the endpoint response and the circuit /// report are now bounded; these tests pin the bounds on the C# side of the contract. /// public sealed class MarkerPayloadBoundTests { // ~150 KB of log-ish text: every line is several C# errors. private static string NonCSharpPaste() { var sb = new StringBuilder(); for (var i = 0; sb.Length < 150 * 1024; i++) sb.Append($"2026-10-22 12:00:{i % 60:00} INFO [worker-{i}] value={i}, state=ok; next -> {i + 1} @@ ##\n"); return sb.ToString(); } [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; markers.Count.ShouldBe(ScriptEditorLimits.MaxDiagnosticMarkers); // positive control: the cap was hit markers.ShouldAllBe(m => m.Message.Length <= ScriptEditorLimits.MaxDiagnosticMessageChars); markers[0].Severity.ShouldBe(8); // errors first — publish rejects, so must the editor markers.Select(m => (m.Severity, m.StartLineNumber, m.StartColumn)) .ShouldBe(ScriptProblems.Order(markers).Select(m => (m.Severity, m.StartLineNumber, m.StartColumn))); } [Fact] 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; markers.ShouldNotBeEmpty(); markers.ShouldAllBe(m => m.Message.Length <= ScriptEditorLimits.MaxDiagnosticMessageChars); markers.ShouldContain(m => m.Message.EndsWith("…")); } [Fact] 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; markers.ShouldContain(m => m.Severity == 8 && !m.Message.EndsWith("…")); } [Fact] public void The_worst_case_circuit_report_fits_inside_the_sync_budget() { // The JS sends at most MaxReportedMarkers markers, each message cut to MaxReportedMessageChars. Worst case: // control characters (\u0001 → 6 JSON bytes, 7 when escaped again) and the largest line/column numbers. var message = new string('\u0001', ScriptEditorLimits.MaxReportedMessageChars); var marker = new DiagnosticMarker(8, int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue, message, "OTSCRIPT_SETVTAG_INERT"); var report = Enumerable.Repeat(marker, ScriptEditorLimits.MaxReportedMarkers).ToArray(); var once = JsonSerializer.Serialize(new object[] { report, int.MaxValue }); var twice = Encoding.UTF8.GetByteCount(JsonSerializer.Serialize(once)); twice.ShouldBeLessThan(ScriptEditorLimits.MaxEditorSyncBytes); ScriptEditorLimits.MaxEditorSyncBytes.ShouldBeLessThan((int)ScriptEditorLimits.HubMaximumReceiveMessageSize); } [Fact] public void A_truncated_report_says_how_many_were_left_out() { var shown = Enumerable.Range(1, 3).Select(i => new DiagnosticMarker(8, i, 1, i, 2, "m", "CS1002")).ToArray(); var report = new ScriptProblemReport(shown, 4321); report.Omitted.ShouldBe(4318); ScriptProblems.Summary(report).ShouldBe("3 errors (showing 3 of 4321)"); // Positive control: an untruncated report carries no suffix. ScriptProblems.Summary(new ScriptProblemReport(shown, 3)).ShouldBe("3 errors"); ScriptProblemReport.Empty.Omitted.ShouldBe(0); } [Fact] public void A_report_from_js_is_clamped_even_if_the_js_cap_regressed() { var flood = Enumerable.Range(1, 5000).Select(i => new DiagnosticMarker(4, i, 1, i, 2, new string('m', 2000), "W")).ToArray(); var report = ScriptProblemReport.FromEditor(flood, total: 5000); report.Markers.Count.ShouldBe(ScriptEditorLimits.MaxReportedMarkers); report.Total.ShouldBe(5000); report.Markers.ShouldAllBe(m => m.Message.Length <= ScriptEditorLimits.MaxReportedMessageChars); ScriptProblemReport.FromEditor(null, 0).ShouldBe(ScriptProblemReport.Empty); } }