# 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
