// 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);
}
}
}