Files
agent-framework/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillScriptExecutorTests.cs
T
SergeyMenshykh c9cd067be6 .NET: Support hosted code interpreter for skill script execution (#4192)
* support script execution by code interpretor

* improve the instruction prompt

* Add DefaultAzureCredential production warning to AgentSkills samples

Add the standard three-line WARNING comment about DefaultAzureCredential
production considerations to both AgentSkills sample Program.cs files,
matching the convention used in all other GettingStarted/Agents samples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address pr review comments

* address feedback

* rename Skill* types to FileAgentSkill* prefix for consistency

- Rename SkillFrontmatter -> FileAgentSkillFrontmatter
- Rename SkillScriptExecutor -> FileAgentSkillScriptExecutor
- Add FileAgentSkillScriptExecutionContext and FileAgentSkillScriptExecutionDetails
- Update sample, provider, loader, and tests accordingly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* reorder usings

* use set for props initialization instead of init

* rename HostedCodeInterpreterSkillScriptExecutor

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-26 13:17:21 +00:00

171 lines
6.5 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
/// <summary>
/// Unit tests for <see cref="FileAgentSkillScriptExecutor"/> and its integration with <see cref="FileAgentSkillsProvider"/>.
/// </summary>
public sealed class FileAgentSkillScriptExecutorTests : IDisposable
{
private readonly string _testRoot;
private readonly TestAIAgent _agent = new();
private static readonly FileAgentSkillScriptExecutionContext s_emptyContext = new(
new Dictionary<string, FileAgentSkill>(StringComparer.OrdinalIgnoreCase),
new FileAgentSkillLoader(NullLogger.Instance));
public FileAgentSkillScriptExecutorTests()
{
this._testRoot = Path.Combine(Path.GetTempPath(), "skill-executor-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(this._testRoot);
}
public void Dispose()
{
if (Directory.Exists(this._testRoot))
{
Directory.Delete(this._testRoot, recursive: true);
}
}
[Fact]
public void HostedCodeInterpreter_ReturnsNonNullInstance()
{
// Act
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
// Assert
Assert.NotNull(executor);
}
[Fact]
public void HostedCodeInterpreter_GetExecutionDetails_ReturnsNonNullInstructions()
{
// Arrange
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
// Act
var details = executor.GetExecutionDetails(s_emptyContext);
// Assert
Assert.NotNull(details);
Assert.NotNull(details.Instructions);
Assert.NotEmpty(details.Instructions);
}
[Fact]
public void HostedCodeInterpreter_GetExecutionDetails_ReturnsNonEmptyToolsList()
{
// Arrange
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
// Act
var details = executor.GetExecutionDetails(s_emptyContext);
// Assert
Assert.NotNull(details);
Assert.NotNull(details.Tools);
Assert.NotEmpty(details.Tools);
}
[Fact]
public async Task Provider_WithExecutor_IncludesExecutorInstructionsInPromptAsync()
{
// Arrange
CreateSkill(this._testRoot, "exec-skill", "Executor test", "Body.");
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
var options = new FileAgentSkillsProviderOptions { ScriptExecutor = executor };
var provider = new FileAgentSkillsProvider(this._testRoot, options);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert — executor instructions should be merged into the prompt
Assert.NotNull(result.Instructions);
Assert.Contains("code interpreter", result.Instructions, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Provider_WithExecutor_IncludesExecutorToolsAsync()
{
// Arrange
CreateSkill(this._testRoot, "tools-exec-skill", "Executor tools test", "Body.");
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
var options = new FileAgentSkillsProviderOptions { ScriptExecutor = executor };
var provider = new FileAgentSkillsProvider(this._testRoot, options);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert — should have 3 tools: load_skill, read_skill_resource, and HostedCodeInterpreterTool
Assert.NotNull(result.Tools);
Assert.Equal(3, result.Tools!.Count());
var toolNames = result.Tools!.Select(t => t.Name).ToList();
Assert.Contains("load_skill", toolNames);
Assert.Contains("read_skill_resource", toolNames);
Assert.Single(result.Tools!, t => t is HostedCodeInterpreterTool);
}
[Fact]
public async Task Provider_WithoutExecutor_DoesNotIncludeExecutorToolsAsync()
{
// Arrange
CreateSkill(this._testRoot, "no-exec-skill", "No executor test", "Body.");
var provider = new FileAgentSkillsProvider(this._testRoot);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert — should only have the two base tools
Assert.NotNull(result.Tools);
Assert.Equal(2, result.Tools!.Count());
}
[Fact]
public async Task Provider_WithHostedCodeInterpreter_MergesScriptInstructionsIntoPromptAsync()
{
// Arrange
CreateSkill(this._testRoot, "merge-skill", "Merge test", "Body.");
var executor = FileAgentSkillScriptExecutor.HostedCodeInterpreter();
var options = new FileAgentSkillsProviderOptions { ScriptExecutor = executor };
var provider = new FileAgentSkillsProvider(this._testRoot, options);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert — prompt should contain both the skill listing and the executor's script instructions
Assert.NotNull(result.Instructions);
string instructions = result.Instructions!;
// Skill listing is present
Assert.Contains("merge-skill", instructions);
Assert.Contains("Merge test", instructions);
// Hosted code interpreter script instructions are merged into the prompt
Assert.Contains("executable scripts", instructions);
Assert.Contains("read_skill_resource", instructions);
Assert.Contains("Execute the script using the code interpreter", instructions);
}
private static void CreateSkill(string root, string name, string description, string body)
{
string skillDir = Path.Combine(root, name);
Directory.CreateDirectory(skillDir);
File.WriteAllText(
Path.Combine(skillDir, "SKILL.md"),
$"---\nname: {name}\ndescription: {description}\n---\n{body}");
}
}