using Microsoft.Extensions.Caching.Memory; using NSubstitute; using ScadaLink.CentralUI.ScriptAnalysis; using ScadaLink.TemplateEngine; namespace ScadaLink.CentralUI.Tests.ScriptAnalysis; /// /// Regression tests for CentralUI-013. ResolveCalledShape resolved shared /// script shapes with _sharedScripts.GetShapesAsync().GetAwaiter().GetResult() /// — a sync-over-async block on a request thread that risks thread-pool /// starvation and deadlock. Hover and SignatureHelp were synchronous /// purely to accommodate that block. The fix makes both methods async and /// awaits the catalog. /// public class ScriptAnalysisAsyncResolveTests { private readonly ISharedScriptCatalog _catalog = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 }); private readonly IServiceProvider _services = Substitute.For(); private readonly ScriptAnalysisService _svc; public ScriptAnalysisAsyncResolveTests() { _catalog.GetShapesAsync().Returns(Array.Empty()); _svc = new ScriptAnalysisService(_catalog, _cache, _services); } private static ScriptShape Shape(string name, params ParameterShape[] ps) => new(name, ps, null); private static ParameterShape Param(string name, string type) => new(name, type, true); [Fact] public async Task HoverAsync_OnSharedCallName_AwaitsCatalog_AndResolvesShape() { // The catalog only completes after yielding — a truly asynchronous // source. The fixed Hover awaits it instead of blocking. _catalog.GetShapesAsync().Returns(async _ => { await Task.Yield(); return (IReadOnlyList)new[] { Shape("Aggregate", Param("window", "Integer")), }; }); var resp = await _svc.Hover(new HoverRequest( CodeText: "var r = Scripts.CallShared(\"Aggregate\");", Line: 1, Column: 30)); Assert.NotNull(resp.Markdown); Assert.Contains("shared script", resp.Markdown); Assert.Contains("Aggregate", resp.Markdown); } [Fact] public async Task SignatureHelpAsync_InsideSharedCall_AwaitsCatalog_AndResolvesParameters() { _catalog.GetShapesAsync().Returns(async _ => { await Task.Yield(); return (IReadOnlyList)new[] { Shape("Aggregate", Param("window", "Integer"), Param("mode", "String")), }; }); var resp = await _svc.SignatureHelp(new SignatureHelpRequest( CodeText: "var r = Scripts.CallShared(\"Aggregate\", ", Line: 1, Column: 41)); Assert.NotNull(resp.Label); Assert.Contains("Aggregate", resp.Label!); Assert.Equal(2, resp.Parameters!.Count); } [Fact] public void HoverAndSignatureHelp_AreAsync_NotSyncOverAsync() { // Structural guard: the methods must return Task so the catalog can be // awaited rather than blocked with .GetAwaiter().GetResult(). var hover = typeof(ScriptAnalysisService).GetMethod(nameof(ScriptAnalysisService.Hover)); var sigHelp = typeof(ScriptAnalysisService).GetMethod(nameof(ScriptAnalysisService.SignatureHelp)); Assert.NotNull(hover); Assert.NotNull(sigHelp); Assert.Equal(typeof(Task), hover!.ReturnType); Assert.Equal(typeof(Task), sigHelp!.ReturnType); } }