Categories
Implementation Specs (157)
- specs/AnomalyDetection/TASK1.1.md
- specs/AnomalyDetection/TASK1.2.md
- specs/AnomalyDetection/TASK1.3.md
- specs/AnomalyDetection/TASK1.4.md
- specs/AnomalyDetection/TASK2.1.md
- specs/AnomalyDetection/TASK2.2.md
- specs/AnomalyDetection/TASK3.1.md
- specs/AnomalyDetection/TASK3.2.md
- specs/AnomalyDetection/TASK3.3.md
- specs/AnomalyDetection/TASK4.1.md
- specs/AnomalyDetection/TASK4.2.md
- specs/AnomalyDetection/TASK4.3.md
- specs/AnomalyDetection/TASK4.4.md
- specs/AnomalyDetection/TASK5.1.md
- specs/AnomalyDetection/TASK5.2.md
- specs/AnomalyDetection/TASK5.3.md
- specs/AnomalyDetection/TASK6.1.md
- specs/AnomalyDetection/TASK6.2.md
- specs/AnomalyDetection/TASK6.3.md
- specs/AnomalyDetection/TASKS.md
- specs/ApiServicePostgresTestStability/TASK.md
- specs/BackendDatabaseConsolidation/TASK.md
- specs/BackendDatabaseConsolidation/TASK1.1.md
- specs/BackendDatabaseConsolidation/TASK1.2.md
- specs/BackendDatabaseConsolidation/TASK1.3.md
- specs/BackendDatabaseConsolidation/TASK2.1.md
- specs/BackendDatabaseConsolidation/TASK2.2.md
- specs/BackendDatabaseConsolidation/TASK2.3.md
- specs/BackendDatabaseConsolidation/TASK3.1.md
- specs/BackendDatabaseConsolidation/TASK3.2.md
- specs/BackendDatabaseConsolidation/TASK4.1.md
- specs/BackendDatabaseConsolidation/TASK4.2.md
- specs/BackendDatabaseConsolidation/TASK4.3.md
- specs/BackendDatabaseConsolidation/TASK5.1.md
- specs/BackendDatabaseConsolidation/TASK5.2.md
- specs/BrinkLaborGetShiftsStaging/TASK.md
- specs/BrinkPosOrdersFirstEtlJob/TASK.md
- specs/BrinkPosOrdersFirstEtlJob/TASK0.md
- specs/BrinkPosOrdersFirstEtlJob/TASK1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.md
- specs/BrinkPosOrdersFirstEtlJob/TASK7.md
- specs/BrinkPosOrdersFirstEtlJob/TASK8.md
- specs/BrinkPosOrdersFirstEtlJob/TASK9.md
- specs/BrinkSettings2ToStaging/TASK.MD
- specs/CuratedToS3Stage/TASK.md
- specs/CuratedToS3Stage/TASK0.md
- specs/CuratedToS3Stage/TASK1.md
- specs/CuratedToS3Stage/TASK2.md
- specs/CuratedToS3Stage/TASK3.md
- specs/CuratedToS3Stage/TASK4.md
- specs/CustomerCredentialCustomHeaders/TASK.md
- specs/CustomerCredentialCustomHeaders/TASK1.1.md
- specs/CustomerCredentialCustomHeaders/TASK1.2.md
- specs/CustomerCredentialCustomHeaders/TASK2.1.md
- specs/CustomerCredentialCustomHeaders/TASK2.2.md
- specs/CustomerCredentialCustomHeaders/TASK3.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK.md
- specs/CustomerCredentialTriggeredProvisioning/TASK1.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK1.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK2.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK2.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK3.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK3.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK4.1.md
- specs/EtlSourceAdapterFramework/TASK.md
- specs/EtlSourceAdapterFramework/TASK0.md
- specs/EtlSourceAdapterFramework/TASK1.md
- specs/EtlSourceAdapterFramework/TASK2.md
- specs/EtlSourceAdapterFramework/TASK3.md
- specs/EtlSourceAdapterFramework/TASK4.md
- specs/EtlSourceAdapterFramework/TASK5.md
- specs/EtlSourceAdapterFramework/TASK6.md
- specs/KdsCdcDebeziumIntegration/TASK.md
- specs/KdsCdcDebeziumIntegration/TASK1.md
- specs/KdsCdcDebeziumIntegration/TASK2.md
- specs/KdsCdcDebeziumIntegration/TASK3.md
- specs/KdsCdcDebeziumIntegration/TASK4.md
- specs/KdsCdcDebeziumIntegration/TASK5.md
- specs/KdsCdcDurableRelationRouting/TASK.md
- specs/KdsCdcDurableRelationRouting/TASK1.md
- specs/KdsCdcDurableRelationRouting/TASK2.md
- specs/KdsCdcDurableRelationRouting/TASK3.md
- specs/KdsCdcDurableRelationRouting/TASK4.md
- specs/KdsCdcDurableRelationRouting/TASK5.md
- specs/KdsCdcIngestion/TASK.md
- specs/KdsCdcIngestion/TASK1.md
- specs/KdsCdcIngestion/TASK2.md
- specs/KdsCdcIngestion/TASK3.md
- specs/KdsCdcIngestion/TASK4.md
- specs/KdsCdcIngestion/TASK5.md
- specs/KdsCdcIngestion/TASK6.md
- specs/KdsCdcIngestion/TASK7.md
- specs/KdsCdcIngestion/TASK8.md
- specs/KdsCdcIngestion/TASK9.md
- specs/KdsCdcReplayExecution/TASK.md
- specs/KdsCdcReplayExecution/TASK1.md
- specs/KdsCdcReplayExecution/TASK2.md
- specs/KdsCdcReplayExecution/TASK3.md
- specs/KdsCdcReplayExecution/TASK4.md
- specs/KdsMonitorServingContractMigration/TASK.md
- specs/KdsMonitorServingContractMigration/TASK1.1.md
- specs/KdsMonitorServingContractMigration/TASK1.2.md
- specs/KdsMonitorServingContractMigration/TASK2.1.md
- specs/KdsMonitorServingContractMigration/TASK2.2.md
- specs/KdsMonitorServingContractMigration/TASK3.1.md
- specs/KdsMonitorServingContractMigration/TASK3.2.md
- specs/RedshiftFoundation/TASK.md
- specs/RedshiftFoundation/TASK1.1.md
- specs/RedshiftFoundation/TASK1.2.md
- specs/RedshiftFoundation/TASK1.3.md
- specs/RedshiftFoundation/TASK1.4.md
- specs/RedshiftFoundation/TASK2.1.md
- specs/RedshiftFoundation/TASK2.2.md
- specs/RedshiftFoundation/TASK3.1.md
- specs/RedshiftFoundation/TASK3.2.md
- specs/RedshiftFoundation/TASK3.3.md
- specs/RedshiftSyntheticDataFeed/TASK.md
- specs/RedshiftSyntheticDataFeed/TASK1.1.md
- specs/RedshiftSyntheticDataFeed/TASK1.2.md
- specs/RedshiftSyntheticDataFeed/TASK1.3.md
- specs/RedshiftSyntheticDataFeed/TASK2.1.md
- specs/RedshiftSyntheticDataFeed/TASK2.2.md
- specs/RedshiftSyntheticDataFeed/TASK3.1.md
- specs/RedshiftSyntheticDataFeed/TASK3.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK1.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK1.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.3.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK3.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK3.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK4.1.md
TASK 6.1: Create Mock Testing Infrastructure
Summary
Create mock and fake implementations that allow the anomaly detection system to run without a real MCP server, MySQL database, or LLM, enabling local development and automated testing. Since the system is generic (no typed data access services), mocking happens at two levels:
- Mock
IChatClient- returns pre-scripted agent responses (no LLM calls) - Fake
IMcpToolProvider- provides fake MCP tools that return canned SQL results
Prerequisite Refactor: Extract IMcpToolProvider Interface
The AnomalyDetectorAgent currently depends on the concrete McpToolProvider class, which makes it impossible to substitute a fake in tests. Before creating fakes, extract an IMcpToolProvider interface from McpToolProvider so that both the real provider and the fake implement the same contract. This follows the codebase pattern: public interface, internal sealed implementation.
Dependencies
- TASK 1.4 (McpToolProvider)
- TASK 2.1 (IAnomalyDetectorAgent, AnomalyDetectionResult)
- TASK 2.2 (AnomalyDetectorAgent implementation)
Design Principles
- No deleted interfaces: No
IKdsDataAccessService, noIMcpClientwrapper, no domain DTOs - Mock at boundaries: Mock the
IChatClient(LLM) andIMcpToolProvider(MCP server), not internal code - Generic: Mock infrastructure knows nothing about prep times or KDSDev - it returns generic SQL results
- Deterministic: Tests produce repeatable results
Files to Modify (Prerequisite Refactor)
0a. backend/Kds.Ai.AnomalyDetector/Mcp/IMcpToolProvider.cs (New)
Extract a public interface from McpToolProvider:
using Microsoft.Extensions.AI;
namespace Kds.Ai.AnomalyDetector.Mcp;
/// <summary>
/// Provides MCP tools (e.g., mysql_query) as AITool objects for agent use.
/// </summary>
public interface IMcpToolProvider
{
/// <summary>
/// Retrieves available tools as AITool objects
/// that can be passed directly to an agent.
/// </summary>
Task<IList<AITool>> GetToolsAsync(CancellationToken ct = default);
}0b. backend/Kds.Ai.AnomalyDetector/Mcp/McpToolProvider.cs (Modify)
Add IMcpToolProvider to the implementation:
using Kds.Ai.AnomalyDetector.Settings;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
namespace Kds.Ai.AnomalyDetector.Mcp;
/// <summary>
/// Manages the MCP client lifecycle and provides tools from the MySQL MCP server.
/// </summary>
internal sealed class McpToolProvider(McpMySqlSettings settings) : IMcpToolProvider, IAsyncDisposable
{
private McpClient? _client;
/// <summary>
/// Creates the MCP client and connects to the MySQL MCP server.
/// </summary>
public async Task<McpClient> GetClientAsync(CancellationToken ct = default)
{
if (_client is not null)
{
return _client;
}
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "mysql-mcp-server",
Command = settings.Command,
Arguments = settings.Arguments,
EnvironmentVariables = settings.EnvironmentVariables
});
_client = await McpClient.CreateAsync(transport, cancellationToken: ct);
return _client;
}
/// <inheritdoc />
public async Task<IList<AITool>> GetToolsAsync(CancellationToken ct = default)
{
var client = await GetClientAsync(ct);
var tools = await client.ListToolsAsync(cancellationToken: ct);
return [.. tools.Cast<AITool>()];
}
public async ValueTask DisposeAsync()
{
if (_client is not null)
{
await _client.DisposeAsync();
_client = null;
}
}
}Key changes:
- Now implements
IMcpToolProvider - Changed from
public sealedtointernal sealed(follows codebase pattern) - Uses primary constructor
0c. backend/Kds.Ai.AnomalyDetector/Agents/AnomalyDetectorAgent.cs (Modify)
Change from concrete McpToolProvider to IMcpToolProvider:
// Change the constructor parameter type:
public sealed class AnomalyDetectorAgent(
IChatClient chatClient,
IMcpToolProvider mcpToolProvider, // was: McpToolProvider
ILogger<AnomalyDetectorAgent> logger) : IAnomalyDetectorAgent0d. backend/Kds.Ai.AnomalyDetector/DependencyInjectionExtensions.cs (Modify)
Register McpToolProvider as IMcpToolProvider:
builder.Services.AddSingleton<IMcpToolProvider>(provider =>
{
var settings = provider.GetRequiredService<IOptions<McpMySqlSettings>>();
return new McpToolProvider(settings.Value);
});Files to Create
1. backend/Kds.Ai.AnomalyDetector.Tests/Fakes/FakeMcpToolProvider.cs
A fake that replaces the real McpToolProvider with in-process tool handlers:
using Kds.Ai.AnomalyDetector.Mcp;
using Microsoft.Extensions.AI;
namespace Kds.Ai.AnomalyDetector.Tests.Fakes;
/// <summary>
/// Fake MCP tool provider that returns canned SQL query results
/// without needing a real MCP server process.
/// </summary>
public sealed class FakeMcpToolProvider : IMcpToolProvider
{
private readonly Dictionary<string, Func<string, string>> _queryHandlers = new();
/// <summary>
/// Register a handler for SQL queries matching a pattern.
/// The handler receives the full SQL query and returns a JSON result.
/// </summary>
public FakeMcpToolProvider OnQuery(string pattern, Func<string, string> handler)
{
_queryHandlers[pattern] = handler;
return this;
}
/// <summary>
/// Register a handler that returns a fixed result for queries matching a pattern.
/// </summary>
public FakeMcpToolProvider OnQuery(string pattern, string fixedResult)
{
_queryHandlers[pattern] = _ => fixedResult;
return this;
}
/// <inheritdoc />
public Task<IList<AITool>> GetToolsAsync(CancellationToken ct = default)
{
var tool = AIFunctionFactory.Create(
(string query) =>
{
foreach (var (pattern, handler) in _queryHandlers)
{
if (query.Contains(pattern, StringComparison.OrdinalIgnoreCase))
{
return handler(query);
}
}
// Default: return empty result set
return "[]";
},
name: "mysql_query",
description: "Execute a SQL query against the MySQL database");
return Task.FromResult<IList<AITool>>([tool]);
}
}2. backend/Kds.Ai.AnomalyDetector.Tests/Fakes/FakeChatClient.cs
A fake IChatClient that returns scripted responses, simulating the LLM:
using Microsoft.Extensions.AI;
namespace Kds.Ai.AnomalyDetector.Tests.Fakes;
/// <summary>
/// Fake IChatClient that returns scripted responses without calling an LLM.
/// Supports function calling by detecting tool call requests and routing them.
/// </summary>
public sealed class FakeChatClient : IChatClient
{
private readonly Queue<ChatResponse> _responses = new();
/// <summary>
/// Enqueue a text response (the agent will receive this as assistant message)
/// </summary>
public FakeChatClient EnqueueTextResponse(string text)
{
_responses.Enqueue(new ChatResponse(new ChatMessage(ChatRole.Assistant, text)));
return this;
}
/// <summary>
/// Enqueue a function call response (simulates agent requesting a tool call)
/// </summary>
public FakeChatClient EnqueueFunctionCall(string functionName, string argumentsJson)
{
var functionCall = new FunctionCallContent(
callId: Guid.NewGuid().ToString(),
name: functionName,
arguments: System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object?>>(argumentsJson));
var message = new ChatMessage(ChatRole.Assistant, [functionCall]);
_responses.Enqueue(new ChatResponse(message));
return this;
}
public Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
if (_responses.Count == 0)
{
throw new InvalidOperationException(
"FakeChatClient has no more scripted responses. " +
"Enqueue more responses or check the test setup.");
}
return Task.FromResult(_responses.Dequeue());
}
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
throw new NotSupportedException("Streaming not supported in fake");
}
public ChatClientMetadata Metadata => new("FakeChatClient");
public void Dispose() { }
}3. backend/Kds.Ai.AnomalyDetector.Tests/Fakes/ScriptedAgentScenarios.cs
Pre-built test scenarios that combine fake tools + fake LLM responses:
using Kds.Ai.Data.Enums;
namespace Kds.Ai.AnomalyDetector.Tests.Fakes;
/// <summary>
/// Pre-built scenarios for testing.
/// Each scenario configures a FakeMcpToolProvider and FakeChatClient
/// to simulate a specific detection outcome.
/// </summary>
public static class ScriptedAgentScenarios
{
/// <summary>
/// Scenario: Agent queries data, finds no anomalies, calls emit_conclusion with anomalyDetected=false
/// </summary>
public static (FakeMcpToolProvider Tools, FakeChatClient Chat) NoAnomaly()
{
var tools = new FakeMcpToolProvider()
.OnQuery("SELECT", "[]"); // All queries return empty
var chat = new FakeChatClient()
// First: agent requests mysql_query
.EnqueueFunctionCall("mysql_query", """{"query": "SELECT COUNT(*) as cnt FROM orders WHERE done=1"}""")
// Second: after getting tool result, agent calls emit_conclusion
.EnqueueFunctionCall("emit_conclusion", """
{
"anomalyDetected": false,
"summary": "No anomalies detected in the analyzed period",
"findings": []
}
""");
return (tools, chat);
}
/// <summary>
/// Scenario: Agent queries data, finds anomaly, calls emit_conclusion with findings
/// </summary>
public static (FakeMcpToolProvider Tools, FakeChatClient Chat) SingleAnomaly(
EventSeverity severity = EventSeverity.Medium)
{
var tools = new FakeMcpToolProvider()
.OnQuery("SELECT", """[{"hour": 12, "avg_prep": 450, "baseline": 280}]""");
var chat = new FakeChatClient()
.EnqueueFunctionCall("mysql_query", """{"query": "SELECT AVG(preparation_time) FROM orders"}""")
.EnqueueFunctionCall("emit_conclusion", $$"""
{
"anomalyDetected": true,
"severity": "{{severity}}",
"summary": "Significant deviation detected during lunch hours",
"detailedAnalysis": "Analysis found 60% increase in average values during 12:00-13:00 compared to 28-day baseline.",
"findings": [
{
"description": "60% increase during 12:00-13:00",
"severity": "{{severity}}",
"affectedPeriod": "2025-01-15 12:00-13:00",
"evidence": "Average 450s vs baseline 280s (2.8 standard deviations)"
}
]
}
""");
return (tools, chat);
}
/// <summary>
/// Scenario: Agent encounters an error during data access
/// </summary>
public static (FakeMcpToolProvider Tools, FakeChatClient Chat) DataError()
{
var tools = new FakeMcpToolProvider()
.OnQuery("SELECT", _ => throw new InvalidOperationException("Connection refused"));
var chat = new FakeChatClient()
.EnqueueFunctionCall("mysql_query", """{"query": "SELECT 1"}""");
return (tools, chat);
}
}4. backend/Kds.Ai.AnomalyDetector.Tests/Fakes/TestingExtensions.cs
DI helpers for test setup:
using Kds.Ai.AnomalyDetector.Mcp;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
namespace Kds.Ai.AnomalyDetector.Tests.Fakes;
public static class TestingExtensions
{
/// <summary>
/// Replace real services with fakes for testing.
/// Uses a scripted scenario to configure the fake chat client and MCP tools.
/// </summary>
public static IServiceCollection UseFakes(
this IServiceCollection services,
FakeChatClient fakeChatClient,
FakeMcpToolProvider fakeMcpToolProvider)
{
// Replace IChatClient with fake
services.AddSingleton<IChatClient>(fakeChatClient);
// Replace IMcpToolProvider with fake
services.AddSingleton<IMcpToolProvider>(fakeMcpToolProvider);
return services;
}
}Usage Examples
In Unit Tests
[Fact]
public async Task DetectsAnomaly_WhenDataShowsDeviation()
{
var (tools, chat) = ScriptedAgentScenarios.SingleAnomaly(EventSeverity.High);
// Create agent with fakes
var agent = new AnomalyDetectorAgent(chat, tools, NullLogger<AnomalyDetectorAgent>.Instance);
var spec = new MonitorSpec
{
Name = "Test Monitor",
SystemPrompt = "Analyze data and detect anomalies",
State = MonitorState.Active
};
var result = await agent.RunDetectionAsync(spec, DateTimeOffset.UtcNow);
Assert.True(result.AnomalyDetected);
Assert.Single(result.Findings);
Assert.Contains("12:00", result.Findings[0].AffectedPeriod);
}In CLI for Local Testing
// In Program.cs, add a --mock flag
if (args.Contains("--mock"))
{
var (tools, chat) = ScriptedAgentScenarios.SingleAnomaly();
builder.Services.UseFakes(chat, tools);
}Changes from Old Design
- Deleted
MockKdsDataAccessServiceentirely (IKdsDataAccessService deleted) - Deleted
MockMcpCliententirely (IMcpClient wrapper deleted) - Deleted
MockDataScenarioenum with domain-specific scenarios (AnomalyAt12, AnomalyInDineIn, etc.) - Deleted all domain DTOs (PrepTimeHourlyStats, OrderTypeBreakdown, etc.)
- New approach: Mock at
IChatClient+IMcpToolProviderboundaries FakeChatClientscripts the LLM conversation (no real model calls)FakeMcpToolProviderimplementsIMcpToolProviderwith fakeAIToolobjects returning canned responsesScriptedAgentScenarioscombines both for common test patterns- Extracted
IMcpToolProviderinterface fromMcpToolProvider(prerequisite refactor for testability)
Acceptance Criteria
- [ ]
IMcpToolProviderinterface extracted fromMcpToolProvider - [ ]
McpToolProviderchanged tointernal sealed, implementsIMcpToolProvider - [ ]
AnomalyDetectorAgentdepends onIMcpToolProvider(not concreteMcpToolProvider) - [ ] DI registers
McpToolProviderasIMcpToolProvider - [ ]
FakeMcpToolProviderimplementsIMcpToolProviderwith pattern-matching query handlers - [ ]
FakeChatClientsupports scripted text and function call responses - [ ]
ScriptedAgentScenariosprovides at least: NoAnomaly, SingleAnomaly, DataError - [ ]
TestingExtensionsregisters fakes asIMcpToolProvider(not concrete type) - [ ] No references to
IKdsDataAccessService,IMcpClient, or domain DTOs - [ ] Fakes produce deterministic results
- [ ] Solution builds without errors
Notes
- The fake approach tests the full agent flow including tool handling
FakeChatClientscripts are ordered (first call returns first response, etc.)- For more sophisticated testing, consider recording real MCP responses and replaying them
- The
FakeMcpToolProviderreturns realAIToolobjects viaIMcpToolProvider.GetToolsAsync(), so the agent's tool-calling logic is exercised - The
IMcpToolProviderextraction is a prerequisite refactor — it changesMcpToolProviderfrompublic sealedtointernal sealedand adds the interface. This must be done before creating the fakes.