diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index 08781ce209..e631716982 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -54,13 +54,7 @@ public async Task Should_Create_A_Session_With_Appended_SystemMessage_Config() SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = systemMessageSuffix } }); - await session.SendAsync(new MessageOptions { Prompt = "What is your full name?" }); - var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session); - Assert.NotNull(assistantMessage); - - var content = assistantMessage!.Data.Content ?? string.Empty; - Assert.Contains("GitHub", content); - Assert.Contains("Have a nice day!", content); + await AssertAppendedSystemMessageResponseAsync(session, TimeSpan.FromSeconds(120)); var traffic = await Ctx.GetExchangesAsync(); Assert.NotEmpty(traffic); @@ -69,6 +63,18 @@ public async Task Should_Create_A_Session_With_Appended_SystemMessage_Config() Assert.Contains(systemMessageSuffix, systemMessage); } + internal static async Task AssertAppendedSystemMessageResponseAsync(CopilotSession session, TimeSpan timeout) + { + // Subscribe before sending: session.idle is ephemeral and cannot be recovered from history. + var assistantMessage = await session.SendAndWaitAsync( + new MessageOptions { Prompt = "What is your full name?" }, timeout); + Assert.NotNull(assistantMessage); + + var content = assistantMessage.Data.Content ?? string.Empty; + Assert.Contains("GitHub", content); + Assert.Contains("Have a nice day!", content); + } + [Fact] public async Task Should_Create_A_Session_With_Replaced_SystemMessage_Config() { diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index dd7fdc2bbb..55abc0d41b 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -1717,6 +1717,47 @@ private static void AssertMessageSource(JsonElement request, string? source) Assert.False(request.TryGetProperty("wait", out _)); } + [Fact] + public async Task Appended_System_Message_Observes_Idle_Before_Send_Reply() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + var timeout = TimeSpan.FromSeconds(5); + const string content = "I am GitHub Copilot. Have a nice day!"; + var drained = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var subscription = session.On(_ => drained.TrySetResult()); + server.BeforeResponseAsync = async (request, cancellationToken) => + { + if (request.Method != "session.send") + { + return; + } + + await server.SendSessionEventAsync(session.SessionId, "user.message", new() + { + ["content"] = request.Params.GetProperty("prompt").GetString() + }); + await server.SendSessionEventAsync(session.SessionId, "assistant.message", new() + { + ["messageId"] = "appended-system-message", + ["content"] = content + }); + await server.SendSessionEventAsync(session.SessionId, "session.idle", new()); + // Drain the idle notification before replying to session.send. + await server.SendSessionEventAsync(session.SessionId, "session.title_changed", new() { ["title"] = "fence" }); + await drained.Task.WaitAsync(timeout, cancellationToken); + }; + + await E2E.SessionE2ETests.AssertAppendedSystemMessageResponseAsync(session, timeout); + + var request = Assert.Single(server.Requests, request => request.Method == "session.send"); + Assert.Equal("What is your full name?", request.Params.GetProperty("prompt").GetString()); + var history = await session.GetEventsAsync(); + Assert.DoesNotContain(history, evt => evt is SessionIdleEvent); + Assert.Equal(content, Assert.Single(history.OfType()).Data.Content); + } + [Theory] [InlineData(true)] [InlineData(false)]