|
1 | 1 | using Microsoft.AspNetCore.Builder; |
2 | 2 | using Microsoft.Extensions.DependencyInjection; |
| 3 | +using Microsoft.Extensions.Logging; |
3 | 4 | using ModelContextProtocol.AspNetCore.Tests.Utils; |
4 | 5 | using ModelContextProtocol.Protocol; |
5 | 6 | using ModelContextProtocol.Server; |
@@ -829,6 +830,84 @@ public async Task LowLevel_ToolFallsBackGracefully_WithoutMrtr() |
829 | 830 | Assert.Equal("lowlevel-unsupported:MRTR is not available", text); |
830 | 831 | } |
831 | 832 |
|
| 833 | + [Fact] |
| 834 | + public async Task SessionDelete_CancelsPendingMrtrContinuation() |
| 835 | + { |
| 836 | + await StartAsync(); |
| 837 | + await InitializeWithMrtrAsync(); |
| 838 | + |
| 839 | + // 1. Call a tool that suspends at ElicitAsync (high-level MRTR path). |
| 840 | + var response = await PostJsonRpcAsync(CallTool("elicit-tool", """{"message":"Please confirm"}""")); |
| 841 | + var rpcResponse = await AssertSingleSseResponseAsync(response); |
| 842 | + |
| 843 | + // Verify we got an IncompleteResult (handler is now suspended, continuation stored). |
| 844 | + var resultObj = Assert.IsType<JsonObject>(rpcResponse.Result); |
| 845 | + Assert.Equal("incomplete", resultObj["result_type"]?.GetValue<string>()); |
| 846 | + var requestState = resultObj["requestState"]!.GetValue<string>(); |
| 847 | + Assert.False(string.IsNullOrEmpty(requestState)); |
| 848 | + |
| 849 | + // 2. DELETE the session while the handler is suspended. |
| 850 | + using var deleteResponse = await HttpClient.DeleteAsync("", TestContext.Current.CancellationToken); |
| 851 | + Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode); |
| 852 | + |
| 853 | + // Allow a moment for the async cancellation to propagate through the handler task. |
| 854 | + await Task.Delay(100, TestContext.Current.CancellationToken); |
| 855 | + |
| 856 | + // 3. Verify that the MRTR cancellation was logged at Debug level. |
| 857 | + var mrtrCancelledLog = MockLoggerProvider.LogMessages |
| 858 | + .Where(m => m.Message.Contains("pending MRTR continuation")) |
| 859 | + .ToList(); |
| 860 | + Assert.Single(mrtrCancelledLog); |
| 861 | + Assert.Equal(LogLevel.Debug, mrtrCancelledLog[0].LogLevel); |
| 862 | + Assert.Contains("1", mrtrCancelledLog[0].Message); |
| 863 | + |
| 864 | + // 4. Verify no error-level log was emitted for the cancellation. |
| 865 | + // The handler's OperationCanceledException should be silently observed, not logged as an error. |
| 866 | + var errorLogs = MockLoggerProvider.LogMessages |
| 867 | + .Where(m => m.LogLevel >= LogLevel.Error && m.Message.Contains("elicit")) |
| 868 | + .ToList(); |
| 869 | + Assert.Empty(errorLogs); |
| 870 | + } |
| 871 | + |
| 872 | + [Fact] |
| 873 | + public async Task SessionDelete_RetryAfterDelete_ReturnsSessionNotFound() |
| 874 | + { |
| 875 | + await StartAsync(); |
| 876 | + await InitializeWithMrtrAsync(); |
| 877 | + |
| 878 | + // 1. Call a tool that suspends at ElicitAsync. |
| 879 | + var response = await PostJsonRpcAsync(CallTool("elicit-tool", """{"message":"Please confirm"}""")); |
| 880 | + var rpcResponse = await AssertSingleSseResponseAsync(response); |
| 881 | + |
| 882 | + var resultObj = Assert.IsType<JsonObject>(rpcResponse.Result); |
| 883 | + var requestState = resultObj["requestState"]!.GetValue<string>(); |
| 884 | + var inputRequests = resultObj["inputRequests"]!.AsObject(); |
| 885 | + var inputKey = inputRequests.First().Key; |
| 886 | + |
| 887 | + // 2. DELETE the session. |
| 888 | + using var deleteResponse = await HttpClient.DeleteAsync("", TestContext.Current.CancellationToken); |
| 889 | + Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode); |
| 890 | + |
| 891 | + // 3. Attempt to retry with the old requestState — session is gone. |
| 892 | + var inputResponse = InputResponse.FromElicitResult(new ElicitResult { Action = "accept" }); |
| 893 | + var retryParams = new JsonObject |
| 894 | + { |
| 895 | + ["name"] = "elicit-tool", |
| 896 | + ["arguments"] = new JsonObject { ["message"] = "Please confirm" }, |
| 897 | + ["requestState"] = requestState, |
| 898 | + ["inputResponses"] = new JsonObject |
| 899 | + { |
| 900 | + [inputKey] = JsonSerializer.SerializeToNode(inputResponse, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(InputResponse))) |
| 901 | + }, |
| 902 | + }; |
| 903 | + |
| 904 | + using var retryResponse = await PostJsonRpcAsync(Request("tools/call", retryParams.ToJsonString())); |
| 905 | + |
| 906 | + // The session was deleted, so we should get a 404 with a JSON-RPC error. |
| 907 | + Assert.Equal(HttpStatusCode.NotFound, retryResponse.StatusCode); |
| 908 | + Assert.Equal("application/json", retryResponse.Content.Headers.ContentType?.MediaType); |
| 909 | + } |
| 910 | + |
832 | 911 | // --- Helpers --- |
833 | 912 |
|
834 | 913 | private static StringContent JsonContent(string json) => new(json, Encoding.UTF8, "application/json"); |
|
0 commit comments