File: PromptAgentTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Foundry.Tests\Aspire.Hosting.Foundry.Tests.csproj (Aspire.Hosting.Foundry.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIRECOMPUTE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREFOUNDRY001 // Preview tool types
#pragma warning disable OPENAI001 // Responses API is experimental
 
using System.ClientModel.Primitives;
using System.Net;
using System.Text.Json;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Hosting.Foundry.Tests;
 
public class PromptAgentTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    public async Task ToolResources_SerializeInPromptAgentDefinition()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var project = builder.AddFoundry("account").AddProject("my-project");
        var search = builder.AddAzureSearch("search");
        var searchTool = project.AddAISearchTool("search-tool").WithReference(search);
        searchTool.Resource.IndexName = "docs";
        searchTool.Resource.Connection!.Outputs["id"] = "/connections/search";
        var bingConnection = project.AddBingGroundingConnection("bing-connection", "/bing/resource");
        bingConnection.Resource.Outputs["id"] = "/connections/bing";
        var bingTool = project.AddBingGroundingTool("bing").WithReference(bingConnection);
        var sharePoint = project.AddSharePointTool("sharepoint", "/connections/sharepoint");
        var fabric = project.AddFabricTool("fabric", "/connections/fabric");
        var function = project.AddAzureFunctionTool(
            "function", "get_weather", "Get the weather.",
            BinaryData.FromString("""{"type":"object","properties":{"city":{"type":"string"}}}"""),
            "https://storage.queue.core.windows.net", "input",
            "https://storage.queue.core.windows.net", "output");
        var definition = new DeclarativeAgentDefinition("gpt-4.1")
        {
            Tools =
            {
                await searchTool.Resource.ToAgentToolAsync(),
                await bingTool.Resource.ToAgentToolAsync(),
                await sharePoint.Resource.ToAgentToolAsync(),
                await fabric.Resource.ToAgentToolAsync(),
                await function.Resource.ToAgentToolAsync()
            }
        };
        var options = new ProjectsAgentVersionCreationOptions(definition);
 
        await Verify(ModelReaderWriter.Write(
            options, ModelReaderWriterOptions.Json, AzureAIProjectsAgentsContext.Default).ToString(), "json");
    }
 
    [Fact]
    public void AddPromptAgent_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        var agent = project.AddPromptAgent("my-agent", model, instructions: "You tell jokes.");
 
        Assert.NotNull(agent);
        Assert.NotNull(agent.Resource);
        Assert.Equal("my-agent", agent.Resource.Name);
        Assert.IsType<AzurePromptAgentResource>(agent.Resource);
    }
 
    [Fact]
    public void AddPromptAgent_SetsModelAndInstructions()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        var agent = project.AddPromptAgent("my-agent", model, instructions: "You tell jokes.");
 
        Assert.Equal("gpt41", agent.Resource.Model);
        Assert.Equal("You tell jokes.", agent.Resource.Instructions);
    }
 
    [Fact]
    public void AddPromptAgent_SetsProjectReference()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        var agent = project.AddPromptAgent("my-agent", model);
 
        Assert.Same(project.Resource, agent.Resource.Project);
    }
 
    [Fact]
    public void AddPromptAgent_ConfiguresSendMessageCommand()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        project.AddPromptAgent("my-agent", model);
 
        builder.Build();
 
        var resource = builder.Resources.Single(r => r.Name == "my-agent");
        var command = Assert.Single(resource.Annotations.OfType<ResourceCommandAnnotation>());
        Assert.Equal("Send Message", command.DisplayName);
        Assert.Equal("ChatSparkle", command.IconName);
        Assert.Equal(IconVariant.Regular, command.IconVariant);
        Assert.True(command.IsHighlighted);
    }
 
    [Fact]
    public async Task SendMessage_ResponsesClient_CreatesAgentResponse()
    {
        using var handler = new SequenceHttpMessageHandler(new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("""
                {
                  "id": "resp_test",
                  "object": "response",
                  "created_at": 0,
                  "status": "completed",
                  "model": "gpt-4.1",
                  "output": [
                    {
                      "type": "message",
                      "id": "msg_test",
                      "status": "completed",
                      "role": "assistant",
                      "content": [
                        { "type": "output_text", "text": "Hello from the agent!", "annotations": [] }
                      ]
                    }
                  ]
                }
                """, System.Text.Encoding.UTF8, "application/json")
        });
        using var httpClient = new HttpClient(handler);
        var projectClient = new AIProjectClient(
            new Uri("https://example.invalid/api/projects/my-project"),
            new TestTokenCredential(),
            new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) });
 
        // Exercise the SDK calls used by Send Message: incompatible Azure/OpenAI versions
        // can compile successfully but throw MissingMethodException when constructing this client.
        var responseClient = projectClient.ProjectOpenAIClient.GetProjectResponsesClientForAgent(new AgentReference("my-agent"));
        var response = await responseClient.CreateResponseAsync("Hello", cancellationToken: TestContext.Current.CancellationToken);
 
        Assert.Equal("Hello from the agent!", response.Value.GetOutputText());
        var request = Assert.Single(handler.Requests);
        using var body = JsonDocument.Parse(request.Content);
        Assert.Equal("my-agent", body.RootElement.GetProperty("agent_reference").GetProperty("name").GetString());
        var input = Assert.Single(body.RootElement.GetProperty("input").EnumerateArray());
        Assert.Equal("user", input.GetProperty("role").GetString());
        var content = Assert.Single(input.GetProperty("content").EnumerateArray());
        Assert.Equal("Hello", content.GetProperty("text").GetString());
    }
 
    [Fact]
    public async Task AddPromptAgent_SendMessageCommand_WhenCanceled_ReturnsCanceled()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
        var agent = project.AddPromptAgent("my-agent", model);
        project.Resource.Outputs["endpoint"] = "https://account.services.ai.azure.com/api/projects/my-project";
 
        using var serviceProvider = builder.Services.BuildServiceProvider();
        using var cts = new CancellationTokenSource();
        cts.Cancel();
 
        var command = Assert.Single(agent.Resource.Annotations.OfType<ResourceCommandAnnotation>());
        var result = await command.ExecuteCommand(new ExecuteCommandContext
        {
            Services = serviceProvider,
            ResourceName = agent.Resource.Name,
            CancellationToken = cts.Token,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection(
            [
                new InteractionInput
                {
                    Name = "message",
                    InputType = InputType.Text,
                    Value = "Hello"
                }
            ])
        }).WaitAsync(TimeSpan.FromSeconds(10));
 
        Assert.False(result.Success);
        Assert.True(result.Canceled, result.Message);
    }
 
    [Fact]
    public void AddPromptAgent_WithNullName_Throws()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        Assert.Throws<ArgumentException>(() => project.AddPromptAgent("", model));
    }
 
    [Fact]
    public void AddPromptAgent_WithNullModel_Throws()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        Assert.Throws<ArgumentNullException>(() => project.AddPromptAgent("my-agent", null!));
    }
 
    [Fact]
    public void AddPromptAgent_InstructionsAreOptional()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        var agent = project.AddPromptAgent("my-agent", model);
 
        Assert.Null(agent.Resource.Instructions);
    }
 
    [Fact]
    public void AddPromptAgent_WithTools_AddToolsToAgent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        var codeInterp = project.AddCodeInterpreterTool("ci");
        var webSearch = project.AddWebSearchTool("ws");
 
        var agent = project.AddPromptAgent("my-agent", model,
            instructions: "You tell jokes.")
            .WithTool(codeInterp)
            .WithTool(webSearch);
 
        Assert.Equal(2, agent.Resource.Tools.Count);
        Assert.IsType<CodeInterpreterToolResource>(agent.Resource.Tools[0]);
        Assert.IsType<WebSearchToolResource>(agent.Resource.Tools[1]);
    }
 
    [Fact]
    public void AddCodeInterpreterTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddCodeInterpreterTool("code-interp");
 
        Assert.NotNull(tool);
        Assert.Equal("code-interp", tool.Resource.Name);
        Assert.IsType<CodeInterpreterToolResource>(tool.Resource);
        Assert.Same(project.Resource, tool.Resource.Project);
    }
 
    [Fact]
    public void AddFileSearchTool_CreatesResourceWithVectorStoreIds()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddFileSearchTool("fs", "store-1", "store-2");
 
        Assert.NotNull(tool);
        Assert.IsType<FileSearchToolResource>(tool.Resource);
        Assert.Equal(["store-1", "store-2"], tool.Resource.VectorStoreIds);
    }
 
    [Fact]
    public void AddWebSearchTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddWebSearchTool("ws");
 
        Assert.NotNull(tool);
        Assert.IsType<WebSearchToolResource>(tool.Resource);
    }
 
    [Fact]
    public void AddAISearchTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddAISearchTool("search");
 
        Assert.NotNull(tool);
        Assert.IsType<AzureAISearchToolResource>(tool.Resource);
        Assert.Same(project.Resource, tool.Resource.Project);
        Assert.Null(tool.Resource.SearchResource);
    }
 
    [Fact]
    public void AddAISearchTool_WithReference_LinksSearchResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var search = builder.AddAzureSearch("search");
 
        var tool = project.AddAISearchTool("search-tool").WithReference(search);
 
        Assert.NotNull(tool.Resource.SearchResource);
        Assert.Same(search.Resource, tool.Resource.SearchResource);
        Assert.NotNull(tool.Resource.Connection);
    }
 
    [Fact]
    public void AddAISearchTool_WithReference_Twice_Throws()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var search = builder.AddAzureSearch("search");
        var search2 = builder.AddAzureSearch("search2");
 
        var tool = project.AddAISearchTool("search-tool").WithReference(search);
 
        Assert.Throws<InvalidOperationException>(() => tool.WithReference(search2));
    }
 
    [Fact]
    public void AddBingGroundingTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddBingGroundingTool("bing");
 
        Assert.NotNull(tool);
        Assert.IsType<BingGroundingToolResource>(tool.Resource);
        Assert.Null(tool.Resource.Connection);
    }
 
    [Fact]
    public void AddBingGroundingTool_WithReference_LinksConnection()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var bingResourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Bing/accounts/bing-test";
        var connection = project.AddBingGroundingConnection("bing-conn", bingResourceId);
 
        var tool = project.AddBingGroundingTool("bing").WithReference(connection);
 
        Assert.NotNull(tool.Resource.Connection);
        Assert.Same(connection.Resource, tool.Resource.Connection);
    }
 
    [Fact]
    public void AddBingGroundingTool_WithReference_Twice_Throws()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var bingResourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Bing/accounts/bing-test";
        var conn1 = project.AddBingGroundingConnection("bing-conn-1", bingResourceId);
        var conn2 = project.AddBingGroundingConnection("bing-conn-2", bingResourceId);
 
        var tool = project.AddBingGroundingTool("bing").WithReference(conn1);
 
        Assert.Throws<InvalidOperationException>(() => tool.WithReference(conn2));
    }
 
    [Fact]
    public void AddBingGroundingTool_WithResourceId_CreatesConnection()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var bingResourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Bing/accounts/bing-test";
        var tool = project.AddBingGroundingTool("bing").WithReference(bingResourceId);
 
        Assert.NotNull(tool.Resource.Connection);
    }
 
    [Fact]
    public void AddBingGroundingTool_WithResourceId_Twice_Throws()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var bingResourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Bing/accounts/bing-test";
        var tool = project.AddBingGroundingTool("bing").WithReference(bingResourceId);
 
        Assert.Throws<InvalidOperationException>(() => tool.WithReference(bingResourceId));
    }
 
    [Fact]
    public void AddBingGroundingConnection_CreatesConnection()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var bingResourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Bing/accounts/bing-test";
        var connection = project.AddBingGroundingConnection("bing-conn", bingResourceId);
 
        Assert.NotNull(connection);
        Assert.IsType<BingGroundingConnectionResource>(connection.Resource);
    }
 
    [Fact]
    public void AddBingGroundingTool_WithParameterReference_SetsConnection()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var bingResourceId = builder.AddParameter("bingResourceId");
        var tool = project.AddBingGroundingTool("bing").WithReference(bingResourceId);
 
        Assert.NotNull(tool.Resource.Connection);
    }
 
    [Fact]
    public void AddBingGroundingConnection_WithParameter_CreatesConnection()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var bingResourceId = builder.AddParameter("bingResourceId");
        var connection = project.AddBingGroundingConnection("bing-conn", bingResourceId);
 
        Assert.NotNull(connection);
        Assert.IsType<BingGroundingConnectionResource>(connection.Resource);
    }
 
    [Fact]
    public void AddSharePointTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddSharePointTool("sp", "conn-1", "conn-2");
 
        Assert.NotNull(tool);
        Assert.IsType<SharePointToolResource>(tool.Resource);
        Assert.Equal(["conn-1", "conn-2"], tool.Resource.ProjectConnectionIds);
    }
 
    [Fact]
    public void AddFabricTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddFabricTool("fab", "fab-conn-1");
 
        Assert.NotNull(tool);
        Assert.IsType<FabricToolResource>(tool.Resource);
        Assert.Equal(["fab-conn-1"], tool.Resource.ProjectConnectionIds);
    }
 
    [Fact]
    public void AddAzureFunctionTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddAzureFunctionTool(
            "func-tool",
            "myFunc",
            "Does something useful",
            BinaryData.FromString("""{"type":"object","properties":{}}"""),
            "https://queue.core.windows.net",
            "input-queue",
            "https://queue.core.windows.net",
            "output-queue");
 
        Assert.NotNull(tool);
        Assert.IsType<AzureFunctionToolResource>(tool.Resource);
        Assert.Equal("myFunc", tool.Resource.FunctionName);
    }
 
    [Fact]
    public void AddFunctionTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddFunctionTool(
            "func",
            "get_weather",
            BinaryData.FromString("""{"type":"object","properties":{"location":{"type":"string"}}}"""),
            description: "Gets the current weather");
 
        Assert.NotNull(tool);
        Assert.IsType<FunctionToolResource>(tool.Resource);
        Assert.Equal("get_weather", tool.Resource.FunctionName);
        Assert.Equal("Gets the current weather", tool.Resource.Description);
    }
 
    [Fact]
    public void AddPromptAgent_WithMixedToolTypes()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
        var search = builder.AddAzureSearch("search");
 
        var codeInterp = project.AddCodeInterpreterTool("ci");
        var webSearch = project.AddWebSearchTool("ws");
        var aiSearch = project.AddAISearchTool("search-tool").WithReference(search);
        var sharePoint = project.AddSharePointTool("sp", "sp-conn");
 
        var agent = project.AddPromptAgent("my-agent", model,
            instructions: "You tell jokes.")
            .WithTool(codeInterp)
            .WithTool(webSearch)
            .WithTool(aiSearch)
            .WithTool(sharePoint);
 
        Assert.Equal(4, agent.Resource.Tools.Count);
        Assert.IsType<CodeInterpreterToolResource>(agent.Resource.Tools[0]);
        Assert.IsType<WebSearchToolResource>(agent.Resource.Tools[1]);
        Assert.IsType<AzureAISearchToolResource>(agent.Resource.Tools[2]);
        Assert.IsType<SharePointToolResource>(agent.Resource.Tools[3]);
    }
 
    [Fact]
    public void AddPromptAgent_ToolReusedAcrossAgents()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        var codeInterp = project.AddCodeInterpreterTool("ci");
 
        var agent1 = project.AddPromptAgent("agent-1", model).WithTool(codeInterp);
        var agent2 = project.AddPromptAgent("agent-2", model).WithTool(codeInterp);
 
        Assert.Single(agent1.Resource.Tools);
        Assert.Single(agent2.Resource.Tools);
        Assert.Same(codeInterp.Resource, agent1.Resource.Tools[0]);
        Assert.Same(codeInterp.Resource, agent2.Resource.Tools[0]);
    }
 
    [Fact]
    public void AddPromptAgent_CrossProjectTool_Throws()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var foundry = builder.AddFoundry("account");
        var project1 = foundry.AddProject("proj-1");
        var project2 = foundry.AddProject("proj-2");
        var model = project1.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        var toolFromProject2 = project2.AddCodeInterpreterTool("ci");
 
        Assert.Throws<InvalidOperationException>(() =>
            project1.AddPromptAgent("agent", model).WithTool(toolFromProject2));
    }
 
    [Fact]
    public async Task AddPromptAgent_WithReference_ShouldBindConnectionString()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("test-account")
            .AddProject("test-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
        var agent = project.AddPromptAgent("my-agent", model, instructions: "You tell jokes.");
 
        var pyapp = builder.AddPythonApp("app", "./app.py", "main:app")
            .WithReference(agent);
 
        builder.Build();
        var envVars = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
            pyapp.Resource, DistributedApplicationOperation.Publish, TestServiceProvider.Instance);
 
        Assert.Contains(envVars, kvp => kvp.Key is "ConnectionStrings__my-agent");
    }
 
    [Fact]
    public void PromptAgentResource_ImplementsExpectedInterfaces()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
        var agent = project.AddPromptAgent("my-agent", model);
 
        Assert.IsAssignableFrom<IResourceWithConnectionString>(agent.Resource);
        Assert.IsAssignableFrom<IResourceWithEnvironment>(agent.Resource);
        Assert.IsNotAssignableFrom<IComputeResource>(agent.Resource);
    }
 
    [Fact]
    public void AddImageGenerationTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddImageGenerationTool("img-gen");
 
        Assert.NotNull(tool);
        Assert.IsType<ImageGenerationToolResource>(tool.Resource);
    }
 
    [Fact]
    public void AddComputerUseTool_CreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var tool = project.AddComputerUseTool("computer", displayWidth: 1920, displayHeight: 1080);
 
        Assert.NotNull(tool);
        var computerTool = Assert.IsType<ComputerToolResource>(tool.Resource);
        Assert.Equal(1920, computerTool.DisplayWidth);
        Assert.Equal(1080, computerTool.DisplayHeight);
        Assert.Equal("browser", computerTool.Environment);
    }
 
    [Fact]
    public void ToolResources_HaveProjectReference()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
 
        var ci = project.AddCodeInterpreterTool("ci");
        var fs = project.AddFileSearchTool("fs");
        var ws = project.AddWebSearchTool("ws");
 
        Assert.Same(project.Resource, ci.Resource.Project);
        Assert.Same(project.Resource, fs.Resource.Project);
        Assert.Same(project.Resource, ws.Resource.Project);
    }
 
    [Fact]
    public async Task AddPromptAgent_RunMode_BeforeStartStepDependsOnPrepareResources()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
 
        var agent = project.AddPromptAgent("my-agent", model, instructions: "Test agent");
 
        builder.Build();
 
        var annotations = agent.Resource.Annotations.OfType<PipelineStepAnnotation>().ToList();
        Assert.NotEmpty(annotations);
 
        using var serviceProvider = builder.Services.BuildServiceProvider();
        var pipelineContext = new PipelineContext(
            serviceProvider.GetRequiredService<DistributedApplicationModel>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            serviceProvider,
            NullLogger.Instance,
            CancellationToken.None);
 
        var steps = new List<PipelineStep>();
        foreach (var annotation in annotations)
        {
            steps.AddRange(await annotation.CreateStepsAsync(new PipelineStepFactoryContext
            {
                PipelineContext = pipelineContext,
                Resource = agent.Resource
            }));
        }
 
        var beforeStartStep = steps.SingleOrDefault(s => s.Name == "deploy-my-agent-before-start");
        Assert.NotNull(beforeStartStep);
 
        Assert.Equal(new[] { AzureEnvironmentResource.PrepareResourcesStepName }, beforeStartStep.DependsOnSteps);
    }
 
    [Fact]
    public async Task AddPromptAgent_RunMode_BeforeStartPipelineExecutesSuccessfully()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var activityReporter = new TestPipelineActivityReporter(testOutputHelper);
        builder.Services.AddSingleton<IPipelineActivityReporter>(activityReporter);
 
        var project = builder.AddFoundry("account")
            .AddProject("my-project");
        var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);
        project.AddPromptAgent("my-agent", model, instructions: "Test agent");
 
        using var app = builder.Build();
 
        // This calls the real pipeline resolution + validation + execution.
        // If any DependsOnSteps reference a non-existent step, it throws InvalidOperationException.
        await app.ExecuteBeforeStartHooksAsync(default);
 
        // Verify the prompt agent's deploy step was created and executed by the pipeline.
        Assert.Contains("deploy-my-agent-before-start", activityReporter.CreatedSteps);
        // Verify the azure-prepare-resources step (the dependency) was also executed.
        Assert.Contains(AzureEnvironmentResource.PrepareResourcesStepName, activityReporter.CreatedSteps);
    }
}