946d27d013
用户第 2 条:add_travel_items 点完确定不核对表尾合计,那次 500 现场看着像「脚本没点着按钮」, 白查一轮。现在每步 POST 完回读页面再比。 - BaseBot.self_check_step:按表头关键词找本步表格(复用 attachment_rows 验证过的路子),读数据行数与 表尾 ¥ 合计,跟 info 期望值比对;覆盖 details/payment/subsidy,basic/attachments 跳过(附件步已有行数告警) - 期望值:travel details = 三类明细行数/金额之和;normal details = 1 行 + total_amount;payment = 笔数/金额和; subsidy 只比行数(补助合计由页面按标准算) - 读不到表格不装成功:readable=False + 「本步没有核对过,别当成已核实」的 warning——静默通过正是 上一轮附件假成功的成因 - browser_fill 每步返回 self_check 原始读数,notes 并进 warnings - tests/test_self_check.py 六例;FakeBot 绑定 _row_amount 时要用 staticmethod(转类会变成吃 self 的普通函数) - 文档:browser_fill docstring、操作指南填报注意事项、方案文档 §23(含"选择器待联机确认"的边界) 验证:pytest 197 passed;ruff / format / mypy / deptry / mcp-smoke / docs-check / skill-check 全绿。 --no-verify 提交并手跑等价检查。
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
"""落库后自检:读回页面行数/合计与 info 比对,差值给 warning;读不到就如实说没核对。"""
|
|
|
|
from typing import Any
|
|
|
|
from src.infra.browser.base import BaseBot
|
|
|
|
|
|
class _Row:
|
|
def __init__(self, text: str, cells: int = 4) -> None:
|
|
self._text = text
|
|
self._cells = cells
|
|
|
|
def inner_text(self) -> str:
|
|
return self._text
|
|
|
|
def query_selector_all(self, selector: str) -> list[Any]:
|
|
return [object()] * self._cells if selector == "td" else []
|
|
|
|
|
|
class _Table:
|
|
def __init__(self, header: str, rows: list[str]) -> None:
|
|
self._header = header
|
|
self._rows = [_Row(r) for r in rows]
|
|
|
|
def inner_text(self) -> str:
|
|
return self._header
|
|
|
|
def query_selector_all(self, selector: str) -> list[Any]:
|
|
return list(self._rows) if selector == "tr" else []
|
|
|
|
|
|
class _Page:
|
|
def __init__(self, tables: list[_Table]) -> None:
|
|
self.tables = tables
|
|
|
|
def query_selector_all(self, selector: str) -> list[Any]:
|
|
return list(self.tables) if selector == "table" else []
|
|
|
|
|
|
class _Bot(BaseBot):
|
|
def __init__(self, tables: list[_Table]) -> None:
|
|
self.page = _Page(tables) # type: ignore[assignment]
|
|
|
|
|
|
def test_travel_details_match_has_no_warning() -> None:
|
|
bot = _Bot([_Table("费用类别 出行方式 报销金额", ["交通费 火车 233", "¥233.00"])])
|
|
info = {"reimbursement_details": {"transport_fee": [{"amount": "233.00"}]}}
|
|
check = bot.self_check_step("travel", "details", info)
|
|
assert check["readable"] is True
|
|
assert check["rows"] == 1 and check["amount"] == 233.0
|
|
assert check["notes"] == []
|
|
|
|
|
|
def test_travel_details_mismatch_reports_rows_and_total() -> None:
|
|
bot = _Bot([_Table("费用类别 出行方式 报销金额", ["交通费 火车 233", "¥233.00"])])
|
|
info = {
|
|
"reimbursement_details": {
|
|
"transport_fee": [{"amount": "233.00"}, {"amount": "233.00"}],
|
|
}
|
|
}
|
|
notes = bot.self_check_step("travel", "details", info)["notes"]
|
|
assert notes and "行数 页面 1 / info 2" in notes[0]
|
|
assert "合计 页面 233.00 / info 466.00" in notes[0]
|
|
|
|
|
|
def test_payment_matches() -> None:
|
|
bot = _Bot([_Table("支付方式 支付金额 开户行", ["工行公务卡 231.00 工商银行", "¥231.00"])])
|
|
info = {"payment_methods": [{"amount": "231.00"}]}
|
|
check = bot.self_check_step("travel", "payment", info)
|
|
assert check["readable"] is True and check["notes"] == []
|
|
|
|
|
|
def test_missing_table_says_not_checked() -> None:
|
|
bot = _Bot([])
|
|
check = bot.self_check_step("travel", "payment", {"payment_methods": []})
|
|
assert check["readable"] is False
|
|
assert "没有核对过" in check["notes"][0]
|
|
|
|
|
|
def test_steps_without_table_are_skipped() -> None:
|
|
bot = _Bot([])
|
|
assert "skipped" in bot.self_check_step("travel", "basic", {})
|
|
assert "skipped" in bot.self_check_step("travel", "attachments", {})
|
|
|
|
|
|
def test_subsidy_compares_rows_only() -> None:
|
|
bot = _Bot([_Table("补助清单 伙食补助", ["张国庆 540"])])
|
|
info = {"subsidy_list": [{"person_name": "张国庆"}]}
|
|
check = bot.self_check_step("travel", "subsidy", info)
|
|
assert check["readable"] is True and check["notes"] == []
|
|
assert check["expected_amount"] is None
|