purewhiter--mobilegym
661 行
28 KiB
Python
661 行
28 KiB
Python
"""
|
|
crossapp_content task correctness tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import inspect
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
import pytest
|
|
|
|
from bench_env.task.base import BaseTask
|
|
from bench_env.task.bilibili.app import Bilibili
|
|
from bench_env.task.crossapp_content import tasks as _tasks_module
|
|
from bench_env.task.ebay.app import expect_top
|
|
from bench_env.task.judge import JudgeInput
|
|
from bench_env.task.notes.app import Notes
|
|
from bench_env.task.redbook.app import Redbook
|
|
from bench_env.task.reddit.app import Reddit
|
|
from bench_env.task.sms.app import Sms
|
|
from bench_env.task.spotify.app import Spotify
|
|
from bench_env.task.wechat.app import Wechat
|
|
from bench_env.task.wechat_reading.app import WechatReading
|
|
from bench_env.task.x.app import X
|
|
from bench_env.tests.conftest import make_judge_input
|
|
from bench_env.tests.bilibili.test_tasks import BASE_STATE as BILIBILI_BASE_STATE, _sanlian_video
|
|
from bench_env.tests.notes.test_tasks import BASE_STATE as NOTES_BASE_STATE, _add_note
|
|
from bench_env.tests.redbook.test_tasks import (
|
|
BASE_STATE as REDBOOK_BASE_STATE,
|
|
_append_chat_message as _append_redbook_chat_message,
|
|
_collect_note,
|
|
_publish_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.spotify.test_tasks import BASE_STATE as SPOTIFY_BASE_STATE, _track
|
|
from bench_env.tests.wechat_reading.test_tasks import (
|
|
BASE_STATE as WECHAT_READING_BASE_STATE,
|
|
TEST_OS_STATE,
|
|
)
|
|
|
|
ROOT = Path(__file__).resolve().parents[3]
|
|
DEFAULT_ROUTE = {"app": "launcher", "path": "/"}
|
|
|
|
|
|
def test_mobile_gym_env_maps_notes_display_name():
|
|
from bench_env.env.mobile_gym import MobileGymEnv
|
|
|
|
assert MobileGymEnv.APP_NAME_MAP["笔记"] == "notes"
|
|
|
|
|
|
def _load_json(*parts: str) -> dict[str, Any]:
|
|
return json.loads(ROOT.joinpath(*parts).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")
|
|
X_USERS = _load_json("apps", "X", "data", "users.json")
|
|
EBAY_BASE_STATE = _load_json("apps", "Ebay", "data", "defaults.json")
|
|
_REDDIT_DEFAULTS = _load_json("apps", "Reddit", "data", "defaults.json")
|
|
REDDIT_BASE_STATE = {
|
|
"user": {
|
|
**copy.deepcopy(_REDDIT_DEFAULTS["user"]),
|
|
"postIds": copy.deepcopy(_REDDIT_DEFAULTS["user"].get("postIds", [])),
|
|
"commentIds": copy.deepcopy(_REDDIT_DEFAULTS["user"].get("commentIds", [])),
|
|
"savedPostIds": copy.deepcopy(_REDDIT_DEFAULTS["user"].get("savedPostIds", [])),
|
|
"joinedCommunityIds": copy.deepcopy(_REDDIT_DEFAULTS["user"].get("joinedCommunityIds", [])),
|
|
"postVotes": copy.deepcopy(_REDDIT_DEFAULTS["user"].get("postVotes", {})),
|
|
"commentVotes": copy.deepcopy(_REDDIT_DEFAULTS["user"].get("commentVotes", {})),
|
|
},
|
|
"settings": copy.deepcopy(_REDDIT_DEFAULTS["settings"]),
|
|
"posts": copy.deepcopy(_REDDIT_DEFAULTS.get("posts", {})),
|
|
"comments": copy.deepcopy(_REDDIT_DEFAULTS.get("comments", {})),
|
|
"chatThreads": copy.deepcopy(_REDDIT_DEFAULTS["chatThreads"]),
|
|
"chatReplies": copy.deepcopy(_REDDIT_DEFAULTS["chatReplies"]),
|
|
}
|
|
|
|
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_x_state() -> dict[str, Any]:
|
|
return {
|
|
"user": {
|
|
"id": "u_me",
|
|
"name": "小明",
|
|
"handle": "@xiaoming",
|
|
"screenName": "xiaoming",
|
|
"avatar": "",
|
|
"verified": False,
|
|
"bio": "",
|
|
"location": "",
|
|
"website": "",
|
|
"joinDate": "Fri Jan 01 00:00:00 +0000 2026",
|
|
"following": 0,
|
|
"followers": 0,
|
|
"postIds": [],
|
|
"replyIds": [],
|
|
"followedUserIds": [],
|
|
"followerUserIds": [],
|
|
"likedPostIds": [],
|
|
"retweetedPostIds": [],
|
|
"bookmarkedPostIds": [],
|
|
},
|
|
"posts": {},
|
|
"conversations": [],
|
|
"trends": [],
|
|
"notifications": [],
|
|
"searchHistory": [],
|
|
"settings": {},
|
|
"suggestedFollowingIds": [],
|
|
}
|
|
|
|
|
|
def _base_apps() -> dict[str, Any]:
|
|
redbook = copy.deepcopy(REDBOOK_BASE_STATE)
|
|
return {
|
|
"spotify": copy.deepcopy(SPOTIFY_BASE_STATE),
|
|
"bilibili": copy.deepcopy(BILIBILI_BASE_STATE),
|
|
"ebay": copy.deepcopy(EBAY_BASE_STATE),
|
|
"redbook": redbook,
|
|
"wechat_reading": copy.deepcopy(WECHAT_READING_BASE_STATE),
|
|
"wechat": copy.deepcopy(WECHAT_BASE_STATE),
|
|
"notes": copy.deepcopy(NOTES_BASE_STATE),
|
|
"sms": copy.deepcopy(SMS_APP_STATE),
|
|
"x": _base_x_state(),
|
|
"reddit": copy.deepcopy(REDDIT_BASE_STATE),
|
|
"file_manager": {"clipboardItems": [], "clipboardOperation": None},
|
|
"gallery": {},
|
|
}
|
|
|
|
|
|
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),
|
|
},
|
|
"fileSystem": {
|
|
"nodes": [
|
|
{"id": "root", "name": "/", "type": "directory", "parentId": None, "path": "/", "size": 0, "createdAt": 0, "modifiedAt": 0, "storage": "memory"},
|
|
{"id": "dir_sdcard", "name": "sdcard", "type": "directory", "parentId": "root", "path": "/sdcard", "size": 0, "createdAt": 0, "modifiedAt": 0, "storage": "memory"},
|
|
{"id": "dir_download", "name": "Download", "type": "directory", "parentId": "dir_sdcard", "path": "/sdcard/Download", "size": 0, "createdAt": 0, "modifiedAt": 0, "storage": "memory"},
|
|
{"id": "dir_pictures", "name": "Pictures", "type": "directory", "parentId": "dir_sdcard", "path": "/sdcard/Pictures", "size": 0, "createdAt": 0, "modifiedAt": 0, "storage": "memory"},
|
|
{"id": "file_download", "name": "downloaded_image.jpg", "type": "file", "parentId": "dir_download", "path": "/sdcard/Download/downloaded_image.jpg", "size": 1024, "mimeType": "image/jpeg", "createdAt": 0, "modifiedAt": 0, "storage": "indexeddb"},
|
|
{"id": "file_picture", "name": "downloaded_image_copy.jpg", "type": "file", "parentId": "dir_pictures", "path": "/sdcard/Pictures/downloaded_image_copy.jpg", "size": 1024, "mimeType": "image/jpeg", "createdAt": 0, "modifiedAt": 0, "storage": "indexeddb"},
|
|
]
|
|
},
|
|
}
|
|
|
|
|
|
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"wx_out_{len(chat['messages']) + 1}",
|
|
"type": "text",
|
|
"content": content,
|
|
"senderId": state["user"]["wxid"],
|
|
"timestamp": TEST_OS_STATE["time"]["timestamp"],
|
|
}
|
|
)
|
|
|
|
|
|
def _append_wechat_moment(state: dict[str, Any], content: str, *, images: list[str] | None = None) -> None:
|
|
state["moments"].insert(
|
|
0,
|
|
{
|
|
"id": f"mo_test_{len(state['moments']) + 1}",
|
|
"wxid": state["user"]["wxid"],
|
|
"userName": state["user"]["name"],
|
|
"userAvatar": state["user"]["avatar"],
|
|
"content": content,
|
|
"timestamp": TEST_OS_STATE["time"]["timestamp"],
|
|
"images": list(images or []),
|
|
},
|
|
)
|
|
|
|
|
|
def _append_x_post(state: dict[str, Any], content: str, *, thread_id: str | None = None) -> None:
|
|
state.setdefault("posts", {})
|
|
post_id = f"x_post_{len(state['posts']) + 1}"
|
|
state["posts"][post_id] = {
|
|
"id": post_id,
|
|
"authorId": state["user"]["id"],
|
|
"content": content,
|
|
"threadId": thread_id,
|
|
"timestamp": TEST_OS_STATE["time"]["timestamp"],
|
|
}
|
|
if thread_id:
|
|
state["user"]["replyIds"] = [post_id, *state["user"].get("replyIds", [])]
|
|
else:
|
|
state["user"]["postIds"] = [post_id, *state["user"].get("postIds", [])]
|
|
|
|
|
|
def _append_reddit_comment(
|
|
state: dict[str, Any],
|
|
*,
|
|
post_id: str,
|
|
body: str,
|
|
) -> None:
|
|
state.setdefault("comments", {})
|
|
comment_id = f"reddit_comment_{post_id}_{len(state['comments']) + 1}"
|
|
state["comments"][comment_id] = {
|
|
"id": comment_id,
|
|
"postId": post_id,
|
|
"author": state.get("user", {}).get("username", "me"),
|
|
"body": body,
|
|
"score": 1,
|
|
}
|
|
state.setdefault("user", {}).setdefault("commentIds", []).append(comment_id)
|
|
|
|
|
|
def _append_reddit_post(
|
|
state: dict[str, Any],
|
|
*,
|
|
post_id: str,
|
|
subreddit: str,
|
|
title: str,
|
|
) -> None:
|
|
state.setdefault("posts", {})
|
|
state["posts"][post_id] = {
|
|
"id": post_id,
|
|
"subreddit": subreddit,
|
|
"title": title,
|
|
"content": title,
|
|
"author": state.get("user", {}).get("username", "me"),
|
|
"timeAgo": "now",
|
|
"upvotes": "1",
|
|
"comments": "0",
|
|
"isAd": False,
|
|
}
|
|
state.setdefault("user", {}).setdefault("postIds", []).append(post_id)
|
|
|
|
|
|
def _set_ebay_search(
|
|
state: dict[str, Any],
|
|
*,
|
|
query: str,
|
|
conditions: list[str] | None = None,
|
|
sort_option: str | None = None,
|
|
) -> None:
|
|
snapshot = {
|
|
"query": query,
|
|
"categoryId": "",
|
|
"brand": "",
|
|
"buyingFormat": "",
|
|
"conditions": list(conditions or []),
|
|
"location": "",
|
|
"freeShippingOnly": False,
|
|
"priceMin": "",
|
|
"priceMax": "",
|
|
"sortOption": sort_option or "",
|
|
"resultsCount": 20,
|
|
}
|
|
state["search"]["current"] = snapshot
|
|
state["search"]["history"].append(copy.deepcopy(snapshot))
|
|
state["recentSearches"] = [query, *[item for item in state["recentSearches"] if item != query]]
|
|
|
|
|
|
def _build_spotify_now_playing_to_wechat():
|
|
task = _tasks_module.SpotifyNowPlayingToWechat(contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
curr_apps["spotify"]["currentTrack"] = _track("搁浅")
|
|
curr_apps["spotify"]["likedSongs"] = [
|
|
_track("修炼爱情"),
|
|
_track("搁浅"),
|
|
]
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "我现在在听搁浅")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
def _build_bilibili_ranking_to_wechat():
|
|
task = _tasks_module.BilibiliRankingToWechat(partition="音乐", rank=1, contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
title = Bilibili.ranking_entry("音乐", 1)["title"]
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", f"B站榜一是:{title}")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
def _build_redbook_search_title_to_wechat():
|
|
task = _tasks_module.RedbookSearchTitleToWechat(keyword="数分", contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
title = Redbook(curr_apps["redbook"]).first_search_note("数分")["title"]
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", title)
|
|
return task, _make_input(init_apps, curr_apps)
|
|
def _build_spotify_today_nth_play_to_redbook():
|
|
task = _tasks_module.SpotifyTodayNthPlayToRedbook(nth=2)
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
# 今天第 2 首(从早到晚) = recentPlays 倒数第 2 个
|
|
target = curr_apps["spotify"]["recentPlays"][-2]
|
|
_publish_note(curr_apps["redbook"], target["title"], f"{target['artist']} 的歌")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
def _build_wechat_reading_best_book_to_wechat():
|
|
task = _tasks_module.WechatReadingBestBookToWechat(category="商业", contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "推荐《纳瓦尔宝典》,推荐值 92.5")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
def _build_wechat_reading_stats_to_wechat():
|
|
task = _tasks_module.WechatReadingStatsToWechat(contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "1月22日读了226分钟")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
def _build_redbook_author_followers_to_wechat():
|
|
task = _tasks_module.RedbookAuthorFollowersToWechat(keyword="数分", contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
rb = Redbook(curr_apps["redbook"])
|
|
author = rb.note_author(rb.first_search_note("数分"))
|
|
curr_apps["redbook"]["user"]["followingIds"] = [
|
|
*curr_apps["redbook"]["user"].get("followingIds", []),
|
|
author["id"],
|
|
]
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", f"{author['name']}有{author['followers']}粉丝,我刚关注了TA")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
def _build_x_latest_post_to_reddit_with_title_format():
|
|
task = _tasks_module.XLatestPostToReddit_WithTitleFormat(user="elonmusk", subreddit="technology")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
tweet_content = next(
|
|
str(post.get("content") or "").strip()
|
|
for post in X(init_apps["x"]).view_posts()
|
|
if str(post.get("authorId") or "").lower().removeprefix("u_") == "elonmusk"
|
|
)
|
|
_append_reddit_post(
|
|
init_apps["reddit"],
|
|
post_id="reddit_post_1",
|
|
subreddit="technology",
|
|
title="AI companies race to orbit",
|
|
)
|
|
curr_apps["reddit"] = copy.deepcopy(init_apps["reddit"])
|
|
reddit = Reddit(curr_apps["reddit"])
|
|
target_post = next(
|
|
post
|
|
for post in reddit.view_posts_list()
|
|
if str(post.get("subreddit") or "").strip().removeprefix("r/").lower() == "technology"
|
|
)
|
|
_append_reddit_comment(
|
|
curr_apps["reddit"],
|
|
post_id=str(target_post["id"]),
|
|
body=f"elonmusk:{tweet_content}",
|
|
)
|
|
return task, _make_input(init_apps, curr_apps)
|
|
def _build_file_manager_send_file_to_wechat_contact():
|
|
task = _tasks_module.FileManagerSendFileToWechatContact(contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "第一个副本文件名是 downloaded_image.jpg")
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "第二个副本文件名是 downloaded_image_copy.jpg")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
def _build_redbook_following_note_count_to_sms():
|
|
task = _tasks_module.RedbookFollowingNoteCountToSms(username="西柚慢行", contact="中国联通")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
count = Redbook(curr_apps["redbook"]).followed_user_note_count("西柚慢行")
|
|
curr_os = _base_os()
|
|
_append_sms_outgoing(curr_os["providers"]["sms"], "中国联通", f"西柚慢行发了{count}篇笔记")
|
|
return task, _make_input(init_apps, curr_apps, curr_os=curr_os)
|
|
def _build_spotify_song_full_details_to_redbook():
|
|
task = _tasks_module.SpotifySongFullDetailsToRedbook(song="搁浅")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
_publish_note(curr_apps["redbook"], "搁浅", "周杰伦 演唱,时长 3:58")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
def _build_bilibili_triple_like_then_moments():
|
|
task = _tasks_module.BilibiliTripleLikeThenMoments(partition="音乐", rank=1)
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
title = Bilibili.ranking_entry("音乐", 1)["title"]
|
|
_sanlian_video(curr_apps["bilibili"], title)
|
|
_append_wechat_moment(curr_apps["wechat"], f"推荐一个视频:{title}")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
def _build_bilibili_triple_like_then_moments_duplicate_title():
|
|
task = _tasks_module.BilibiliTripleLikeThenMoments(partition="电影", rank=7)
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
entry = Bilibili.ranking_entry("电影", 7)
|
|
video_id = str(entry["id"])
|
|
title = str(entry["title"])
|
|
bili_user = curr_apps["bilibili"]["user"]
|
|
if video_id not in bili_user["likedVideoIds"]:
|
|
bili_user["likedVideoIds"].append(video_id)
|
|
coined = bili_user.setdefault("coinedVideoCoins", {})
|
|
coined[video_id] = max(int(coined.get(video_id, 0)), 1)
|
|
for folder in bili_user["favoritesFolders"]:
|
|
if folder["id"] == "fav_default" and video_id not in folder["videoIds"]:
|
|
folder["videoIds"].append(video_id)
|
|
curr_apps["bilibili"]["activeVideoId"] = video_id
|
|
_append_wechat_moment(curr_apps["wechat"], f"推荐一个视频:{title}")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
def _build_redbook_dm_then_wechat_report():
|
|
task = _tasks_module.RedbookDmThenWechatReport(
|
|
username="西柚慢行",
|
|
message="你好呀",
|
|
contact="陈静",
|
|
)
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
user = Redbook(curr_apps["redbook"]).require_user_by_name("西柚慢行")
|
|
_append_redbook_chat_message(curr_apps["redbook"], user["id"], "你好呀")
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "已经联系西柚慢行了")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
def _build_notes_content_to_redbook_and_x():
|
|
task = _tasks_module.NotesContentToRedbookAndX(topic="AI代理")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
content = "AI代理需要更强的任务判定和更清晰的执行边界。"
|
|
_add_note(curr_apps["notes"], "AI代理想法", content=content)
|
|
_publish_note(curr_apps["redbook"], "AI代理想法", content)
|
|
_append_x_post(curr_apps["x"], content)
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
def _build_cultural_checklist_to_redbook():
|
|
task = _tasks_module.CulturalChecklistToRedbook()
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
song = curr_apps["spotify"]["recentPlays"][-1]["title"]
|
|
book = curr_apps["wechat_reading"]["hotSearch"][0]["title"]
|
|
_add_note(curr_apps["notes"], "今日文化清单", content=f"{song}\n{book}")
|
|
_publish_note(curr_apps["redbook"], "今日文化清单", f"{song}\n{book}")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
def _build_daily_log_to_moments():
|
|
task = _tasks_module.DailyLogToMoments()
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
_add_note(init_apps["notes"], "买咖啡", content="下午提神")
|
|
_add_note(init_apps["notes"], "写周报", content="整理本周进度")
|
|
curr_apps["notes"] = copy.deepcopy(init_apps["notes"])
|
|
_append_wechat_moment(curr_apps["wechat"], "今天主要做了两件事:买咖啡、写周报。")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
def _build_ebay_cheap_to_redbook():
|
|
task = _tasks_module.EbayCheapToRedbook(product="电风扇")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
_set_ebay_search(curr_apps["ebay"], query=task.p.product, sort_option="priceLow")
|
|
top = expect_top(query=task.p.product, sort_id="priceLow", n=1)[0]
|
|
_publish_note(curr_apps["redbook"], top.title, f"商品推荐:{top.title},这款最便宜,值得入手。")
|
|
return task, _make_input(init_apps, curr_apps)
|
|
|
|
|
|
POSITIVE_CASES: list[tuple[str, Callable[[], tuple[BaseTask, JudgeInput]]]] = [
|
|
("SpotifyNowPlayingToWechat", _build_spotify_now_playing_to_wechat),
|
|
("BilibiliRankingToWechat", _build_bilibili_ranking_to_wechat),
|
|
("RedbookSearchTitleToWechat", _build_redbook_search_title_to_wechat),
|
|
("SpotifyTodayNthPlayToRedbook", _build_spotify_today_nth_play_to_redbook),
|
|
("WechatReadingBestBookToWechat", _build_wechat_reading_best_book_to_wechat),
|
|
("WechatReadingStatsToWechat", _build_wechat_reading_stats_to_wechat),
|
|
("RedbookAuthorFollowersToWechat", _build_redbook_author_followers_to_wechat),
|
|
("XLatestPostToReddit_WithTitleFormat", _build_x_latest_post_to_reddit_with_title_format),
|
|
("FileManagerSendFileToWechatContact", _build_file_manager_send_file_to_wechat_contact),
|
|
("RedbookFollowingNoteCountToSms", _build_redbook_following_note_count_to_sms),
|
|
("SpotifySongFullDetailsToRedbook", _build_spotify_song_full_details_to_redbook),
|
|
("BilibiliTripleLikeThenMoments", _build_bilibili_triple_like_then_moments),
|
|
("RedbookDmThenWechatReport", _build_redbook_dm_then_wechat_report),
|
|
("NotesContentToRedbookAndX", _build_notes_content_to_redbook_and_x),
|
|
("CulturalChecklistToRedbook", _build_cultural_checklist_to_redbook),
|
|
("DailyLogToMoments", _build_daily_log_to_moments),
|
|
("EbayCheapToRedbook", _build_ebay_cheap_to_redbook),
|
|
]
|
|
|
|
|
|
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()}
|
|
assert "{" not in task.description
|
|
|
|
@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
|
|
|
|
|
|
class TestCrossappContentOfflineJudge:
|
|
@pytest.mark.parametrize("task_id,builder", POSITIVE_CASES, ids=[case[0] for case in POSITIVE_CASES])
|
|
def test_positive_cases(self, task_id: str, builder: Callable[[], tuple[BaseTask, JudgeInput]]):
|
|
task, judge_input = builder()
|
|
checks = task.check_goals(judge_input)
|
|
assert checks, task_id
|
|
assert all(check["passed"] for check in checks), (task_id, checks)
|
|
|
|
def test_redbook_search_title_to_wechat_allows_search_history_side_effect(self):
|
|
task = _tasks_module.RedbookSearchTitleToWechat(keyword="美食", contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
title = Redbook(curr_apps["redbook"]).first_search_note("美食")["title"]
|
|
history = curr_apps["redbook"].setdefault("searchHistory", [])
|
|
curr_apps["redbook"]["searchHistory"] = ["美食", *[item for item in history if item != "美食"]]
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", title)
|
|
|
|
result = task.evaluate(_make_input(init_apps, curr_apps))
|
|
|
|
assert result.success
|
|
assert result.clean, result.warnings
|
|
|
|
def test_spotify_today_nth_play_to_redbook_song_title_in_content_passes(self):
|
|
"""歌名出现在正文(非标题)时也应通过——任务模板说"标题或正文"。"""
|
|
task = _tasks_module.SpotifyTodayNthPlayToRedbook(nth=3)
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
# 今天第 3 首(从早到晚) = recentPlays 倒数第 3 个
|
|
target = curr_apps["spotify"]["recentPlays"][-3]
|
|
# 标题写通用语,歌名和艺人都写在正文
|
|
_publish_note(curr_apps["redbook"], "推荐一首歌", f"{target['title']} {target['artist']} 强烈推荐")
|
|
checks = task.check_goals(_make_input(init_apps, curr_apps))
|
|
assert all(check["passed"] for check in checks), checks
|
|
|
|
def test_spotify_today_nth_play_to_redbook_wrong_song_fails(self):
|
|
"""发了其他位置的歌(不是第 N 首)应判定失败。"""
|
|
task = _tasks_module.SpotifyTodayNthPlayToRedbook(nth=1)
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
# 任务要第 1 首(recentPlays[-1]),但 agent 发的是 recentPlays[0](最新的)
|
|
wrong = curr_apps["spotify"]["recentPlays"][0]
|
|
_publish_note(curr_apps["redbook"], wrong["title"], f"{wrong['artist']} 强烈推荐")
|
|
checks = task.check_goals(_make_input(init_apps, curr_apps))
|
|
assert any(not check["passed"] for check in checks), checks
|
|
def test_spotify_now_playing_to_wechat_requires_liked_song(self):
|
|
task = _tasks_module.SpotifyNowPlayingToWechat(contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
curr_apps["spotify"]["currentTrack"] = _track("搁浅")
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "我现在在听搁浅")
|
|
checks = task.check_goals(_make_input(init_apps, curr_apps))
|
|
assert any(
|
|
check["field"] == "spotify_liked" and check["passed"] is False
|
|
for check in checks
|
|
), checks
|
|
|
|
def test_redbook_author_followers_to_wechat_requires_following_author(self):
|
|
task = _tasks_module.RedbookAuthorFollowersToWechat(keyword="数分", contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
rb = Redbook(curr_apps["redbook"])
|
|
author = rb.note_author(rb.first_search_note("数分"))
|
|
curr_apps["redbook"]["user"]["followingIds"] = [
|
|
item
|
|
for item in curr_apps["redbook"]["user"].get("followingIds", [])
|
|
if item != author["id"]
|
|
]
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", f"{author['name']}有{author['followers']}粉丝")
|
|
checks = task.check_goals(_make_input(init_apps, curr_apps))
|
|
assert any(
|
|
check["field"] == "redbook_following" and check["passed"] is False
|
|
for check in checks
|
|
), checks
|
|
|
|
def test_bilibili_triple_like_then_moments_uses_ranking_entry_id_for_duplicate_titles(self):
|
|
task, judge_input = _build_bilibili_triple_like_then_moments_duplicate_title()
|
|
checks = task.check_goals(judge_input)
|
|
assert all(check["passed"] for check in checks), checks
|
|
|
|
def test_bilibili_triple_like_then_moments_allows_normalized_moment_title(self):
|
|
task = _tasks_module.BilibiliTripleLikeThenMoments(partition="全站", rank=1)
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
title = str(Bilibili.ranking_entry("全站", 1)["title"])
|
|
_sanlian_video(curr_apps["bilibili"], title)
|
|
_append_wechat_moment(curr_apps["wechat"], f"推荐一个视频:{title.replace(' AI ', 'AI')}")
|
|
|
|
checks = task.check_goals(_make_input(init_apps, curr_apps))
|
|
|
|
assert all(check["passed"] for check in checks), checks
|
|
|
|
def test_file_manager_send_file_to_wechat_contact_requires_two_distinct_names(self):
|
|
task = _tasks_module.FileManagerSendFileToWechatContact(contact="陈静")
|
|
init_apps = _apps_state()
|
|
curr_apps = _apps_state()
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "downloaded_image.jpg")
|
|
_append_wechat_outgoing(curr_apps["wechat"], "陈静", "downloaded_image.jpg")
|
|
checks = task.check_goals(_make_input(init_apps, curr_apps))
|
|
assert any(
|
|
check["field"] == "wechat_file_names" and check["passed"] is False
|
|
for check in checks
|
|
), checks
|