using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using PolyTrader.Modules.Supervisor.Agent; using Xunit; namespace PolyTrader.Tests { /// /// Sicherheitsnetz für den S-2-Agenten: Tool-Registry (Ausführung, Fehler, Unbekanntes), /// Function-Calling-Loop (Tool-Ergebnisse fließen zurück, Iterationsgrenze) und die /// OpenRouter-Serialisierung (Request-Body pur, Response-Parsing). /// public class SupervisorAgentTests { // ----- Registry ----- private static SupervisorToolRegistry RegistryWithEcho() { var reg = new SupervisorToolRegistry(); reg.Register(new SupervisorTool("echo", "Echo", """{"type":"object","properties":{"text":{"type":"string"}}}""", args => "ECHO:" + (SupervisorToolRegistry.GetString(args, "text") ?? ""))); reg.Register(new SupervisorTool("boom", "wirft", """{"type":"object","properties":{}}""", _ => throw new InvalidOperationException("kaputt"))); return reg; } [Fact] public void Registry_executes_tool_with_args() { Assert.Equal("ECHO:hallo", RegistryWithEcho().Execute("echo", """{"text":"hallo"}""")); } [Fact] public void Registry_unknown_tool_and_broken_args_return_error_strings() { var reg = RegistryWithEcho(); Assert.StartsWith("FEHLER: Unbekanntes Tool", reg.Execute("gibtsnicht", "{}")); Assert.StartsWith("FEHLER: Ungültige Tool-Argumente", reg.Execute("echo", "{kein json")); Assert.StartsWith("FEHLER bei Tool 'boom'", reg.Execute("boom", "{}")); // Exception -> Text, wirft nie } // ----- Agent-Loop ----- private sealed class ScriptedChatClient : IChatCompletionClient { private readonly Queue _script; public List> Requests { get; } = new(); public ScriptedChatClient(params ChatResponse[] script) => _script = new Queue(script); public Task CompleteAsync(string model, IReadOnlyList messages, IReadOnlyList tools, CancellationToken ct) { Requests.Add(new List(messages)); return Task.FromResult(_script.Count > 0 ? _script.Dequeue() : new ChatResponse { Content = "leer" }); } } [Fact] public async Task Agent_executes_tool_calls_and_feeds_results_back() { var chat = new ScriptedChatClient( new ChatResponse { ToolCalls = { new ToolCall("c1", "echo", """{"text":"daten"}""") } }, new ChatResponse { Content = "Fertige Analyse." }); var agent = new SupervisorAgent(chat, RegistryWithEcho()); var result = await agent.AskAsync("Warum X?"); Assert.Equal("Fertige Analyse.", result.Answer); Assert.Single(result.ToolInvocations); Assert.Equal(("echo", """{"text":"daten"}""", "ECHO:daten"), result.ToolInvocations[0]); // Zweiter Request enthält Assistant-ToolCall + Tool-Ergebnis. var second = chat.Requests[1]; Assert.Contains(second, m => m.Role == "assistant" && m.ToolCalls is { Count: 1 }); Assert.Contains(second, m => m.Role == "tool" && m.ToolCallId == "c1" && m.Content == "ECHO:daten"); // System-Prompt enthält den Architektur-Kontext. Assert.Contains("ARCHITEKTUR-KONTEXT", second[0].Content); } [Fact] public async Task Agent_stops_at_max_iterations() { // Modell fordert ENDLOS Tools an -> Agent bricht kontrolliert ab. var endless = Enumerable.Range(0, SupervisorAgent.MaxIterations + 2) .Select(i => new ChatResponse { ToolCalls = { new ToolCall($"c{i}", "echo", "{}") } }) .ToArray(); var agent = new SupervisorAgent(new ScriptedChatClient(endless), RegistryWithEcho()); var result = await agent.AskAsync("loop"); Assert.Contains("maximale Tool-Iterationen", result.Answer); Assert.Equal(SupervisorAgent.MaxIterations, result.ToolInvocations.Count); } // ----- OpenRouter-Serialisierung (pur) ----- [Fact] public void BuildRequestBody_produces_openai_compatible_json() { var messages = new List { ChatMessage.System("sys"), ChatMessage.User("frage"), ChatMessage.Assistant(null, new List { new("c1", "echo", """{"text":"x"}""") }), ChatMessage.ToolResult("c1", "ergebnis") }; var tools = new List { new("echo", "Echo-Tool", """{"type":"object","properties":{"text":{"type":"string"}}}""", _ => "") }; string body = OpenRouterClient.BuildRequestBody("openrouter/auto", messages, tools); using var doc = JsonDocument.Parse(body); // valides JSON var root = doc.RootElement; Assert.Equal("openrouter/auto", root.GetProperty("model").GetString()); var msgs = root.GetProperty("messages"); Assert.Equal(4, msgs.GetArrayLength()); Assert.Equal("function", msgs[2].GetProperty("tool_calls")[0].GetProperty("type").GetString()); Assert.Equal("echo", msgs[2].GetProperty("tool_calls")[0].GetProperty("function").GetProperty("name").GetString()); Assert.Equal("c1", msgs[3].GetProperty("tool_call_id").GetString()); Assert.Equal("echo", root.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); } [Fact] public void ParseResponse_reads_content_toolcalls_and_usage() { const string json = """ {"choices":[{"message":{"content":null,"tool_calls":[ {"id":"call_1","type":"function","function":{"name":"query_trades","arguments":"{\"limit\":5}"}}]}}], "usage":{"prompt_tokens":120,"completion_tokens":30}} """; var r = OpenRouterClient.ParseResponse(json); Assert.Null(r.Content); Assert.Single(r.ToolCalls); Assert.Equal("query_trades", r.ToolCalls[0].Name); Assert.Equal("""{"limit":5}""", r.ToolCalls[0].ArgumentsJson); Assert.Equal(120, r.PromptTokens); Assert.Equal(30, r.CompletionTokens); } [Fact] public void ArchitectureContext_is_embedded_and_loads() { string ctx = ArchitectureContext.Load(); Assert.Contains("PolyTrader", ctx); Assert.Contains("read-only", ctx); } } }