From 965c1bee46703ee90b1c888c36f7dd5b34d5c464 Mon Sep 17 00:00:00 2001 From: lilydu Date: Thu, 2 Apr 2026 11:23:10 -0700 Subject: [PATCH 1/6] fix validation error on MessageUpdate when text is missing --- .../api/activities/message/message_update.py | 2 +- .../api/tests/unit/test_message_activities.py | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/message/message_update.py b/packages/api/src/microsoft_teams/api/activities/message/message_update.py index 78f6ab80..cb342e9d 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message_update.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message_update.py @@ -49,7 +49,7 @@ class _MessageUpdateBase(CustomBaseModel): class MessageUpdateActivity(_MessageUpdateBase, ActivityBase): """Output model for received message update activities with required fields and read-only properties.""" - text: str # pyright: ignore [reportGeneralTypeIssues] + text: str = "" # pyright: ignore [reportGeneralTypeIssues, reportIncompatibleVariableOverride] """The text content of the message.""" channel_data: MessageUpdateChannelData # pyright: ignore [reportGeneralTypeIssues] diff --git a/packages/api/tests/unit/test_message_activities.py b/packages/api/tests/unit/test_message_activities.py index cf09f16a..5ee803f0 100644 --- a/packages/api/tests/unit/test_message_activities.py +++ b/packages/api/tests/unit/test_message_activities.py @@ -7,6 +7,7 @@ from datetime import datetime from typing import cast +from microsoft_teams.api.activities import ActivityTypeAdapter from microsoft_teams.api.activities.message import ( MessageActivity, MessageActivityInput, @@ -259,7 +260,7 @@ def test_is_recipient_mentioned_false(self): activity.recipient = recipient # Mention someone else - other_account = Account(id="user-456", name="User", role="user") + other_account = Account(id="user-456", name="User") mention = MentionEntity(mentioned=other_account, text="User") activity.entities = [mention] @@ -511,6 +512,40 @@ def test_message_update_activity_creation_undelete(self): assert activity.type == "messageUpdate" assert activity.channel_data.event_type == "undeleteMessage" + def test_message_update_activity_text_defaults_to_empty_string(self): + """Test that text field defaults to empty string when absent (e.g. attachment-only update)""" + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + channel_data = MessageUpdateChannelData(event_type="editMessage") + + activity = MessageUpdateActivity( + id="update-no-text", + channel_data=channel_data, + from_=from_account, + conversation=conversation, + recipient=recipient, + ) + + assert activity.text == "" + + def test_inbound_message_update_without_text_does_not_throw(self): + """Test that an inbound messageUpdate payload with no text (attachment-only) parses without error""" + + payload = { + "type": "messageUpdate", + "id": "msg-123", + "from": {"id": "user-123", "name": "Test User"}, + "conversation": {"id": "conv-456", "conversationType": "personal"}, + "recipient": {"id": "bot-789", "name": "Test Bot"}, + "channelData": {"eventType": "editMessage"}, + } + + activity = ActivityTypeAdapter.validate_python(payload) + + assert isinstance(activity, MessageUpdateActivity) + assert activity.text == "" + def test_message_update_activity_optional_fields(self): """Test message update activity with optional fields""" from_account = Account(id="bot-123", name="Test Bot") From b65855d9d9cc50591e3b7e25e22103e63e9b9de2 Mon Sep 17 00:00:00 2001 From: lilydu Date: Thu, 2 Apr 2026 15:16:27 -0700 Subject: [PATCH 2/6] add custom feedback form --- .../api/activities/invoke/__init__.py | 13 ++++- .../api/activities/invoke/message/__init__.py | 14 +++++- .../activities/invoke/message/fetch_task.py | 49 +++++++++++++++++++ .../microsoft_teams/api/models/activity.py | 21 +++++--- .../api/models/channel_data/__init__.py | 11 ++++- .../api/models/channel_data/channel_data.py | 27 +++++++++- .../api/models/channel_data/feedback_loop.py | 15 ++++++ packages/api/tests/unit/test_activity.py | 12 ++++- .../apps/routing/activity_route_configs.py | 12 +++++ .../apps/routing/generated_handlers.py | 34 +++++++++++++ 10 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 packages/api/src/microsoft_teams/api/activities/invoke/message/fetch_task.py create mode 100644 packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/__init__.py b/packages/api/src/microsoft_teams/api/activities/invoke/__init__.py index 4b647d98..14a87969 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/__init__.py @@ -14,7 +14,13 @@ from .execute_action import ExecuteActionInvokeActivity from .file_consent import FileConsentInvokeActivity from .handoff_action import HandoffActionInvokeActivity -from .message import MessageSubmitActionInvokeActivity +from .message import ( + MessageFetchTaskActionValue, + MessageFetchTaskData, + MessageFetchTaskInvokeActivity, + MessageFetchTaskInvokeValue, + MessageSubmitActionInvokeActivity, +) from .message_extension import * # noqa: F403 from .message_extension import MessageExtensionInvokeActivity from .sign_in import * # noqa: F403 @@ -32,6 +38,7 @@ ConfigInvokeActivity, TabInvokeActivity, TaskInvokeActivity, + MessageFetchTaskInvokeActivity, MessageSubmitActionInvokeActivity, HandoffActionInvokeActivity, SignInInvokeActivity, @@ -48,6 +55,10 @@ "ConfigInvokeActivity", "TabInvokeActivity", "TaskInvokeActivity", + "MessageFetchTaskActionValue", + "MessageFetchTaskData", + "MessageFetchTaskInvokeActivity", + "MessageFetchTaskInvokeValue", "MessageSubmitActionInvokeActivity", "HandoffActionInvokeActivity", "SignInInvokeActivity", diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message/__init__.py b/packages/api/src/microsoft_teams/api/activities/invoke/message/__init__.py index a2bbc3e3..53c1d3c5 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message/__init__.py @@ -3,6 +3,18 @@ Licensed under the MIT License. """ +from .fetch_task import ( + MessageFetchTaskActionValue, + MessageFetchTaskData, + MessageFetchTaskInvokeActivity, + MessageFetchTaskInvokeValue, +) from .submit_action import MessageSubmitActionInvokeActivity -__all__ = ["MessageSubmitActionInvokeActivity"] +__all__ = [ + "MessageFetchTaskActionValue", + "MessageFetchTaskData", + "MessageFetchTaskInvokeActivity", + "MessageFetchTaskInvokeValue", + "MessageSubmitActionInvokeActivity", +] diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message/fetch_task.py b/packages/api/src/microsoft_teams/api/activities/invoke/message/fetch_task.py new file mode 100644 index 00000000..52b8596c --- /dev/null +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message/fetch_task.py @@ -0,0 +1,49 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Literal + +from ....models import CustomBaseModel +from ...invoke_activity import InvokeActivity + + +class MessageFetchTaskActionValue(CustomBaseModel): + """The nested action value containing the user's reaction.""" + + reaction: Literal["like", "dislike"] + """The feedback button the user clicked.""" + + +class MessageFetchTaskData(CustomBaseModel): + """The data payload nested inside the fetch task value.""" + + action_name: Literal["feedback"] = "feedback" + """The name of the action.""" + + action_value: MessageFetchTaskActionValue + """Contains the user's reaction.""" + + +class MessageFetchTaskInvokeValue(CustomBaseModel): + """ + Represents the value associated with a message fetch task. + """ + + data: MessageFetchTaskData + """The data payload containing action name and value.""" + + +class MessageFetchTaskInvokeActivity(InvokeActivity): + """ + Represents an activity sent when a message has a custom feedback loop + and the user clicks a feedback button. + The bot should respond with a task module (dialog) to collect feedback. + """ + + name: Literal["message/fetchTask"] = "message/fetchTask" + """The name of the operation associated with an invoke or event activity.""" + + value: MessageFetchTaskInvokeValue + """The value associated with the activity.""" diff --git a/packages/api/src/microsoft_teams/api/models/activity.py b/packages/api/src/microsoft_teams/api/models/activity.py index 68db57bd..cf957a01 100644 --- a/packages/api/src/microsoft_teams/api/models/activity.py +++ b/packages/api/src/microsoft_teams/api/models/activity.py @@ -5,10 +5,10 @@ import warnings from datetime import datetime -from typing import Any, List, Optional, Self +from typing import Any, List, Literal, Optional, Self from microsoft_teams.api.models.account import Account, ConversationAccount -from microsoft_teams.api.models.channel_data.channel_data import ChannelData +from microsoft_teams.api.models.channel_data.channel_data import ChannelData, FeedbackLoop from microsoft_teams.api.models.channel_data.channel_info import ChannelInfo from microsoft_teams.api.models.channel_data.notification_info import NotificationInfo from microsoft_teams.api.models.channel_data.team_info import TeamInfo @@ -229,12 +229,19 @@ def add_ai_generated(self) -> Self: return self - def add_feedback(self) -> Self: - """Enable message feedback.""" + def add_feedback(self, mode: Literal["default", "custom"] = "default") -> Self: + """ + Enable message feedback. + + Args: + mode: "default" shows Teams' built-in thumbs up/down UI. + "custom" triggers a message/fetchTask invoke so the bot + can return its own task module dialog. + """ if not self.channel_data: - self.channel_data = ChannelData(feedback_loop_enabled=True) - else: - self.channel_data.feedback_loop_enabled = True + self.channel_data = ChannelData() + self.channel_data.feedback_loop = FeedbackLoop(type=mode) + self.channel_data.feedback_loop_enabled = None return self def add_citation(self, position: int, appearance: CitationAppearance) -> Self: diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/__init__.py b/packages/api/src/microsoft_teams/api/models/channel_data/__init__.py index dd960c93..3f808b27 100644 --- a/packages/api/src/microsoft_teams/api/models/channel_data/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/channel_data/__init__.py @@ -5,9 +5,18 @@ from .channel_data import ChannelData from .channel_info import ChannelInfo +from .feedback_loop import FeedbackLoop from .notification_info import NotificationInfo from .settings import ChannelDataSettings from .team_info import TeamInfo from .tenant_info import TenantInfo -__all__ = ["ChannelInfo", "NotificationInfo", "ChannelDataSettings", "TeamInfo", "TenantInfo", "ChannelData"] +__all__ = [ + "ChannelInfo", + "NotificationInfo", + "ChannelDataSettings", + "TeamInfo", + "TenantInfo", + "ChannelData", + "FeedbackLoop", +] diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py b/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py index 216d56e0..8d3618b5 100644 --- a/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py +++ b/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py @@ -3,11 +3,14 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal, Optional, Self + +from pydantic import model_validator from ..custom_base_model import CustomBaseModel from ..meetings import MeetingInfo from .channel_info import ChannelInfo +from .feedback_loop import FeedbackLoop from .notification_info import NotificationInfo from .settings import ChannelDataSettings from .team_info import TeamInfo @@ -41,7 +44,27 @@ class ChannelData(CustomBaseModel): "Information about the settings in which the message was sent." feedback_loop_enabled: Optional[bool] = None - "Whether or not the feedback loop feature is enabled." + """ + Legacy feedback loop flag. Setting this to True is equivalent to feedback_loop=FeedbackLoop(type="default"). + Recommended to use feedback_loop directly. This field is normalized on model creation. + """ + + feedback_loop: Optional[FeedbackLoop] = None + """ + Feedback loop configuration. Set type to 'custom' to show a task module dialog. + Set to 'default' otherwise for standard feedback handling. + """ + + @model_validator(mode="after") + def normalize_feedback(self) -> Self: + """ + Normalize the feedback loop configuration. + This is necessary as the client only accepts either/or. + """ + if self.feedback_loop_enabled is True and self.feedback_loop is None: + self.feedback_loop = FeedbackLoop(type="default") + self.feedback_loop_enabled = None + return self stream_id: Optional[str] = None "ID of the stream. Assigned after the initial update is sent." diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py b/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py new file mode 100644 index 00000000..49f2173b --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py @@ -0,0 +1,15 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Literal + +from ..custom_base_model import CustomBaseModel + + +class FeedbackLoop(CustomBaseModel): + """Configuration for a custom feedback loop on a message.""" + + type: Literal["custom", "default"] = "custom" + """The type of feedback loop. Use "custom" to show a task module dialog.""" diff --git a/packages/api/tests/unit/test_activity.py b/packages/api/tests/unit/test_activity.py index 0d643f0a..675a33a7 100644 --- a/packages/api/tests/unit/test_activity.py +++ b/packages/api/tests/unit/test_activity.py @@ -124,7 +124,17 @@ def test_should_add_feedback_label(self, test_activity: ConcreteTestActivity) -> activity = test_activity.add_feedback() assert activity.type == "test" - assert activity.channel_data and activity.channel_data.feedback_loop_enabled is True + assert activity.channel_data and activity.channel_data.feedback_loop is not None + assert activity.channel_data.feedback_loop.type == "default" + assert activity.channel_data.feedback_loop_enabled is None + + def test_should_add_custom_feedback_label(self, test_activity: ConcreteTestActivity) -> None: + activity = test_activity.add_feedback(mode="custom") + + assert activity.type == "test" + assert activity.channel_data and activity.channel_data.feedback_loop is not None + assert activity.channel_data.feedback_loop.type == "custom" + assert activity.channel_data.feedback_loop_enabled is None def test_should_add_citation(self, test_activity: ConcreteTestActivity) -> None: activity = test_activity.add_citation(0, CitationAppearance(abstract="test", name="test")) diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py b/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py index fe13c3c4..cb9f9362 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py @@ -37,6 +37,7 @@ MessageExtensionSelectItemInvokeActivity, MessageExtensionSettingInvokeActivity, MessageExtensionSubmitActionInvokeActivity, + MessageFetchTaskInvokeActivity, MessageReactionActivity, MessageSubmitActionInvokeActivity, MessageUpdateActivity, @@ -49,6 +50,7 @@ TabFetchInvokeActivity, TabInvokeResponse, TabSubmitInvokeActivity, + TaskModuleInvokeResponse, TraceActivity, TypingActivity, UninstalledActivity, @@ -451,6 +453,16 @@ class ActivityConfig: output_type_name="TabInvokeResponse", is_invoke=True, ), + "message.fetch-task": ActivityConfig( + name="message.fetch-task", + method_name="on_message_fetch_task", + input_model=MessageFetchTaskInvokeActivity, + selector=lambda activity: activity.type == "invoke" + and cast(InvokeActivity, activity).name == "message/fetchTask", + output_model=TaskModuleInvokeResponse, + output_type_name="TaskModuleInvokeResponse", + is_invoke=True, + ), "message.submit": ActivityConfig( name="message.submit", method_name="on_message_submit", diff --git a/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py b/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py index 803fc8aa..b6b17977 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py +++ b/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py @@ -43,6 +43,7 @@ MessageExtensionSelectItemInvokeActivity, MessageExtensionSettingInvokeActivity, MessageExtensionSubmitActionInvokeActivity, + MessageFetchTaskInvokeActivity, MessageReactionActivity, MessageSubmitActionInvokeActivity, MessageUpdateActivity, @@ -62,6 +63,7 @@ MessagingExtensionActionInvokeResponse, MessagingExtensionInvokeResponse, TabInvokeResponse, + TaskModuleInvokeResponse, TokenExchangeInvokeResponseType, ) @@ -1251,6 +1253,38 @@ def decorator( return decorator(handler) return decorator + @overload + def on_message_fetch_task( + self, handler: InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse] + ) -> InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]: ... + + @overload + def on_message_fetch_task( + self, + ) -> Callable[ + [InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]], + InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse], + ]: ... + + def on_message_fetch_task( + self, handler: Optional[InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]] = None + ) -> InvokeHandlerUnion[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]: + """Register a message.fetch-task activity handler.""" + + def decorator( + func: InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse], + ) -> InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]: + validate_handler_type( + func, MessageFetchTaskInvokeActivity, "on_message_fetch_task", "MessageFetchTaskInvokeActivity" + ) + config = ACTIVITY_ROUTES["message.fetch-task"] + self.router.add_handler(config.selector, func) + return func + + if handler is not None: + return decorator(handler) + return decorator + @overload def on_message_submit( self, handler: VoidInvokeHandler[MessageSubmitActionInvokeActivity] From cbe60f29122b0d2f5dae732f30264865cf0c16f3 Mon Sep 17 00:00:00 2001 From: Lily Du Date: Thu, 2 Apr 2026 15:39:37 -0700 Subject: [PATCH 3/6] Apply suggestion from @Copilot - default should be the norm Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../microsoft_teams/api/models/channel_data/feedback_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py b/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py index 49f2173b..a26e048e 100644 --- a/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py +++ b/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py @@ -11,5 +11,5 @@ class FeedbackLoop(CustomBaseModel): """Configuration for a custom feedback loop on a message.""" - type: Literal["custom", "default"] = "custom" - """The type of feedback loop. Use "custom" to show a task module dialog.""" + type: Literal["custom", "default"] = "default" + """The type of feedback loop. Use "custom" to show a task module dialog; defaults to "default"."""" From 78161f709bcb7b4915e96a388afb9e2d554c5b50 Mon Sep 17 00:00:00 2001 From: Lily Du Date: Thu, 2 Apr 2026 15:43:41 -0700 Subject: [PATCH 4/6] Apply suggestion from @Copilot - stronger cond Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../microsoft_teams/api/models/channel_data/channel_data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py b/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py index 8d3618b5..171ad49c 100644 --- a/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py +++ b/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py @@ -61,7 +61,9 @@ def normalize_feedback(self) -> Self: Normalize the feedback loop configuration. This is necessary as the client only accepts either/or. """ - if self.feedback_loop_enabled is True and self.feedback_loop is None: + if self.feedback_loop is not None: + self.feedback_loop_enabled = None + elif self.feedback_loop_enabled is True: self.feedback_loop = FeedbackLoop(type="default") self.feedback_loop_enabled = None return self From ceb48d75aaa8583c77bf982b7ed13f37f1e607d0 Mon Sep 17 00:00:00 2001 From: lilydu Date: Thu, 2 Apr 2026 15:47:31 -0700 Subject: [PATCH 5/6] add test and fix lint --- .../api/models/channel_data/feedback_loop.py | 2 +- packages/apps/tests/test_feedback_routing.py | 98 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/apps/tests/test_feedback_routing.py diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py b/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py index 49f2173b..ba859f7c 100644 --- a/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py +++ b/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py @@ -12,4 +12,4 @@ class FeedbackLoop(CustomBaseModel): """Configuration for a custom feedback loop on a message.""" type: Literal["custom", "default"] = "custom" - """The type of feedback loop. Use "custom" to show a task module dialog.""" + """The type of feedback loop. Use 'custom' to show a task module dialog.""" diff --git a/packages/apps/tests/test_feedback_routing.py b/packages/apps/tests/test_feedback_routing.py new file mode 100644 index 00000000..ccaedbfa --- /dev/null +++ b/packages/apps/tests/test_feedback_routing.py @@ -0,0 +1,98 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +# pyright: basic + +from unittest.mock import MagicMock + +import pytest +from microsoft_teams.api import ( + Account, + ConversationAccount, + MessageFetchTaskActionValue, + MessageFetchTaskData, + MessageFetchTaskInvokeActivity, + MessageFetchTaskInvokeValue, + TaskFetchInvokeActivity, + TaskModuleMessageResponse, + TaskModuleRequest, + TaskModuleResponse, +) +from microsoft_teams.apps import ActivityContext, App + + +class TestFeedbackRouting: + """Test cases for custom feedback routing functionality.""" + + @pytest.fixture + def app(self): + return App(storage=MagicMock(), client_id="test-client-id", client_secret="test-secret") + + @pytest.fixture + def fetch_task_activity(self): + return MessageFetchTaskInvokeActivity( + id="activity-1", + type="invoke", + name="message/fetchTask", + from_=Account(id="user-1", name="User"), + recipient=Account(id="bot-1", name="Bot"), + conversation=ConversationAccount(id="conv-1", conversation_type="personal"), + channel_id="msteams", + value=MessageFetchTaskInvokeValue( + data=MessageFetchTaskData(action_value=MessageFetchTaskActionValue(reaction="like")) + ), + ) + + def test_on_message_fetch_task_registers_handler( + self, app: App, fetch_task_activity: MessageFetchTaskInvokeActivity + ) -> None: + @app.on_message_fetch_task + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="feedback form")) + + handlers = app.router.select_handlers(fetch_task_activity) + assert len(handlers) == 1 + assert handlers[0] == handler + + def test_on_message_fetch_task_does_not_match_other_invokes(self, app: App) -> None: + @app.on_message_fetch_task + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="feedback form")) + + other_activity = TaskFetchInvokeActivity( + id="activity-2", + type="invoke", + name="task/fetch", + from_=Account(id="user-1", name="User"), + recipient=Account(id="bot-1", name="Bot"), + conversation=ConversationAccount(id="conv-1", conversation_type="personal"), + channel_id="msteams", + value=TaskModuleRequest(data={}), + ) + + handlers = app.router.select_handlers(other_activity) + assert len(handlers) == 0 + + def test_on_message_fetch_task_reaction_dislike(self, app: App) -> None: + @app.on_message_fetch_task + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="feedback form")) + + dislike_activity = MessageFetchTaskInvokeActivity( + id="activity-3", + type="invoke", + name="message/fetchTask", + from_=Account(id="user-1", name="User"), + recipient=Account(id="bot-1", name="Bot"), + conversation=ConversationAccount(id="conv-1", conversation_type="personal"), + channel_id="msteams", + value=MessageFetchTaskInvokeValue( + data=MessageFetchTaskData(action_value=MessageFetchTaskActionValue(reaction="dislike")) + ), + ) + + handlers = app.router.select_handlers(dislike_activity) + assert len(handlers) == 1 + assert handlers[0] == handler From d01e13feb54f11b56c14708195a379705d0b1378 Mon Sep 17 00:00:00 2001 From: lilydu Date: Fri, 3 Apr 2026 14:23:19 -0700 Subject: [PATCH 6/6] fix test --- packages/apps/tests/test_feedback_routing.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/apps/tests/test_feedback_routing.py b/packages/apps/tests/test_feedback_routing.py index ccaedbfa..c691f763 100644 --- a/packages/apps/tests/test_feedback_routing.py +++ b/packages/apps/tests/test_feedback_routing.py @@ -16,9 +16,9 @@ MessageFetchTaskInvokeActivity, MessageFetchTaskInvokeValue, TaskFetchInvokeActivity, + TaskModuleInvokeResponse, TaskModuleMessageResponse, TaskModuleRequest, - TaskModuleResponse, ) from microsoft_teams.apps import ActivityContext, App @@ -49,8 +49,8 @@ def test_on_message_fetch_task_registers_handler( self, app: App, fetch_task_activity: MessageFetchTaskInvokeActivity ) -> None: @app.on_message_fetch_task - async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleResponse: - return TaskModuleResponse(task=TaskModuleMessageResponse(value="feedback form")) + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleInvokeResponse: + return TaskModuleInvokeResponse(task=TaskModuleMessageResponse(value="feedback form")) handlers = app.router.select_handlers(fetch_task_activity) assert len(handlers) == 1 @@ -58,8 +58,8 @@ async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskM def test_on_message_fetch_task_does_not_match_other_invokes(self, app: App) -> None: @app.on_message_fetch_task - async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleResponse: - return TaskModuleResponse(task=TaskModuleMessageResponse(value="feedback form")) + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleInvokeResponse: + return TaskModuleInvokeResponse(task=TaskModuleMessageResponse(value="feedback form")) other_activity = TaskFetchInvokeActivity( id="activity-2", @@ -77,8 +77,8 @@ async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskM def test_on_message_fetch_task_reaction_dislike(self, app: App) -> None: @app.on_message_fetch_task - async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleResponse: - return TaskModuleResponse(task=TaskModuleMessageResponse(value="feedback form")) + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleInvokeResponse: + return TaskModuleInvokeResponse(task=TaskModuleMessageResponse(value="feedback form")) dislike_activity = MessageFetchTaskInvokeActivity( id="activity-3",