Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

883 lines
34 KiB
Python

"""
crossapp_work task correctness tests.
This suite has no dedicated app accessor (`bench_env/task/crossapp_work/app.py`),
so we cover:
1. task definition validation
2. cross-app metadata sanity checks from bench_env/docs/task/TASK_CODE_SPEC.md
3. offline judge positive/negative matrix
4. extra branch cases for conditional tasks
"""
from __future__ import annotations
import copy
import datetime
import importlib
import inspect
import json
import re
from pathlib import Path
from typing import Any
import pytest
from bench_env.task.base import BaseTask
from bench_env.task.calendar.app import Calendar
from bench_env.task.clock.app import Clock
from bench_env.task.crossapp_work import tasks as _tasks_module
from bench_env.task.judge import JudgeInput
from bench_env.task.map.app import Map
from bench_env.task.sms.app import Sms
from bench_env.task.tencent_meeting.app import TencentMeeting
from bench_env.task.utils import parse_duration_to_minutes, sim_today
from bench_env.task.weather.app import Weather
from bench_env.task.wechat.app import Wechat
from bench_env.tests.conftest import make_judge_input
from bench_env.tests.calendar.test_tasks import BASE_STATE as CALENDAR_BASE_STATE, _add_event
from bench_env.tests.clock.test_tasks import DEFAULTS as CLOCK_BASE_STATE, _with_new_alarm
from bench_env.tests.map.test_tasks import BASE_STATE as MAP_BASE_STATE, _with_new_search, _state as _map_state
from bench_env.tests.notes.test_tasks import BASE_STATE as NOTES_BASE_STATE, _add_note
from bench_env.tests.sms.test_tasks import (
BASE_APP_STATE as SMS_APP_STATE,
BASE_STATE as SMS_PROVIDER_STATE,
_append_outgoing_message as _append_sms_outgoing,
)
from bench_env.tests.tencent_meeting.test_tasks import (
BASE_STATE as TM_BASE_STATE,
TEST_OS_STATE,
_make_active_meeting,
_make_new_scheduled_meeting,
)
from bench_env.tests.weather.test_tasks import BASE_STATE as WEATHER_BASE_STATE
ROOT = Path(__file__).resolve().parents[3]
DEFAULT_ROUTE = {"app": "launcher", "path": "/"}
def _load_json(*parts: str) -> dict[str, Any]:
path = ROOT.joinpath(*parts)
return json.loads(path.read_text(encoding="utf-8"))
WECHAT_BASE_STATE = _load_json("apps", "Wechat", "data", "defaults.json")
CONTACTS_PROVIDER_STATE = _load_json("os", "providers", "defaults", "contacts.json")
ALL_TASK_CLASSES: list[type[BaseTask]] = [
obj
for _, obj in inspect.getmembers(_tasks_module, inspect.isclass)
if issubclass(obj, BaseTask) and obj is not BaseTask and obj.__module__ == _tasks_module.__name__
]
ALL_TASK_IDS = [cls.__name__ for cls in ALL_TASK_CLASSES]
def _base_apps() -> dict[str, Any]:
return {
"calendar": copy.deepcopy(CALENDAR_BASE_STATE),
"clock": copy.deepcopy(CLOCK_BASE_STATE),
"map": copy.deepcopy(MAP_BASE_STATE),
"notes": copy.deepcopy(NOTES_BASE_STATE),
"sms": copy.deepcopy(SMS_APP_STATE),
"tencent_meeting": copy.deepcopy(TM_BASE_STATE),
"weather": copy.deepcopy(WEATHER_BASE_STATE),
"wechat": copy.deepcopy(WECHAT_BASE_STATE),
}
def _apps_state(**patches: dict[str, Any]) -> dict[str, Any]:
apps = _base_apps()
for key, value in patches.items():
apps[key] = copy.deepcopy(value)
return apps
def _base_os() -> dict[str, Any]:
return {
"time": copy.deepcopy(TEST_OS_STATE["time"]),
"providers": {
"contacts": copy.deepcopy(CONTACTS_PROVIDER_STATE),
"sms": copy.deepcopy(SMS_PROVIDER_STATE),
},
}
def _make_input(
init_apps: dict[str, Any],
curr_apps: dict[str, Any],
*,
init_os: dict[str, Any] | None = None,
curr_os: dict[str, Any] | None = None,
route: dict[str, Any] | None = None,
answer: str | None = None,
) -> JudgeInput:
return make_judge_input(
{"apps": init_apps, "os": init_os or _base_os()},
{"apps": curr_apps, "os": curr_os or _base_os()},
route=route or DEFAULT_ROUTE,
answer=answer,
)
def _ensure_wechat_chat(state: dict[str, Any], contact_name: str) -> dict[str, Any]:
wechat = Wechat(state)
wxid = wechat.require_contact_wxid(contact_name)
chat = next((item for item in state["chats"] if str(item["id"]) == wxid), None)
if chat is not None:
return chat
contact = wechat.contact_by_name(contact_name)
chat = {
"id": wxid,
"user": {
"wxid": wxid,
"name": contact["name"],
"avatar": contact.get("avatar", ""),
},
"isMuted": False,
"isSticky": False,
"isAlert": False,
"messages": [],
}
state["chats"].insert(0, chat)
return chat
def _append_wechat_outgoing(state: dict[str, Any], contact_name: str, content: str) -> None:
chat = _ensure_wechat_chat(state, contact_name)
chat["messages"].append(
{
"id": f"test_out_{len(chat['messages']) + 1}",
"type": "text",
"content": content,
"senderId": state["user"]["wxid"],
"timestamp": TEST_OS_STATE["time"]["timestamp"],
}
)
def _append_wechat_incoming(state: dict[str, Any], contact_name: str, content: str) -> None:
chat = _ensure_wechat_chat(state, contact_name)
chat["messages"].append(
{
"id": f"test_in_{len(chat['messages']) + 1}",
"type": "text",
"content": content,
"senderId": chat["user"]["wxid"],
"timestamp": TEST_OS_STATE["time"]["timestamp"],
}
)
def _map_search_state(query: str) -> dict[str, Any]:
results = Map.geo_search(query, limit=0)
if not results:
raise ValueError(f"No map search results for {query!r}")
return _with_new_search(_map_state(search_results=results, active_poi=results[0]), query)
def _tm_new_meeting(
*,
topic: str,
start_ms: int,
duration: int = 60,
meeting_id: str = "888 666 1234",
password: str | None = None,
invitees: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
meeting = _make_new_scheduled_meeting(
topic=topic,
duration=duration,
password=password,
invitees=invitees,
)
meeting["startTime"] = start_ms
meeting["createdAt"] = TEST_OS_STATE["time"]["timestamp"] + 1000
meeting["meetingId"] = meeting_id
return meeting
def _tm_with_added_meeting(state: dict[str, Any], meeting: dict[str, Any]) -> dict[str, Any]:
next_state = copy.deepcopy(state)
next_state["scheduledMeetings"] = [*next_state["scheduledMeetings"], copy.deepcopy(meeting)]
return next_state
def _tm_without_topic(state: dict[str, Any], topic: str) -> dict[str, Any]:
next_state = copy.deepcopy(state)
next_state["scheduledMeetings"] = [
item for item in next_state["scheduledMeetings"] if str(item["title"]) != topic
]
return next_state
def _tm_with_active_meeting(
state: dict[str, Any],
*,
host_name: str,
topic: str,
self_name: str,
) -> dict[str, Any]:
next_state = copy.deepcopy(state)
ongoing = TencentMeeting(next_state).find_ongoing_entry(host_name, topic)
next_state["activeMeeting"] = _make_active_meeting(
title=topic,
host_id=str(ongoing["hostId"]),
host_name=host_name,
meeting_id=str(ongoing["meetingId"]),
user_name=self_name,
)
return next_state
def _inject_sms_incoming(
provider_state: dict[str, Any],
sender: str,
*,
content: str,
is_unread: bool = True,
) -> dict[str, Any]:
return Sms(copy.deepcopy(provider_state)).prepare_state_with_incoming_message(
sender,
content,
message_id=f"test_in_{sender.encode('utf-8').hex()[:16]}",
timestamp="18:00",
is_unread=is_unread,
)
def _meeting_date_and_time(start_ms: int) -> tuple[str, str]:
dt = datetime.datetime.fromtimestamp(start_ms / 1000.0)
return dt.date().isoformat(), dt.strftime("%H:%M")
class TestTaskDefinitions:
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_instantiation(self, cls):
task = cls()
assert task.name == cls.__name__
assert task.templates
assert len(task.apps) >= 2
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_description_renders(self, cls):
task = cls()
task._env_state = {"os": _base_os()}
desc = task.description
assert desc
assert "{" not in desc
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_required_class_attrs(self, cls):
assert cls.scope in ("S1", "S2", "S3")
assert cls.objective in ("operate", "query", "hybrid")
assert cls.composition in ("atomic", "sequential", "transfer", "deep_dive")
assert cls.difficulty in ("L1", "L2", "L3", "L4")
assert isinstance(cls.capabilities, list) and cls.capabilities
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_parameter_defaults_present(self, cls):
for key, schema in cls.parameters.items():
if key.startswith("_"):
continue
assert "default" in schema
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_expected_changes_use_scoped_paths(self, cls):
for path in getattr(cls, "expected_changes", []):
if path.startswith(("apps.", "os.")):
continue
assert "." in path, f"{cls.__name__}: cross-app expected_changes 必须使用 app.path 形式,收到 {path!r}"
class TestWechatInjectionHelpers:
def test_prepare_state_with_incoming_text_creates_standard_message(self):
state = copy.deepcopy(WECHAT_BASE_STATE)
wechat = Wechat(state)
updated = wechat.prepare_state_with_incoming_text(
"陈静",
"周末去中国国家博物馆怎么样?",
message_id="crossapp_life_place_中国国家博物馆",
timestamp=TEST_OS_STATE["time"]["timestamp"],
)
updated_wechat = Wechat(updated)
assert updated_wechat.last_received_text_from("陈静") == "周末去中国国家博物馆怎么样?"
chat = updated_wechat.get_chat_with("陈静")
assert chat is not None
assert chat["messages"][-1]["id"] == "crossapp_life_place_中国国家博物馆"
class TestSmsInjectionHelpers:
def test_prepare_state_with_incoming_message_creates_contact_conversation(self):
state = copy.deepcopy(SMS_PROVIDER_STATE)
updated = Sms(state).prepare_state_with_incoming_message(
"张三",
"我是张三,明早九点前回我。",
message_id="crossapp_work_sms_contact_zhangsan",
timestamp="18:00",
is_unread=True,
)
sms = Sms(updated)
conv = sms.conversation_by_sender("张三")
assert conv["isUnread"] is True
assert sms.latest_incoming_content_from("张三") == "我是张三,明早九点前回我。"
class TestCrossAppAccessorHelpers:
def test_tencent_meeting_any_new_scheduled_meeting(self):
start_ms = Calendar.tomorrow_timestamp_ms_at_hh_mm(_base_os(), "15:00")
curr_tm = _tm_with_added_meeting(
TM_BASE_STATE,
_tm_new_meeting(topic="临时同步会", start_ms=start_ms, meeting_id="777 888 999"),
)
tm = TencentMeeting(curr_tm, init=TM_BASE_STATE)
meeting = tm.any_new_scheduled_meeting()
assert meeting is not None
assert meeting["title"] == "临时同步会"
def test_tencent_meeting_check_no_new_scheduled_meeting(self):
tm = TencentMeeting(copy.deepcopy(TM_BASE_STATE), init=copy.deepcopy(TM_BASE_STATE))
result = tm.check_no_new_scheduled_meeting("临时协调会", field="busy_no_meeting")
assert result["passed"] is True
def test_calendar_check_no_event_created(self):
calendar = Calendar(copy.deepcopy(CALENDAR_BASE_STATE), init=copy.deepcopy(CALENDAR_BASE_STATE))
result = calendar.check_no_event_created("临时协调会", field="busy_no_calendar_event")
assert result["passed"] is True
def test_clock_check_no_new_alarm_at(self):
clock = Clock(copy.deepcopy(CLOCK_BASE_STATE), init=copy.deepcopy(CLOCK_BASE_STATE))
result = clock.check_no_new_alarm_at(3, 15, field="busy_no_alarm")
assert result["passed"] is True
def test_sms_check_no_new_sent_to(self):
sms = Sms(copy.deepcopy(SMS_PROVIDER_STATE), init=copy.deepcopy(SMS_PROVIDER_STATE))
result = sms.check_no_new_sent_to("张三", field="busy_no_sms")
assert result["passed"] is True
class _FakeEnv:
def __init__(self, state: dict[str, Any]):
self.state = copy.deepcopy(state)
async def get_state(self) -> dict[str, Any]:
return copy.deepcopy(self.state)
async def set_state(self, patch: dict[str, Any], *, deep: bool, reload: bool) -> None:
assert deep is True
assert reload is False
self.state["os"]["providers"]["sms"] = copy.deepcopy(patch["os"]["providers"]["sms"])
def _existing_meeting_to_calendar_positive():
task = _tasks_module.ExistingMeetingToCalendar(topic="项目例会")
meeting = TencentMeeting(copy.deepcopy(TM_BASE_STATE)).find_scheduled_meeting(task.p.topic)
date_value, time_value = _meeting_date_and_time(TencentMeeting.meeting_start_timestamp_ms(meeting))
curr_calendar = _add_event(
CALENDAR_BASE_STATE,
title=task.p.topic,
date_value=date_value,
start=time_value,
end="12:00",
)
return task, _make_input(_apps_state(), _apps_state(calendar=curr_calendar))
def _existing_meeting_to_calendar_negative():
task = _tasks_module.ExistingMeetingToCalendar(topic="项目例会")
curr_calendar = _add_event(
CALENDAR_BASE_STATE,
title=task.p.topic,
date_value="2026-03-16",
start="11:30",
end="12:30",
)
return task, _make_input(_apps_state(), _apps_state(calendar=curr_calendar))
def _calendar_earliest_to_alarm_positive():
task = _tasks_module.CalendarEarliestToAlarm()
tomorrow = (sim_today(_base_os()) + datetime.timedelta(days=1)).isoformat()
calendar_state = _add_event(CALENDAR_BASE_STATE, title="临时早会A", date_value=tomorrow, start="09:30", end="10:30")
calendar_state = _add_event(calendar_state, title="临时早会B", date_value=tomorrow, start="11:00", end="12:00")
clock_state = _with_new_alarm(CLOCK_BASE_STATE, alarm_id="test_earliest", hour=9, minute=0)
return task, _make_input(
_apps_state(calendar=calendar_state),
_apps_state(calendar=calendar_state, clock=clock_state),
)
def _calendar_earliest_to_alarm_negative():
task = _tasks_module.CalendarEarliestToAlarm()
tomorrow = (sim_today(_base_os()) + datetime.timedelta(days=1)).isoformat()
calendar_state = _add_event(CALENDAR_BASE_STATE, title="临时早会A", date_value=tomorrow, start="09:30", end="10:30")
calendar_state = _add_event(calendar_state, title="临时早会B", date_value=tomorrow, start="11:00", end="12:00")
clock_state = _with_new_alarm(CLOCK_BASE_STATE, alarm_id="test_earliest", hour=8, minute=45)
return task, _make_input(
_apps_state(calendar=calendar_state),
_apps_state(calendar=calendar_state, clock=clock_state),
)
def _meeting_longest_info_to_wechat_positive():
task = _tasks_module.MeetingLongestInfoToWechat(date="2026-02-03", contact="陈静")
longest = TencentMeeting(copy.deepcopy(TM_BASE_STATE)).longest_history_meeting_on_date(task.p.date)
mid = re.sub(r"\s+", "", str(longest["meetingId"]))
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"{task.p.date} 开得最久的是{longest['title']},会议号 {mid}")
return task, _make_input(_apps_state(), _apps_state(wechat=curr_wechat))
def _meeting_longest_info_to_wechat_negative():
task = _tasks_module.MeetingLongestInfoToWechat(date="2026-02-03", contact="陈静")
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, "2026-02-03 开得最久的是长时间研讨会,会议号 111222333")
return task, _make_input(_apps_state(), _apps_state(wechat=curr_wechat))
def _meeting_duration_to_wechat_positive():
task = _tasks_module.MeetingDurationToWechat(date="2026-02-03", contact="陈静")
total = TencentMeeting(copy.deepcopy(TM_BASE_STATE)).total_participation_minutes_on_date(task.p.date)
expected = TencentMeeting.format_duration_minutes_zh(total)
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"{task.p.date} 这天我一共开了 {expected}")
return task, _make_input(_apps_state(), _apps_state(wechat=curr_wechat))
def _meeting_duration_to_wechat_negative():
task = _tasks_module.MeetingDurationToWechat(date="2026-02-03", contact="陈静")
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"{task.p.date} 这天我一共开了 4小时8分")
return task, _make_input(_apps_state(), _apps_state(wechat=curr_wechat))
def _weather_conditional_cancel_meeting_positive():
task = _tasks_module.WeatherConditionalCancelMeeting(city="北京", topic="项目例会")
meeting = TencentMeeting(copy.deepcopy(TM_BASE_STATE)).find_scheduled_meeting(task.p.topic)
start_ms = TencentMeeting.meeting_start_timestamp_ms(meeting)
alarm_dt = datetime.datetime.fromtimestamp(start_ms / 1000.0) - datetime.timedelta(minutes=30)
curr_clock = _with_new_alarm(
CLOCK_BASE_STATE,
alarm_id="weather_keep_alarm",
hour=alarm_dt.hour,
minute=alarm_dt.minute,
)
return task, _make_input(_apps_state(), _apps_state(clock=curr_clock))
def _weather_conditional_cancel_meeting_negative():
task = _tasks_module.WeatherConditionalCancelMeeting(city="北京", topic="项目例会")
curr_tm = _tm_without_topic(TM_BASE_STATE, task.p.topic)
return task, _make_input(_apps_state(), _apps_state(tencent_meeting=curr_tm))
def _meeting_join_and_notify_sms_positive():
task = _tasks_module.MeetingJoinAndNotifySms(
host_name="老王",
topic="老王的快速会议",
name="访客小王",
contact="张三",
)
curr_tm = _tm_with_active_meeting(TM_BASE_STATE, host_name=task.p.host_name, topic=task.p.topic, self_name=task.p.name)
curr_os = _base_os()
_append_sms_outgoing(curr_os["providers"]["sms"], task.p.contact, "我已经入会了")
return task, _make_input(_apps_state(), _apps_state(tencent_meeting=curr_tm), curr_os=curr_os)
def _meeting_join_and_notify_sms_negative():
task = _tasks_module.MeetingJoinAndNotifySms(
host_name="老王",
topic="老王的快速会议",
name="访客小王",
contact="张三",
)
curr_tm = _tm_with_active_meeting(TM_BASE_STATE, host_name=task.p.host_name, topic=task.p.topic, self_name="错误昵称")
curr_os = _base_os()
_append_sms_outgoing(curr_os["providers"]["sms"], task.p.contact, "我已经入会了")
return task, _make_input(_apps_state(), _apps_state(tencent_meeting=curr_tm), curr_os=curr_os)
def _meeting_multi_channel_notify_positive():
task = _tasks_module.MeetingMultiChannelNotify(contact1="陈静", contact2="张三")
meeting = _tm_new_meeting(
topic="临时同步会",
start_ms=Calendar.tomorrow_timestamp_ms_at_hh_mm(_base_os(), "15:00"),
meeting_id="777 888 999",
)
curr_tm = _tm_with_added_meeting(TM_BASE_STATE, meeting)
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact1, f"会议号 {meeting['meetingId']}")
curr_os = _base_os()
_append_sms_outgoing(curr_os["providers"]["sms"], task.p.contact2, f"会议号 {meeting['meetingId']}")
return task, _make_input(
_apps_state(),
_apps_state(tencent_meeting=curr_tm, wechat=curr_wechat),
curr_os=curr_os,
)
def _meeting_multi_channel_notify_negative():
task = _tasks_module.MeetingMultiChannelNotify(contact1="陈静", contact2="张三")
meeting = _tm_new_meeting(
topic="临时同步会",
start_ms=Calendar.tomorrow_timestamp_ms_at_hh_mm(_base_os(), "15:00"),
meeting_id="777 888 999",
)
curr_tm = _tm_with_added_meeting(TM_BASE_STATE, meeting)
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact1, f"会议号 {meeting['meetingId']}")
curr_os = _base_os()
_append_sms_outgoing(curr_os["providers"]["sms"], task.p.contact2, "会议号 111 222 333")
return task, _make_input(
_apps_state(),
_apps_state(tencent_meeting=curr_tm, wechat=curr_wechat),
curr_os=curr_os,
)
def _meeting_route_eta_to_wechat_positive():
task = _tasks_module.MeetingRouteEtaToWechat(place="中国国家博物馆", contact="陈静")
route = Map.geo_route_to(task.p.place, "WALKING")
duration = str(route["duration"])
time_str = TencentMeeting(copy.deepcopy(TM_BASE_STATE)).meeting_start_hh_mm(
TencentMeeting(copy.deepcopy(TM_BASE_STATE)).first_upcoming_scheduled(TEST_OS_STATE["time"]["timestamp"])
)
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"走到{task.p.place}大概{duration},会议 {time_str} 开始")
curr_map = _map_search_state(task.p.place)
return task, _make_input(_apps_state(), _apps_state(map=curr_map, wechat=curr_wechat))
def _meeting_route_eta_to_wechat_negative():
task = _tasks_module.MeetingRouteEtaToWechat(place="中国国家博物馆", contact="陈静")
route = Map.geo_route_to(task.p.place, "WALKING")
duration = str(route["duration"])
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"走到{task.p.place}大概{duration}")
curr_map = _map_search_state(task.p.place)
return task, _make_input(_apps_state(), _apps_state(map=curr_map, wechat=curr_wechat))
def _meeting_full_flow_to_wechat_positive():
task = _tasks_module.MeetingFullFlowToWechat(time="10:00", contact="陈静")
start_ms = Calendar.tomorrow_timestamp_ms_at_hh_mm(_base_os(), task.p.time)
date_value, time_value = _meeting_date_and_time(start_ms)
meeting = _tm_new_meeting(topic=task.title_meeting, start_ms=start_ms, meeting_id="321 654 987")
curr_tm = _tm_with_added_meeting(TM_BASE_STATE, meeting)
curr_calendar = _add_event(
CALENDAR_BASE_STATE,
title=task.title_meeting,
date_value=date_value,
start=time_value,
end="11:00",
reminder=15,
alarm=True,
)
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"项目周会会议号 {meeting['meetingId']}")
return task, _make_input(
_apps_state(),
_apps_state(tencent_meeting=curr_tm, calendar=curr_calendar, wechat=curr_wechat),
)
def _meeting_full_flow_to_wechat_negative():
task = _tasks_module.MeetingFullFlowToWechat(time="10:00", contact="陈静")
start_ms = Calendar.tomorrow_timestamp_ms_at_hh_mm(_base_os(), task.p.time)
date_value, time_value = _meeting_date_and_time(start_ms)
meeting = _tm_new_meeting(topic=task.title_meeting, start_ms=start_ms, meeting_id="321 654 987")
curr_tm = _tm_with_added_meeting(TM_BASE_STATE, meeting)
curr_calendar = _add_event(
CALENDAR_BASE_STATE,
title=task.title_meeting,
date_value=date_value,
start=time_value,
end="11:00",
reminder=5,
alarm=True,
)
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"项目周会会议号 {meeting['meetingId']}")
return task, _make_input(
_apps_state(),
_apps_state(tencent_meeting=curr_tm, calendar=curr_calendar, wechat=curr_wechat),
)
def _full_meeting_conflict_check_broadcast_positive():
task = _tasks_module.FullMeetingConflictCheckBroadcast(
time="03:30",
contact="陈静",
contact2="张三",
flow_topic="临时协调会",
)
start_ms = Calendar.tomorrow_timestamp_ms_at_hh_mm(_base_os(), task.p.time)
date_value, time_value = _meeting_date_and_time(start_ms)
meeting = _tm_new_meeting(topic=task.p.flow_topic, start_ms=start_ms, meeting_id="555 444 333")
curr_tm = _tm_with_added_meeting(TM_BASE_STATE, meeting)
curr_calendar = _add_event(
CALENDAR_BASE_STATE,
title=task.p.flow_topic,
date_value=date_value,
start=time_value,
end="04:30",
reminder=15,
alarm=True,
)
curr_clock = _with_new_alarm(CLOCK_BASE_STATE, alarm_id="prep", hour=3, minute=15)
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"会议号 {meeting['meetingId']}")
curr_os = _base_os()
_append_sms_outgoing(curr_os["providers"]["sms"], task.p.contact2, f"会议号 {meeting['meetingId']}")
return task, _make_input(
_apps_state(),
_apps_state(
calendar=curr_calendar,
clock=curr_clock,
tencent_meeting=curr_tm,
wechat=curr_wechat,
),
curr_os=curr_os,
)
def _full_meeting_conflict_check_broadcast_negative():
task = _tasks_module.FullMeetingConflictCheckBroadcast(
time="03:30",
contact="陈静",
contact2="张三",
flow_topic="临时协调会",
)
start_ms = Calendar.tomorrow_timestamp_ms_at_hh_mm(_base_os(), task.p.time)
date_value, time_value = _meeting_date_and_time(start_ms)
meeting = _tm_new_meeting(topic=task.p.flow_topic, start_ms=start_ms, meeting_id="555 444 333")
curr_tm = _tm_with_added_meeting(TM_BASE_STATE, meeting)
curr_calendar = _add_event(
CALENDAR_BASE_STATE,
title=task.p.flow_topic,
date_value=date_value,
start=time_value,
end="04:30",
reminder=15,
alarm=True,
)
curr_clock = _with_new_alarm(CLOCK_BASE_STATE, alarm_id="prep", hour=3, minute=15)
curr_wechat = copy.deepcopy(WECHAT_BASE_STATE)
_append_wechat_outgoing(curr_wechat, task.p.contact, f"会议号 {meeting['meetingId']}")
return task, _make_input(
_apps_state(),
_apps_state(
calendar=curr_calendar,
clock=curr_clock,
tencent_meeting=curr_tm,
wechat=curr_wechat,
),
)
def _meeting_reminder_to_notes_positive():
task = _tasks_module.MeetingReminderToNotes()
tm = TencentMeeting(copy.deepcopy(TM_BASE_STATE))
target_meetings, _ = tm.upcoming_or_ongoing()
curr_notes = copy.deepcopy(NOTES_BASE_STATE)
content = "\n".join(
f"{meeting['title']} {tm.parse_meeting_time(meeting['startTime']).strftime('%H:%M')}"
for meeting in target_meetings
)
_add_note(curr_notes, "会议提醒", content=content)
return task, _make_input(
_apps_state(),
_apps_state(notes=curr_notes),
)
def _meeting_reminder_to_notes_negative():
task = _tasks_module.MeetingReminderToNotes()
tm = TencentMeeting(copy.deepcopy(TM_BASE_STATE))
target_meetings, _ = tm.upcoming_or_ongoing()
curr_notes = copy.deepcopy(NOTES_BASE_STATE)
content = "\n".join(f"{meeting['title']} 00:00" for meeting in target_meetings)
_add_note(curr_notes, "会议提醒", content=content)
return task, _make_input(
_apps_state(),
_apps_state(notes=curr_notes),
)
def _sms_and_calendar_on_date_positive():
task = _tasks_module.SmsAndCalendarOnDate(
contact="张三",
message="明天见",
event_title="约会",
)
# tomorrow is derived from OS time: 2026-03-16 + 1 day = 2026-03-17
tomorrow = "2026-03-17"
curr_os = _base_os()
_append_sms_outgoing(curr_os["providers"]["sms"], task.p.contact, task.p.message)
curr_calendar = _add_event(
CALENDAR_BASE_STATE,
title=task.p.event_title,
date_value=tomorrow,
start="10:00",
end="11:00",
)
return task, _make_input(
_apps_state(),
_apps_state(calendar=curr_calendar),
curr_os=curr_os,
)
def _sms_and_calendar_on_date_negative():
task = _tasks_module.SmsAndCalendarOnDate(
contact="张三",
message="明天见",
event_title="约会",
)
# wrong date: not tomorrow (2026-03-18 instead of 2026-03-17)
curr_os = _base_os()
_append_sms_outgoing(curr_os["providers"]["sms"], task.p.contact, task.p.message)
curr_calendar = _add_event(
CALENDAR_BASE_STATE,
title=task.p.event_title,
date_value="2026-03-18",
start="10:00",
end="11:00",
)
return task, _make_input(
_apps_state(),
_apps_state(calendar=curr_calendar),
curr_os=curr_os,
)
PRIMARY_POSITIVE_CASES = [
("ExistingMeetingToCalendar", _existing_meeting_to_calendar_positive),
("CalendarEarliestToAlarm", _calendar_earliest_to_alarm_positive),
("MeetingLongestInfoToWechat", _meeting_longest_info_to_wechat_positive),
("MeetingDurationToWechat", _meeting_duration_to_wechat_positive),
("WeatherConditionalCancelMeeting", _weather_conditional_cancel_meeting_positive),
("MeetingJoinAndNotifySms", _meeting_join_and_notify_sms_positive),
("MeetingMultiChannelNotify", _meeting_multi_channel_notify_positive),
("MeetingRouteEtaToWechat", _meeting_route_eta_to_wechat_positive),
("MeetingFullFlowToWechat", _meeting_full_flow_to_wechat_positive),
("FullMeetingConflictCheckBroadcast", _full_meeting_conflict_check_broadcast_positive),
("MeetingReminderToNotes", _meeting_reminder_to_notes_positive),
("SmsAndCalendarOnDate", _sms_and_calendar_on_date_positive),
]
PRIMARY_NEGATIVE_CASES = [
("ExistingMeetingToCalendar", _existing_meeting_to_calendar_negative),
("CalendarEarliestToAlarm", _calendar_earliest_to_alarm_negative),
("MeetingLongestInfoToWechat", _meeting_longest_info_to_wechat_negative),
("MeetingDurationToWechat", _meeting_duration_to_wechat_negative),
("WeatherConditionalCancelMeeting", _weather_conditional_cancel_meeting_negative),
("MeetingJoinAndNotifySms", _meeting_join_and_notify_sms_negative),
("MeetingMultiChannelNotify", _meeting_multi_channel_notify_negative),
("MeetingRouteEtaToWechat", _meeting_route_eta_to_wechat_negative),
("MeetingFullFlowToWechat", _meeting_full_flow_to_wechat_negative),
("FullMeetingConflictCheckBroadcast", _full_meeting_conflict_check_broadcast_negative),
("MeetingReminderToNotes", _meeting_reminder_to_notes_negative),
("SmsAndCalendarOnDate", _sms_and_calendar_on_date_negative),
]
EXTRA_POSITIVE_CASES = [
(
"WeatherConditionalCancelMeeting__rainy_branch",
lambda: (
task := _tasks_module.WeatherConditionalCancelMeeting(city="广州", topic="项目例会"),
_make_input(
_apps_state(),
_apps_state(tencent_meeting=_tm_without_topic(TM_BASE_STATE, task.p.topic)),
),
),
),
(
"FullMeetingConflictCheckBroadcast__busy_branch",
lambda: (
task := _tasks_module.FullMeetingConflictCheckBroadcast(
time="03:30",
contact="陈静",
contact2="张三",
flow_topic="临时协调会",
),
(
lambda init_calendar: _make_input(
_apps_state(calendar=init_calendar),
_apps_state(
calendar=init_calendar,
wechat=(
lambda state: (
_append_wechat_outgoing(state, task.p.contact, "那个时间不行,日程有冲突") or state
)
)(copy.deepcopy(WECHAT_BASE_STATE)),
),
)
)(
_add_event(
CALENDAR_BASE_STATE,
title="占用的固定安排",
date_value=(sim_today(_base_os()) + datetime.timedelta(days=1)).isoformat(),
start="02:30",
end="05:30",
)
),
),
),
]
EXTRA_NEGATIVE_CASES = [
(
"FullMeetingConflictCheckBroadcast__busy_branch_missing_reject",
lambda: (
task := _tasks_module.FullMeetingConflictCheckBroadcast(
time="03:30",
contact="陈静",
contact2="张三",
flow_topic="临时协调会",
),
_make_input(
_apps_state(
calendar=_add_event(
CALENDAR_BASE_STATE,
title="占用的固定安排",
date_value=(sim_today(_base_os()) + datetime.timedelta(days=1)).isoformat(),
start="02:30",
end="05:30",
)
),
_apps_state(
calendar=_add_event(
CALENDAR_BASE_STATE,
title="占用的固定安排",
date_value=(sim_today(_base_os()) + datetime.timedelta(days=1)).isoformat(),
start="02:30",
end="05:30",
)
),
),
),
),
]
class TestTaskJudgeMatrixOffline:
@pytest.mark.parametrize(
"task_name,builder",
PRIMARY_POSITIVE_CASES + EXTRA_POSITIVE_CASES,
ids=[name for name, _ in PRIMARY_POSITIVE_CASES + EXTRA_POSITIVE_CASES],
)
def test_positive_case(self, task_name, builder):
task, inp = builder()
result = task.evaluate(inp)
assert result.success, f"{task_name} positive failed: issues={result.issues}, warnings={result.warnings}"
@pytest.mark.parametrize(
"task_name,builder",
PRIMARY_NEGATIVE_CASES + EXTRA_NEGATIVE_CASES,
ids=[name for name, _ in PRIMARY_NEGATIVE_CASES + EXTRA_NEGATIVE_CASES],
)
def test_negative_case(self, task_name, builder):
task, inp = builder()
result = task.evaluate(inp)
assert not result.success, f"{task_name} negative unexpectedly passed"