// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Xunit.Abstractions; namespace AgentConversation.IntegrationTests; /// /// Abstract xunit base class for agent conversation integration tests. /// /// /// /// Subclasses must implement and to provide /// the AI backend and the set of test cases to run. Each subclass will automatically inherit the /// test method, which runs every case returned by /// through the . /// /// /// To generate (and serialize) the initial context for a test case, the same subclass inherits /// , which should be run once outside of normal CI. /// /// /// /// The concrete implementation that provides agent creation /// and compaction for the system under test. /// public abstract class ConversationHarnessTests where TSystem : IConversationTestSystem { private readonly ITestOutputHelper? _output; /// /// Initializes a new instance of . /// protected ConversationHarnessTests() { } /// /// Initializes a new instance of with xunit test output. /// /// The xunit test output helper used to log metrics and step results. protected ConversationHarnessTests(ITestOutputHelper output) { this._output = output; } /// /// Creates the to use for agent creation and compaction. /// protected abstract TSystem CreateTestSystem(); /// /// Returns the set of instances to exercise. /// protected abstract IEnumerable GetTestCases(); /// /// Runs all test cases returned by and logs the metrics report for each. /// [Fact] public virtual async Task RunAllTestCasesAsync() { var system = this.CreateTestSystem(); var harness = new ConversationHarness(system); foreach (var testCase in this.GetTestCases()) { this.Log($"[{testCase.Name}] Running..."); var stopwatch = Stopwatch.StartNew(); var report = await harness.RunAsync(testCase); stopwatch.Stop(); this.Log($"[{testCase.Name}] Completed in {stopwatch.ElapsedMilliseconds}ms. Metrics: {report}"); } } /// /// Generates and serializes the initial context for each test case returned by . /// /// /// This test is skipped during normal test runs because generating contexts requires live AI calls and /// can be expensive. Run it explicitly (e.g., with dotnet test --filter "FullyQualifiedName~Serialize") /// to regenerate the fixture files. After running, commit the generated files alongside the test code. /// [Fact(Skip = "Run explicitly to regenerate initial context fixture files.")] public virtual async Task SerializeAllInitialContextsAsync() { var system = this.CreateTestSystem(); var harness = new ConversationHarness(system); foreach (var testCase in this.GetTestCases()) { var outputPath = GetDefaultContextFilePath(testCase); this.Log($"[{testCase.Name}] Serializing initial context to '{outputPath}'..."); await harness.SerializeInitialContextAsync(testCase, outputPath); this.Log($"[{testCase.Name}] Context serialized successfully."); } } // ------------------------------------------------------------------------- // Protected helpers for subclasses // ------------------------------------------------------------------------- /// /// Computes the default file path used by when /// writing the initial context for . /// /// /// Override this method to change the output location. The default path is /// {TestCase.Name}.context.json relative to the current working directory. /// /// The test case whose default output path is required. /// The absolute or relative file path to write the serialized context to. protected virtual string GetDefaultContextFilePath(IConversationTestCase testCase) => $"{testCase.Name}.context.json"; /// /// Writes a message to the xunit test output, if available, otherwise to the console. /// protected void Log(string message) { if (this._output is not null) { this._output.WriteLine(message); } else { Console.WriteLine(message); } } }