mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8dadfb4a4 | ||
|
|
ec5d9397de |
@@ -85,7 +85,7 @@ jobs:
|
||||
workflow-samples
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.2.0
|
||||
uses: actions/setup-dotnet@v5.1.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
- name: Build dotnet solutions
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.2.0
|
||||
uses: actions/setup-dotnet@v5.1.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
# Generate test reports and check coverage
|
||||
- name: Generate test reports
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.3
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.1
|
||||
with:
|
||||
reports: "./TestResults/Coverage/**/*.cobertura.xml"
|
||||
targetdir: "./TestResults/Reports"
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage report artifact
|
||||
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
|
||||
path: ./TestResults/Reports # Directory containing files to upload
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5.2.0
|
||||
uses: actions/setup-dotnet@v5.1.0
|
||||
with:
|
||||
global-json-file: ${{ github.workspace }}/dotnet/global.json
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
- name: Upload dependency range report
|
||||
# Always publish the report so failures are inspectable even when validation fails.
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dependency-range-results
|
||||
path: python/scripts/dependencies/dependency-range-results.json
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
cd scripts && uv run python -m sample_validation --subdir 01-get-started --save-report --report-name 01-get-started
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-01-get-started
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
cd scripts && uv run python -m sample_validation --subdir 02-agents --save-report --report-name 02-agents
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-02-agents
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
cd scripts && uv run python -m sample_validation --subdir 03-workflows --save-report --report-name 03-workflows
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-03-workflows
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
cd scripts && uv run python -m sample_validation --subdir 04-hosting --save-report --report-name 04-hosting
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-04-hosting
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
cd scripts && uv run python -m sample_validation --subdir 05-end-to-end --save-report --report-name 05-end-to-end
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-05-end-to-end
|
||||
@@ -249,7 +249,7 @@ jobs:
|
||||
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-autogen-migration
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
cd scripts && uv run python -m sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration
|
||||
|
||||
- name: Upload validation report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: validation-report-semantic-kernel-migration
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
|
||||
- name: Pytest coverage comment
|
||||
id: coverageComment
|
||||
uses: MishaKav/pytest-coverage-comment@v1.6.0
|
||||
uses: MishaKav/pytest-coverage-comment@v1.2.0
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Check coverage threshold
|
||||
run: python ${{ github.workspace }}/.github/workflows/python-check-coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }}
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: |
|
||||
python/python-coverage.xml
|
||||
|
||||
@@ -61,6 +61,12 @@ public static class Program
|
||||
{
|
||||
Console.WriteLine($"{outputEvent}");
|
||||
}
|
||||
|
||||
if (evt is WorkflowErrorEvent errorEvent)
|
||||
{
|
||||
Console.WriteLine($"Workflow error: {errorEvent.Exception?.Message}");
|
||||
Console.WriteLine($"Details: {errorEvent.Exception}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +181,9 @@ internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEve
|
||||
/// <summary>
|
||||
/// A custom executor that uses an AI agent to provide feedback on a slogan.
|
||||
/// </summary>
|
||||
internal sealed class FeedbackExecutor : Executor<SloganResult>
|
||||
[SendsMessage(typeof(FeedbackResult))]
|
||||
[YieldsOutput(typeof(string))]
|
||||
internal sealed partial class FeedbackExecutor : Executor<SloganResult>
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
private AgentSession? _session;
|
||||
|
||||
@@ -104,17 +104,15 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
{
|
||||
State state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
|
||||
{
|
||||
// Reduce existing messages before adding new messages from the current turn.
|
||||
// This ensures messages from the current turn (including function calls and tool results)
|
||||
// are always preserved in full and are not immediately reduced.
|
||||
await ReduceMessagesAsync(this.ChatReducer, state, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Add request and response messages to the provider
|
||||
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
|
||||
state.Messages.AddRange(allNewMessages);
|
||||
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
|
||||
{
|
||||
// Apply pre-write reduction strategy if configured
|
||||
await ReduceMessagesAsync(this.ChatReducer, state, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReduceMessagesAsync(IChatReducer reducer, State state, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -68,7 +68,7 @@ internal static class SemanticAnalyzer
|
||||
string classKey = GetClassKey(classSymbol);
|
||||
bool isPartialClass = IsPartialClass(classSymbol, cancellationToken);
|
||||
bool derivesFromExecutor = DerivesFromExecutor(classSymbol);
|
||||
bool configureProtocol = HasConfigureProtocolDefined(classSymbol);
|
||||
bool hasManualConfigureProtocol = HasConfigureProtocolDefined(classSymbol);
|
||||
|
||||
// Extract class metadata
|
||||
string? @namespace = classSymbol.ContainingNamespace?.IsGlobalNamespace == true
|
||||
@@ -97,7 +97,7 @@ internal static class SemanticAnalyzer
|
||||
return new MethodAnalysisResult(
|
||||
classKey, @namespace, className, genericParameters, isNested, containingTypeChain,
|
||||
baseHasConfigureProtocol, classSendTypes, classYieldTypes,
|
||||
isPartialClass, derivesFromExecutor, configureProtocol,
|
||||
isPartialClass, derivesFromExecutor, hasManualConfigureProtocol,
|
||||
classLocation,
|
||||
handler,
|
||||
Diagnostics: new ImmutableEquatableArray<DiagnosticInfo>(methodDiagnostics.ToImmutable()));
|
||||
@@ -149,7 +149,7 @@ internal static class SemanticAnalyzer
|
||||
return AnalysisResult.WithDiagnostics(allDiagnostics.ToImmutable());
|
||||
}
|
||||
|
||||
if (first.HasManualConfigureRoutes)
|
||||
if (first.HasManualConfigureProtocol)
|
||||
{
|
||||
allDiagnostics.Add(Diagnostic.Create(
|
||||
DiagnosticDescriptors.ConfigureProtocolAlreadyDefined,
|
||||
@@ -212,6 +212,7 @@ internal static class SemanticAnalyzer
|
||||
bool isPartialClass = IsPartialClass(classSymbol, cancellationToken);
|
||||
bool derivesFromExecutor = DerivesFromExecutor(classSymbol);
|
||||
bool hasManualConfigureProtocol = HasConfigureProtocolDefined(classSymbol);
|
||||
bool baseHasConfigureProtocol = BaseHasConfigureProtocol(classSymbol);
|
||||
|
||||
string? @namespace = classSymbol.ContainingNamespace?.IsGlobalNamespace == true
|
||||
? null
|
||||
@@ -241,6 +242,7 @@ internal static class SemanticAnalyzer
|
||||
isPartialClass,
|
||||
derivesFromExecutor,
|
||||
hasManualConfigureProtocol,
|
||||
baseHasConfigureProtocol,
|
||||
classLocation,
|
||||
typeName,
|
||||
attributeKind));
|
||||
@@ -321,7 +323,7 @@ internal static class SemanticAnalyzer
|
||||
first.GenericParameters,
|
||||
first.IsNested,
|
||||
first.ContainingTypeChain,
|
||||
BaseHasConfigureProtocol: false, // Not relevant for protocol-only
|
||||
first.BaseHasConfigureProtocol,
|
||||
Handlers: ImmutableEquatableArray<HandlerInfo>.Empty,
|
||||
ClassSendTypes: new ImmutableEquatableArray<string>(sendTypes.ToImmutable()),
|
||||
ClassYieldTypes: new ImmutableEquatableArray<string>(yieldTypes.ToImmutable()));
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Models;
|
||||
/// <summary>
|
||||
/// Represents protocol type information extracted from class-level [SendsMessage] or [YieldsOutput] attributes.
|
||||
/// Used by the incremental generator pipeline to capture classes that declare protocol types
|
||||
/// but may not have [MessageHandler] methods (e.g., when ConfigureRoutes is manually implemented).
|
||||
/// but may not have [MessageHandler] methods (e.g., when ConfigureProtocol is manually implemented).
|
||||
/// </summary>
|
||||
/// <param name="ClassKey">Unique identifier for the class (fully qualified name).</param>
|
||||
/// <param name="Namespace">The namespace of the class.</param>
|
||||
@@ -15,7 +15,8 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Models;
|
||||
/// <param name="ContainingTypeChain">The chain of containing types for nested classes. Empty if not nested.</param>
|
||||
/// <param name="IsPartialClass">Whether the class is declared as partial.</param>
|
||||
/// <param name="DerivesFromExecutor">Whether the class derives from Executor.</param>
|
||||
/// <param name="HasManualConfigureRoutes">Whether the class has a manually defined ConfigureRoutes method.</param>
|
||||
/// <param name="HasManualConfigureProtocol">Whether the class has a manually defined ConfigureProtocol method.</param>
|
||||
/// <param name="BaseHasConfigureProtocol">Whether a base class already overrides ConfigureProtocol.</param>
|
||||
/// <param name="ClassLocation">Location info for diagnostics.</param>
|
||||
/// <param name="TypeName">The fully qualified type name from the attribute.</param>
|
||||
/// <param name="AttributeKind">Whether this is from a SendsMessage or YieldsOutput attribute.</param>
|
||||
@@ -28,7 +29,8 @@ internal sealed record ClassProtocolInfo(
|
||||
string ContainingTypeChain,
|
||||
bool IsPartialClass,
|
||||
bool DerivesFromExecutor,
|
||||
bool HasManualConfigureRoutes,
|
||||
bool HasManualConfigureProtocol,
|
||||
bool BaseHasConfigureProtocol,
|
||||
DiagnosticLocationInfo? ClassLocation,
|
||||
string TypeName,
|
||||
ProtocolAttributeKind AttributeKind)
|
||||
@@ -38,5 +40,5 @@ internal sealed record ClassProtocolInfo(
|
||||
/// </summary>
|
||||
public static ClassProtocolInfo Empty { get; } = new(
|
||||
string.Empty, null, string.Empty, null, false, string.Empty,
|
||||
false, false, false, null, string.Empty, ProtocolAttributeKind.Send);
|
||||
false, false, false, false, null, string.Empty, ProtocolAttributeKind.Send);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Models;
|
||||
/// Uses value-equatable types to support incremental generator caching.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Class-level validation (IsPartialClass, DerivesFromExecutor, HasManualConfigureRoutes)
|
||||
/// Class-level validation (IsPartialClass, DerivesFromExecutor, HasManualConfigureProtocol)
|
||||
/// is extracted here but validated once per class in CombineMethodResults to avoid
|
||||
/// redundant validation work when a class has multiple handlers.
|
||||
/// </remarks>
|
||||
@@ -29,7 +29,7 @@ internal sealed record MethodAnalysisResult(
|
||||
// Class-level facts (used for validation in CombineMethodResults)
|
||||
bool IsPartialClass,
|
||||
bool DerivesFromExecutor,
|
||||
bool HasManualConfigureRoutes,
|
||||
bool HasManualConfigureProtocol,
|
||||
|
||||
// Class location for diagnostics (value-equatable)
|
||||
DiagnosticLocationInfo? ClassLocation,
|
||||
|
||||
+5
-62
@@ -243,8 +243,7 @@ public class InMemoryChatHistoryProviderTests
|
||||
var session = CreateMockSession();
|
||||
|
||||
// Arrange
|
||||
// Existing messages in state from a previous turn.
|
||||
var existingMessages = new List<ChatMessage>
|
||||
var originalMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
@@ -254,78 +253,22 @@ public class InMemoryChatHistoryProviderTests
|
||||
new(ChatRole.User, "Reduced")
|
||||
};
|
||||
|
||||
// New messages being added in the current turn.
|
||||
var newRequestMessage = new ChatMessage(ChatRole.User, "New message");
|
||||
var newResponseMessage = new ChatMessage(ChatRole.Assistant, "New response");
|
||||
|
||||
var reducerMock = new Mock<IChatReducer>();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(existingMessages)), It.IsAny<CancellationToken>()))
|
||||
.Setup(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(reducedMessages);
|
||||
|
||||
var provider = new InMemoryChatHistoryProvider(new() { ChatReducer = reducerMock.Object, ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded });
|
||||
provider.SetMessages(session, new List<ChatMessage>(existingMessages));
|
||||
|
||||
// Act
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, [newRequestMessage], [newResponseMessage]);
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, originalMessages, []);
|
||||
await provider.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
// The reducer is called on existing messages before the new ones are added.
|
||||
reducerMock.Verify(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(existingMessages)), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
// Final state: reduced existing messages + new current-turn messages (preserved in full).
|
||||
var messages = provider.GetMessages(session);
|
||||
Assert.Equal(3, messages.Count);
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("Reduced", messages[0].Text);
|
||||
Assert.Equal("New message", messages[1].Text);
|
||||
Assert.Equal("New response", messages[2].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMessagesAsync_WithReducer_AfterMessageAdded_PreservesCurrentTurnFunctionCallsAsync()
|
||||
{
|
||||
var session = CreateMockSession();
|
||||
|
||||
// Arrange - verify that function call and tool result messages from the current turn are preserved
|
||||
// even when a reducer is configured with AfterMessageAdded trigger. The reducer should only
|
||||
// be applied to existing (previous-turn) messages, not to the new messages being added.
|
||||
var existingMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Previous question"),
|
||||
new(ChatRole.Assistant, "Previous answer")
|
||||
};
|
||||
|
||||
var reducerMock = new Mock<IChatReducer>();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([]); // Simulates an aggressive reducer that clears all messages it receives
|
||||
|
||||
var provider = new InMemoryChatHistoryProvider(new() { ChatReducer = reducerMock.Object, ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded });
|
||||
provider.SetMessages(session, new List<ChatMessage>(existingMessages));
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "What is the weather in Taggia?")
|
||||
};
|
||||
var responseMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.Assistant, [new FunctionCallContent("call1", "GetWeather", new Dictionary<string, object?> { ["location"] = "Taggia" })]),
|
||||
new(ChatRole.Tool, [new FunctionResultContent("call1", "Cloudy with a high of 15°C")]),
|
||||
new(ChatRole.Assistant, "The weather in Taggia is cloudy with a high of 15°C.")
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages, responseMessages);
|
||||
await provider.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - all current-turn messages (including function call and tool result) are preserved
|
||||
var messages = provider.GetMessages(session);
|
||||
Assert.Equal(4, messages.Count);
|
||||
Assert.Equal("What is the weather in Taggia?", messages[0].Text);
|
||||
Assert.True(messages[1].Contents.OfType<FunctionCallContent>().Any(), "Function call message should be preserved");
|
||||
Assert.True(messages[2].Contents.OfType<FunctionResultContent>().Any(), "Tool result message should be preserved");
|
||||
Assert.Equal("The weather in Taggia is cloudy with a high of 15°C.", messages[3].Text);
|
||||
reducerMock.Verify(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+2
-8
@@ -21,12 +21,6 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi
|
||||
private const string RedisPort = "6379";
|
||||
|
||||
private static readonly string s_dotnetTargetFramework = GetTargetFramework();
|
||||
|
||||
#if DEBUG
|
||||
private const string BuildConfiguration = "Debug";
|
||||
#else
|
||||
private const string BuildConfiguration = "Release";
|
||||
#endif
|
||||
private static readonly HttpClient s_sharedHttpClient = new();
|
||||
private static readonly IConfiguration s_configuration =
|
||||
new ConfigurationBuilder()
|
||||
@@ -831,7 +825,7 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi
|
||||
ProcessStartInfo buildInfo = new()
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = $"build -f {s_dotnetTargetFramework} -c {BuildConfiguration}",
|
||||
Arguments = $"build -f {s_dotnetTargetFramework}",
|
||||
WorkingDirectory = samplePath,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
@@ -861,7 +855,7 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = $"run --no-build -f {s_dotnetTargetFramework} -c {BuildConfiguration} --port {AzureFunctionsPort}",
|
||||
Arguments = $"run --no-build -f {s_dotnetTargetFramework} --port {AzureFunctionsPort}",
|
||||
WorkingDirectory = samplePath,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
|
||||
+1
-7
@@ -20,12 +20,6 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) :
|
||||
private const string DtsPort = "8080";
|
||||
|
||||
private static readonly string s_dotnetTargetFramework = GetTargetFramework();
|
||||
|
||||
#if DEBUG
|
||||
private const string BuildConfiguration = "Debug";
|
||||
#else
|
||||
private const string BuildConfiguration = "Release";
|
||||
#endif
|
||||
private static readonly HttpClient s_sharedHttpClient = new();
|
||||
private static readonly IConfiguration s_configuration =
|
||||
new ConfigurationBuilder()
|
||||
@@ -443,7 +437,7 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) :
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = $"run -f {s_dotnetTargetFramework} -c {BuildConfiguration} --port {AzureFunctionsPort}",
|
||||
Arguments = $"run -f {s_dotnetTargetFramework} --port {AzureFunctionsPort}",
|
||||
WorkingDirectory = samplePath,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
|
||||
+81
-2
@@ -651,7 +651,7 @@ public class ExecutorRouteGeneratorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartialClass_SendsYieldsInBothFiles_GeneratesAlOverrides()
|
||||
public void PartialClass_SendsYieldsInBothFiles_GeneratesAllOverrides()
|
||||
{
|
||||
// File 1: Partial with one handler
|
||||
var file1 = """
|
||||
@@ -700,7 +700,7 @@ public class ExecutorRouteGeneratorTests
|
||||
generated.Should().RegisterSentMessageType("string")
|
||||
.And.RegisterSentMessageType("int")
|
||||
.And.RegisterYieldedOutputType("string")
|
||||
.And.RegisterYieldedOutputType("string");
|
||||
.And.RegisterYieldedOutputType("int");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1046,6 +1046,85 @@ public class ExecutorRouteGeneratorTests
|
||||
.And.RegisterSentMessageType("global::TestNamespace.BroadcastMessage");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtocolOnly_DerivesFromExecutorOfT_GeneratesBaseCall()
|
||||
{
|
||||
// A protocol-only partial executor deriving from Executor<T>
|
||||
// has a base class that already overrides ConfigureProtocol. The generator must emit
|
||||
// "return base.ConfigureProtocol(protocolBuilder)" so inherited handler registrations
|
||||
// are preserved — not "return protocolBuilder" which silently drops them.
|
||||
var source = """
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace TestNamespace;
|
||||
|
||||
public class FeedbackResult { }
|
||||
|
||||
[SendsMessage(typeof(FeedbackResult))]
|
||||
[YieldsOutput(typeof(string))]
|
||||
public partial class FeedbackExecutor : Executor<string>
|
||||
{
|
||||
public FeedbackExecutor() : base("feedback") { }
|
||||
|
||||
public override System.Threading.Tasks.ValueTask HandleAsync(string message, IWorkflowContext context, System.Threading.CancellationToken cancellationToken = default)
|
||||
=> default;
|
||||
}
|
||||
""";
|
||||
|
||||
var result = GeneratorTestHelper.RunGenerator(source);
|
||||
|
||||
result.RunResult.GeneratedTrees.Should().HaveCount(1);
|
||||
result.RunResult.Diagnostics.Should().BeEmpty();
|
||||
|
||||
var generated = result.RunResult.GeneratedTrees[0].ToString();
|
||||
|
||||
// Base class Executor<T> overrides ConfigureProtocol, so the generated override
|
||||
// must chain to base to preserve the inherited handler registration.
|
||||
generated.Should().Contain("return base.ConfigureProtocol(protocolBuilder)",
|
||||
because: "Executor<T> overrides ConfigureProtocol, so base must be called to preserve its handler registration");
|
||||
generated.Should().Contain(".SendsMessage<global::TestNamespace.FeedbackResult>()");
|
||||
generated.Should().Contain(".YieldsOutput<string>()");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtocolOnly_DerivesDirectlyFromExecutor_DoesNotGenerateBaseCall()
|
||||
{
|
||||
// A protocol-only partial executor deriving directly from Executor (abstract base
|
||||
// with no non-abstract ConfigureProtocol override) should generate "return protocolBuilder"
|
||||
// rather than "return base.ConfigureProtocol(protocolBuilder)".
|
||||
var source = """
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace TestNamespace;
|
||||
|
||||
public class BroadcastMessage { }
|
||||
|
||||
[SendsMessage(typeof(BroadcastMessage))]
|
||||
public partial class BroadcastExecutor : Executor
|
||||
{
|
||||
public BroadcastExecutor() : base("broadcast") { }
|
||||
}
|
||||
""";
|
||||
|
||||
var result = GeneratorTestHelper.RunGenerator(source);
|
||||
|
||||
result.RunResult.GeneratedTrees.Should().HaveCount(1);
|
||||
result.RunResult.Diagnostics.Should().BeEmpty();
|
||||
|
||||
var generated = result.RunResult.GeneratedTrees[0].ToString();
|
||||
|
||||
// Executor's ConfigureProtocol is abstract — no base call needed.
|
||||
generated.Should().Contain("return protocolBuilder",
|
||||
because: "Executor base class has no non-abstract ConfigureProtocol, so no base call is needed");
|
||||
generated.Should().NotContain("base.ConfigureProtocol");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Generic Executor Tests
|
||||
|
||||
Generated
+3
-3
@@ -5047,11 +5047,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.12.0"
|
||||
version = "2.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
Reference in New Issue
Block a user