From 051ba3cbe5f8bf4f2240b99b92b4ffd880276568 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Wed, 21 Oct 2026 16:46:25 +0200 Subject: [PATCH] Fix misleading mock sort comment and use stable sort in MockGeneratorPlugin. Closes #1974 The comment claimed mocks are sorted by URL length so that the most specific comes first, but the code sorts by descending ordinal URL. Generated mocks use exact URLs, so the order doesn't affect matching. Update the comment to reflect this and replace the unstable List.Sort with a stable OrderByDescending so repeated requests to the same URL keep their recorded order. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e010e7fb-27f1-4b60-b3a6-1717b55ced91 --- .../GenerationPluginsIntegrationTests.cs | 43 +++++++++++++++++++ .../Generation/MockGeneratorPlugin.cs | 6 ++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/DevProxy.Integration.Tests/GenerationPluginsIntegrationTests.cs b/DevProxy.Integration.Tests/GenerationPluginsIntegrationTests.cs index f9b0bfe05..d47eeb24f 100644 --- a/DevProxy.Integration.Tests/GenerationPluginsIntegrationTests.cs +++ b/DevProxy.Integration.Tests/GenerationPluginsIntegrationTests.cs @@ -160,6 +160,49 @@ public async Task MockGenerator_WritesMockFile() } } + [Fact] + public async Task MockGenerator_PreservesRecordedOrderForSameUrl() + { + var plugin = new MockGeneratorPlugin(NullLogger.Instance, Watch); + + // enough entries to exceed the insertion-sort threshold of an unstable sort + const int count = 40; + var logs = Enumerable.Range(0, count) + .Select(i => TestExchange + .Request("GET", i % 2 == 0 ? "https://api.contoso.com/a" : "https://api.contoso.com/b") + .WithResponse( + HttpStatusCode.OK, + headers: [("Content-Type", "application/json")], + body: $$"""{ "seq": {{i}} }""") + .AsRequestLog(MessageType.InterceptedResponse)) + .ToList(); + + var args = Recording(logs); + var dir = await InTempCwdAsync(() => plugin.AfterRecordingStopAsync(args, CancellationToken.None)); + try + { + var mockFile = dir.GetFiles("mocks-*.json").Single(); + using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(mockFile.FullName)); + var mocks = doc.RootElement.GetProperty("mocks").EnumerateArray() + .Select(m => ( + Url: m.GetProperty("request").GetProperty("url").GetString(), + Seq: m.GetProperty("response").GetProperty("body").GetProperty("seq").GetInt32())) + .ToArray(); + + Assert.Equal(count, mocks.Length); + + var expected = Enumerable.Range(0, count).Where(i => i % 2 == 1) + .Select(i => ("https://api.contoso.com/b", i)) + .Concat(Enumerable.Range(0, count).Where(i => i % 2 == 0) + .Select(i => ("https://api.contoso.com/a", i))); + Assert.Equal(expected, mocks.Select(m => (m.Url!, m.Seq))); + } + finally + { + dir.Delete(recursive: true); + } + } + [Fact] public async Task HttpFileGenerator_WritesHttpFile() { diff --git a/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs b/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs index 3f354b551..22be7e4ba 100644 --- a/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs +++ b/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs @@ -81,8 +81,10 @@ request.Context.Session is null || } Logger.LogDebug("Sorting mocks..."); - // sort mocks descending by url length so that most specific mocks are first - mocks.Sort((a, b) => string.CompareOrdinal(b.Request!.Url, a.Request!.Url)); + // group mocks by URL (descending ordinal order) for readability. Generated + // mocks use exact URLs, so this order doesn't affect matching. The sort is + // stable so that repeated requests to the same URL keep their recorded order. + mocks = [.. mocks.OrderByDescending(m => m.Request!.Url, StringComparer.Ordinal)]; var mocksFile = new MockResponseConfiguration { Mocks = mocks };