< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Testing.AgentScenarioRunner
Assembly: NexusLabs.Needlr.AgentFramework.Testing
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Testing/AgentScenarioRunner.cs
Line coverage
93%
Covered lines: 46
Uncovered lines: 3
Coverable lines: 49
Total lines: 150
Line coverage: 93.8%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%66100%
RunAsync()50%2292.1%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Testing/AgentScenarioRunner.cs

#LineLine coverage
 1using NexusLabs.Needlr.AgentFramework.Context;
 2using NexusLabs.Needlr.AgentFramework.Diagnostics;
 3using NexusLabs.Needlr.AgentFramework.Workspace;
 4
 5namespace NexusLabs.Needlr.AgentFramework.Testing;
 6
 7/// <summary>
 8/// Executes <see cref="IAgentScenario"/> instances with proper workspace isolation,
 9/// execution context scoping, and diagnostics capture.
 10/// </summary>
 11/// <remarks>
 12/// <para>
 13/// The runner handles the full seed → execute → verify lifecycle:
 14/// </para>
 15/// <list type="number">
 16///   <item>Creates an <see cref="InMemoryWorkspace"/> and calls <see cref="IAgentScenario.SeedWorkspace"/>.</item>
 17///   <item>Establishes an <see cref="IAgentExecutionContext"/> with user ID and workspace in properties.</item>
 18///   <item>Opens diagnostics capture.</item>
 19///   <item>Invokes the agent with the scenario's system and user prompts.</item>
 20///   <item>Calls <see cref="IAgentScenario.Verify"/> with the post-execution workspace and diagnostics.</item>
 21/// </list>
 22/// <para>
 23/// <strong>Deterministic testing (no real LLM):</strong> The runner uses the <see cref="IAgentFactory"/>
 24/// from DI, which respects the <c>ChatClientFactory</c> configured on the syringe. To run scenarios
 25/// without real LLM calls, wire a mock <c>IChatClient</c> via the syringe:
 26/// </para>
 27/// <code>
 28/// var sp = new Syringe()
 29///     .UsingReflection()
 30///     .UsingAgentFramework(af => af
 31///         .Configure(opts => opts.ChatClientFactory = _ => mockChatClient.Object))
 32///     .BuildServiceProvider(config);
 33///
 34/// var runner = new AgentScenarioRunner(
 35///     sp.GetRequiredService&lt;IAgentFactory&gt;(),
 36///     sp.GetRequiredService&lt;IAgentExecutionContextAccessor&gt;(),
 37///     sp.GetRequiredService&lt;IAgentDiagnosticsAccessor&gt;());
 38/// </code>
 39/// </remarks>
 40public sealed class AgentScenarioRunner
 41{
 42    private readonly IAgentFactory _agentFactory;
 43    private readonly IAgentExecutionContextAccessor _contextAccessor;
 44    private readonly IAgentDiagnosticsAccessor _diagnosticsAccessor;
 45
 46    /// <param name="agentFactory">Factory for creating agents.</param>
 47    /// <param name="contextAccessor">Execution context accessor for scoping.</param>
 48    /// <param name="diagnosticsAccessor">Diagnostics accessor for capture.</param>
 949    public AgentScenarioRunner(
 950        IAgentFactory agentFactory,
 951        IAgentExecutionContextAccessor contextAccessor,
 952        IAgentDiagnosticsAccessor diagnosticsAccessor)
 53    {
 954        _agentFactory = agentFactory ?? throw new ArgumentNullException(nameof(agentFactory));
 855        _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor));
 756        _diagnosticsAccessor = diagnosticsAccessor ?? throw new ArgumentNullException(nameof(diagnosticsAccessor));
 657    }
 58
 59    /// <summary>
 60    /// Runs the scenario with full lifecycle management: seed workspace, establish context,
 61    /// capture diagnostics, execute agent, verify outcomes.
 62    /// </summary>
 63    /// <param name="scenario">The scenario to run.</param>
 64    /// <param name="cancellationToken">Cancellation token.</param>
 65    /// <remarks>
 66    /// <see cref="ScenarioRunResult.Diagnostics"/> will be <see langword="null"/> unless the
 67    /// <see cref="IAgentFactory"/> was configured with <c>UsingDiagnostics()</c> on the syringe.
 68    /// Without diagnostics middleware, <see cref="IAgentScenario.Verify"/> receives null diagnostics.
 69    /// </remarks>
 70    /// <returns>The result of the scenario run, including diagnostics and verification outcome.</returns>
 71    public async Task<ScenarioRunResult> RunAsync(
 72        IAgentScenario scenario,
 73        CancellationToken cancellationToken = default)
 74    {
 675        ArgumentNullException.ThrowIfNull(scenario);
 76
 77        // 1. Create and seed workspace
 578        var workspace = new InMemoryWorkspace();
 579        scenario.SeedWorkspace(workspace);
 80
 81        // 2. Create agent with scenario's prompts
 582        var agent = _agentFactory.CreateAgent(opts =>
 583        {
 584            opts.Name = $"Scenario-{scenario.Name}";
 585            opts.Instructions = scenario.SystemPrompt;
 1086        });
 87
 88        // 3. Establish execution context with workspace in properties
 589        var executionContext = new AgentExecutionContext(
 590            UserId: $"scenario-runner",
 591            OrchestrationId: $"scenario-{scenario.Name}-{Guid.NewGuid():N}",
 592            Properties: new Dictionary<string, object> { ["workspace"] = workspace });
 93
 594        IAgentRunDiagnostics? diagnostics = null;
 595        string? responseText = null;
 596        Exception? executionError = null;
 97
 98        // 4. Run with context + diagnostics scoping
 599        using (_contextAccessor.BeginScope(executionContext))
 5100        using (_diagnosticsAccessor.BeginCapture())
 101        {
 102            try
 103            {
 5104                var response = await agent.RunAsync(
 5105                    scenario.UserPrompt,
 5106                    cancellationToken: cancellationToken);
 107
 5108                responseText = response.ToString();
 5109            }
 0110            catch (Exception ex)
 111            {
 0112                executionError = ex;
 0113            }
 114
 5115            diagnostics = _diagnosticsAccessor.LastRunDiagnostics;
 5116        }
 117
 118        // 5. Verify
 5119        Exception? verificationError = null;
 120        try
 121        {
 5122            scenario.Verify(workspace, diagnostics);
 3123        }
 124        catch (Exception ex)
 125        {
 2126            verificationError = ex;
 2127        }
 128
 5129        return new ScenarioRunResult(
 5130            ScenarioName: scenario.Name,
 5131            Workspace: workspace,
 5132            Diagnostics: diagnostics,
 5133            ResponseText: responseText,
 5134            ExecutionError: executionError,
 5135            VerificationError: verificationError,
 5136            Succeeded: executionError is null && verificationError is null);
 5137    }
 138}
 139
 140/// <summary>
 141/// Result of running a single <see cref="IAgentScenario"/>.
 142/// </summary>
 143public sealed record ScenarioRunResult(
 144    string ScenarioName,
 145    IWorkspace Workspace,
 146    IAgentRunDiagnostics? Diagnostics,
 147    string? ResponseText,
 148    Exception? ExecutionError,
 149    Exception? VerificationError,
 150    bool Succeeded);