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

813 lines
28 KiB
Python

"""Payment suite offline judge tests."""
from __future__ import annotations
import copy
import datetime
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.common_tasks import AnswerTask
from bench_env.task.payment import tasks as payment_tasks
from bench_env.tests.conftest import make_judge_input
ROOT = Path(__file__).resolve().parents[3]
DEFAULT_ROUTE = {"app": "launcher", "path": "/"}
TEST_TS = int(datetime.datetime(2026, 3, 27, 12, 0, 0).timestamp() * 1000)
BASE_OS_STATE = {
"time": {"timestamp": TEST_TS},
"providers": {"sms": {"conversations": [], "messagesByConversationId": {}}},
"runningApps": [],
}
def _load_json(*parts: str) -> dict[str, Any]:
return json.loads(Path(ROOT, *parts).read_text(encoding="utf-8"))
ALIPAY_DEFAULTS = _load_json("apps", "Alipay", "data", "defaults.json")
WECHAT_DEFAULTS = _load_json("apps", "Wechat", "data", "defaults.json")
BILIBILI_DEFAULTS = _load_json("apps", "Bilibili", "data", "defaults.json")
RAILWAY_DEFAULTS = _load_json("apps", "Railway12306", "data", "defaults.json")
ALL_TASK_CLASSES: list[type[BaseTask]] = [
obj
for _, obj in inspect.getmembers(payment_tasks, inspect.isclass)
if issubclass(obj, BaseTask) and obj is not BaseTask and obj.__module__ == payment_tasks.__name__
]
ALL_TASK_IDS = [cls.__name__ for cls in ALL_TASK_CLASSES]
ANSWER_TASK_CLASSES = [cls for cls in ALL_TASK_CLASSES if issubclass(cls, AnswerTask)]
def _copy(value: Any) -> Any:
return copy.deepcopy(value)
def _base_notes_state() -> dict[str, Any]:
return {
"notes": [],
"todos": [],
"folders": [{"id": "unfiled", "name": "未分类"}],
"settings": {"notesViewMode": "grid", "showWordCount": True},
}
def _today_ymd() -> str:
return datetime.datetime.fromtimestamp(TEST_TS / 1000).date().isoformat()
def _tomorrow_ymd() -> str:
return (datetime.datetime.fromtimestamp(TEST_TS / 1000).date() + datetime.timedelta(days=1)).isoformat()
def _make_input(
*,
apps_init: dict[str, Any],
apps_curr: dict[str, Any],
os_init: dict[str, Any] | None = None,
os_curr: dict[str, Any] | None = None,
) -> Any:
return make_judge_input(
{"apps": apps_init, "os": _copy(os_init or BASE_OS_STATE)},
{"apps": apps_curr, "os": _copy(os_curr or os_init or BASE_OS_STATE)},
route=DEFAULT_ROUTE,
)
def _append_note(state: dict[str, Any], *, title: str, content: str) -> dict[str, Any]:
next_state = _copy(state)
notes = next_state.setdefault("notes", [])
notes.append(
{
"id": f"note_{len(notes) + 1}",
"title": title,
"content": content,
"updatedAt": TEST_TS + len(notes) + 1,
"folderId": "unfiled",
}
)
return next_state
def _ensure_wechat_chat(state: dict[str, Any], contact_name: str) -> tuple[dict[str, Any], str]:
contacts = state.setdefault("contacts", [])
contact = next((item for item in contacts if str(item.get("name") or "") == contact_name), None)
if contact is None:
raise ValueError(f"wechat fixture missing contact: {contact_name}")
wxid = str(contact["wxid"])
chats = state.setdefault("chats", [])
chat = next((item for item in chats if str(item.get("id") or "") == wxid), None)
if chat is None:
chat = {
"id": wxid,
"name": contact_name,
"contactName": contact_name,
"user": {"wxid": wxid, "name": contact_name},
"isMuted": False,
"isSticky": False,
"isAlert": False,
"messages": [],
}
chats.append(chat)
return chat, wxid
def _append_wechat_text(
state: dict[str, Any],
*,
contact_name: str,
content: str,
outgoing: bool,
) -> dict[str, Any]:
next_state = _copy(state)
chat, wxid = _ensure_wechat_chat(next_state, contact_name)
sender = str(next_state.get("user", {}).get("wxid") or "") if outgoing else wxid
messages = chat.setdefault("messages", [])
messages.append(
{
"id": f"msg_{len(messages) + 1}",
"type": "text",
"content": content,
"senderId": sender,
"timestamp": TEST_TS + len(messages) + 1,
}
)
return next_state
def _append_alipay_record(
state: dict[str, Any],
*,
record_id: str,
kind: str,
delta: float,
method_id: str,
counterparty: str,
timestamp: int,
transfer_note: str | None = None,
target_account: str | None = None,
order_id: str | None = None,
) -> dict[str, Any]:
next_state = _copy(state)
record: dict[str, Any] = {
"id": record_id,
"counterpartyName": counterparty,
"displayTitle": counterparty,
"delta": float(delta),
"timestamp": int(timestamp),
"methodId": method_id,
"kind": kind,
}
if transfer_note is not None:
record["transferNote"] = transfer_note
if target_account is not None:
record["targetAccount"] = target_account
if order_id is not None:
record["orderId"] = order_id
next_state.setdefault("transferRecords", [])
next_state["transferRecords"] = [record, *next_state["transferRecords"]]
return next_state
def _rail_order(
*,
order_id: str,
status: str,
date: str,
from_station: str = "上海",
to_station: str = "南京",
train_no: str = "G1001",
passenger: str = "赵宇轩",
seat_type: str = "二等座",
seat_no: str = "06车 15B号",
price: float = 120.0,
depart_time: str = "08:00",
arrive_time: str = "09:25",
) -> dict[str, Any]:
return {
"id": order_id,
"trainNo": train_no,
"fromStation": from_station,
"toStation": to_station,
"departTime": depart_time,
"arriveTime": arrive_time,
"date": date,
"status": status,
"createTime": f"{date}T07:00:00.000Z",
"tickets": [
{
"passengerName": passenger,
"ticketType": "成人票",
"seatType": seat_type,
"seatNo": seat_no,
"price": float(price),
}
],
}
class TestTaskDefinitions:
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_instantiation(self, cls: type[BaseTask]) -> None:
task = cls()
assert task.name == cls.__name__
assert task.templates
assert task.apps
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_description_renders(self, cls: type[BaseTask]) -> None:
task = cls()
desc = task.description
assert desc
assert "{" not in desc
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_task_docstring_present(self, cls: type[BaseTask]) -> None:
assert cls.__doc__ is not None
assert cls.__doc__.strip()
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_meta_attrs(self, cls: type[BaseTask]) -> None:
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")
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_parameters_have_defaults(self, cls: type[BaseTask]) -> None:
for key, schema in cls.parameters.items():
if key.startswith("_"):
continue
assert "default" in schema
@pytest.mark.parametrize("cls", ANSWER_TASK_CLASSES, ids=[c.__name__ for c in ANSWER_TASK_CLASSES])
def test_answer_task_has_answer(self, cls: type[BaseTask]) -> None:
# payment suite 当前无 AnswerTask,保留占位以防后续新增遗漏
pass
def test_multichannel_task_removed(self) -> None:
assert not hasattr(payment_tasks, "AlipayTransferMultiChannel")
def test_transfer_with_note_prompt_mentions_password(self) -> None:
task = payment_tasks.TransferToContactWithNote()
assert "我的密码是123456" in task.description
def _bind_multiple_cards_positive() -> tuple[BaseTask, Any]:
task = payment_tasks.AlipayBindMultipleCardsTransferAndRecordSuccessfulCards()
init_alipay = _copy(ALIPAY_DEFAULTS)
init_alipay["balance"] = {"total": 200.0}
init_alipay["bankCards"] = [
{"id": "ccb", "bankName": "建设银行储蓄卡", "cardNumber": "6227000000000005445", "last4": "5445", "bound": True, "available": 20000},
{"id": "icbc", "bankName": "工商银行储蓄卡", "cardNumber": "6222000000000001234", "last4": "1234", "bound": False, "available": 500},
{"id": "abc", "bankName": "农业银行储蓄卡", "cardNumber": "6228480000000006789", "last4": "6789", "bound": False, "available": 20000},
]
init_alipay["transferRecords"] = []
init_alipay["lastPaymentHint"] = ""
curr_alipay = _copy(init_alipay)
for card in curr_alipay["bankCards"]:
card["bound"] = True
target = str(task.p.targetAccount)
curr_alipay = _append_alipay_record(
curr_alipay,
record_id="bind_tx_1",
kind="transfer",
delta=-float(task.p.amount1),
method_id="ccb",
counterparty=f"转账-{target}",
target_account=target,
transfer_note="第一笔",
timestamp=TEST_TS + 1000,
)
curr_alipay = _append_alipay_record(
curr_alipay,
record_id="bind_tx_2",
kind="transfer",
delta=0.0,
method_id="icbc",
counterparty=f"转账-{target}",
target_account=target,
transfer_note="第二笔工行余额不足",
timestamp=TEST_TS + 2000,
)
curr_alipay = _append_alipay_record(
curr_alipay,
record_id="bind_tx_3",
kind="transfer",
delta=-float(task.p.amount3),
method_id="abc",
counterparty=f"转账-{target}",
target_account=target,
transfer_note="第三笔",
timestamp=TEST_TS + 3000,
)
init_notes = _base_notes_state()
curr_notes = _append_note(
init_notes,
title=str(task.p.noteTitle),
content="成功银行卡:建设银行储蓄卡(5445)、农业银行储蓄卡(6789)",
)
return task, _make_input(
apps_init={"alipay": init_alipay, "notes": init_notes},
apps_curr={"alipay": curr_alipay, "notes": curr_notes},
)
def _continuous_payments_positive() -> tuple[BaseTask, Any]:
task = payment_tasks.AlipayContinuousPaymentsToContactsRecordBalances()
init_alipay = _copy(ALIPAY_DEFAULTS)
init_alipay["balance"] = {"total": 2000.0}
init_alipay["transferRecords"] = []
curr_alipay = _copy(init_alipay)
contacts = [
str(task.p.contact1),
str(task.p.contact2),
str(task.p.contact3),
str(task.p.contact4),
str(task.p.contact5),
]
amounts = [
float(task.p.amount1),
float(task.p.amount2),
float(task.p.amount3),
float(task.p.amount4),
float(task.p.amount5),
]
running = float(init_alipay["balance"]["total"])
balances: list[float] = []
for index, (contact, amount) in enumerate(zip(contacts, amounts, strict=True), start=1):
curr_alipay = _append_alipay_record(
curr_alipay,
record_id=f"cont_tx_{index}",
kind="transfer",
delta=-amount,
method_id="balance",
counterparty=contact,
target_account=contact,
transfer_note="发工资",
timestamp=TEST_TS + index * 1000,
)
running -= amount
balances.append(running)
curr_alipay["balance"]["total"] = balances[-1]
init_notes = _base_notes_state()
curr_notes = _append_note(
init_notes,
title=str(task.p.noteTitle),
content="\n".join(f"第{i}次后余额:{balance:g}" for i, balance in enumerate(balances, start=1)),
)
return task, _make_input(
apps_init={"alipay": init_alipay, "notes": init_notes},
apps_curr={"alipay": curr_alipay, "notes": curr_notes},
)
def _continuous_payments_negative() -> tuple[BaseTask, Any]:
task = payment_tasks.AlipayContinuousPaymentsToContactsRecordBalances()
init_alipay = _copy(ALIPAY_DEFAULTS)
init_alipay["balance"] = {"total": 2000.0}
init_alipay["transferRecords"] = []
curr_alipay = _copy(init_alipay)
contacts = [
str(task.p.contact1),
str(task.p.contact2),
str(task.p.contact3),
str(task.p.contact4),
str(task.p.contact5),
]
amounts = [
float(task.p.amount1),
float(task.p.amount2),
float(task.p.amount3),
float(task.p.amount4),
float(task.p.amount5),
]
running = float(init_alipay["balance"]["total"])
recorded_balances: list[float] = []
for index, (contact, amount) in enumerate(zip(contacts, amounts, strict=True), start=1):
curr_alipay = _append_alipay_record(
curr_alipay,
record_id=f"cont_neg_tx_{index}",
kind="transfer",
delta=-amount,
method_id="balance",
counterparty=contact,
target_account=contact,
transfer_note="发工资",
timestamp=TEST_TS + index * 1000,
)
running -= amount
if index < 5:
recorded_balances.append(running)
curr_alipay["balance"]["total"] = running
init_notes = _base_notes_state()
curr_notes = _append_note(
init_notes,
title=str(task.p.noteTitle),
content="\n".join(f"第{i}次后余额:{balance:g}" for i, balance in enumerate(recorded_balances, start=1)),
)
return task, _make_input(
apps_init={"alipay": init_alipay, "notes": init_notes},
apps_curr={"alipay": curr_alipay, "notes": curr_notes},
)
def _bind_multiple_cards_negative() -> tuple[BaseTask, Any]:
task = payment_tasks.AlipayBindMultipleCardsTransferAndRecordSuccessfulCards()
init_alipay = _copy(ALIPAY_DEFAULTS)
init_alipay["balance"] = {"total": 200.0}
init_alipay["bankCards"] = [
{"id": "ccb", "bankName": "建设银行储蓄卡", "cardNumber": "6227000000000005445", "last4": "5445", "bound": True, "available": 20000},
{"id": "icbc", "bankName": "工商银行储蓄卡", "cardNumber": "6222000000000001234", "last4": "1234", "bound": False, "available": 500},
{"id": "abc", "bankName": "农业银行储蓄卡", "cardNumber": "6228480000000006789", "last4": "6789", "bound": False, "available": 20000},
]
init_alipay["transferRecords"] = []
init_alipay["lastPaymentHint"] = ""
curr_alipay = _copy(init_alipay)
for card in curr_alipay["bankCards"]:
card["bound"] = True
target = str(task.p.targetAccount)
curr_alipay = _append_alipay_record(
curr_alipay,
record_id="bind_neg_tx_1",
kind="transfer",
delta=-float(task.p.amount1),
method_id="ccb",
counterparty=f"转账-{target}",
target_account=target,
transfer_note="第一笔",
timestamp=TEST_TS + 1000,
)
curr_alipay = _append_alipay_record(
curr_alipay,
record_id="bind_neg_tx_2",
kind="transfer",
delta=-float(task.p.amount2),
method_id="icbc",
counterparty=f"转账-{target}",
target_account=target,
transfer_note="第二笔",
timestamp=TEST_TS + 2000,
)
init_notes = _base_notes_state()
curr_notes = _append_note(
init_notes,
title=str(task.p.noteTitle),
content="成功银行卡:建设银行储蓄卡(5445)、工商银行储蓄卡(1234)",
)
return task, _make_input(
apps_init={"alipay": init_alipay, "notes": init_notes},
apps_curr={"alipay": curr_alipay, "notes": curr_notes},
)
def _change_password_positive() -> tuple[BaseTask, Any]:
task = payment_tasks.AlipayChangePaymentPasswordThenPay()
init_alipay = _copy(ALIPAY_DEFAULTS)
init_alipay.setdefault("userInfo", {})
init_alipay["userInfo"]["paymentPassword"] = "000000"
curr_alipay = _copy(init_alipay)
curr_alipay["userInfo"]["paymentPassword"] = str(task.p.newPassword)
curr_alipay = _append_alipay_record(
curr_alipay,
record_id="pwd_pay_1",
kind="payment",
delta=-float(task.p.amount),
method_id="balance",
counterparty=str(task.p.contact),
timestamp=TEST_TS + 1000,
)
return task, _make_input(
apps_init={"alipay": init_alipay},
apps_curr={"alipay": curr_alipay},
)
def _change_password_negative() -> tuple[BaseTask, Any]:
task = payment_tasks.AlipayChangePaymentPasswordThenPay()
init_alipay = _copy(ALIPAY_DEFAULTS)
init_alipay.setdefault("userInfo", {})
init_alipay["userInfo"]["paymentPassword"] = "000000"
curr_alipay = _copy(init_alipay)
curr_alipay["userInfo"]["paymentPassword"] = str(task.p.newPassword)
return task, _make_input(
apps_init={"alipay": init_alipay},
apps_curr={"alipay": curr_alipay},
)
def _transfer_with_note_positive() -> tuple[BaseTask, Any]:
task = payment_tasks.TransferToContactWithNote()
init_alipay = _copy(ALIPAY_DEFAULTS)
init_alipay["transferRecords"] = []
curr_alipay = _append_alipay_record(
init_alipay,
record_id="transfer_with_note_1",
kind="transfer",
delta=-float(task.p.amount),
method_id="balance",
counterparty=str(task.p.name),
target_account=str(task.p.name),
transfer_note=str(task.p.note),
timestamp=TEST_TS + 1000,
)
curr_alipay["balance"]["total"] = round(
float(init_alipay["balance"]["total"]) - float(task.p.amount),
2,
)
return task, _make_input(
apps_init={"alipay": init_alipay},
apps_curr={"alipay": curr_alipay},
)
def _transfer_with_note_negative() -> tuple[BaseTask, Any]:
task = payment_tasks.TransferToContactWithNote()
init_alipay = _copy(ALIPAY_DEFAULTS)
init_alipay["transferRecords"] = []
curr_alipay = _append_alipay_record(
init_alipay,
record_id="transfer_with_note_wrong_note",
kind="transfer",
delta=-float(task.p.amount),
method_id="balance",
counterparty=str(task.p.name),
target_account=str(task.p.name),
transfer_note="别的备注",
timestamp=TEST_TS + 1000,
)
curr_alipay["balance"]["total"] = round(
float(init_alipay["balance"]["total"]) - float(task.p.amount),
2,
)
return task, _make_input(
apps_init={"alipay": init_alipay},
apps_curr={"alipay": curr_alipay},
)
def _subscribe_membership_positive() -> tuple[BaseTask, Any]:
task = payment_tasks.SubscribeMembershipAutoRenewThenCancelInWechat()
init_wechat = _copy(WECHAT_DEFAULTS)
init_wechat["subscriptions"] = []
curr_wechat = _copy(init_wechat)
curr_wechat["subscriptions"] = [
{
"id": "sub_task_1",
"membershipType": str(task.p.membershipType),
"price": float(task.p.price),
"billingCycle": str(task.p.billingCycle),
"autoRenew": False,
"createdAt": TEST_TS,
"source": "哔哩哔哩",
}
]
init_bilibili = _copy(BILIBILI_DEFAULTS)
curr_bilibili = _copy(init_bilibili)
curr_bilibili["user"]["isVip"] = True
curr_bilibili["user"]["vipExpireAt"] = TEST_TS + 30 * 24 * 60 * 60 * 1000
return task, _make_input(
apps_init={"wechat": init_wechat, "bilibili": init_bilibili},
apps_curr={"wechat": curr_wechat, "bilibili": curr_bilibili},
)
def _subscribe_membership_negative() -> tuple[BaseTask, Any]:
task = payment_tasks.SubscribeMembershipAutoRenewThenCancelInWechat()
init_wechat = _copy(WECHAT_DEFAULTS)
init_wechat["subscriptions"] = []
curr_wechat = _copy(init_wechat)
curr_wechat["subscriptions"] = [
{
"id": "sub_task_2",
"membershipType": str(task.p.membershipType),
"price": float(task.p.price),
"billingCycle": str(task.p.billingCycle),
"autoRenew": True,
"createdAt": TEST_TS,
"source": "哔哩哔哩",
}
]
init_bilibili = _copy(BILIBILI_DEFAULTS)
curr_bilibili = _copy(init_bilibili)
curr_bilibili["user"]["isVip"] = True
curr_bilibili["user"]["vipExpireAt"] = TEST_TS + 30 * 24 * 60 * 60 * 1000
return task, _make_input(
apps_init={"wechat": init_wechat, "bilibili": init_bilibili},
apps_curr={"wechat": curr_wechat, "bilibili": curr_bilibili},
)
def _transfer_notify_positive() -> tuple[BaseTask, Any]:
task = payment_tasks.AlipayTransferAndNotify()
init_alipay = _copy(ALIPAY_DEFAULTS)
curr_alipay = _append_alipay_record(
init_alipay,
record_id="notify_tx_1",
kind="transfer",
delta=-float(task.p.amount),
method_id="balance",
counterparty=str(task.p.alipay_contact),
target_account=str(task.p.alipay_contact),
transfer_note=str(task.p.note),
timestamp=TEST_TS + 1000,
)
init_wechat = _copy(WECHAT_DEFAULTS)
curr_wechat = _append_wechat_text(
init_wechat,
contact_name=str(task.p.contact),
content=f"我刚给你转账{float(task.p.amount):g}元,记得查收。",
outgoing=True,
)
return task, _make_input(
apps_init={"alipay": init_alipay, "wechat": init_wechat},
apps_curr={"alipay": curr_alipay, "wechat": curr_wechat},
)
def _transfer_notify_negative() -> tuple[BaseTask, Any]:
task = payment_tasks.AlipayTransferAndNotify()
init_alipay = _copy(ALIPAY_DEFAULTS)
curr_alipay = _append_alipay_record(
init_alipay,
record_id="notify_tx_neg_1",
kind="transfer",
delta=-float(task.p.amount),
method_id="balance",
counterparty=str(task.p.alipay_contact),
target_account=str(task.p.alipay_contact),
transfer_note=str(task.p.note),
timestamp=TEST_TS + 1000,
)
init_wechat = _copy(WECHAT_DEFAULTS)
curr_wechat = _append_wechat_text(
init_wechat,
contact_name=str(task.p.contact),
content="晚点联系你。",
outgoing=True,
)
return task, _make_input(
apps_init={"alipay": init_alipay, "wechat": init_wechat},
apps_curr={"alipay": curr_alipay, "wechat": curr_wechat},
)
def _wechat_extract_positive() -> tuple[BaseTask, Any]:
task = payment_tasks.WechatExtractAmountTransfer()
amount = float(task.p.requestAmount)
init_wechat = _copy(WECHAT_DEFAULTS)
curr_wechat = _append_wechat_text(
init_wechat,
contact_name=str(task.p.contact),
content=f"麻烦你转{amount:g}元给我。",
outgoing=False,
)
curr_wechat = _append_wechat_text(
curr_wechat,
contact_name=str(task.p.contact),
content=str(task.p.reply),
outgoing=True,
)
init_alipay = _copy(ALIPAY_DEFAULTS)
curr_alipay = _append_alipay_record(
init_alipay,
record_id="extract_tx_1",
kind="transfer",
delta=-amount,
method_id="balance",
counterparty=str(task.p.alipay_contact),
target_account=str(task.p.alipay_contact),
transfer_note="按微信请求转账",
timestamp=TEST_TS + 1000,
)
return task, _make_input(
apps_init={"wechat": init_wechat, "alipay": init_alipay},
apps_curr={"wechat": curr_wechat, "alipay": curr_alipay},
)
def _wechat_extract_negative() -> tuple[BaseTask, Any]:
task = payment_tasks.WechatExtractAmountTransfer()
amount = float(task.p.requestAmount)
init_wechat = _copy(WECHAT_DEFAULTS)
curr_wechat = _append_wechat_text(
init_wechat,
contact_name=str(task.p.contact),
content=f"麻烦你转{amount:g}元给我。",
outgoing=False,
)
init_alipay = _copy(ALIPAY_DEFAULTS)
curr_alipay = _append_alipay_record(
init_alipay,
record_id="extract_tx_neg_1",
kind="transfer",
delta=-(amount + 5.0),
method_id="balance",
counterparty=str(task.p.alipay_contact),
target_account=str(task.p.alipay_contact),
transfer_note="转错金额",
timestamp=TEST_TS + 1000,
)
return task, _make_input(
apps_init={"wechat": init_wechat, "alipay": init_alipay},
apps_curr={"wechat": curr_wechat, "alipay": curr_alipay},
)
def _wechat_extract_missing_reply() -> tuple[BaseTask, Any]:
task = payment_tasks.WechatExtractAmountTransfer()
amount = float(task.p.requestAmount)
init_wechat = _copy(WECHAT_DEFAULTS)
curr_wechat = _append_wechat_text(
init_wechat,
contact_name=str(task.p.contact),
content=f"麻烦你转{amount:g}元给我。",
outgoing=False,
)
init_alipay = _copy(ALIPAY_DEFAULTS)
curr_alipay = _append_alipay_record(
init_alipay,
record_id="extract_tx_no_reply_1",
kind="transfer",
delta=-amount,
method_id="balance",
counterparty=str(task.p.alipay_contact),
target_account=str(task.p.alipay_contact),
transfer_note="按微信请求转账",
timestamp=TEST_TS + 1000,
)
return task, _make_input(
apps_init={"wechat": init_wechat, "alipay": init_alipay},
apps_curr={"wechat": curr_wechat, "alipay": curr_alipay},
)
OFFLINE_JUDGE_TASK_NAMES = {
"AlipayContinuousPaymentsToContactsRecordBalances",
"AlipayBindMultipleCardsTransferAndRecordSuccessfulCards",
"AlipayChangePaymentPasswordThenPay",
"SubscribeMembershipAutoRenewThenCancelInWechat",
"TransferToContactWithNote",
"AlipayTransferAndNotify",
"WechatExtractAmountTransfer",
}
OFFLINE_POSITIVE_CASES: list[tuple[str, Callable[[], tuple[BaseTask, Any]]]] = [
("AlipayContinuousPaymentsToContactsRecordBalances", _continuous_payments_positive),
("AlipayBindMultipleCardsTransferAndRecordSuccessfulCards", _bind_multiple_cards_positive),
("AlipayChangePaymentPasswordThenPay", _change_password_positive),
("SubscribeMembershipAutoRenewThenCancelInWechat", _subscribe_membership_positive),
("TransferToContactWithNote", _transfer_with_note_positive),
("AlipayTransferAndNotify", _transfer_notify_positive),
("WechatExtractAmountTransfer", _wechat_extract_positive),
]
OFFLINE_NEGATIVE_CASES: list[tuple[str, Callable[[], tuple[BaseTask, Any]]]] = [
("AlipayContinuousPaymentsToContactsRecordBalances", _continuous_payments_negative),
("AlipayBindMultipleCardsTransferAndRecordSuccessfulCards", _bind_multiple_cards_negative),
("AlipayChangePaymentPasswordThenPay", _change_password_negative),
("SubscribeMembershipAutoRenewThenCancelInWechat", _subscribe_membership_negative),
("TransferToContactWithNote", _transfer_with_note_negative),
("AlipayTransferAndNotify", _transfer_notify_negative),
("WechatExtractAmountTransfer", _wechat_extract_negative),
]
class TestTaskJudgeMatrixOffline:
@pytest.mark.parametrize("name,factory", OFFLINE_POSITIVE_CASES)
def test_positive(self, name: str, factory: Callable[[], tuple[BaseTask, Any]]) -> None:
task, inp = factory()
result = task.evaluate(inp)
assert result.success, f"positive case {name} failed: {result.issues}"
@pytest.mark.parametrize("name,factory", OFFLINE_NEGATIVE_CASES)
def test_negative(self, name: str, factory: Callable[[], tuple[BaseTask, Any]]) -> None:
task, inp = factory()
result = task.evaluate(inp)
assert not result.success, f"negative case {name} unexpectedly passed"
def test_wechat_extract_requires_reply(self) -> None:
task, inp = _wechat_extract_missing_reply()
result = task.evaluate(inp)
assert not result.success
def test_matrix_complete(self) -> None:
pos_names = {name for name, _ in OFFLINE_POSITIVE_CASES}
neg_names = {name for name, _ in OFFLINE_NEGATIVE_CASES}
assert pos_names == OFFLINE_JUDGE_TASK_NAMES
assert neg_names == OFFLINE_JUDGE_TASK_NAMES