// Copyright (c) Microsoft. All rights reserved.
using System;
using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using OpenAI.Files;
using OpenAI.Responses;
using OpenAI.VectorStores;
using Shared.IntegrationTests;
namespace Foundry.IntegrationTests;
///
/// Integration tests for the file and vector-store forwarder extensions on
/// declared in . End-to-end
/// counterparts of the unit tests in
/// FoundryAgentExtensionsTests that exercise the live Foundry project pipeline.
///
///
/// Mirrors
/// in shape (file upload → vector store creation → FileSearchTool answer → cleanup), but routes
/// every helper call through the new extensions instead of the raw
/// projectOpenAIClient.GetProjectFilesClient() / GetProjectVectorStoresClient()
/// path. Skipped by default for the same reasons as the existing vector-store IT (cost and
/// runtime); flip Skip to run manually after seeding the right Foundry project.
///
public class FoundryAgentExtensionsTests
{
private readonly AIProjectClient _client = new(
new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)),
TestAzureCliCredentials.CreateAzureCliCredential());
[Fact(Skip = "For manual testing only")]
public async Task UploadFileAsync_ViaAgentExtension_UploadsToProjectAsync()
{
// Arrange — non-versioned Responses Agent (Mode 1) so we do not have to provision a server-side agent.
var agent = this._client.AsAIAgent(
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
instructions: "Be helpful.");
var foundryAgent = this.WrapAsFoundryAgent(agent);
var filePath = Path.GetTempFileName() + ".txt";
File.WriteAllText(filePath, "agent-extensions integration test payload");
OpenAIFile? uploaded = null;
try
{
// Act.
uploaded = await foundryAgent.UploadFileAsync(filePath, FileUploadPurpose.Assistants);
// Assert.
Assert.NotNull(uploaded);
Assert.False(string.IsNullOrEmpty(uploaded.Id));
Assert.Equal(Path.GetFileName(filePath), uploaded.Filename);
}
finally
{
if (uploaded is not null)
{
await foundryAgent.DeleteFileAsync(uploaded.Id);
}
File.Delete(filePath);
}
}
[Fact(Skip = "For manual testing only")]
public async Task DeleteFileAsync_ViaAgentExtension_RemovesUploadedFileAsync()
{
var agent = this._client.AsAIAgent(
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
instructions: "Be helpful.");
var foundryAgent = this.WrapAsFoundryAgent(agent);
var filePath = Path.GetTempFileName() + ".txt";
File.WriteAllText(filePath, "delete-me payload");
try
{
var uploaded = await foundryAgent.UploadFileAsync(filePath, FileUploadPurpose.Assistants);
// Act.
var result = await foundryAgent.DeleteFileAsync(uploaded.Id);
// Assert.
Assert.NotNull(result);
Assert.Equal(uploaded.Id, result.FileId);
Assert.True(result.Deleted);
}
finally
{
File.Delete(filePath);
}
}
[Fact(Skip = "For manual testing only")]
public async Task CreateVectorStoreAsync_ViaAgentExtension_BuildsStoreAndAnswersFileSearchQuestionAsync()
{
// Mirrors CreateAgent_CreatesAgentWithVectorStoresAsync but the upload-then-create-store
// sequence routes through the FoundryAgent.CreateVectorStoreAsync extension (single call
// that uploads, creates the store, and polls until ready). The resulting vector store id
// is then wired to a versioned agent's FileSearch tool and queried for a known value.
string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("VectorStoreExtAgent");
const string AgentInstructions = """
You are a helpful agent that can help fetch data from files you know about.
Use the File Search Tool to look up codes for words.
Do not answer a question unless you can find the answer using the File Search Tool.
""";
// Non-versioned helper agent that owns the upload pipeline.
var helperAgent = this._client.AsAIAgent(
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
instructions: "Be helpful.");
var helperFoundryAgent = this.WrapAsFoundryAgent(helperAgent);
var searchFilePath = Path.GetTempFileName() + "wordcodelookup.txt";
File.WriteAllText(searchFilePath, "The word 'apple' uses the code 442345, while the word 'banana' uses the code 673457.");
VectorStore? vectorStore = null;
FoundryAgent? versionedAgent = null;
try
{
// Act — single agent-level helper call uploads, creates, and waits until ready.
vectorStore = await helperFoundryAgent.CreateVectorStoreAsync(
"WordCodeLookup_ExtensionVectorStore",
new[] { searchFilePath });
Assert.NotNull(vectorStore);
Assert.False(string.IsNullOrEmpty(vectorStore.Id));
Assert.NotEqual(VectorStoreStatus.InProgress, vectorStore.Status);
// Wire the store id into a versioned agent's FileSearch tool to prove it is actually usable.
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
{
Instructions = AgentInstructions,
Tools = { ResponseTool.CreateFileSearchTool(vectorStoreIds: [vectorStore.Id]) },
};
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
AgentName,
new ProjectsAgentVersionCreationOptions(definition));
versionedAgent = this._client.AsAIAgent(agentVersion);
// Assert.
var result = await versionedAgent.RunAsync("Can you give me the documented code for 'banana'?");
Assert.Contains("673457", result.ToString());
}
finally
{
if (versionedAgent is not null)
{
await this._client.AgentAdministrationClient.DeleteAgentAsync(versionedAgent.Name);
}
// Cleanup the vector store via the new extension too.
if (vectorStore is not null)
{
await helperFoundryAgent.DeleteVectorStoreAsync(vectorStore.Id);
}
File.Delete(searchFilePath);
}
}
[Fact(Skip = "For manual testing only")]
public async Task DeleteVectorStoreAsync_ViaAgentExtension_RemovesStoreAsync()
{
var agent = this._client.AsAIAgent(
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
instructions: "Be helpful.");
var foundryAgent = this.WrapAsFoundryAgent(agent);
var filePath = Path.GetTempFileName() + ".txt";
File.WriteAllText(filePath, "delete-store payload");
VectorStore? vectorStore = null;
try
{
vectorStore = await foundryAgent.CreateVectorStoreAsync(
"DeleteVectorStore_ExtensionTest",
new[] { filePath });
// Act.
var result = await foundryAgent.DeleteVectorStoreAsync(vectorStore.Id);
// Assert.
Assert.NotNull(result);
Assert.Equal(vectorStore.Id, result.VectorStoreId);
Assert.True(result.Deleted);
vectorStore = null;
}
finally
{
if (vectorStore is not null)
{
await foundryAgent.DeleteVectorStoreAsync(vectorStore.Id);
}
File.Delete(filePath);
}
}
///
/// Resolves the underlying from an handle
/// returned by AIProjectClient.AsAIAgent(model, instructions). The Mode 1 overload
/// returns a ; the extension forwarders we test live on
/// , so callers wanting them through this entry point need to
/// reach for the FoundryAgent constructor instead. This helper makes the test setup
/// consistent across the four IT scenarios.
///
private FoundryAgent WrapAsFoundryAgent(AIAgent agent)
{
// The Mode 1 AsAIAgent overload returns ChatClientAgent rather than FoundryAgent; use
// the FoundryAgent projectEndpoint+model+instructions ctor to get the same underlying
// FoundryChatClient surfaced through a FoundryAgent typed handle.
_ = agent;
return new FoundryAgent(
projectEndpoint: new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)),
credential: TestAzureCliCredentials.CreateAzureCliCredential(),
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
instructions: "Be helpful.");
}
}