from copy import deepcopy from channel.feishu.feishu_scheduler_card import ( build_scheduler_card, handle_scheduler_action, tasks_for_receivers, ) class FakeTaskStore: def __init__(self, tasks): self.tasks = {task["id"]: deepcopy(task) for task in tasks} self.enable_calls = [] self.delete_calls = [] def list_tasks(self): return list(self.tasks.values()) def get_task(self, task_id): return self.tasks.get(task_id) def enable_task(self, task_id, enabled=True): self.enable_calls.append((task_id, enabled)) self.tasks[task_id]["enabled"] = enabled def delete_task(self, task_id): self.delete_calls.append(task_id) del self.tasks[task_id] def _task(task_id, receiver, enabled=True): return { "id": task_id, "name": "Daily report", "enabled": enabled, "schedule": {"type": "cron", "expression": "0 9 * * *"}, "next_run_at": "2026-07-18T09:00:00", "action": {"receiver": receiver, "channel_type": "feishu"}, } def test_scheduler_card_has_explicit_idempotent_actions(): card = build_scheduler_card([_task("task-1", "chat-1")]) assert card["schema"] == "2.0" assert card["config"]["enable_forward_interaction"] is False assert card["header"]["title"]["content"] == "Scheduled tasks" action_row = next( element for element in card["body"]["elements"] if element.get("tag") == "column_set" ) values = [column["elements"][0]["value"] for column in action_row["columns"]] assert values == [ { "cowagent": "scheduler", "action": "disable", "task_id": "task-1", "receiver": "chat-1", }, { "cowagent": "scheduler", "action": "delete", "task_id": "task-1", "receiver": "chat-1", }, ] def test_tasks_are_scoped_to_callback_chat_or_operator(): tasks = [ _task("group", "chat-1"), _task("private", "user-1"), _task("other", "chat-2"), ] visible = tasks_for_receivers(tasks, {"chat-1", "user-1"}) assert [task["id"] for task in visible] == ["group", "private"] def test_disable_action_is_idempotent_and_returns_refreshed_card(): store = FakeTaskStore([_task("task-1", "chat-1")]) value = {"cowagent": "scheduler", "action": "disable", "task_id": "task-1"} first = handle_scheduler_action(value, store, {"chat-1"}) second = handle_scheduler_action(value, store, {"chat-1"}) assert store.enable_calls == [("task-1", False), ("task-1", False)] assert first["toast"] == {"type": "success", "content": "Task disabled"} assert second["card"]["type"] == "raw" refreshed_columns = next( element for element in second["card"]["data"]["body"]["elements"] if element.get("tag") == "column_set" )["columns"] assert refreshed_columns[0]["elements"][0]["value"]["action"] == "enable" def test_action_rejects_task_from_another_receiver(): store = FakeTaskStore([_task("task-1", "chat-2")]) response = handle_scheduler_action( {"cowagent": "scheduler", "action": "delete", "task_id": "task-1"}, store, {"chat-1", "user-1"}, ) assert response["toast"] == {"type": "error", "content": "Task is not available in this chat"} assert store.delete_calls == [] def test_action_rejects_mismatched_receiver_embedded_in_card_value(): store = FakeTaskStore([_task("task-1", "user-1")]) response = handle_scheduler_action( { "cowagent": "scheduler", "action": "delete", "task_id": "task-1", "receiver": "chat-1", }, store, {"chat-1"}, ) assert response["toast"] == {"type": "error", "content": "Task is not available in this chat"} assert store.delete_calls == [] def test_delete_action_removes_owned_task(): store = FakeTaskStore([_task("task-1", "user-1")]) response = handle_scheduler_action( {"cowagent": "scheduler", "action": "delete", "task_id": "task-1"}, store, {"user-1"}, ) assert store.delete_calls == ["task-1"] assert response["toast"] == {"type": "success", "content": "Task deleted"} assert "No scheduled tasks" in response["card"]["data"]["body"]["elements"][0]["content"]