using System.Text.Json; using ClawdDotNet.Core.Api; using ClawdDotNet.Core.Api.Models; namespace ClawdDotNet.Core.Tests.Infrastructure; /// /// Ersetzt den OpenRouterClient in Tests. Liefert eine vorprogrammierte Antwortfolge /// und schreibt jeden empfangenen Request mit. /// /// Wichtig: Requests werden tief kopiert. Die Engine reicht dieselbe List<ChatMessage> /// weiter und verändert sie danach — ohne Kopie würden Tests den Endzustand prüfen /// statt dessen, was tatsächlich gesendet wurde. /// internal sealed class FakeChatClient : IChatCompletionClient { private readonly Queue> _responses = new(); private Func? _fallback; /// Alle empfangenen Requests, als tiefe Kopien. public List ReceivedRequests { get; } = new(); public int CallCount => ReceivedRequests.Count; // ─── Programmierung der Antworten ─── public FakeChatClient RespondsWithText(string text, Usage? usage = null) { _responses.Enqueue(_ => TextResponse(text, usage)); return this; } public FakeChatClient RespondsWithToolCall(string toolName, string argumentsJson = "{}", string? id = null) => RespondsWithToolCalls((toolName, argumentsJson, id)); public FakeChatClient RespondsWithToolCalls(params (string Tool, string Args, string? Id)[] calls) { var toolCalls = calls.Select((c, i) => new ToolCall { Id = c.Id ?? $"call_{Guid.NewGuid():N}"[..12], Type = "function", Function = new ToolCallFunction { Name =c.Tool, Arguments = c.Args } }).ToList(); _responses.Enqueue(_ => new ChatResponse { Id = "resp_" + ReceivedRequests.Count, Choices = [new Choice { Index = 0, Message = new ChatMessage { Role = "assistant", ToolCalls = toolCalls } }], Usage = new Usage { PromptTokens = 100, CompletionTokens = 20, TotalTokens = 120 } }); return this; } /// Antwortet mit einer bestimmten Prompt-Token-Zahl — für Compaction-Schwellen. public FakeChatClient RespondsWithTokens(int promptTokens, string text = "fertig") { _responses.Enqueue(_ => TextResponse(text, new Usage { PromptTokens = promptTokens, CompletionTokens = 10, TotalTokens = promptTokens + 10 })); return this; } public FakeChatClient Throws(Exception ex) { _responses.Enqueue(_ => throw ex); return this; } /// Antwort für alle Aufrufe, die über die programmierte Folge hinausgehen. public FakeChatClient AlwaysRespondsWithText(string text) { _fallback = _ => TextResponse(text); return this; } // ─── IChatCompletionClient ─── public Task CompleteAsync(ChatRequest request, CancellationToken ct) { ct.ThrowIfCancellationRequested(); ReceivedRequests.Add(DeepClone(request)); if (_responses.Count > 0) return Task.FromResult(_responses.Dequeue()(request)); if (_fallback is not null) return Task.FromResult(_fallback(request)); throw new InvalidOperationException( $"FakeChatClient: unerwarteter Aufruf Nr. {ReceivedRequests.Count} — " + "keine Antwort mehr programmiert."); } // ─── Helfer ─── private static ChatResponse TextResponse(string text, Usage? usage = null) => new() { Id = "resp", Choices = [new Choice { Index = 0, Message = new ChatMessage { Role = "assistant", Content = text }, FinishReason = "stop" }], Usage = usage ?? new Usage { PromptTokens = 100, CompletionTokens = 20, TotalTokens = 120 } }; private static ChatRequest DeepClone(ChatRequest request) => new() { Model = request.Model, Stream = request.Stream, Temperature = request.Temperature, MaxTokens = request.MaxTokens, ToolChoice = request.ToolChoice, Tools = request.Tools?.ToList(), Messages = request.Messages.Select(CloneMessage).ToList() }; private static ChatMessage CloneMessage(ChatMessage m) => new() { Role = m.Role, Content = m.Content, ToolCallId = m.ToolCallId, ToolCalls = m.ToolCalls?.Select(tc => new ToolCall { Id = tc.Id, Type = tc.Type, Function = new ToolCallFunction { Name =tc.Function.Name, Arguments = tc.Function.Arguments } }).ToList() }; }