From 52e2670438cc6b705441dafb913f9b4d5fb7d430 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:42:08 +0000 Subject: [PATCH 1/4] Initial plan From b92c12cc8c6f0b9b2baac15110d92970b6cdffc9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:00:55 +0000 Subject: [PATCH 2/4] chore: outline plan for SDK 3.59.0-preview.0 compat fixes Agent-Logs-Url: https://github.com/lemonlion/CosmosDB.InMemoryEmulator/sessions/a860859e-ed0f-455a-9e91-8dd3b6321e36 Co-authored-by: lemonlion <8618373+lemonlion@users.noreply.github.com> --- src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj b/src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj index ec105c7..5722357 100644 --- a/src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj +++ b/src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj @@ -23,7 +23,7 @@ - + From 7663256e2f9693f6f718ce53ae9292ea3000d4df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:34:48 +0000 Subject: [PATCH 3/4] fix: add SDK 3.59.0-preview.0 compat and fix latest-stable-windows CI failure Agent-Logs-Url: https://github.com/lemonlion/CosmosDB.InMemoryEmulator/sessions/a860859e-ed0f-455a-9e91-8dd3b6321e36 Co-authored-by: lemonlion <8618373+lemonlion@users.noreply.github.com> --- .github/workflows/sdk-compat-check.yml | 10 +- Directory.Build.props | 21 ++ .../CosmosDB.InMemoryEmulator.csproj | 10 +- .../InMemoryChangeFeedProcessor.cs | 69 +++++ .../InMemoryContainer.cs | 115 ++++++++ .../PartitionKeyCapturingContainer.cs | 17 ++ src/Directory.Build.props | 2 +- .../PreviewSdkContainerMethodTests.cs | 267 ++++++++++++++++++ 8 files changed, 500 insertions(+), 11 deletions(-) create mode 100644 tests/CosmosDB.InMemoryEmulator.Tests.Unit/PreviewSdkContainerMethodTests.cs diff --git a/.github/workflows/sdk-compat-check.yml b/.github/workflows/sdk-compat-check.yml index 9ccbe78..26c4929 100644 --- a/.github/workflows/sdk-compat-check.yml +++ b/.github/workflows/sdk-compat-check.yml @@ -63,10 +63,14 @@ jobs: echo "## SDK Version: $VERSION" >> $GITHUB_STEP_SUMMARY - name: Override Cosmos SDK version + shell: bash run: | - dotnet add src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj \ - package Microsoft.Azure.Cosmos \ - --version ${{ steps.sdk-version.outputs.version }} + VERSION="${{ steps.sdk-version.outputs.version }}" + # Update CosmosSDKVersion in the root Directory.Build.props — this single property + # drives the PackageReference, the HybridRow hint path, AND the + # COSMOS_SDK_PREVIEW_METHODS compile-time symbol across all projects in the repo. + sed -i "s|[^<]*|${VERSION}|" \ + Directory.Build.props - name: Build run: dotnet build --configuration Release diff --git a/Directory.Build.props b/Directory.Build.props index 4be44e3..e9c1bda 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,4 +8,25 @@ true + + + 3.58.0 + + + + + <_CosmosVersionBase>$([System.Text.RegularExpressions.Regex]::Replace($(CosmosSDKVersion), '-.*$', '')) + + + + + $(DefineConstants);COSMOS_SDK_PREVIEW_METHODS + + diff --git a/src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj b/src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj index 5722357..7237df6 100644 --- a/src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj +++ b/src/CosmosDB.InMemoryEmulator/CosmosDB.InMemoryEmulator.csproj @@ -16,14 +16,10 @@ - - - 3.58.0 - - - + + diff --git a/src/CosmosDB.InMemoryEmulator/InMemoryChangeFeedProcessor.cs b/src/CosmosDB.InMemoryEmulator/InMemoryChangeFeedProcessor.cs index 090dbcc..3022da8 100644 --- a/src/CosmosDB.InMemoryEmulator/InMemoryChangeFeedProcessor.cs +++ b/src/CosmosDB.InMemoryEmulator/InMemoryChangeFeedProcessor.cs @@ -353,6 +353,75 @@ private async Task PollAsync(CancellationToken cancellationToken) } } +#if COSMOS_SDK_PREVIEW_METHODS +/// +/// A implementation that delivers all versions and deletes +/// of every change (including intermediate updates) as objects, +/// matching the behaviour of GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes. +/// +internal sealed class InMemoryAllVersionsChangeFeedProcessor : ChangeFeedProcessor +{ + private readonly InMemoryContainer _container; + private readonly Container.ChangeFeedHandler> _handler; + private CancellationTokenSource _cts; + private Task _pollingTask; + private long _checkpoint; + + internal InMemoryAllVersionsChangeFeedProcessor( + InMemoryContainer container, + Container.ChangeFeedHandler> handler) + { + _container = container; + _handler = handler; + } + + public override Task StartAsync() + { + _checkpoint = _container.GetChangeFeedCheckpoint(); + _cts = new CancellationTokenSource(); + _pollingTask = PollAsync(_cts.Token); + return Task.CompletedTask; + } + + public override async Task StopAsync() + { + if (_cts != null) + { + await _cts.CancelAsync(); + try { await _pollingTask; } + catch (OperationCanceledException) { } + _cts.Dispose(); + _cts = null; + } + } + + private async Task PollAsync(CancellationToken cancellationToken) + { + var context = new InMemoryChangeFeedProcessorContext(); + + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken); + + var feedItems = _container.GetAllVersionsChangeFeedItems(_checkpoint); + + if (feedItems.Count > 0) + { + try + { + await _handler(context, feedItems, cancellationToken); + _checkpoint += feedItems.Count; + } + catch (Exception) when (!cancellationToken.IsCancellationRequested) + { + // Handler threw — do not advance checkpoint so the same batch is redelivered + } + } + } + } +} +#endif + internal static class ChangeFeedProcessorBuilderFactory { private static readonly Assembly CosmosAssembly = typeof(Container).Assembly; diff --git a/src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs b/src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs index f259e0d..fccb092 100644 --- a/src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs +++ b/src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Net; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using Microsoft.Azure.Cosmos; @@ -1967,6 +1968,73 @@ public FeedIterator GetChangeFeedIterator(long checkpoint) return new InMemoryFeedIterator(items); } +#if COSMOS_SDK_PREVIEW_METHODS + /// + /// Returns all change feed entries starting from as + /// objects, preserving every version and delete. + /// Used by . + /// + internal IReadOnlyList> GetAllVersionsChangeFeedItems(long fromCheckpoint) + { + lock (_changeFeedLock) + { + var start = (int)Math.Max(0, fromCheckpoint); + var result = new List>(_changeFeed.Count - start); + + for (var i = start; i < _changeFeed.Count; i++) + { + var entry = _changeFeed[i]; + var opType = DetermineChangeFeedOperationType(i); + var metadata = CreateChangeFeedMetadata(opType, i, entry.Id); + var feedItem = new ChangeFeedItem { Metadata = metadata }; + if (!entry.IsDelete) + feedItem.Current = JsonConvert.DeserializeObject(entry.Json, JsonSettings); + result.Add(feedItem); + } + + return result; + } + } + + private static readonly Type ChangeFeedMetadataType = typeof(ChangeFeedMetadata); + private static readonly PropertyInfo CfmOperationTypeProp = ChangeFeedMetadataType.GetProperty("OperationType"); + private static readonly PropertyInfo CfmLsnProp = ChangeFeedMetadataType.GetProperty("Lsn"); + private static readonly PropertyInfo CfmIdProp = ChangeFeedMetadataType.GetProperty("Id"); + + private static ChangeFeedMetadata CreateChangeFeedMetadata( + ChangeFeedOperationType operationType, long lsn, string id) + { + var metadata = (ChangeFeedMetadata)RuntimeHelpers.GetUninitializedObject(ChangeFeedMetadataType); + CfmOperationTypeProp?.SetMethod?.Invoke(metadata, new object[] { operationType }); + CfmLsnProp?.SetMethod?.Invoke(metadata, new object[] { lsn }); + CfmIdProp?.SetMethod?.Invoke(metadata, new object[] { id }); + return metadata; + } + + /// + /// Determines whether the change feed entry at position represents + /// a Create, Replace, or Delete by scanning prior entries for the same item. + /// Must be called while is held. + /// + private ChangeFeedOperationType DetermineChangeFeedOperationType(int index) + { + var entry = _changeFeed[index]; + if (entry.IsDelete) + return ChangeFeedOperationType.Delete; + + // Scan backwards to find the most recent prior entry for this (Id, PartitionKey) + for (var j = index - 1; j >= 0; j--) + { + var prev = _changeFeed[j]; + if (prev.Id == entry.Id && prev.PartitionKey == entry.PartitionKey) + return prev.IsDelete ? ChangeFeedOperationType.Create : ChangeFeedOperationType.Replace; + } + + // No prior entry → this is the first occurrence (Create) + return ChangeFeedOperationType.Create; + } +#endif + public override FeedIterator GetChangeFeedStreamIterator( ChangeFeedStartFrom changeFeedStartFrom, ChangeFeedMode changeFeedMode, ChangeFeedRequestOptions changeFeedRequestOptions = null) @@ -2229,6 +2297,13 @@ public override ChangeFeedProcessorBuilder GetChangeFeedEstimatorBuilder( TimeSpan? estimationPeriod = null) => ChangeFeedProcessorBuilderFactory.Create(processorName, new NoOpChangeFeedProcessor()); +#if COSMOS_SDK_PREVIEW_METHODS + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( + string processorName, Container.ChangeFeedHandler> onChangesDelegate) + => ChangeFeedProcessorBuilderFactory.Create(processorName, + new InMemoryAllVersionsChangeFeedProcessor(this, onChangesDelegate)); +#endif + // ═══════════════════════════════════════════════════════════════════════════ // Feed Ranges // ═══════════════════════════════════════════════════════════════════════════ @@ -2249,6 +2324,46 @@ public override Task> GetFeedRangesAsync(CancellationTo return Task.FromResult>(ranges); } +#if COSMOS_SDK_PREVIEW_METHODS + public override Task> GetPartitionKeyRangesAsync( + FeedRange feedRange, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var count = Math.Max(1, FeedRangeCount); + var step = 0x1_0000_0000L / count; + var (queryMin, queryMax) = ParseFeedRangeBoundaries(feedRange); + + var rangeIds = new List(); + for (var i = 0; i < count; i++) + { + if (queryMin == null || queryMax == null) + { + rangeIds.Add(i.ToString()); + continue; + } + + var rangeMin = (uint)(i * step); + var rangeMax = i == count - 1 ? uint.MaxValue : (uint)((i + 1) * step) - 1u; + + // Overlap check: intervals [rangeMin, rangeMax] and [queryMin, queryMax] + if (rangeMin <= queryMax.Value && queryMin.Value <= rangeMax) + rangeIds.Add(i.ToString()); + } + + return Task.FromResult>(rangeIds); + } + + public override Task SemanticRerankAsync( + string rerankContext, + IEnumerable documents, + IDictionary options, + CancellationToken cancellationToken = default) + => throw new NotSupportedException( + "SemanticRerankAsync is not supported by the in-memory emulator. " + + "This API requires a live Cosmos DB endpoint with semantic ranking capabilities."); +#endif + // ═══════════════════════════════════════════════════════════════════════════ // Container management // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/CosmosDB.InMemoryEmulator/PartitionKeyCapturingContainer.cs b/src/CosmosDB.InMemoryEmulator/PartitionKeyCapturingContainer.cs index 76f54a2..5c1bcaa 100644 --- a/src/CosmosDB.InMemoryEmulator/PartitionKeyCapturingContainer.cs +++ b/src/CosmosDB.InMemoryEmulator/PartitionKeyCapturingContainer.cs @@ -241,11 +241,28 @@ public override ChangeFeedProcessorBuilder GetChangeFeedEstimatorBuilder( string processorName, ChangesEstimationHandler estimationDelegate, TimeSpan? estimationPeriod = null) => _inner.GetChangeFeedEstimatorBuilder(processorName, estimationDelegate, estimationPeriod); +#if COSMOS_SDK_PREVIEW_METHODS + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( + string processorName, ChangeFeedHandler> onChangesDelegate) + => _inner.GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes(processorName, onChangesDelegate); +#endif + // ── Feed ranges ───────────────────────────────────────────────── public override Task> GetFeedRangesAsync(CancellationToken cancellationToken = default) => _inner.GetFeedRangesAsync(cancellationToken); +#if COSMOS_SDK_PREVIEW_METHODS + public override Task> GetPartitionKeyRangesAsync( + FeedRange feedRange, CancellationToken cancellationToken = default) + => _inner.GetPartitionKeyRangesAsync(feedRange, cancellationToken); + + public override Task SemanticRerankAsync( + string rerankContext, IEnumerable documents, + IDictionary options, CancellationToken cancellationToken = default) + => _inner.SemanticRerankAsync(rerankContext, documents, options, cancellationToken); +#endif + // ── Batch ─────────────────────────────────────────────────────── public override TransactionalBatch CreateTransactionalBatch(PartitionKey partitionKey) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 0d197fa..478c359 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ true - 4.0.5 + 4.0.6 lemonlion Copyright (c) 2026 lemonlion MIT diff --git a/tests/CosmosDB.InMemoryEmulator.Tests.Unit/PreviewSdkContainerMethodTests.cs b/tests/CosmosDB.InMemoryEmulator.Tests.Unit/PreviewSdkContainerMethodTests.cs new file mode 100644 index 0000000..8de87d9 --- /dev/null +++ b/tests/CosmosDB.InMemoryEmulator.Tests.Unit/PreviewSdkContainerMethodTests.cs @@ -0,0 +1,267 @@ +using AwesomeAssertions; +using Microsoft.Azure.Cosmos; +using Xunit; + +namespace CosmosDB.InMemoryEmulator.Tests; + +/// +/// Tests for Container methods that became public abstract in 3.59.0-preview.0: +/// +/// +/// +/// +/// +/// Tests that require types that were internal in 3.58.x are compiled only when targeting +/// 3.59.0 or later (guarded by the COSMOS_SDK_PREVIEW_METHODS symbol). +/// +public class PreviewSdkContainerMethodTests +{ + private readonly InMemoryContainer _container = new("test-container", "/partitionKey"); + +#if COSMOS_SDK_PREVIEW_METHODS + + // ── GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes ──────────────── + + [Fact] + public async Task AllVersionsProcessor_Create_ReportsCreateOperationType() + { + var received = new List>(); + var tcs = new TaskCompletionSource(); + + var processor = _container + .GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( + "avd-processor", + (_, changes, _) => + { + lock (received) received.AddRange(changes); + tcs.TrySetResult(true); + return Task.CompletedTask; + }) + .WithInstanceName("i1") + .WithInMemoryLeaseContainer() + .Build(); + + await processor.StartAsync(); + + await _container.CreateItemAsync( + new TestDocument { Id = "1", PartitionKey = "pk1", Name = "Alice" }, + new PartitionKey("pk1")); + + await Task.WhenAny(tcs.Task, Task.Delay(5_000)); + await processor.StopAsync(); + + received.Should().ContainSingle(); + received[0].Metadata.OperationType.Should().Be(ChangeFeedOperationType.Create); + received[0].Current.Should().NotBeNull(); + received[0].Current!.Name.Should().Be("Alice"); + } + + [Fact] + public async Task AllVersionsProcessor_Upsert_ReportsReplaceOperationType() + { + await _container.CreateItemAsync( + new TestDocument { Id = "1", PartitionKey = "pk1", Name = "V1" }, + new PartitionKey("pk1")); + + var received = new List>(); + var tcs = new TaskCompletionSource(); + + var processor = _container + .GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( + "avd-processor", + (_, changes, _) => + { + lock (received) received.AddRange(changes); + tcs.TrySetResult(true); + return Task.CompletedTask; + }) + .WithInstanceName("i1") + .WithInMemoryLeaseContainer() + .Build(); + + await processor.StartAsync(); + + await _container.UpsertItemAsync( + new TestDocument { Id = "1", PartitionKey = "pk1", Name = "V2" }, + new PartitionKey("pk1")); + + await Task.WhenAny(tcs.Task, Task.Delay(5_000)); + await processor.StopAsync(); + + received.Should().ContainSingle(); + received[0].Metadata.OperationType.Should().Be(ChangeFeedOperationType.Replace); + } + + [Fact] + public async Task AllVersionsProcessor_Delete_ReportsDeleteOperationType() + { + await _container.CreateItemAsync( + new TestDocument { Id = "1", PartitionKey = "pk1", Name = "Alice" }, + new PartitionKey("pk1")); + + var received = new List>(); + var tcs = new TaskCompletionSource(); + + var processor = _container + .GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( + "avd-processor", + (_, changes, _) => + { + lock (received) received.AddRange(changes); + tcs.TrySetResult(true); + return Task.CompletedTask; + }) + .WithInstanceName("i1") + .WithInMemoryLeaseContainer() + .Build(); + + await processor.StartAsync(); + + await _container.DeleteItemAsync("1", new PartitionKey("pk1")); + + await Task.WhenAny(tcs.Task, Task.Delay(5_000)); + await processor.StopAsync(); + + received.Should().ContainSingle(); + received[0].Metadata.OperationType.Should().Be(ChangeFeedOperationType.Delete); + } + + [Fact] + public async Task AllVersionsProcessor_DeliverAllVersionsWithoutDeduplication() + { + var received = new List>(); + var targetCount = 3; + var tcs = new TaskCompletionSource(); + + var processor = _container + .GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( + "avd-processor", + (_, changes, _) => + { + lock (received) + { + received.AddRange(changes); + if (received.Count >= targetCount) tcs.TrySetResult(true); + } + return Task.CompletedTask; + }) + .WithInstanceName("i1") + .WithInMemoryLeaseContainer() + .Build(); + + await processor.StartAsync(); + + await _container.CreateItemAsync( + new TestDocument { Id = "1", PartitionKey = "pk1", Name = "V1" }, + new PartitionKey("pk1")); + await _container.UpsertItemAsync( + new TestDocument { Id = "1", PartitionKey = "pk1", Name = "V2" }, + new PartitionKey("pk1")); + await _container.DeleteItemAsync("1", new PartitionKey("pk1")); + + await Task.WhenAny(tcs.Task, Task.Delay(5_000)); + await processor.StopAsync(); + + received.Should().HaveCount(3); + received[0].Metadata.OperationType.Should().Be(ChangeFeedOperationType.Create); + received[1].Metadata.OperationType.Should().Be(ChangeFeedOperationType.Replace); + received[2].Metadata.OperationType.Should().Be(ChangeFeedOperationType.Delete); + } + + [Fact] + public async Task AllVersionsProcessor_RecreatedItem_ReportsCreateAfterDelete() + { + // Create, delete, then re-create the same item id + await _container.CreateItemAsync( + new TestDocument { Id = "1", PartitionKey = "pk1", Name = "First" }, + new PartitionKey("pk1")); + await _container.DeleteItemAsync("1", new PartitionKey("pk1")); + + var received = new List>(); + var tcs = new TaskCompletionSource(); + + var processor = _container + .GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( + "avd-processor", + (_, changes, _) => + { + lock (received) + { + received.AddRange(changes); + if (received.Count >= 1) tcs.TrySetResult(true); + } + return Task.CompletedTask; + }) + .WithInstanceName("i1") + .WithInMemoryLeaseContainer() + .Build(); + + await processor.StartAsync(); + + await _container.CreateItemAsync( + new TestDocument { Id = "1", PartitionKey = "pk1", Name = "Recreated" }, + new PartitionKey("pk1")); + + await Task.WhenAny(tcs.Task, Task.Delay(5_000)); + await processor.StopAsync(); + + received.Should().ContainSingle(); + received[0].Metadata.OperationType.Should().Be(ChangeFeedOperationType.Create, + "re-creating an item after a delete should report Create, not Replace"); + } + + // ── GetPartitionKeyRangesAsync ──────────────────────────────────────────── + + [Fact] + public async Task GetPartitionKeyRangesAsync_SingleRange_ReturnsZero() + { + var container = new InMemoryContainer("test", "/partitionKey") { FeedRangeCount = 1 }; + var feedRanges = await container.GetFeedRangesAsync(); + + var rangeIds = (await container.GetPartitionKeyRangesAsync(feedRanges[0])).ToList(); + + rangeIds.Should().ContainSingle().Which.Should().Be("0"); + } + + [Fact] + public async Task GetPartitionKeyRangesAsync_MultipleRanges_ReturnsMatchingIds() + { + var container = new InMemoryContainer("test", "/partitionKey") { FeedRangeCount = 4 }; + var feedRanges = await container.GetFeedRangesAsync(); + + var allRangeIds = new List(); + foreach (var fr in feedRanges) + { + var ids = await container.GetPartitionKeyRangesAsync(fr); + allRangeIds.AddRange(ids); + } + + // Each feed range should map to exactly one partition key range + allRangeIds.Should().HaveCount(4); + allRangeIds.Should().BeEquivalentTo(new[] { "0", "1", "2", "3" }); + } + + // ── SemanticRerankAsync ──────────────────────────────────────────────────── + + [Fact] + public async Task SemanticRerankAsync_ThrowsNotSupportedException() + { + var act = async () => await _container.SemanticRerankAsync( + "my context", new[] { "doc1", "doc2" }, null); + + await act.Should().ThrowAsync() + .WithMessage("*SemanticRerankAsync*"); + } + +#else + [Fact] + public void PreviewMethods_NotAvailableIn358_SkipCoverage() + { + // The three abstract methods (GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes, + // GetPartitionKeyRangesAsync, SemanticRerankAsync) were internal in SDK 3.58.x. + // Full test coverage is enabled by the COSMOS_SDK_PREVIEW_METHODS symbol when building + // against 3.59.0 or later. + Assert.True(true, "Placeholder — preview method tests compiled out for 3.58.x."); + } +#endif +} From a8b4fa37ea87f19d41361791ee899800bfc5cc7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:58:33 +0000 Subject: [PATCH 4/4] fix: SemanticRerankAsync should throw CosmosException not NotSupportedException Agent-Logs-Url: https://github.com/lemonlion/CosmosDB.InMemoryEmulator/sessions/10ecf0ad-82a5-4c10-8865-83e847777836 Co-authored-by: lemonlion <8618373+lemonlion@users.noreply.github.com> --- src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs | 8 ++++++-- src/Directory.Build.props | 2 +- .../PreviewSdkContainerMethodTests.cs | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs b/src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs index fccb092..f7094d0 100644 --- a/src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs +++ b/src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs @@ -2359,9 +2359,13 @@ public override Task SemanticRerankAsync( IEnumerable documents, IDictionary options, CancellationToken cancellationToken = default) - => throw new NotSupportedException( + => throw new InMemoryCosmosException( "SemanticRerankAsync is not supported by the in-memory emulator. " + - "This API requires a live Cosmos DB endpoint with semantic ranking capabilities."); + "This API requires a live Cosmos DB endpoint with semantic ranking capabilities.", + HttpStatusCode.BadRequest, + 0, + Guid.NewGuid().ToString(), + SyntheticRequestCharge); #endif // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 478c359..ce85e67 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ true - 4.0.6 + 4.0.7 lemonlion Copyright (c) 2026 lemonlion MIT diff --git a/tests/CosmosDB.InMemoryEmulator.Tests.Unit/PreviewSdkContainerMethodTests.cs b/tests/CosmosDB.InMemoryEmulator.Tests.Unit/PreviewSdkContainerMethodTests.cs index 8de87d9..a1aad09 100644 --- a/tests/CosmosDB.InMemoryEmulator.Tests.Unit/PreviewSdkContainerMethodTests.cs +++ b/tests/CosmosDB.InMemoryEmulator.Tests.Unit/PreviewSdkContainerMethodTests.cs @@ -244,12 +244,14 @@ public async Task GetPartitionKeyRangesAsync_MultipleRanges_ReturnsMatchingIds() // ── SemanticRerankAsync ──────────────────────────────────────────────────── [Fact] - public async Task SemanticRerankAsync_ThrowsNotSupportedException() + public async Task SemanticRerankAsync_ThrowsCosmosException() { var act = async () => await _container.SemanticRerankAsync( "my context", new[] { "doc1", "doc2" }, null); - await act.Should().ThrowAsync() + // The in-memory emulator throws CosmosException (BadRequest) to match the exception + // type that the real service returns, so callers catching CosmosException work correctly. + await act.Should().ThrowAsync() .WithMessage("*SemanticRerankAsync*"); }