From ade585c930399abcbee5f2cfa64e1e6e63307ab8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:41:25 +0000 Subject: [PATCH 1/9] Initial plan From d04e6b54525abcbdcfbd3069addf3a8dd2b4fa4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:51:24 +0000 Subject: [PATCH 2/9] Add failing test for Unix epoch timestamp issue Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../ChatResponseUpdateExtensionsTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 50bd9c86293..42980d696f0 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -359,6 +359,29 @@ public async Task ToChatResponse_UsageContentExtractedFromContents() Assert.Equal("Hello, world!", Assert.IsType(Assert.Single(Assert.Single(response.Messages).Contents)).Text); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) + { + DateTimeOffset now = DateTimeOffset.UtcNow; + + ChatResponseUpdate[] updates = + [ + new(ChatRole.Tool, "f") { MessageId = "4", CreatedAt = now }, + new(null, "g") { CreatedAt = DateTimeOffset.UnixEpoch }, + ]; + + ChatResponse response = useAsync ? + updates.ToChatResponse() : + await YieldAsync(updates).ToChatResponseAsync(); + Assert.Single(response.Messages); + + Assert.Equal("fg", response.Messages[0].Text); + Assert.Equal(ChatRole.Tool, response.Messages[0].Role); + Assert.Equal(now, response.Messages[0].CreatedAt); + } + private static async IAsyncEnumerable YieldAsync(IEnumerable updates) { foreach (ChatResponseUpdate update in updates) From a2a35dcf145e1da630b1de3547f884de2892478c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:59:17 +0000 Subject: [PATCH 3/9] Fix CreatedAt timestamp overwrite by only updating if later Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../ChatCompletion/ChatResponseExtensions.cs | 4 ++-- .../ChatCompletion/ChatResponseUpdateExtensionsTests.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 81ac83cd59c..87a957130f5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -346,7 +346,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon message.AuthorName = update.AuthorName; } - if (update.CreatedAt is not null) + if (message.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > message.CreatedAt)) { message.CreatedAt = update.CreatedAt; } @@ -391,7 +391,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon response.ConversationId = update.ConversationId; } - if (update.CreatedAt is not null) + if (response.CreatedAt is null || (update.CreatedAt is not null && update.CreatedAt > response.CreatedAt)) { response.CreatedAt = update.CreatedAt; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 42980d696f0..9c989aa704d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -380,6 +380,7 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) Assert.Equal("fg", response.Messages[0].Text); Assert.Equal(ChatRole.Tool, response.Messages[0].Role); Assert.Equal(now, response.Messages[0].CreatedAt); + Assert.Equal(now, response.CreatedAt); } private static async IAsyncEnumerable YieldAsync(IEnumerable updates) From f1e37703822e8d245c4ce786b4ae6c243f555f0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:20:32 +0000 Subject: [PATCH 4/9] Enhance timestamp test with comprehensive scenarios Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../ChatResponseUpdateExtensionsTests.cs | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 9c989aa704d..44bb414508a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -364,12 +364,30 @@ public async Task ToChatResponse_UsageContentExtractedFromContents() [InlineData(true)] public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) { - DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset early = new(2024, 1, 1, 10, 0, 0, TimeSpan.Zero); + DateTimeOffset middle = new(2024, 1, 1, 11, 0, 0, TimeSpan.Zero); + DateTimeOffset late = new(2024, 1, 1, 12, 0, 0, TimeSpan.Zero); ChatResponseUpdate[] updates = [ - new(ChatRole.Tool, "f") { MessageId = "4", CreatedAt = now }, - new(null, "g") { CreatedAt = DateTimeOffset.UnixEpoch }, + + // Start with an early timestamp + new(ChatRole.Tool, "a") { MessageId = "4", CreatedAt = early }, + + // Unix epoch (as "null") should not overwrite + new(null, "b") { CreatedAt = DateTimeOffset.UnixEpoch }, + + // Newer timestamp should overwrite + new(null, "c") { CreatedAt = middle }, + + // Older timestamp should not overwrite + new(null, "d") { CreatedAt = early }, + + // Even newer timestamp should overwrite + new(null, "e") { CreatedAt = late }, + + // Unix epoch should not overwrite again + new(null, "f") { CreatedAt = DateTimeOffset.UnixEpoch }, ]; ChatResponse response = useAsync ? @@ -377,10 +395,10 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) await YieldAsync(updates).ToChatResponseAsync(); Assert.Single(response.Messages); - Assert.Equal("fg", response.Messages[0].Text); + Assert.Equal("abcdef", response.Messages[0].Text); Assert.Equal(ChatRole.Tool, response.Messages[0].Role); - Assert.Equal(now, response.Messages[0].CreatedAt); - Assert.Equal(now, response.CreatedAt); + Assert.Equal(late, response.Messages[0].CreatedAt); + Assert.Equal(late, response.CreatedAt); } private static async IAsyncEnumerable YieldAsync(IEnumerable updates) From d053f6dcce5254387c7bebd67ffc7a5cb8cc9723 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:52:16 +0000 Subject: [PATCH 5/9] Replace DateTimeOffset.UnixEpoch with manual construction for compatibility Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../ChatCompletion/ChatResponseUpdateExtensionsTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 44bb414508a..a0815731ade 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -367,6 +367,7 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) DateTimeOffset early = new(2024, 1, 1, 10, 0, 0, TimeSpan.Zero); DateTimeOffset middle = new(2024, 1, 1, 11, 0, 0, TimeSpan.Zero); DateTimeOffset late = new(2024, 1, 1, 12, 0, 0, TimeSpan.Zero); + DateTimeOffset unixEpoch = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); ChatResponseUpdate[] updates = [ @@ -375,7 +376,7 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) new(ChatRole.Tool, "a") { MessageId = "4", CreatedAt = early }, // Unix epoch (as "null") should not overwrite - new(null, "b") { CreatedAt = DateTimeOffset.UnixEpoch }, + new(null, "b") { CreatedAt = unixEpoch }, // Newer timestamp should overwrite new(null, "c") { CreatedAt = middle }, @@ -387,7 +388,7 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) new(null, "e") { CreatedAt = late }, // Unix epoch should not overwrite again - new(null, "f") { CreatedAt = DateTimeOffset.UnixEpoch }, + new(null, "f") { CreatedAt = unixEpoch }, ]; ChatResponse response = useAsync ? From 44de1fcb2902c936a8ced0589e59c4657d8dbfc4 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 2 Oct 2025 21:14:47 -0400 Subject: [PATCH 6/9] Apply suggestions from code review --- .../ChatCompletion/ChatResponseUpdateExtensionsTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index a0815731ade..a144ff26eb6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -371,7 +371,6 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) ChatResponseUpdate[] updates = [ - // Start with an early timestamp new(ChatRole.Tool, "a") { MessageId = "4", CreatedAt = early }, @@ -389,6 +388,9 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) // Unix epoch should not overwrite again new(null, "f") { CreatedAt = unixEpoch }, + + // null should not overwrite + new(null, "g") { CreatedAt = null }, ]; ChatResponse response = useAsync ? @@ -396,7 +398,7 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) await YieldAsync(updates).ToChatResponseAsync(); Assert.Single(response.Messages); - Assert.Equal("abcdef", response.Messages[0].Text); + Assert.Equal("abcdefg", response.Messages[0].Text); Assert.Equal(ChatRole.Tool, response.Messages[0].Role); Assert.Equal(late, response.Messages[0].CreatedAt); Assert.Equal(late, response.CreatedAt); From 5a7d3a684ea0939f420555d2ab2873681d48bae2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 2 Oct 2025 22:29:29 -0400 Subject: [PATCH 7/9] Update test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs --- .../ChatCompletion/ChatResponseUpdateExtensionsTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index a144ff26eb6..0a54c08b4c4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -371,6 +371,7 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) ChatResponseUpdate[] updates = [ + // Start with an early timestamp new(ChatRole.Tool, "a") { MessageId = "4", CreatedAt = early }, From 0747f831b5cca5a295e65d8496e1e3ba8a05a541 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:47:12 +0000 Subject: [PATCH 8/9] Add theory test for timestamp folding with pairs of timestamps Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../ChatResponseUpdateExtensionsTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 0a54c08b4c4..308d8b3e4e3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -405,6 +405,43 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) Assert.Equal(late, response.CreatedAt); } + [Theory] + [InlineData(false, null, null, null)] + [InlineData(true, null, null, null)] + [InlineData(false, "2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z")] + [InlineData(true, "2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z")] + [InlineData(false, null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z")] + [InlineData(true, null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z")] + [InlineData(false, "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T11:00:00Z")] + [InlineData(true, "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T11:00:00Z")] + [InlineData(false, "2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z")] + [InlineData(true, "2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z")] + [InlineData(false, "2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z")] + [InlineData(true, "2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z")] + [InlineData(false, "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z")] + [InlineData(true, "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z")] + public async Task ToChatResponse_TimestampFolding(bool useAsync, string? timestamp1, string? timestamp2, string? expectedTimestamp) + { + DateTimeOffset? first = timestamp1 is not null ? DateTimeOffset.Parse(timestamp1) : null; + DateTimeOffset? second = timestamp2 is not null ? DateTimeOffset.Parse(timestamp2) : null; + DateTimeOffset? expected = expectedTimestamp is not null ? DateTimeOffset.Parse(expectedTimestamp) : null; + + ChatResponseUpdate[] updates = + [ + new(ChatRole.Assistant, "a") { CreatedAt = first }, + new(null, "b") { CreatedAt = second }, + ]; + + ChatResponse response = useAsync ? + updates.ToChatResponse() : + await YieldAsync(updates).ToChatResponseAsync(); + + Assert.Single(response.Messages); + Assert.Equal("ab", response.Messages[0].Text); + Assert.Equal(expected, response.Messages[0].CreatedAt); + Assert.Equal(expected, response.CreatedAt); + } + private static async IAsyncEnumerable YieldAsync(IEnumerable updates) { foreach (ChatResponseUpdate update in updates) From 212b4eabb032885f72404d39dc2c0e030b5c90ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:00:34 +0000 Subject: [PATCH 9/9] Refactor ToChatResponse_TimestampFolding to use MemberData instead of duplicated InlineData Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../ChatResponseUpdateExtensionsTests.cs | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 308d8b3e4e3..3328d0be083 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -405,21 +405,30 @@ public async Task ToChatResponse_AlternativeTimestamps(bool useAsync) Assert.Equal(late, response.CreatedAt); } + public static IEnumerable ToChatResponse_TimestampFolding_MemberData() + { + // Base test cases + var testCases = new (string? timestamp1, string? timestamp2, string? expectedTimestamp)[] + { + (null, null, null), + ("2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z"), + (null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"), + ("2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T11:00:00Z"), + ("2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z"), + ("2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z"), + ("1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"), + }; + + // Yield each test case twice, once for useAsync = false and once for useAsync = true + foreach (var (timestamp1, timestamp2, expectedTimestamp) in testCases) + { + yield return new object?[] { false, timestamp1, timestamp2, expectedTimestamp }; + yield return new object?[] { true, timestamp1, timestamp2, expectedTimestamp }; + } + } + [Theory] - [InlineData(false, null, null, null)] - [InlineData(true, null, null, null)] - [InlineData(false, "2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z")] - [InlineData(true, "2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z")] - [InlineData(false, null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z")] - [InlineData(true, null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z")] - [InlineData(false, "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T11:00:00Z")] - [InlineData(true, "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T11:00:00Z")] - [InlineData(false, "2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z")] - [InlineData(true, "2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z")] - [InlineData(false, "2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z")] - [InlineData(true, "2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z")] - [InlineData(false, "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z")] - [InlineData(true, "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z")] + [MemberData(nameof(ToChatResponse_TimestampFolding_MemberData))] public async Task ToChatResponse_TimestampFolding(bool useAsync, string? timestamp1, string? timestamp2, string? expectedTimestamp) { DateTimeOffset? first = timestamp1 is not null ? DateTimeOffset.Parse(timestamp1) : null;