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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ConversationOptions>()
|
||||
.Bind(configuration.GetSection("AdvisoryAI:Chat"))
|
||||
.ValidateOnStart();
|
||||
services.TryAddSingleton<IGuidGenerator, DefaultGuidGenerator>();
|
||||
services.TryAddSingleton<StellaOps.AdvisoryAI.Chat.IGuidGenerator, StellaOps.AdvisoryAI.Chat.DefaultGuidGenerator>();
|
||||
services.TryAddSingleton<StellaOps.AdvisoryAI.Runs.IGuidGenerator, StellaOps.AdvisoryAI.Runs.DefaultGuidGenerator>();
|
||||
services.TryAddSingleton<IRunStore, InMemoryRunStore>();
|
||||
services.TryAddSingleton<IRunService, RunService>();
|
||||
services.TryAddSingleton<IConversationService, ConversationService>();
|
||||
services.TryAddSingleton<ConversationContextBuilder>();
|
||||
services.TryAddSingleton<ChatPromptAssembler>();
|
||||
|
||||
@@ -29,7 +29,7 @@ public static class RunEndpoints
|
||||
/// <returns>The route group builder.</returns>
|
||||
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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace StellaOps.AdvisoryAI.Runs;
|
||||
/// Implementation of the run service.
|
||||
/// Sprint: SPRINT_20260109_011_003_BE Task: RUN-003
|
||||
/// </summary>
|
||||
internal sealed class RunService : IRunService
|
||||
public sealed class RunService : IRunService
|
||||
{
|
||||
private readonly IRunStore _store;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
@@ -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<StellaOps.AdvisoryAI.WebService.Program> _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<RunResponse>();
|
||||
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<RunQueryResponse>();
|
||||
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<RunResponse>();
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user