From 05e5c6a1a76dc8c019d4c0204941b04ca78dd74d Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Mon, 19 Oct 2026 03:22:33 -0300 Subject: [PATCH] Deprecate AI memory HTTP contracts for ORB-229 (part 1) --- .../Controllers/ProfileController.cs | 1 + .../Controllers/UserFactsController.cs | 3 + ...viceCollectionExtensions.Infrastructure.cs | 6 ++ src/Orbit.Api/openapi.json | 15 +++-- src/Orbit.Api/openapi_v2.json | 15 +++-- .../Profile/Queries/GetProfileQuery.cs | 1 + .../Controllers/AiMemoryDeprecationTests.cs | 67 +++++++++++++++++++ .../Controllers/ProfileControllerTests.cs | 2 + .../Controllers/UserFactsControllerTests.cs | 6 ++ 9 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 tests/Orbit.Infrastructure.Tests/Controllers/AiMemoryDeprecationTests.cs diff --git a/src/Orbit.Api/Controllers/ProfileController.cs b/src/Orbit.Api/Controllers/ProfileController.cs index 7aff47e40..b9a0c43dd 100644 --- a/src/Orbit.Api/Controllers/ProfileController.cs +++ b/src/Orbit.Api/Controllers/ProfileController.cs @@ -105,6 +105,7 @@ public async Task SetName( } [HttpPut("ai-memory")] + [Obsolete("AI memory is deprecated and will be removed.")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] diff --git a/src/Orbit.Api/Controllers/UserFactsController.cs b/src/Orbit.Api/Controllers/UserFactsController.cs index f1d84b422..5ea1acc5e 100644 --- a/src/Orbit.Api/Controllers/UserFactsController.cs +++ b/src/Orbit.Api/Controllers/UserFactsController.cs @@ -20,6 +20,7 @@ public partial class UserFactsController(IMediator mediator, ILogger FactIds); [HttpGet] + [Obsolete("AI memory is deprecated and will be removed.")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -31,6 +32,7 @@ public async Task GetUserFacts(CancellationToken cancellationToke } [HttpDelete("{id:guid}")] + [Obsolete("AI memory is deprecated and will be removed.")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -46,6 +48,7 @@ public async Task DeleteUserFact(Guid id, CancellationToken cance } [HttpDelete("bulk")] + [Obsolete("AI memory is deprecated and will be removed.")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] diff --git a/src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs b/src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs index 22657eef4..774b365da 100644 --- a/src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs +++ b/src/Orbit.Api/Extensions/ServiceCollectionExtensions.Infrastructure.cs @@ -342,6 +342,12 @@ private static void ConfigureOrbitOpenApi(Microsoft.AspNetCore.OpenApi.OpenApiOp operation.Deprecated = true; return Task.CompletedTask; }); + options.AddSchemaTransformer((schema, context, _) => + { + if (context.JsonPropertyInfo?.AttributeProvider?.IsDefined(typeof(ObsoleteAttribute), false) == true) + schema.Deprecated = true; + return Task.CompletedTask; + }); } private static void AddResponseCompression(WebApplicationBuilder builder) diff --git a/src/Orbit.Api/openapi.json b/src/Orbit.Api/openapi.json index 4aa90aa56..1ec41bdce 100644 --- a/src/Orbit.Api/openapi.json +++ b/src/Orbit.Api/openapi.json @@ -11665,7 +11665,8 @@ } } } - } + }, + "deprecated": true } }, "/api/Profile/ai-summary": { @@ -15156,7 +15157,8 @@ } } } - } + }, + "deprecated": true } }, "/api/user-facts/{id}": { @@ -15226,7 +15228,8 @@ } } } - } + }, + "deprecated": true } }, "/api/user-facts/bulk": { @@ -15307,7 +15310,8 @@ } } } - } + }, + "deprecated": true } }, "/api/Waitlist": { @@ -18567,7 +18571,8 @@ ] }, "aiMemoryEnabled": { - "type": "boolean" + "type": "boolean", + "deprecated": true }, "aiSummaryEnabled": { "type": "boolean" diff --git a/src/Orbit.Api/openapi_v2.json b/src/Orbit.Api/openapi_v2.json index 923665640..5cb348007 100644 --- a/src/Orbit.Api/openapi_v2.json +++ b/src/Orbit.Api/openapi_v2.json @@ -11818,7 +11818,8 @@ } } } - } + }, + "deprecated": true } }, "/api/Profile/ai-summary": { @@ -15354,7 +15355,8 @@ } } } - } + }, + "deprecated": true } }, "/api/user-facts/{id}": { @@ -15425,7 +15427,8 @@ } } } - } + }, + "deprecated": true } }, "/api/user-facts/bulk": { @@ -15507,7 +15510,8 @@ } } } - } + }, + "deprecated": true } }, "/api/Waitlist": { @@ -18768,7 +18772,8 @@ ] }, "aiMemoryEnabled": { - "type": "boolean" + "type": "boolean", + "deprecated": true }, "aiSummaryEnabled": { "type": "boolean" diff --git a/src/Orbit.Application/Profile/Queries/GetProfileQuery.cs b/src/Orbit.Application/Profile/Queries/GetProfileQuery.cs index e4edaee15..fe2218e02 100644 --- a/src/Orbit.Application/Profile/Queries/GetProfileQuery.cs +++ b/src/Orbit.Application/Profile/Queries/GetProfileQuery.cs @@ -16,6 +16,7 @@ public record ProfileResponse( string Name, string Email, string? TimeZone, + [property: Obsolete("AI memory is deprecated and will be removed.")] bool AiMemoryEnabled, bool AiSummaryEnabled, bool HasCompletedOnboarding, diff --git a/tests/Orbit.Infrastructure.Tests/Controllers/AiMemoryDeprecationTests.cs b/tests/Orbit.Infrastructure.Tests/Controllers/AiMemoryDeprecationTests.cs new file mode 100644 index 000000000..0e3419b76 --- /dev/null +++ b/tests/Orbit.Infrastructure.Tests/Controllers/AiMemoryDeprecationTests.cs @@ -0,0 +1,67 @@ +using System.Reflection; +using System.Text.Json; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Routing; +using Orbit.Api.Controllers; + +namespace Orbit.Infrastructure.Tests.Controllers; + +public class AiMemoryDeprecationTests +{ + [Theory] + [InlineData(typeof(UserFactsController), "GetUserFacts", null, "GET")] + [InlineData(typeof(UserFactsController), "DeleteUserFact", "{id:guid}", "DELETE")] + [InlineData(typeof(UserFactsController), "BulkDeleteUserFacts", "bulk", "DELETE")] + [InlineData(typeof(ProfileController), "SetAiMemory", "ai-memory", "PUT")] + public void RouteRemainsAvailableAndDeprecated(Type controller, string action, string? template, string verb) + { + var method = controller.GetMethod(action); + + method.Should().NotBeNull(); + var route = method!.GetCustomAttributes().Should().ContainSingle().Subject; + route.Template.Should().Be(template); + route.HttpMethods.Should().ContainSingle().Which.Should().Be(verb); + method.GetCustomAttributes().Should().ContainSingle(); + } + + [Theory] + [InlineData("openapi.json", "/api/user-facts", "get")] + [InlineData("openapi.json", "/api/user-facts/{id}", "delete")] + [InlineData("openapi.json", "/api/user-facts/bulk", "delete")] + [InlineData("openapi.json", "/api/Profile/ai-memory", "put")] + [InlineData("openapi_v2.json", "/api/user-facts", "get")] + [InlineData("openapi_v2.json", "/api/user-facts/{id}", "delete")] + [InlineData("openapi_v2.json", "/api/user-facts/bulk", "delete")] + [InlineData("openapi_v2.json", "/api/Profile/ai-memory", "put")] + public void GeneratedOpenApiMarksRouteDeprecated(string file, string route, string verb) + { + using var document = ReadOpenApi(file); + var operation = document.RootElement.GetProperty("paths").GetProperty(route).GetProperty(verb); + + operation.TryGetProperty("deprecated", out var deprecated).Should().BeTrue(); + deprecated.GetBoolean().Should().BeTrue(); + } + + [Theory] + [InlineData("openapi.json")] + [InlineData("openapi_v2.json")] + public void GeneratedOpenApiKeepsMemorySettingRequiredAndMarksItDeprecated(string file) + { + using var document = ReadOpenApi(file); + var profile = document.RootElement.GetProperty("components").GetProperty("schemas").GetProperty("ProfileResponse"); + var setting = profile.GetProperty("properties").GetProperty("aiMemoryEnabled"); + + profile.GetProperty("required").EnumerateArray().Select(value => value.GetString()) + .Should().Contain("aiMemoryEnabled"); + setting.GetProperty("type").GetString().Should().Be("boolean"); + setting.TryGetProperty("deprecated", out var deprecated).Should().BeTrue(); + deprecated.GetBoolean().Should().BeTrue(); + } + + private static JsonDocument ReadOpenApi(string file) + { + var path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, + "../../../../..", "src/Orbit.Api", file)); + return JsonDocument.Parse(File.ReadAllText(path)); + } +} diff --git a/tests/Orbit.Infrastructure.Tests/Controllers/ProfileControllerTests.cs b/tests/Orbit.Infrastructure.Tests/Controllers/ProfileControllerTests.cs index 7413eda9a..229e940d2 100644 --- a/tests/Orbit.Infrastructure.Tests/Controllers/ProfileControllerTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Controllers/ProfileControllerTests.cs @@ -97,6 +97,7 @@ public async Task SetTimezone_Failure_ReturnsBadRequest() } [Fact] + [Obsolete("Covers the deprecated PUT /api/Profile/ai-memory route.")] public async Task SetAiMemory_Success_ReturnsNoContent() { _mediator.Send(Arg.Any(), Arg.Any()) @@ -109,6 +110,7 @@ public async Task SetAiMemory_Success_ReturnsNoContent() } [Fact] + [Obsolete("Covers the deprecated PUT /api/Profile/ai-memory route.")] public async Task SetAiMemory_Failure_ReturnsBadRequest() { _mediator.Send(Arg.Any(), Arg.Any()) diff --git a/tests/Orbit.Infrastructure.Tests/Controllers/UserFactsControllerTests.cs b/tests/Orbit.Infrastructure.Tests/Controllers/UserFactsControllerTests.cs index 730d713e7..127901f79 100644 --- a/tests/Orbit.Infrastructure.Tests/Controllers/UserFactsControllerTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Controllers/UserFactsControllerTests.cs @@ -33,6 +33,7 @@ public UserFactsControllerTests() } [Fact] + [Obsolete("Covers the deprecated GET /api/user-facts route.")] public async Task GetUserFacts_Success_ReturnsOk() { _mediator.Send(Arg.Any(), Arg.Any()) @@ -44,6 +45,7 @@ public async Task GetUserFacts_Success_ReturnsOk() } [Fact] + [Obsolete("Covers the deprecated GET /api/user-facts route.")] public async Task GetUserFacts_Failure_ReturnsBadRequest() { _mediator.Send(Arg.Any(), Arg.Any()) @@ -55,6 +57,7 @@ public async Task GetUserFacts_Failure_ReturnsBadRequest() } [Fact] + [Obsolete("Covers the deprecated DELETE /api/user-facts/{id} route.")] public async Task DeleteUserFact_Success_ReturnsNoContent() { _mediator.Send(Arg.Any(), Arg.Any()) @@ -66,6 +69,7 @@ public async Task DeleteUserFact_Success_ReturnsNoContent() } [Fact] + [Obsolete("Covers the deprecated DELETE /api/user-facts/{id} route.")] public async Task DeleteUserFact_NotFound_ReturnsNotFound() { _mediator.Send(Arg.Any(), Arg.Any()) @@ -77,6 +81,7 @@ public async Task DeleteUserFact_NotFound_ReturnsNotFound() } [Fact] + [Obsolete("Covers the deprecated DELETE /api/user-facts/bulk route.")] public async Task BulkDeleteUserFacts_Success_ReturnsOk() { _mediator.Send(Arg.Any(), Arg.Any()) @@ -89,6 +94,7 @@ public async Task BulkDeleteUserFacts_Success_ReturnsOk() } [Fact] + [Obsolete("Covers the deprecated DELETE /api/user-facts/bulk route.")] public async Task BulkDeleteUserFacts_Failure_ReturnsBadRequest() { _mediator.Send(Arg.Any(), Arg.Any())