using Agent.App.Logging; using Agent.Core.Localization; using Agent.Core.Ollama; using Agent.Core.Runtime; using Agent.Core.Tools; using Agent.Mcp; using System.Diagnostics; using System.Text; using System.Text.Json; namespace Agent.Core.Tests; public sealed class ObservabilityTests { [Fact] public void Llm_log_carries_correlation_usage_and_tool_iteration_fields() { using var directory = new TestDirectory(); Guid sessionId = Guid.NewGuid(); Guid taskId = Guid.NewGuid(); Guid plannerTaskId = Guid.NewGuid(); Guid callId = Guid.NewGuid(); var context = new AgentLlmCallContext(callId, sessionId, taskId, plannerTaskId, "replan", 7, 2, "model-a"); using (var observer = new JsonLinesRuntimeObserver(directory.Path, 1024 * 1024, 2)) { observer.LlmRequest(context, [new OllamaMessage("assistant", "request", Thinking: "private request reasoning")]); observer.LlmResponse(context, new OllamaResult("response", 11, 13, Thinking: "private response reasoning")); observer.ToolResult(context, "file_read", new ToolExecutionResult(true, "ok", false)); observer.LlmFailure(context, "planning_failed", "invalid plan"); } JsonElement[] entries = File.ReadLines(Path.Combine(directory.Path, "llm.jsonl")) .Select(static line => JsonDocument.Parse(line).RootElement.Clone()).ToArray(); Assert.Equal(4, entries.Length); Assert.All(entries, entry => { Assert.Equal(callId, entry.GetProperty("call_id").GetGuid()); Assert.Equal(taskId, entry.GetProperty("task_id").GetGuid()); Assert.Equal(sessionId, entry.GetProperty("session_id").GetGuid()); Assert.Equal(plannerTaskId, entry.GetProperty("planner_task_id").GetGuid()); Assert.Equal(7, entry.GetProperty("iteration").GetInt32()); Assert.Equal(2, entry.GetProperty("replan").GetInt32()); }); Assert.Equal(11, entries[1].GetProperty("input_tokens").GetInt64()); Assert.Equal("file_read", entries[2].GetProperty("tool_name").GetString()); Assert.Equal("llm.failure", entries[3].GetProperty("event_name").GetString()); Assert.Equal("planning_failed", entries[3].GetProperty("error_code").GetString()); string log = File.ReadAllText(Path.Combine(directory.Path, "llm.jsonl")); Assert.DoesNotContain("private request reasoning", log, StringComparison.Ordinal); Assert.DoesNotContain("private response reasoning", log, StringComparison.Ordinal); } [Fact] public void Read_agent_log_script_exposes_read_only_filters() { DirectoryInfo? root = new(AppContext.BaseDirectory); while (root is not null && !File.Exists(Path.Combine(root.FullName, "scripts", "Read-AgentLog.ps1"))) root = root.Parent; Assert.NotNull(root); string scriptPath = Path.Combine(root!.FullName, "scripts", "Read-AgentLog.ps1"); string script = File.ReadAllText(scriptPath); Assert.Contains("[string]$TaskId", script, StringComparison.Ordinal); Assert.Contains("[string]$SessionId", script, StringComparison.Ordinal); Assert.Contains("[string]$Event", script, StringComparison.Ordinal); Assert.Contains("[string]$CallId", script, StringComparison.Ordinal); Assert.Contains("[int]$Tail", script, StringComparison.Ordinal); Assert.DoesNotContain("Set-Content", script, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("Remove-Item", script, StringComparison.OrdinalIgnoreCase); } [Fact] public void Ready_is_always_visible_even_when_log_threshold_is_fatal() { using var directory = new TestDirectory(); LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en"); using (var application = new RollingFileLog(directory.Path, 1024 * 1024, 2, "fatal")) using (var observer = new McpHostLogObserver(application, locale, directory.Path, false, 1024 * 1024, 2)) observer.Ready(new McpReadyInfo("http", "http://127.0.0.1:8080/mcp", 42, "2026-07-28", true, true)); Assert.Contains("Agent is ready", File.ReadAllText(Path.Combine(directory.Path, "application.log")), StringComparison.Ordinal); } [Fact] public void Ready_and_http_request_are_logged_without_sensitive_payloads() { using var directory = new TestDirectory(); LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en"); using (var application = new RollingFileLog(directory.Path, 1024 * 1024, 2)) { using var observer = new McpHostLogObserver(application, locale, directory.Path, true, 1024 * 1024, 2); observer.Ready(new McpReadyInfo("http", "http://127.0.0.1:8080/mcp", 42, "2026-07-28", true, true)); observer.HttpRequestStarted(new McpHttpRequestStartInfo("connection", "trace", "127.0.0.1", 50123, "POST", "/mcp")); observer.HttpRequest(new McpHttpRequestInfo(DateTimeOffset.UtcNow, "connection", "trace", "127.0.0.1", 50123, "127.0.0.1", 8080, "POST", "/mcp", 200, 12.5, "application/json", 100, "test-client", "2026-07-28", "tools/list", "agent_run")); } string applicationText = File.ReadAllText(Path.Combine(directory.Path, "application.log")); Assert.Contains("Agent is ready", applicationText, StringComparison.Ordinal); Assert.Contains("remote=127.0.0.1:50123", applicationText, StringComparison.Ordinal); string httpText = File.ReadAllText(Path.Combine(directory.Path, "http.jsonl")); Assert.Contains("\"method\":\"POST\"", httpText, StringComparison.Ordinal); Assert.Contains("\"remote_ip\":\"127.0.0.1\"", httpText, StringComparison.Ordinal); Assert.DoesNotContain("Authorization", httpText, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("Cookie", httpText, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("prompt", httpText, StringComparison.OrdinalIgnoreCase); } [Fact] public void Http_request_callbacks_do_not_wait_for_a_blocked_console_writer() { using var directory = new TestDirectory(); using var console = new BlockingTextWriter(); LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en"); using var application = new RollingFileLog(directory.Path, 1024 * 1024, 2, consoleWriter: console); using var observer = new McpHostLogObserver(application, locale, directory.Path, true, 1024 * 1024, 2); observer.HttpRequestStarted(new McpHttpRequestStartInfo("connection", "trace-1", "127.0.0.1", 50123, "POST", "/mcp")); Assert.True(console.WaitUntilBlocked(TimeSpan.FromSeconds(5)), "Background writer did not reach the blocking console."); var stopwatch = Stopwatch.StartNew(); observer.HttpRequest(new McpHttpRequestInfo(DateTimeOffset.UtcNow, "connection", "trace-2", "127.0.0.1", 50124, "127.0.0.1", 8080, "POST", "/mcp", 200, 12.5, "application/json", 100, "test-client", "2026-07-28", "tools/list", null)); stopwatch.Stop(); Assert.True(stopwatch.Elapsed < TimeSpan.FromMilliseconds(250), $"HTTP callback blocked for {stopwatch.Elapsed}."); console.Release(); } private sealed class BlockingTextWriter : TextWriter { private readonly ManualResetEventSlim _entered = new(false); private readonly ManualResetEventSlim _release = new(false); public override Encoding Encoding => Encoding.UTF8; public override void WriteLine(string? value) { _entered.Set(); _release.Wait(TimeSpan.FromSeconds(30)); } public bool WaitUntilBlocked(TimeSpan timeout) => _entered.Wait(timeout); public void Release() => _release.Set(); protected override void Dispose(bool disposing) { if (disposing) { _release.Set(); _entered.Dispose(); _release.Dispose(); } base.Dispose(disposing); } } }