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);
}
}