From e9da53cd3b841631b10a1b84a20916a81b812446 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 9 Jul 2026 17:46:17 -0700 Subject: [PATCH 1/2] Add agent lifecycle event handlers Add Agent 365 agentLifecycle event activity models, typed value payloads, and handler registration extensions for all lifecycle variants. Cover valueType discrimination, event serialization, generic event compatibility, and typed handler dispatch in Apps unit tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AgentLifecycleHandler.Activity.cs | 338 +++++++++++++ .../Handlers/AgentLifecycleHandler.Values.cs | 234 +++++++++ .../Handlers/AgentLifecycleHandler.cs | 262 ++++++++++ .../Handlers/EventHandler.Activity.cs | 32 +- .../Schema/TeamsActivity.cs | 17 +- .../AgentLifecycleHandlerTests.cs | 463 ++++++++++++++++++ 6 files changed, 1339 insertions(+), 7 deletions(-) create mode 100644 core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Activity.cs create mode 100644 core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Values.cs create mode 100644 core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.cs create mode 100644 core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs diff --git a/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Activity.cs b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Activity.cs new file mode 100644 index 00000000..9be3d6bf --- /dev/null +++ b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Activity.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Teams.Apps.Handlers; + +/// +/// Represents an Agent 365 agentLifecycle event activity. +/// +public class AgentLifecycleEventActivity : EventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgentLifecycleEventActivity() : base() + { + Name = EventNames.AgentLifecycle; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgentLifecycleEventActivity(EventActivity activity) : base(activity) + { + Name ??= EventNames.AgentLifecycle; + } + + /// + /// Creates the most-specific Agent 365 lifecycle activity type for the supplied event activity. + /// + /// The source event activity. + /// A typed lifecycle event activity when the valueType is known; otherwise a base lifecycle activity. + internal static AgentLifecycleEventActivity FromEventActivity(EventActivity activity) + { + ArgumentNullException.ThrowIfNull(activity); + + return activity.ValueType switch + { + AgentLifecycleEventValueTypes.AgenticUserIdentityCreated => new AgenticUserIdentityCreatedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated => new AgenticUserIdentityUpdatedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserManagerUpdated => new AgenticUserManagerUpdatedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserEnabled => new AgenticUserEnabledActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserDisabled => new AgenticUserDisabledActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserDeleted => new AgenticUserDeletedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserUndeleted => new AgenticUserUndeletedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated => new AgenticUserWorkloadOnboardingUpdatedActivity(activity), + _ => new AgentLifecycleEventActivity(activity), + }; + } +} + +/// +/// Represents an Agent 365 agentLifecycle event activity with a strongly-typed value payload. +/// +/// The lifecycle event value payload type. +public class AgentLifecycleEventActivity : AgentLifecycleEventActivity +{ + /// + /// Gets or sets the strongly-typed value associated with the lifecycle event activity. + /// + [JsonPropertyName("value")] + public new TValue? Value + { + get => base.Value != null ? JsonSerializer.Deserialize(base.Value.ToJsonString()) : default; + set => base.Value = value != null ? JsonSerializer.SerializeToNode(value) : null; + } + + /// + /// Initializes a new instance of the class. + /// + internal AgentLifecycleEventActivity() : base() + { + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgentLifecycleEventActivity(EventActivity activity) : base(activity) + { + } +} + +/// +/// Fired when an agentic user identity is created. +/// +public class AgenticUserIdentityCreatedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserIdentityCreatedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserIdentityCreated; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserIdentityCreatedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserIdentityCreated; + } +} + +/// +/// Fired when an agentic user identity property changes. +/// +public class AgenticUserIdentityUpdatedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserIdentityUpdatedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserIdentityUpdatedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated; + } +} + +/// +/// Fired when an agentic user's manager changes. +/// +public class AgenticUserManagerUpdatedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserManagerUpdatedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserManagerUpdated; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserManagerUpdatedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserManagerUpdated; + } +} + +/// +/// Fired when an agentic user is enabled. +/// +public class AgenticUserEnabledActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserEnabledActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserEnabled; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserEnabledActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserEnabled; + } +} + +/// +/// Fired when an agentic user is disabled. +/// +public class AgenticUserDisabledActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserDisabledActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserDisabled; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserDisabledActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserDisabled; + } +} + +/// +/// Fired when an agentic user is deleted. +/// +public class AgenticUserDeletedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserDeletedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserDeleted; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserDeletedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserDeleted; + } +} + +/// +/// Fired when a previously deleted agentic user is restored. +/// +public class AgenticUserUndeletedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserUndeletedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserUndeleted; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserUndeletedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserUndeleted; + } +} + +/// +/// Fired when a workload onboarding state changes for an agentic user. +/// +public class AgenticUserWorkloadOnboardingUpdatedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserWorkloadOnboardingUpdatedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserWorkloadOnboardingUpdatedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated; + } +} + +/// +/// String constants for Agent 365 lifecycle activity valueType values. +/// +public static class AgentLifecycleEventValueTypes +{ + /// Agentic user identity created value type. + public const string AgenticUserIdentityCreated = "AgenticUserIdentityCreated"; + + /// Agentic user identity updated value type. + public const string AgenticUserIdentityUpdated = "AgenticUserIdentityUpdated"; + + /// Agentic user manager updated value type. + public const string AgenticUserManagerUpdated = "AgenticUserManagerUpdated"; + + /// Agentic user enabled value type. + public const string AgenticUserEnabled = "AgenticUserEnabled"; + + /// Agentic user disabled value type. + public const string AgenticUserDisabled = "AgenticUserDisabled"; + + /// Agentic user deleted value type. + public const string AgenticUserDeleted = "AgenticUserDeleted"; + + /// Agentic user undeleted value type. + public const string AgenticUserUndeleted = "AgenticUserUndeleted"; + + /// Agentic user workload onboarding updated value type. + public const string AgenticUserWorkloadOnboardingUpdated = "AgenticUserWorkloadOnboardingUpdated"; +} + +/// +/// String constants for Agent 365 lifecycle payload eventType values. +/// +public static class AgentLifecycleEventTypes +{ + /// Agentic user identity created event type. + public const string AgenticUserIdentityCreated = "agenticUserIdentityCreated"; + + /// Agentic user identity updated event type. + public const string AgenticUserIdentityUpdated = "agenticUserIdentityUpdated"; + + /// Agentic user manager updated event type. + public const string AgenticUserManagerUpdated = "agenticUserManagerUpdated"; + + /// Agentic user enabled event type. + public const string AgenticUserEnabled = "agenticUserEnabled"; + + /// Agentic user disabled event type. + public const string AgenticUserDisabled = "agenticUserDisabled"; + + /// Agentic user deleted event type. + public const string AgenticUserDeleted = "agenticUserDeleted"; + + /// Agentic user undeleted event type. + public const string AgenticUserUndeleted = "agenticUserUndeleted"; + + /// Agentic user workload onboarding updated event type. + public const string AgenticUserWorkloadOnboardingUpdated = "agenticUserWorkloadOnboardingUpdated"; +} diff --git a/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Values.cs b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Values.cs new file mode 100644 index 00000000..8838c20b --- /dev/null +++ b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Values.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Microsoft.Teams.Apps.Handlers; + +/// +/// Manager profile carried by the AgenticUserIdentityCreated lifecycle event. +/// +public class AgentLifecycleManager +{ + /// + /// Gets or sets the Entra object ID of the manager. + /// + [JsonPropertyName("userId")] + public string? UserId { get; set; } + + /// + /// Gets or sets the manager's email address. + /// + [JsonPropertyName("email")] + public string? Email { get; set; } + + /// + /// Gets or sets the manager's display name. + /// + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } +} + +/// +/// Manager reference carried by the AgenticUserManagerUpdated lifecycle event. +/// +public class AgentLifecycleManagerRef +{ + /// + /// Gets or sets the Entra object ID of the manager. + /// + [JsonPropertyName("managerId")] + public string? ManagerId { get; set; } +} + +/// +/// A single property change carried by the AgenticUserIdentityUpdated lifecycle event. +/// +public class AgentLifecycleUpdatedProperty +{ + /// + /// Gets or sets the name of the property that changed. + /// + [JsonPropertyName("propertyName")] + public required string PropertyName { get; set; } + + /// + /// Gets or sets the new value of the property. + /// + [JsonPropertyName("propertyValue")] + public string? PropertyValue { get; set; } +} + +/// +/// Fields shared by every Agent 365 lifecycle event payload. +/// +public abstract class AgentLifecycleValueBase +{ + /// + /// Gets or sets the tenant that the agentic user belongs to. + /// + [JsonPropertyName("tenantId")] + public string? TenantId { get; set; } + + /// + /// Gets or sets the Agent ID user-shaped identity object ID. + /// + [JsonPropertyName("agenticUserId")] + public string? AgenticUserId { get; set; } + + /// + /// Gets or sets the concrete agent app instance ID. + /// + [JsonPropertyName("agenticAppInstanceId")] + public string? AgenticAppInstanceId { get; set; } + + /// + /// Gets or sets the agent identity blueprint app ID. + /// + [JsonPropertyName("agentIdentityBlueprintId")] + public string? AgentIdentityBlueprintId { get; set; } + + /// + /// Gets or sets the monotonic version of the agentic user state when provided by the service. + /// + [JsonPropertyName("version")] + public int? Version { get; set; } +} + +/// +/// Payload for the AgenticUserIdentityCreated lifecycle event. +/// +public class AgenticUserIdentityCreatedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserIdentityCreated; + + /// + /// Gets or sets the manager assigned to the agentic user at creation. + /// + [JsonPropertyName("manager")] + public AgentLifecycleManager? Manager { get; set; } + + /// + /// Gets or sets when the agentic user identity expires. + /// + [JsonPropertyName("expirationDateTime")] + public DateTimeOffset? ExpirationDateTime { get; set; } +} + +/// +/// Payload for the AgenticUserIdentityUpdated lifecycle event. +/// +public class AgenticUserIdentityUpdatedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserIdentityUpdated; + + /// + /// Gets or sets the property that changed. + /// + [JsonPropertyName("updatedProperty")] + public required AgentLifecycleUpdatedProperty UpdatedProperty { get; set; } +} + +/// +/// Payload for the AgenticUserManagerUpdated lifecycle event. +/// +public class AgenticUserManagerUpdatedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserManagerUpdated; + + /// + /// Gets or sets the new manager reference. The value is absent when the manager was removed. + /// + [JsonPropertyName("manager")] + public AgentLifecycleManagerRef? Manager { get; set; } +} + +/// +/// Payload for the AgenticUserEnabled lifecycle event. +/// +public class AgenticUserEnabledValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserEnabled; +} + +/// +/// Payload for the AgenticUserDisabled lifecycle event. +/// +public class AgenticUserDisabledValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserDisabled; +} + +/// +/// Payload for the AgenticUserDeleted lifecycle event. +/// +public class AgenticUserDeletedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserDeleted; + + /// + /// Gets or sets the reason the agentic user was deleted. + /// + [JsonPropertyName("deletionReason")] + public string? DeletionReason { get; set; } +} + +/// +/// Payload for the AgenticUserUndeleted lifecycle event. +/// +public class AgenticUserUndeletedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserUndeleted; +} + +/// +/// Payload for the AgenticUserWorkloadOnboardingUpdated lifecycle event. +/// +public class AgenticUserWorkloadOnboardingUpdatedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserWorkloadOnboardingUpdated; + + /// + /// Gets or sets the workload being onboarded. + /// + [JsonPropertyName("workloadName")] + public string? WorkloadName { get; set; } + + /// + /// Gets or sets the onboarding state for the workload. + /// + [JsonPropertyName("workloadOnboardingState")] + public string? WorkloadOnboardingState { get; set; } +} diff --git a/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.cs b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.cs new file mode 100644 index 00000000..7c0ec862 --- /dev/null +++ b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.cs @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Apps.Routing; +using Microsoft.Teams.Apps.Schema; + +namespace Microsoft.Teams.Apps.Handlers; + +/// +/// Delegate for handling all Agent 365 lifecycle event activity variants. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgentLifecycleHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user identity created lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserIdentityCreatedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user identity updated lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserIdentityUpdatedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user manager updated lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserManagerUpdatedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user enabled lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserEnabledHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user disabled lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserDisabledHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user deleted lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserDeletedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user undeleted lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserUndeletedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user workload onboarding updated lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserWorkloadOnboardingUpdatedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Extension methods for registering Agent 365 lifecycle event activity handlers. +/// +public static class AgentLifecycleExtensions +{ + /// + /// Registers a handler for all Agent 365 agentLifecycle event activity variants. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgentLifecycle(this TeamsBotApplication app, AgentLifecycleHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + EventNames.AgentLifecycle, + valueType: null, + AgentLifecycleEventActivity.FromEventActivity, + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserIdentityCreated lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserIdentityCreated(this TeamsBotApplication app, AgenticUserIdentityCreatedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserIdentityCreated, + AgentLifecycleEventValueTypes.AgenticUserIdentityCreated, + activity => new AgenticUserIdentityCreatedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserIdentityUpdated lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserIdentityUpdated(this TeamsBotApplication app, AgenticUserIdentityUpdatedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated, + AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated, + activity => new AgenticUserIdentityUpdatedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserManagerUpdated lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserManagerUpdated(this TeamsBotApplication app, AgenticUserManagerUpdatedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, + AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, + activity => new AgenticUserManagerUpdatedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserEnabled lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserEnabled(this TeamsBotApplication app, AgenticUserEnabledHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserEnabled, + AgentLifecycleEventValueTypes.AgenticUserEnabled, + activity => new AgenticUserEnabledActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserDisabled lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserDisabled(this TeamsBotApplication app, AgenticUserDisabledHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserDisabled, + AgentLifecycleEventValueTypes.AgenticUserDisabled, + activity => new AgenticUserDisabledActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserDeleted lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserDeleted(this TeamsBotApplication app, AgenticUserDeletedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserDeleted, + AgentLifecycleEventValueTypes.AgenticUserDeleted, + activity => new AgenticUserDeletedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserUndeleted lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserUndeleted(this TeamsBotApplication app, AgenticUserUndeletedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserUndeleted, + AgentLifecycleEventValueTypes.AgenticUserUndeleted, + activity => new AgenticUserUndeletedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserWorkloadOnboardingUpdated lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserWorkloadOnboardingUpdated(this TeamsBotApplication app, AgenticUserWorkloadOnboardingUpdatedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated, + AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated, + activity => new AgenticUserWorkloadOnboardingUpdatedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + private static TeamsBotApplication RegisterAgentLifecycleRoute( + this TeamsBotApplication app, + string routeSuffix, + string? valueType, + Func createActivity, + Func, CancellationToken, Task> handler) where TActivity : AgentLifecycleEventActivity + { + ArgumentNullException.ThrowIfNull(app, nameof(app)); + ArgumentException.ThrowIfNullOrWhiteSpace(routeSuffix); + ArgumentNullException.ThrowIfNull(createActivity, nameof(createActivity)); + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + + string routeName = valueType is null + ? string.Join("/", TeamsActivityTypes.Event, routeSuffix) + : string.Join("/", TeamsActivityTypes.Event, EventNames.AgentLifecycle, routeSuffix); + + app.Router.Register(new Route + { + Name = routeName, + Selector = activity => + activity.Name == EventNames.AgentLifecycle + && (valueType is null || activity.ValueType == valueType), + Handler = async (ctx, cancellationToken) => + { + TActivity typedActivity = createActivity(ctx.Activity); + var typedContext = ctx.CreateDerivedContext(typedActivity); + await handler(typedContext, cancellationToken).ConfigureAwait(false); + } + }); + + return app; + } +} diff --git a/core/src/Microsoft.Teams.Apps/Handlers/EventHandler.Activity.cs b/core/src/Microsoft.Teams.Apps/Handlers/EventHandler.Activity.cs index c06830ce..ad792020 100644 --- a/core/src/Microsoft.Teams.Apps/Handlers/EventHandler.Activity.cs +++ b/core/src/Microsoft.Teams.Apps/Handlers/EventHandler.Activity.cs @@ -20,7 +20,10 @@ public class EventActivity : TeamsActivity public static new EventActivity FromActivity(CoreActivity activity) { ArgumentNullException.ThrowIfNull(activity); - return new EventActivity(activity); + EventActivity eventActivity = new(activity); + return eventActivity.Name == EventNames.AgentLifecycle + ? AgentLifecycleEventActivity.FromEventActivity(eventActivity) + : eventActivity; } /// @@ -29,12 +32,21 @@ public class EventActivity : TeamsActivity [JsonPropertyName("name")] public string? Name { get; set; } + /// + /// Gets or sets the value payload type for event activities that carry typed variants. + /// + [JsonPropertyName("valueType")] + public string? ValueType { get; set; } + /// /// Gets or sets the value payload of the event activity. /// [JsonPropertyName("value")] public JsonNode? Value { get; set; } + /// + public override string ToJson() => CoreActivity.ToJson(this); + /// /// Initializes a new instance of the class. /// @@ -48,10 +60,17 @@ internal EventActivity() : base(TeamsActivityTypes.Event) /// internal EventActivity(CoreActivity activity) : base(activity) { - Name = activity.Properties.Extract("name"); - Value = activity is EventActivity evt - ? evt.Value - : activity.Properties.Extract("value"); + if (activity is EventActivity evt) + { + Name = evt.Name; + ValueType = evt.ValueType; + Value = evt.Value; + return; + } + + Name = Properties.Extract("name"); + ValueType = Properties.Extract("valueType"); + Value = Properties.Extract("value"); } } @@ -91,6 +110,9 @@ internal EventActivity(EventActivity activity) : base(activity) /// public static class EventNames { + /// Agent 365 lifecycle event name. + public const string AgentLifecycle = "agentLifecycle"; + /// Meeting start event name. public const string MeetingStart = "application/vnd.microsoft.meetingStart"; diff --git a/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs b/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs index 54347ad6..19e4d5d5 100644 --- a/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs +++ b/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs @@ -68,8 +68,21 @@ internal TeamsActivity(CoreActivity activity) : base(activity) base.From = TeamsChannelAccount.FromChannelAccount(activity.From) ?? new TeamsChannelAccount(); base.Recipient = TeamsChannelAccount.FromChannelAccount(activity.Recipient) ?? new TeamsChannelAccount(); base.Conversation = TeamsConversation.FromConversation(activity.Conversation) ?? new TeamsConversation(); - ChannelData = activity.Properties.Extract("channelData"); - Entities = activity.Properties.Extract("entities"); + if (activity is TeamsActivity teamsActivity) + { + ChannelData = teamsActivity.ChannelData; + Entities = teamsActivity.Entities; + Timestamp = teamsActivity.Timestamp; + LocalTimestamp = teamsActivity.LocalTimestamp; + Locale = teamsActivity.Locale; + LocalTimezone = teamsActivity.LocalTimezone; + SuggestedActions = teamsActivity.SuggestedActions; + } + else + { + ChannelData = Properties.Extract("channelData"); + Entities = Properties.Extract("entities"); + } } /// diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs new file mode 100644 index 00000000..4cae0dfb --- /dev/null +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs @@ -0,0 +1,463 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Teams.Apps.Api.Clients; +using Microsoft.Teams.Apps.Handlers; +using Microsoft.Teams.Apps.Schema; +using Microsoft.Teams.Apps.State; +using Microsoft.Teams.Core; +using Microsoft.Teams.Core.Schema; +using Moq; + +namespace Microsoft.Teams.Apps.UnitTests; + +public class AgentLifecycleHandlerTests +{ + private const string TenantId = "00000000-0000-0000-0000-000000000001"; + private const string AgenticUserId = "00000000-0000-0000-0000-000000000002"; + private const string AppId = "00000000-0000-0000-0000-000000000003"; + private const string AgenticAppInstanceId = "00000000-0000-0000-0000-000000000004"; + private const string BlueprintId = "00000000-0000-0000-0000-000000000005"; + private const string ManagerId = "3c22b565-74f3-48b0-aa18-1dc03b8ec270"; + + public static TheoryData VariantTypes => new() + { + { AgentLifecycleEventValueTypes.AgenticUserIdentityCreated, typeof(AgenticUserIdentityCreatedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated, typeof(AgenticUserIdentityUpdatedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, typeof(AgenticUserManagerUpdatedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserEnabled, typeof(AgenticUserEnabledActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserDisabled, typeof(AgenticUserDisabledActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserDeleted, typeof(AgenticUserDeletedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserUndeleted, typeof(AgenticUserUndeletedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated, typeof(AgenticUserWorkloadOnboardingUpdatedActivity) }, + }; + + [Theory] + [MemberData(nameof(VariantTypes))] + public void FromActivity_DiscriminatesAgentLifecycleVariants(string valueType, Type expectedType) + { + TeamsActivity activity = ParseLifecycleActivity(valueType); + + Assert.Equal(expectedType, activity.GetType()); + + EventActivity eventActivity = Assert.IsAssignableFrom(activity); + Assert.Equal(TeamsActivityTypes.Event, eventActivity.Type); + Assert.Equal(EventNames.AgentLifecycle, eventActivity.Name); + Assert.Equal(valueType, eventActivity.ValueType); + Assert.Equal(TenantId, eventActivity.ChannelData?.Tenant?.Id); + AssertLifecycleValue(activity, valueType); + } + + [Fact] + public void FromActivity_KeepsUnknownAgentLifecycleValueTypeAsBaseLifecycleActivity() + { + TeamsActivity activity = ParseLifecycleActivity("UnknownLifecycleValueType", CommonValue("unknownLifecycleEvent")); + + AgentLifecycleEventActivity lifecycleActivity = Assert.IsType(activity); + Assert.Equal(EventNames.AgentLifecycle, lifecycleActivity.Name); + Assert.Equal("UnknownLifecycleValueType", lifecycleActivity.ValueType); + Assert.NotNull(lifecycleActivity.Value); + } + + [Fact] + public void ToJson_PreservesAgentLifecycleEnvelope() + { + AgenticUserManagerUpdatedActivity activity = Assert.IsType( + ParseLifecycleActivity(AgentLifecycleEventValueTypes.AgenticUserManagerUpdated)); + + string json = activity.ToJson(); + using JsonDocument document = JsonDocument.Parse(json); + JsonElement root = document.RootElement; + + Assert.Equal(1, CountOccurrences(json, "\"valueType\"")); + Assert.Equal(1, CountOccurrences(json, "\"channelData\"")); + Assert.Equal(TeamsActivityTypes.Event, root.GetProperty("type").GetString()); + Assert.Equal(EventNames.AgentLifecycle, root.GetProperty("name").GetString()); + Assert.Equal(AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, root.GetProperty("valueType").GetString()); + Assert.Equal(AgenticAppInstanceId, root.GetProperty("value").GetProperty("agenticAppInstanceId").GetString()); + Assert.Equal(ManagerId, root.GetProperty("value").GetProperty("manager").GetProperty("managerId").GetString()); + } + + [Fact] + public async Task OnEvent_StillReceivesAgentLifecycleEvents() + { + TeamsBotApplication app = CreateApp(); + bool called = false; + + app.OnEvent((ctx, _) => + { + called = true; + Assert.IsType(ctx.Activity); + Assert.Equal(EventNames.AgentLifecycle, ctx.Activity.Name); + Assert.Equal(AgentLifecycleEventValueTypes.AgenticUserEnabled, ctx.Activity.ValueType); + return Task.CompletedTask; + }); + + await DispatchAsync(app, AgentLifecycleEventValueTypes.AgenticUserEnabled); + + Assert.True(called); + } + + [Theory] + [MemberData(nameof(VariantTypes))] + public async Task VariantHandlers_DispatchTypedContextAndPropagateState(string valueType, Type expectedType) + { + TeamsBotApplication app = CreateApp(); + TurnStateContainer state = CreateState(); + int lifecycleHandlerCalls = 0; + int variantHandlerCalls = 0; + + app.OnAgentLifecycle((ctx, _) => + { + lifecycleHandlerCalls++; + Assert.Same(state, ctx.State); + Assert.Equal(valueType, ctx.Activity.ValueType); + Assert.Equal(expectedType, AgentLifecycleEventActivity.FromEventActivity(ctx.Activity).GetType()); + return Task.CompletedTask; + }); + + RegisterVariantHandler(app, valueType, expectedType, state, () => variantHandlerCalls++); + + await DispatchAsync(app, valueType, state); + + Assert.Equal(1, lifecycleHandlerCalls); + Assert.Equal(1, variantHandlerCalls); + } + + [Fact] + public async Task VariantHandler_DoesNotDispatchForDifferentValueType() + { + TeamsBotApplication app = CreateApp(); + bool called = false; + + app.OnAgenticUserEnabled((_, _) => + { + called = true; + return Task.CompletedTask; + }); + + await DispatchAsync(app, AgentLifecycleEventValueTypes.AgenticUserDisabled); + + Assert.False(called); + } + + private static void RegisterVariantHandler( + TeamsBotApplication app, + string valueType, + Type expectedType, + TurnStateContainer state, + Action onCalled) + { + switch (valueType) + { + case AgentLifecycleEventValueTypes.AgenticUserIdentityCreated: + app.OnAgenticUserIdentityCreated((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(ManagerId, ctx.Activity.Value?.Manager?.UserId); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated: + app.OnAgenticUserIdentityUpdated((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal("Mail", ctx.Activity.Value?.UpdatedProperty.PropertyName); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserManagerUpdated: + app.OnAgenticUserManagerUpdated((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(ManagerId, ctx.Activity.Value?.Manager?.ManagerId); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserEnabled: + app.OnAgenticUserEnabled((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(6, ctx.Activity.Value?.Version); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserDisabled: + app.OnAgenticUserDisabled((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(7, ctx.Activity.Value?.Version); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserDeleted: + app.OnAgenticUserDeleted((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal("UserSoftDelete", ctx.Activity.Value?.DeletionReason); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserUndeleted: + app.OnAgenticUserUndeleted((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(9, ctx.Activity.Value?.Version); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated: + app.OnAgenticUserWorkloadOnboardingUpdated((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal("Teams", ctx.Activity.Value?.WorkloadName); + Assert.Equal("succeeded", ctx.Activity.Value?.WorkloadOnboardingState); + return Task.CompletedTask; + }); + break; + } + } + + private static void AssertVariantContext( + Context context, + Type expectedType, + string valueType, + TurnStateContainer state, + Action onCalled) where TActivity : AgentLifecycleEventActivity + { + onCalled(); + Assert.Same(state, context.State); + Assert.Equal(expectedType, context.Activity.GetType()); + Assert.Equal(EventNames.AgentLifecycle, context.Activity.Name); + Assert.Equal(valueType, context.Activity.ValueType); + } + + private static void AssertLifecycleValue(TeamsActivity activity, string valueType) + { + switch (valueType) + { + case AgentLifecycleEventValueTypes.AgenticUserIdentityCreated: + AgenticUserIdentityCreatedActivity created = Assert.IsType(activity); + Assert.Equal(AgentLifecycleEventTypes.AgenticUserIdentityCreated, created.Value?.EventType); + Assert.Equal(AgenticUserId, created.Value?.AgenticUserId); + Assert.Equal(ManagerId, created.Value?.Manager?.UserId); + Assert.Equal("manager@example.test", created.Value?.Manager?.Email); + Assert.Equal(1, created.Value?.ExpirationDateTime?.Year); + break; + + case AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated: + AgenticUserIdentityUpdatedActivity updated = Assert.IsType(activity); + Assert.Equal("Mail", updated.Value?.UpdatedProperty.PropertyName); + Assert.Equal("newinstance4@teamssdk.onmicrosoft.com", updated.Value?.UpdatedProperty.PropertyValue); + Assert.Equal(4, updated.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserManagerUpdated: + AgenticUserManagerUpdatedActivity managerUpdated = Assert.IsType(activity); + Assert.Equal(ManagerId, managerUpdated.Value?.Manager?.ManagerId); + Assert.Equal(6, managerUpdated.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserEnabled: + AgenticUserEnabledActivity enabled = Assert.IsType(activity); + Assert.Equal(AgentLifecycleEventTypes.AgenticUserEnabled, enabled.Value?.EventType); + Assert.Equal(6, enabled.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserDisabled: + AgenticUserDisabledActivity disabled = Assert.IsType(activity); + Assert.Equal(AgentLifecycleEventTypes.AgenticUserDisabled, disabled.Value?.EventType); + Assert.Equal(7, disabled.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserDeleted: + AgenticUserDeletedActivity deleted = Assert.IsType(activity); + Assert.Equal("UserSoftDelete", deleted.Value?.DeletionReason); + Assert.Equal(8, deleted.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserUndeleted: + AgenticUserUndeletedActivity undeleted = Assert.IsType(activity); + Assert.Equal(AgentLifecycleEventTypes.AgenticUserUndeleted, undeleted.Value?.EventType); + Assert.Equal(9, undeleted.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated: + AgenticUserWorkloadOnboardingUpdatedActivity workload = Assert.IsType(activity); + Assert.Equal("Teams", workload.Value?.WorkloadName); + Assert.Equal("succeeded", workload.Value?.WorkloadOnboardingState); + break; + } + } + + private static Task DispatchAsync(TeamsBotApplication app, string valueType, TurnStateContainer? state = null) + { + EventActivity activity = Assert.IsAssignableFrom(ParseLifecycleActivity(valueType)); + Context context = new(app, activity); + if (state is not null) + { + context.State = state; + } + + return app.Router.DispatchAsync(context); + } + + private static TeamsActivity ParseLifecycleActivity(string valueType) + => ParseLifecycleActivity(valueType, ValueFor(valueType)); + + private static TeamsActivity ParseLifecycleActivity(string valueType, string valueJson) + { + string json = $$""" + { + "recipient": { + "agenticUserId": "{{AgenticUserId}}", + "agenticAppId": "{{AppId}}", + "agenticAppBlueprintId": "{{BlueprintId}}", + "callbackUri": "https://example.test/api/messages", + "tenantId": "{{TenantId}}", + "role": "agenticUser", + "id": "{{AgenticUserId}}" + }, + "type": "event", + "id": "activity-id", + "timestamp": "2026-06-29T00:00:00Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/tenant/", + "channelId": "agents", + "from": { "id": "system", "name": "System", "tenantId": "{{TenantId}}" }, + "conversation": { "tenantId": "{{TenantId}}", "id": "conversation-id", "topic": null }, + "channelData": { "tenant": { "id": "{{TenantId}}" }, "productContext": null }, + "valueType": "{{valueType}}", + "value": {{valueJson}}, + "name": "agentLifecycle" + } + """; + + return TeamsActivity.FromActivity(CoreActivity.FromJsonString(json)); + } + + private static string ValueFor(string valueType) + => valueType switch + { + AgentLifecycleEventValueTypes.AgenticUserIdentityCreated => CommonValue( + AgentLifecycleEventTypes.AgenticUserIdentityCreated, + $$""" + "expirationDateTime": "0001-01-01T00:00:00+00:00", + "manager": { + "displayName": null, + "userId": "{{ManagerId}}", + "email": "manager@example.test" + } + """), + AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated => CommonValue( + AgentLifecycleEventTypes.AgenticUserIdentityUpdated, + """ + "updatedProperty": { + "propertyName": "Mail", + "propertyValue": "newinstance4@teamssdk.onmicrosoft.com" + }, + "version": 4 + """), + AgentLifecycleEventValueTypes.AgenticUserManagerUpdated => CommonValue( + AgentLifecycleEventTypes.AgenticUserManagerUpdated, + $$""" + "manager": { "managerId": "{{ManagerId}}" }, + "version": 6 + """), + AgentLifecycleEventValueTypes.AgenticUserEnabled => CommonValue( + AgentLifecycleEventTypes.AgenticUserEnabled, + """ + "version": 6 + """), + AgentLifecycleEventValueTypes.AgenticUserDisabled => CommonValue( + AgentLifecycleEventTypes.AgenticUserDisabled, + """ + "version": 7 + """), + AgentLifecycleEventValueTypes.AgenticUserDeleted => CommonValue( + AgentLifecycleEventTypes.AgenticUserDeleted, + """ + "deletionReason": "UserSoftDelete", + "version": 8 + """), + AgentLifecycleEventValueTypes.AgenticUserUndeleted => CommonValue( + AgentLifecycleEventTypes.AgenticUserUndeleted, + """ + "version": 9 + """), + AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated => CommonValue( + AgentLifecycleEventTypes.AgenticUserWorkloadOnboardingUpdated, + """ + "workloadName": "Teams", + "workloadOnboardingState": "succeeded" + """), + _ => CommonValue("unknownLifecycleEvent"), + }; + + private static string CommonValue(string eventType, string additionalJson = "") + { + string separator = string.IsNullOrWhiteSpace(additionalJson) ? string.Empty : "," + Environment.NewLine + additionalJson; + return $$""" + { + "tenantId": "{{TenantId}}", + "agenticUserId": "{{AgenticUserId}}", + "agenticAppInstanceId": "{{AgenticAppInstanceId}}", + "agentIdentityBlueprintId": "{{BlueprintId}}", + "eventType": "{{eventType}}"{{separator}} + } + """; + } + + private static int CountOccurrences(string value, string match) + { + int count = 0; + int index = 0; + while ((index = value.IndexOf(match, index, StringComparison.Ordinal)) >= 0) + { + count++; + index += match.Length; + } + + return count; + } + + private static TurnStateContainer CreateState() + { + TurnState conversationState = new(); + conversationState.Set("test-key", "test-value"); + return new TurnStateContainer(conversationState, new TurnState()); + } + + private static TeamsBotApplication CreateApp() + { + Mock mockUserTokenClient = new( + new HttpClient(), + new Mock().Object, + NullLogger.Instance); + + Mock mockConversationClient = new( + new HttpClient(), + NullLogger.Instance); + + ApiClient apiClient = new( + new HttpClient(), + mockConversationClient.Object, + mockUserTokenClient.Object); + + return new TeamsBotApplication( + apiClient, + new HttpContextAccessor(), + NullLogger.Instance, + new TeamsBotApplicationOptions { AppId = "test-app-id" }); + } +} From 82d7c9214c64a2364f940013d024843e558bea69 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 9 Jul 2026 18:07:58 -0700 Subject: [PATCH 2/2] Keep lifecycle event changes scoped Remove broad TeamsActivity metadata copy behavior and keep lifecycle tests focused on routing, valueType discrimination, and payload binding. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Schema/TeamsActivity.cs | 17 ++--------------- .../AgentLifecycleHandlerTests.cs | 4 +--- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs b/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs index 19e4d5d5..4deb08d3 100644 --- a/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs +++ b/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs @@ -68,21 +68,8 @@ internal TeamsActivity(CoreActivity activity) : base(activity) base.From = TeamsChannelAccount.FromChannelAccount(activity.From) ?? new TeamsChannelAccount(); base.Recipient = TeamsChannelAccount.FromChannelAccount(activity.Recipient) ?? new TeamsChannelAccount(); base.Conversation = TeamsConversation.FromConversation(activity.Conversation) ?? new TeamsConversation(); - if (activity is TeamsActivity teamsActivity) - { - ChannelData = teamsActivity.ChannelData; - Entities = teamsActivity.Entities; - Timestamp = teamsActivity.Timestamp; - LocalTimestamp = teamsActivity.LocalTimestamp; - Locale = teamsActivity.Locale; - LocalTimezone = teamsActivity.LocalTimezone; - SuggestedActions = teamsActivity.SuggestedActions; - } - else - { - ChannelData = Properties.Extract("channelData"); - Entities = Properties.Extract("entities"); - } + ChannelData = Properties.Extract("channelData"); + Entities = Properties.Extract("entities"); } /// diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs index 4cae0dfb..3b8dc46d 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs @@ -48,7 +48,6 @@ public void FromActivity_DiscriminatesAgentLifecycleVariants(string valueType, T Assert.Equal(TeamsActivityTypes.Event, eventActivity.Type); Assert.Equal(EventNames.AgentLifecycle, eventActivity.Name); Assert.Equal(valueType, eventActivity.ValueType); - Assert.Equal(TenantId, eventActivity.ChannelData?.Tenant?.Id); AssertLifecycleValue(activity, valueType); } @@ -72,9 +71,8 @@ public void ToJson_PreservesAgentLifecycleEnvelope() string json = activity.ToJson(); using JsonDocument document = JsonDocument.Parse(json); JsonElement root = document.RootElement; - Assert.Equal(1, CountOccurrences(json, "\"valueType\"")); - Assert.Equal(1, CountOccurrences(json, "\"channelData\"")); + Assert.Equal(1, CountOccurrences(json, "\"valueType\"")); Assert.Equal(TeamsActivityTypes.Event, root.GetProperty("type").GetString()); Assert.Equal(EventNames.AgentLifecycle, root.GetProperty("name").GetString()); Assert.Equal(AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, root.GetProperty("valueType").GetString());