From 354654ea8400baef70ff1708cbde793a02905ed2 Mon Sep 17 00:00:00 2001 From: master <> Date: Mon, 9 Mar 2026 07:53:17 +0200 Subject: [PATCH] feat(advisoryai): register runs service and expose canonical /v1/advisory-ai/runs endpoint - Register RunService and IRunStore (InMemoryRunStore) in DI - Disambiguate IGuidGenerator namespaces (Chat vs Runs) - Mount RunEndpoints at canonical /v1/advisory-ai/runs path - Make RunService public for WebService composition - Add integration tests for runs authorization and CRUD Co-Authored-By: Claude Opus 4.6 --- .../ServiceCollectionExtensions.cs | 6 +- .../Endpoints/RunEndpoints.cs | 2 +- .../Program.cs | 3 + .../StellaOps.AdvisoryAI/Runs/RunService.cs | 2 +- .../RunEndpointsIntegrationTests.cs | 108 ++++++++++++++++++ 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/RunEndpointsIntegrationTests.cs diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.Hosting/ServiceCollectionExtensions.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.Hosting/ServiceCollectionExtensions.cs index 41138cb57..e312afe00 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.Hosting/ServiceCollectionExtensions.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.Hosting/ServiceCollectionExtensions.cs @@ -17,6 +17,7 @@ using StellaOps.AdvisoryAI.PolicyStudio; using StellaOps.AdvisoryAI.Providers; using StellaOps.AdvisoryAI.Queue; using StellaOps.AdvisoryAI.Remediation; +using StellaOps.AdvisoryAI.Runs; using StellaOps.OpsMemory.Storage; using System; using System.Collections.Generic; @@ -137,7 +138,10 @@ public static class ServiceCollectionExtensions services.AddOptions() .Bind(configuration.GetSection("AdvisoryAI:Chat")) .ValidateOnStart(); - services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs index 7fc6b70ad..330ec385b 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs @@ -29,7 +29,7 @@ public static class RunEndpoints /// The route group builder. public static RouteGroupBuilder MapRunEndpoints(this IEndpointRouteBuilder builder) { - var group = builder.MapGroup("/api/v1/runs") + var group = builder.MapGroup("/v1/advisory-ai/runs") .WithTags("Runs") .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) .RequireTenant(); diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs index 849574b88..ec2f32f5b 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs @@ -300,6 +300,9 @@ app.MapChatEndpoints(); // AI Attestations endpoints (Sprint: SPRINT_20260109_011_001 Task: AIAT-009) app.MapAttestationEndpoints(); +// AI Runs endpoints (Sprint: SPRINT_20260109_011_003 Task: RUN-006) +app.MapRunEndpoints(); + // Evidence Pack endpoints (Sprint: SPRINT_20260109_011_005 Task: EVPK-010) app.MapEvidencePackEndpoints(); diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/RunService.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/RunService.cs index 0cb57abd7..1f526905d 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/RunService.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI/Runs/RunService.cs @@ -15,7 +15,7 @@ namespace StellaOps.AdvisoryAI.Runs; /// Implementation of the run service. /// Sprint: SPRINT_20260109_011_003_BE Task: RUN-003 /// -internal sealed class RunService : IRunService +public sealed class RunService : IRunService { private readonly IRunStore _store; private readonly TimeProvider _timeProvider; diff --git a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/RunEndpointsIntegrationTests.cs b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/RunEndpointsIntegrationTests.cs new file mode 100644 index 000000000..3a87c2156 --- /dev/null +++ b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/RunEndpointsIntegrationTests.cs @@ -0,0 +1,108 @@ +using System.Net; +using System.Net.Http.Json; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using StellaOps.TestKit; +using Xunit; + +namespace StellaOps.AdvisoryAI.Tests.Integration; + +[Trait("Category", TestCategories.Integration)] +public sealed class RunEndpointsIntegrationTests : IDisposable +{ + private readonly WebApplicationFactory _factory = new(); + + [Fact] + public async Task QueryRuns_WithoutScope_ReturnsForbidden() + { + using var client = _factory.CreateClient(); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant"); + client.DefaultRequestHeaders.Add("X-Tenant-Id", "test-tenant"); + + var response = await client.GetAsync("/v1/advisory-ai/runs"); + + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task CreateAndQueryRuns_WithOperateAndViewScopes_ReturnExpectedPayloads() + { + using var operateClient = CreateClientWithScopes("advisory-ai:operate advisory-ai:view"); + + var createResponse = await operateClient.PostAsJsonAsync( + "/v1/advisory-ai/runs", + new + { + title = "Investigate CVE-2026-0001", + objective = "Confirm exploitability", + context = new + { + focusedCveId = "CVE-2026-0001", + focusedComponent = "pkg:npm/example@1.0.0" + } + }); + + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var created = await createResponse.Content.ReadFromJsonAsync(); + created.Should().NotBeNull(); + created!.RunId.Should().NotBeNullOrWhiteSpace(); + created.Title.Should().Be("Investigate CVE-2026-0001"); + + using var viewClient = CreateClientWithScopes("advisory-ai:view"); + + var queryResponse = await viewClient.GetAsync("/v1/advisory-ai/runs?take=10"); + queryResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var query = await queryResponse.Content.ReadFromJsonAsync(); + query.Should().NotBeNull(); + query!.TotalCount.Should().BeGreaterThanOrEqualTo(1); + query.Runs.Should().Contain(run => run.RunId == created.RunId); + + var getResponse = await viewClient.GetAsync($"/v1/advisory-ai/runs/{created.RunId}"); + getResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var loaded = await getResponse.Content.ReadFromJsonAsync(); + loaded.Should().NotBeNull(); + loaded!.RunId.Should().Be(created.RunId); + loaded.Context.Should().NotBeNull(); + loaded.Context!.FocusedCveId.Should().Be("CVE-2026-0001"); + } + + public void Dispose() + { + _factory.Dispose(); + } + + private HttpClient CreateClientWithScopes(string scopes) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "test-user"); + client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", scopes); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant"); + client.DefaultRequestHeaders.Add("X-Tenant-Id", "test-tenant"); + client.DefaultRequestHeaders.Add("X-User-Id", "test-user"); + client.DefaultRequestHeaders.Add("X-StellaOps-Client", "test-client"); + return client; + } + + private sealed record RunQueryResponse + { + public required RunResponse[] Runs { get; init; } + + public required int TotalCount { get; init; } + } + + private sealed record RunResponse + { + public required string RunId { get; init; } + + public required string Title { get; init; } + + public RunContextResponse? Context { get; init; } + } + + private sealed record RunContextResponse + { + public string? FocusedCveId { get; init; } + } +}