139 lines
8.4 KiB
Python
139 lines
8.4 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from autotrain_next import ledger, paths, platform, profiles, sampling, vision, web
|
|
from autotrain_next.planner import PlanError, deterministic_plan, validate_llm_steps
|
|
|
|
|
|
class NextGenTests(unittest.TestCase):
|
|
def test_adb_prefers_explicit_environment(self) -> None:
|
|
with patch.dict("os.environ", {"AUTOTRAIN_ADB": "C:/sdk/adb.exe"}, clear=True):
|
|
self.assertEqual(platform.adb_binary(), "C:/sdk/adb.exe")
|
|
|
|
def test_runtime_paths_have_no_parent_project_root(self) -> None:
|
|
self.assertEqual(paths.ROOT, ROOT)
|
|
self.assertEqual(paths.CONFIG.parent, paths.ROOT)
|
|
self.assertEqual(paths.WORK.parent, paths.ROOT)
|
|
|
|
def test_vision_reads_ignored_local_key_when_environment_is_absent(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
config = Path(directory) / "config.local.json"
|
|
config.write_text(json.dumps({"vision": {
|
|
"base_url": "https://api.siliconflow.cn/v1",
|
|
"model": "test-vision-model",
|
|
"api_key": "test-local-key",
|
|
}}), encoding="utf-8")
|
|
with patch.object(vision, "CONFIG", config), patch.dict("os.environ", {}, clear=True):
|
|
resolved = vision._config()
|
|
self.assertEqual(resolved["api_key"], "test-local-key")
|
|
self.assertEqual(resolved["model"], "test-vision-model")
|
|
|
|
def test_normalized_point_scales_with_screen(self) -> None:
|
|
self.assertEqual(profiles.normalized_point((0.5, 0.25), (1080, 2400)), (540, 600))
|
|
|
|
def test_profile_with_rotation_change_is_not_ready(self) -> None:
|
|
profile = {"status": "verified", "device": {"serial": "a", "orientation": "0"}}
|
|
check = profiles.compatibility(profile, {"serial": "a", "orientation": "1"})
|
|
self.assertEqual(check["status"], "draft")
|
|
self.assertIn("屏幕方向已变化", check["reasons"])
|
|
|
|
def test_llm_rejects_unknown_or_unapproved_tap(self) -> None:
|
|
with self.assertRaisesRegex(PlanError, "未注册"):
|
|
validate_llm_steps([{"tool": "shell.rm", "arguments": {}}], allow_submit=False)
|
|
with self.assertRaisesRegex(PlanError, "禁止"):
|
|
validate_llm_steps([{"tool": "ui.tap_normalized", "arguments": {"point": [0.5, 0.5]}}], allow_submit=False)
|
|
|
|
def test_list_page_routes_to_native_open_tool(self) -> None:
|
|
task = {"allow_submit": True, "history": [{"tool": "observe.xml", "result": {"texts": ["张*三", "开具"]}}]}
|
|
self.assertEqual(deterministic_plan(task)[0].tool, "invoice.open_next")
|
|
|
|
def test_unknown_xml_falls_back_to_vision_inspection(self) -> None:
|
|
task = {"allow_submit": True, "history": [{"tool": "observe.xml", "result": {"texts": []}}]}
|
|
self.assertEqual(deterministic_plan(task)[0].tool, "invoice.inspect")
|
|
|
|
def test_vision_inspection_routes_its_detected_page(self) -> None:
|
|
task = {"allow_submit": True, "history": [{"tool": "observe.xml", "result": {"texts": []}}, {"tool": "invoice.inspect", "result": {"page": "verify"}}]}
|
|
self.assertEqual(deterministic_plan(task)[0].tool, "invoice.verify")
|
|
|
|
def test_unrecognized_vision_page_yields_to_llm_or_review(self) -> None:
|
|
task = {"allow_submit": True, "history": [{"tool": "observe.xml", "result": {"texts": []}}, {"tool": "invoice.inspect", "result": {"page": "other"}}]}
|
|
self.assertEqual(deterministic_plan(task), [])
|
|
|
|
def test_qr_task_starts_with_native_push_tool(self) -> None:
|
|
task = {"allow_submit": True, "navigation": {"phase": "push_qr"}, "history": []}
|
|
self.assertEqual(deterministic_plan(task)[0].tool, "nav.push_qr")
|
|
|
|
def test_success_routes_to_download_not_just_status_update(self) -> None:
|
|
task = {"allow_submit": True, "history": [{"tool": "observe.xml", "result": {"texts": ["发票开具成功"]}}]}
|
|
self.assertEqual(deterministic_plan(task)[0].tool, "invoice.download")
|
|
|
|
def test_success_can_use_email_result_action(self) -> None:
|
|
task = {"allow_submit": True, "invoice": {"success_action": "email"}, "history": [{"tool": "observe.xml", "result": {"texts": ["发票开具成功"]}}]}
|
|
self.assertEqual(deterministic_plan(task)[0].tool, "invoice.email")
|
|
|
|
def test_masked_name_match_uses_visible_id_edges(self) -> None:
|
|
roster = [{"姓名": "张三", "身份证号": "1101234567891234", "身份证后8位": "67891234"}]
|
|
self.assertEqual(ledger.match(roster, "张*", "1", "4"), roster)
|
|
self.assertEqual(ledger.match(roster, "张*", "2", "4"), [])
|
|
|
|
def test_import_recording_creates_draft_profile_from_snapshot(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
recording = Path(directory)
|
|
folder = recording / "001_list"; folder.mkdir()
|
|
(folder / "screen.png").write_bytes(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x04\x38\x00\x00\x09\x60")
|
|
(recording / "session.json").write_text(json.dumps({"started_at": "2026-07-13", "device": {"serial": "phone-a"}}), encoding="utf-8")
|
|
(recording / "timeline.json").write_text(json.dumps({"snapshots": [{"folder": "001_list", "page_hint": "list", "timestamp": "now"}]}), encoding="utf-8")
|
|
with patch.object(sampling, "save"):
|
|
result = sampling.import_recording(recording, {"serial": "phone-a", "model": "Test"})
|
|
self.assertEqual(result["status"], "draft")
|
|
self.assertEqual(result["sampling"]["samples"]["list"]["screen"], {"width": 1080, "height": 2400})
|
|
|
|
def test_xml_roster_extracts_name_and_identity_number(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
source = Path(directory) / "roster.xml"
|
|
source.write_text("<root><passenger name='张三' idNo='110101199001011234'/></root>", encoding="utf-8")
|
|
self.assertEqual(ledger.rows_from_xml(source), [{"姓名": "张三", "身份证号": "110101199001011234", "身份证后8位": "01011234", "来源文件": "roster.xml", "来源工作表": "passenger", "来源行号": "2"}])
|
|
|
|
def test_import_xml_roster_preserves_source_and_writes_canonical_csv(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root, source = Path(directory), Path(directory) / "roster.xml"
|
|
source.write_text("<root><passenger><姓名>李四</姓名><身份证号>110101199001011235</身份证号></passenger></root>", encoding="utf-8")
|
|
with patch.object(ledger, "WORK", root / "work"), patch.object(ledger, "ROSTER", root / "work" / "passenger_roster.csv"):
|
|
canonical = ledger.import_roster(source)
|
|
self.assertTrue(canonical.is_file())
|
|
self.assertEqual(ledger.read_roster(), [{"姓名": "李四", "身份证号": "110101199001011235", "身份证后8位": "01011235", "来源文件": "roster.xml", "来源工作表": "passenger", "来源行号": "2"}])
|
|
self.assertEqual(len(list((root / "work" / "imports" / "roster").glob("*.xml"))), 1)
|
|
|
|
def test_xlsx_roster_finds_header_after_title_row(self) -> None:
|
|
from openpyxl import Workbook
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
source = Path(directory) / "roster.xlsx"
|
|
workbook = Workbook(); sheet = workbook.active
|
|
sheet.append(["某次出行名单"])
|
|
sheet.append(["序号", "姓名", "身份证号"])
|
|
sheet.append([1, "王五", "110101199001011236"])
|
|
workbook.save(source)
|
|
self.assertEqual(ledger.rows_from_xlsx(source), [{"姓名": "王五", "身份证号": "110101199001011236", "身份证后8位": "01011236", "来源文件": "roster.xlsx", "来源工作表": "Sheet", "来源行号": "3"}])
|
|
|
|
def test_qr_clipboard_image_accepts_png_and_rejects_non_image(self) -> None:
|
|
png = base64.b64encode(b"\x89PNG\r\n\x1a\nsmall").decode()
|
|
with tempfile.TemporaryDirectory() as directory, patch.object(web, "WORK", Path(directory)):
|
|
saved = web.save_qr_image(f"data:image/png;base64,{png}")
|
|
self.assertTrue(Path(saved["path"]).is_file())
|
|
with self.assertRaisesRegex(RuntimeError, "仅支持"):
|
|
web.save_qr_image("data:image/png;base64," + base64.b64encode(b"not-image").decode())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|