Spensara Spensara Knowledge Base & Documentation Repository

Repository Docs & Knowledge Base

Consolidated Spensara documentation repository (722 Markdown & architecture documents in ../repository)
Built Word Documents (.docx): Spensara Overview (.docx) Capability Assessment (.docx) Brink POS Ingestion Analysis (.docx) The Borg Ingestion Architecture (.docx)

Implementation Specs (157)

Detailed engineering and feature implementation specifications.
implementation-specs/specs/AnomalyDetection/TASK6.2.md 28,025 bytes · Updated Oct 23, 2026 10:44 AM EDT
Raw File

TASK 6.2: Add Integration Tests for End-to-End Flow

Summary

Create integration tests that verify the complete anomaly detection flow works correctly, from orchestrator execution through agent detection to result storage. Tests use the FakeChatClient and FakeMcpToolProvider from TASK 6.1 to avoid real LLM and MCP calls.

Dependencies

  • All previous tasks completed
  • TASK 6.1 (FakeChatClient, FakeMcpToolProvider, ScriptedAgentScenarios, TestingExtensions)

Files to Create

1. backend/Kds.Ai.AnomalyDetector.Tests/Kds.Ai.AnomalyDetector.Tests.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" />
    <PackageReference Include="xunit" />
    <PackageReference Include="xunit.runner.visualstudio">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="FluentAssertions" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Kds.Ai.AnomalyDetector\Kds.Ai.AnomalyDetector.csproj" />
    <ProjectReference Include="..\Kds.Ai.Data\Kds.Ai.Data.csproj" />
  </ItemGroup>
</Project>

Note: No Moq dependency. Tests use FakeChatClient and FakeMcpToolProvider from TASK 6.1 instead of mock frameworks. The fakes exercise the real agent tool-calling logic.

Important: The test project needs InternalsVisibleTo attributes in both referenced projects to access internal sealed implementation classes:

In backend/Kds.Ai.Data/Kds.Ai.Data.csproj:

xml
<ItemGroup>
  <InternalsVisibleTo Include="Kds.Ai.AnomalyDetector.Tests" />
</ItemGroup>

In backend/Kds.Ai.AnomalyDetector/Kds.Ai.AnomalyDetector.csproj:

xml
<ItemGroup>
  <InternalsVisibleTo Include="Kds.Ai.AnomalyDetector.Tests" />
</ItemGroup>

This allows tests to directly instantiate internal sealed classes like EfMonitorSpecRepository, EfMonitorRunRepository, MonitorRunOrchestrator, and AlertEmitter.

2. backend/Kds.Ai.AnomalyDetector.Tests/IntegrationTests/OrchestratorIntegrationTests.cs

csharp
using FluentAssertions;
using Kds.Ai.AnomalyDetector.Agents;
using Kds.Ai.AnomalyDetector.Alerts;
using Kds.Ai.AnomalyDetector.Mcp;
using Kds.Ai.AnomalyDetector.Orchestration;
using Kds.Ai.AnomalyDetector.Repositories;
using Kds.Ai.AnomalyDetector.Seeding;
using Kds.Ai.AnomalyDetector.Tests.Fakes;
using Kds.Ai.Data;
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Kds.Ai.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Kds.Ai.AnomalyDetector.Tests.IntegrationTests;

public sealed class OrchestratorIntegrationTests : IAsyncLifetime
{
    private ServiceProvider _serviceProvider = null!;
    private KdsAiDbContext _dbContext = null!;
    
    public async Task InitializeAsync()
    {
        var services = new ServiceCollection();
        
        // In-memory database
        services.AddDbContext<KdsAiDbContext>(options =>
            options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}"));
        
        // Logging
        services.AddLogging(b => b.AddProvider(NullLoggerProvider.Instance));
        
        // Repositories
        services.AddScoped<IMonitorSpecRepository, EfMonitorSpecRepository>();
        services.AddScoped<IMonitorRunRepository, EfMonitorRunRepository>();
        
        // Alert emitter
        services.AddScoped<IAlertEmitter, AlertEmitter>();
        
        // Seeder
        services.AddScoped<MonitorSpecSeeder>();
        
        _serviceProvider = services.BuildServiceProvider();
        _dbContext = _serviceProvider.GetRequiredService<KdsAiDbContext>();
        await _dbContext.Database.EnsureCreatedAsync();
    }
    
    public async Task DisposeAsync()
    {
        await _dbContext.DisposeAsync();
        await _serviceProvider.DisposeAsync();
    }
    
    [Fact]
    public async Task FullFlow_NoAnomaly_CreatesCompletedRun()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.NoAnomaly();
        var orchestrator = CreateOrchestrator(chat, tools);
        
        var spec = await CreateTestSpec("No Anomaly Test");
        
        // Act
        var run = await orchestrator.ExecuteAsync(spec.Id);
        
        // Assert
        run.Status.Should().Be(MonitorRunStatus.Completed);
        run.AnomaliesDetected.Should().Be(0);
        run.AlertEvents.Should().BeEmpty();
        run.CompletedAt.Should().NotBeNull();
    }
    
    [Fact]
    public async Task FullFlow_WithAnomaly_CreatesAlertAndRun()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.SingleAnomaly(EventSeverity.High);
        var orchestrator = CreateOrchestrator(chat, tools);
        
        var spec = await CreateTestSpec("Anomaly Detection Test");
        
        // Act
        var run = await orchestrator.ExecuteAsync(spec.Id);
        
        // Assert
        run.Status.Should().Be(MonitorRunStatus.Completed);
        run.AnomaliesDetected.Should().BeGreaterThan(0);
        run.AlertEvents.Should().NotBeEmpty();
        
        // Verify alert was created in the database
        var alert = run.AlertEvents.First();
        alert.Id.Should().NotBe(Guid.Empty);
        alert.Severity.Should().Be(EventSeverity.High);
        alert.Type.Should().Be(EventType.AiAnomaly);
    }
    
    [Fact]
    public async Task DryRun_DoesNotPersist()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.SingleAnomaly();
        var spec = await CreateTestSpec("Dry Run Test");
        var orchestrator = CreateOrchestrator(chat, tools);
        
        // Act
        var run = await orchestrator.ExecuteAsync(
            spec, 
            options: new RunOptions { DryRun = true });
        
        // Assert
        run.Status.Should().Be(MonitorRunStatus.Completed);
        
        // Verify nothing was persisted
        var runCount = await _dbContext.MonitorRuns.CountAsync();
        var alertCount = await _dbContext.Events.CountAsync();
        
        runCount.Should().Be(0);
        alertCount.Should().Be(0);
    }
    
    [Fact]
    public async Task SuppressAlerts_NoAlertEmitted()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.SingleAnomaly();
        var spec = await CreateTestSpec("Suppress Alert Test");
        var orchestrator = CreateOrchestrator(chat, tools);
        
        // Act
        var run = await orchestrator.ExecuteAsync(
            spec.Id, 
            options: new RunOptions { SuppressAlerts = true });
        
        // Assert
        run.AlertEvents.Should().BeEmpty();
        
        var alertCount = await _dbContext.Events.CountAsync();
        alertCount.Should().Be(0);
    }
    
    [Fact]
    public async Task SpecNotFound_ThrowsException()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.NoAnomaly();
        var orchestrator = CreateOrchestrator(chat, tools);
        
        // Act & Assert
        var act = () => orchestrator.ExecuteAsync(Guid.NewGuid());
        await act.Should().ThrowAsync<InvalidOperationException>()
            .WithMessage("*not found*");
    }
    
    [Fact]
    public async Task ExecuteAllActive_RunsOnlyActiveMonitors()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.NoAnomaly();
        var orchestrator = CreateOrchestrator(chat, tools);
        
        var specRepo = _serviceProvider.GetRequiredService<IMonitorSpecRepository>();
        
        // Create 3 specs: 2 active, 1 paused
        await CreateTestSpec("Active Monitor 1", MonitorState.Active);
        await CreateTestSpec("Active Monitor 2", MonitorState.Active);
        await CreateTestSpec("Paused Monitor", MonitorState.Paused);
        
        // Act
        var runs = await orchestrator.ExecuteAllActiveAsync();
        
        // Assert
        runs.Should().HaveCount(2);
        runs.Should().OnlyContain(r => r.Status == MonitorRunStatus.Completed);
    }
    
    [Fact]
    public async Task ExecuteAllActive_SkipsDraftAndArchivedMonitors()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.NoAnomaly();
        var orchestrator = CreateOrchestrator(chat, tools);
        
        // Create monitors in each state
        await CreateTestSpec("Draft Monitor", MonitorState.Draft);
        await CreateTestSpec("Active Monitor", MonitorState.Active);
        await CreateTestSpec("Paused Monitor", MonitorState.Paused);
        await CreateTestSpec("Archived Monitor", MonitorState.Archived);
        
        // Act
        var runs = await orchestrator.ExecuteAllActiveAsync();
        
        // Assert - only the Active monitor should run
        runs.Should().HaveCount(1);
    }
    
    [Fact]
    public async Task AgentError_RunMarkedAsFailed()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.DataError();
        var spec = await CreateTestSpec("Error Handling Test");
        var orchestrator = CreateOrchestrator(chat, tools);
        
        // Act
        var run = await orchestrator.ExecuteAsync(spec.Id);
        
        // Assert
        run.Status.Should().Be(MonitorRunStatus.Failed);
        run.ErrorMessage.Should().NotBeNullOrEmpty();
        run.CompletedAt.Should().NotBeNull();
    }
    
    [Fact]
    public async Task Run_RecordsDuration()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.NoAnomaly();
        var spec = await CreateTestSpec("Duration Test");
        var orchestrator = CreateOrchestrator(chat, tools);
        
        // Act
        var run = await orchestrator.ExecuteAsync(spec.Id);
        
        // Assert
        run.DurationMs.Should().BeGreaterOrEqualTo(0);
    }
    
    // === Helper Methods ===
    
    private async Task<MonitorSpec> CreateTestSpec(
        string name, 
        MonitorState state = MonitorState.Active)
    {
        var seeder = _serviceProvider.GetRequiredService<MonitorSpecSeeder>();
        return await seeder.EnsureAsync(
            name, 
            "Test system prompt: Analyze data and detect anomalies.",
            initialState: state);
    }
    
    private IMonitorRunOrchestrator CreateOrchestrator(
        FakeChatClient chat,
        FakeMcpToolProvider tools)
    {
        // Create agent with fakes (single generic agent, no factory)
        var agent = new AnomalyDetectorAgent(
            chat,
            tools,
            NullLogger<AnomalyDetectorAgent>.Instance);
        
        return new MonitorRunOrchestrator(
            _serviceProvider.GetRequiredService<IMonitorSpecRepository>(),
            _serviceProvider.GetRequiredService<IMonitorRunRepository>(),
            agent,
            _serviceProvider.GetRequiredService<IAlertEmitter>(),
            NullLogger<MonitorRunOrchestrator>.Instance);
    }
}

3. backend/Kds.Ai.AnomalyDetector.Tests/UnitTests/AlertEmitterTests.cs

csharp
using FluentAssertions;
using Kds.Ai.AnomalyDetector.Agents;
using Kds.Ai.AnomalyDetector.Alerts;
using Kds.Ai.Data;
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Kds.Ai.AnomalyDetector.Tests.UnitTests;

public sealed class AlertEmitterTests : IAsyncLifetime
{
    private KdsAiDbContext _dbContext = null!;
    private AlertEmitter _emitter = null!;
    
    public async Task InitializeAsync()
    {
        var options = new DbContextOptionsBuilder<KdsAiDbContext>()
            .UseInMemoryDatabase($"AlertTest_{Guid.NewGuid()}")
            .Options;
        
        _dbContext = new KdsAiDbContext(options);
        _emitter = new AlertEmitter(_dbContext, NullLogger<AlertEmitter>.Instance);
        
        await _dbContext.Database.EnsureCreatedAsync();
    }
    
    public async Task DisposeAsync()
    {
        await _dbContext.DisposeAsync();
    }
    
    [Fact]
    public async Task EmitAsync_CreatesAlertWithCorrectFormat()
    {
        // Arrange
        var spec = CreateTestSpec("Prep Time Monitor - Downtown");
        var result = new AnomalyDetectionResult
        {
            AnomalyDetected = true,
            Severity = EventSeverity.High,
            Summary = "Prep times elevated during lunch",
            DetailedAnalysis = "Detailed markdown analysis...",
            Findings =
            [
                new AnomalyFinding
                { 
                    Description = "60% increase during 12:00-13:00",
                    Severity = EventSeverity.High,
                    AffectedPeriod = "2025-01-15 12:00-13:00",
                    Evidence = "Average 450s vs baseline 280s"
                }
            ]
        };
        
        // Act
        var alert = await _emitter.EmitAsync(spec, result);
        
        // Assert
        alert.Id.Should().NotBe(Guid.Empty);
        alert.Type.Should().Be(EventType.AiAnomaly);
        alert.Severity.Should().Be(EventSeverity.High);
        alert.Name.Should().Contain("[Prep Time Monitor - Downtown]");
        alert.Content.Should().Contain("## Findings");
        alert.Content.Should().Contain("60% increase during 12:00-13:00");
        alert.Content.Should().Contain("Evidence");
    }
    
    [Fact]
    public async Task EmitAsync_ThrowsForNonAnomaly()
    {
        // Arrange
        var spec = CreateTestSpec("Test Monitor");
        var result = AnomalyDetectionResult.NoAnomaly();
        
        // Act & Assert
        var act = () => _emitter.EmitAsync(spec, result);
        await act.Should().ThrowAsync<InvalidOperationException>();
    }
    
    [Fact]
    public async Task EmitAsync_MultipleFindings_AllIncludedInContent()
    {
        // Arrange
        var spec = CreateTestSpec("Multi-Finding Monitor");
        var result = new AnomalyDetectionResult
        {
            AnomalyDetected = true,
            Severity = EventSeverity.Medium,
            Summary = "Multiple anomalies detected",
            Findings =
            [
                new AnomalyFinding
                {
                    Description = "First anomaly",
                    Severity = EventSeverity.Medium,
                    AffectedPeriod = "12:00-13:00",
                    Evidence = "Evidence for first"
                },
                new AnomalyFinding
                {
                    Description = "Second anomaly",
                    Severity = EventSeverity.Low,
                    AffectedPeriod = "14:00-15:00",
                    Evidence = "Evidence for second"
                }
            ]
        };
        
        // Act
        var alert = await _emitter.EmitAsync(spec, result);
        
        // Assert
        alert.Content.Should().Contain("First anomaly");
        alert.Content.Should().Contain("Second anomaly");
        alert.Content.Should().Contain("Evidence for first");
        alert.Content.Should().Contain("Evidence for second");
    }
    
    [Fact]
    public async Task ShouldSuppressAsync_SuppressesDuplicateWithinWindow()
    {
        // Arrange
        var spec = CreateTestSpec("Dedup Monitor");
        var result = new AnomalyDetectionResult
        {
            AnomalyDetected = true,
            Severity = EventSeverity.Medium,
            Summary = "Test anomaly"
        };
        
        // Emit first alert
        await _emitter.EmitAsync(spec, result);
        
        // Act - check if second emission should be suppressed
        var shouldSuppress = await _emitter.ShouldSuppressAsync(spec, result);
        
        // Assert
        shouldSuppress.Should().BeTrue();
    }
    
    [Fact]
    public async Task ShouldSuppressAsync_DoesNotSuppressDifferentMonitors()
    {
        // Arrange
        var spec1 = CreateTestSpec("Monitor A");
        var spec2 = CreateTestSpec("Monitor B");
        var result = new AnomalyDetectionResult
        {
            AnomalyDetected = true,
            Severity = EventSeverity.Medium,
            Summary = "Test anomaly"
        };
        
        // Emit alert for monitor A
        await _emitter.EmitAsync(spec1, result);
        
        // Act - check if monitor B should be suppressed
        var shouldSuppress = await _emitter.ShouldSuppressAsync(spec2, result);
        
        // Assert
        shouldSuppress.Should().BeFalse();
    }
    
    // === Helper ===
    
    private static MonitorSpec CreateTestSpec(string name) => new()
    {
        Id = Guid.NewGuid(),
        Name = name,
        SystemPrompt = "Test system prompt",
        State = MonitorState.Active,
        CreatedAt = DateTimeOffset.UtcNow
    };
}

4. backend/Kds.Ai.AnomalyDetector.Tests/UnitTests/AgentDetectionTests.cs

csharp
using FluentAssertions;
using Kds.Ai.AnomalyDetector.Agents;
using Kds.Ai.AnomalyDetector.Tests.Fakes;
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Kds.Ai.AnomalyDetector.Tests.UnitTests;

/// <summary>
/// Tests for the AnomalyDetectorAgent using FakeChatClient and FakeMcpToolProvider.
/// These exercise the agent's tool-calling loop without real LLM or MCP calls.
/// </summary>
public sealed class AgentDetectionTests
{
    [Fact]
    public async Task RunDetection_NoAnomaly_ReturnsNoAnomalyResult()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.NoAnomaly();
        var agent = CreateAgent(chat, tools);
        var spec = CreateTestSpec("No Anomaly Monitor");
        
        // Act
        var result = await agent.RunDetectionAsync(spec, DateTimeOffset.UtcNow);
        
        // Assert
        result.AnomalyDetected.Should().BeFalse();
        result.Findings.Should().BeEmpty();
    }
    
    [Fact]
    public async Task RunDetection_SingleAnomaly_ReturnsFindings()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.SingleAnomaly(EventSeverity.High);
        var agent = CreateAgent(chat, tools);
        var spec = CreateTestSpec("Anomaly Monitor");
        
        // Act
        var result = await agent.RunDetectionAsync(spec, DateTimeOffset.UtcNow);
        
        // Assert
        result.AnomalyDetected.Should().BeTrue();
        result.Severity.Should().Be(EventSeverity.High);
        result.Summary.Should().NotBeNullOrEmpty();
        result.Findings.Should().NotBeEmpty();
        result.Findings[0].AffectedPeriod.Should().Contain("12:00");
        result.Findings[0].Evidence.Should().NotBeNullOrEmpty();
    }
    
    [Fact]
    public async Task RunDetection_DataError_HandlesGracefully()
    {
        // Arrange
        var (tools, chat) = ScriptedAgentScenarios.DataError();
        var agent = CreateAgent(chat, tools);
        var spec = CreateTestSpec("Error Monitor");
        
        // Act & Assert
        // The agent should either return a Failed result or throw,
        // depending on implementation. Either is acceptable.
        var act = async () => await agent.RunDetectionAsync(spec, DateTimeOffset.UtcNow);
        
        // DataError scenario throws during MCP tool execution,
        // so agent should handle it or let it propagate
        await act.Should().ThrowAsync<Exception>();
    }
    
    [Fact]
    public async Task RunDetection_UsesSpecSystemPrompt()
    {
        // Arrange - custom tools that record the query
        var tools = new FakeMcpToolProvider()
            .OnQuery("SELECT", "[]");
        
        var chat = new FakeChatClient()
            .EnqueueFunctionCall("emit_conclusion", """
            {
                "anomalyDetected": false,
                "summary": "No data found",
                "findings": []
            }
            """);
        
        var agent = CreateAgent(chat, tools);
        var spec = CreateTestSpec("Custom Prompt Monitor");
        spec.SystemPrompt = "You are a special-purpose analyzer. Always check the widgets table.";
        
        // Act
        var result = await agent.RunDetectionAsync(spec, DateTimeOffset.UtcNow);
        
        // Assert - the agent processed without error using the custom prompt
        result.Should().NotBeNull();
        result.AnomalyDetected.Should().BeFalse();
    }
    
    // === Helpers ===
    
    private static IAnomalyDetectorAgent CreateAgent(
        FakeChatClient chat,
        FakeMcpToolProvider tools)
    {
        return new AnomalyDetectorAgent(
            chat,
            tools,
            NullLogger<AnomalyDetectorAgent>.Instance);
    }
    
    private static MonitorSpec CreateTestSpec(string name) => new()
    {
        Id = Guid.NewGuid(),
        Name = name,
        SystemPrompt = "Analyze data and detect anomalies. Use mysql_query tool to access data.",
        State = MonitorState.Active,
        CreatedAt = DateTimeOffset.UtcNow
    };
}

5. backend/Kds.Ai.AnomalyDetector.Tests/UnitTests/MonitorSpecSeederTests.cs

csharp
using FluentAssertions;
using Kds.Ai.AnomalyDetector.Seeding;
using Kds.Ai.Data;
using Kds.Ai.Data.Enums;
using Kds.Ai.Data.Repositories;
using Kds.Ai.Data.Seeding;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Kds.Ai.AnomalyDetector.Tests.UnitTests;

public sealed class MonitorSpecSeederTests : IAsyncLifetime
{
    private KdsAiDbContext _dbContext = null!;
    private MonitorSpecSeeder _seeder = null!;
    
    public async Task InitializeAsync()
    {
        var options = new DbContextOptionsBuilder<KdsAiDbContext>()
            .UseInMemoryDatabase($"SeederTest_{Guid.NewGuid()}")
            .Options;
        
        _dbContext = new KdsAiDbContext(options);
        var repo = new EfMonitorSpecRepository(_dbContext, NullLogger<EfMonitorSpecRepository>.Instance);
        _seeder = new MonitorSpecSeeder(repo, NullLogger<MonitorSpecSeeder>.Instance);
        
        await _dbContext.Database.EnsureCreatedAsync();
    }
    
    public async Task DisposeAsync()
    {
        await _dbContext.DisposeAsync();
    }
    
    [Fact]
    public async Task EnsureAsync_CreatesNewMonitor()
    {
        // Act
        var spec = await _seeder.EnsureAsync("Test Monitor", "Test prompt");
        
        // Assert
        spec.Id.Should().NotBe(Guid.Empty);
        spec.Name.Should().Be("Test Monitor");
        spec.SystemPrompt.Should().Be("Test prompt");
        spec.State.Should().Be(MonitorState.Draft); // Default state
    }
    
    [Fact]
    public async Task EnsureAsync_ReturnsExistingByName()
    {
        // Arrange
        var first = await _seeder.EnsureAsync("Duplicate Test", "First prompt");
        
        // Act
        var second = await _seeder.EnsureAsync("Duplicate Test", "Different prompt");
        
        // Assert
        second.Id.Should().Be(first.Id);
        second.SystemPrompt.Should().Be("First prompt"); // Original preserved
    }
    
    [Fact]
    public async Task EnsureAsync_RespectsInitialState()
    {
        // Act
        var spec = await _seeder.EnsureAsync(
            "Active Monitor", 
            "Test prompt",
            initialState: MonitorState.Active);
        
        // Assert
        spec.State.Should().Be(MonitorState.Active);
    }
    
    [Fact]
    public async Task EnsureAsync_ExamplePromptIsValid()
    {
        // Act
        var spec = await _seeder.EnsureAsync(
            "Prep Time Example",
            ExamplePrompts.PrepTimeAnomaly);
        
        // Assert
        spec.SystemPrompt.Should().Contain("mysql_query");
        spec.SystemPrompt.Should().Contain("preparation_time");
        spec.SystemPrompt.Should().Contain("emit_conclusion");
    }
}

Running Tests

bash
# Run all tests
dotnet test backend/Kds.Ai.AnomalyDetector.Tests

# Run with verbose output
dotnet test backend/Kds.Ai.AnomalyDetector.Tests --logger "console;verbosity=detailed"

# Run specific test class
dotnet test backend/Kds.Ai.AnomalyDetector.Tests --filter "FullyQualifiedName~OrchestratorIntegrationTests"

# Run unit tests only
dotnet test backend/Kds.Ai.AnomalyDetector.Tests --filter "FullyQualifiedName~UnitTests"

Changes from Old Design

  • Removed Moq dependency - tests use FakeChatClient and FakeMcpToolProvider from TASK 6.1
  • Removed PrepTimeQueryBuilderTests entirely - PrepTimeQueryBuilder was deleted
  • Removed Mock<IAnomalyDetectorAgentFactory> - no factory pattern, single generic agent
  • Removed mock.SetupGet(a => a.MonitorType) - agent has no MonitorType property
  • Removed PrepTimeMonitorSpecFactory.CreateDefault(storeGuid) - uses MonitorSpecSeeder.EnsureAsync() or inline MonitorSpec creation
  • Removed spec.IsEnabled = false - uses spec.State = MonitorState.Paused
  • Removed run.AlertEventId assertions - uses run.AlertEvents collection (many-to-many)
  • Removed ExecuteAllByTypeAsync("PrepTimeAnomaly") - uses ExecuteAllActiveAsync()
  • Removed MockDataScenario enum references - uses ScriptedAgentScenarios methods
  • Removed AnomalyDetectionResult with Type, DeviationPercent fields - uses Description, Severity, AffectedPeriod, Evidence
  • Added AgentDetectionTests that exercise the agent directly with fakes
  • Added MonitorSpecSeederTests for seeder idempotency
  • All test classes are sealed
  • EfMonitorSpecRepository uses primary constructor (dbContext + logger parameters)
  • IMonitorSpecRepository imported from Kds.Ai.Data.Repositories (registered by AddDataModule())
  • Test project requires InternalsVisibleTo from both Kds.Ai.Data and Kds.Ai.AnomalyDetector to access internal sealed implementations
  • AnomalyDetectorAgent takes IMcpToolProvider interface (from TASK 6.1 refactor), allowing FakeMcpToolProvider substitution

Acceptance Criteria

  • [ ] Test project created with correct references (no Moq)
  • [ ] InternalsVisibleTo added in both Kds.Ai.Data.csproj and Kds.Ai.AnomalyDetector.csproj for test project access to internal sealed classes
  • [ ] Integration tests for orchestrator complete flow using FakeChatClient and FakeMcpToolProvider
  • [ ] Unit tests for agent detection using scripted scenarios
  • [ ] Unit tests for alert emitter with generic findings
  • [ ] Unit tests for seeder idempotency
  • [ ] Tests use in-memory database
  • [ ] Tests use ScriptedAgentScenarios (not mock interfaces)
  • [ ] IMonitorSpecRepository imported from Kds.Ai.Data.Repositories (not Kds.Ai.AnomalyDetector.Repositories)
  • [ ] IMonitorRunRepository imported from Kds.Ai.AnomalyDetector.Repositories
  • [ ] EfMonitorSpecRepository instantiated with primary constructor signature (dbContext + logger)
  • [ ] AnomalyDetectorAgent takes IMcpToolProvider (not concrete McpToolProvider) — FakeMcpToolProvider satisfies this via interface implementation
  • [ ] No references to: IAnomalyDetectorAgentFactory, MonitorType, StoreGuid, IsEnabled, AlertEventId, PrepTimeQueryBuilder, PrepTimeMonitorSpecFactory, MockDataScenario, IKdsDataAccessService, IMcpClient
  • [ ] All uses run.AlertEvents (collection, not single FK)
  • [ ] All uses MonitorState enum (not bool IsEnabled)
  • [ ] All test classes are sealed
  • [ ] All tests pass
  • [ ] Solution builds without errors

Notes

  • Tests exercise the full agent tool-calling loop via fakes (not just mocking the agent interface)
  • FakeChatClient scripts are ordered: first call returns first response, etc.
  • In-memory database resets per test via unique DB names
  • Consider adding tests for timeout handling and concurrent execution in the future
  • The AgentDetectionTests verify the agent works with custom MonitorSpec prompts