From 566b5519ea1d4b6733e46c228d57d72ab41c2d0b Mon Sep 17 00:00:00 2001 From: panding Date: Fri, 26 Jun 2026 01:26:02 +0800 Subject: [PATCH] Add fallback state for missing GUI apps --- .../task/mobile_gui/init_gui_task.py | 46 ++++++++++----- tests/test_mobile_gui_init.py | 56 +++++++++++++++++++ 2 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 tests/test_mobile_gui_init.py diff --git a/src/claw_anything/task/mobile_gui/init_gui_task.py b/src/claw_anything/task/mobile_gui/init_gui_task.py index 34c78d5..85a86fe 100644 --- a/src/claw_anything/task/mobile_gui/init_gui_task.py +++ b/src/claw_anything/task/mobile_gui/init_gui_task.py @@ -2134,6 +2134,24 @@ def inject_fossify_notes(step: dict, task_dir: Path, device: str | None) -> bool MATTERMOST_PACKAGE = "com.mattermost.rnbeta" MATTERMOST_STATE_PATH = f"/sdcard/Android/data/{MATTERMOST_PACKAGE}/files/state.json" +SHADOW_STATE_ROOT = "/data/local/tmp/claw_gui_state" + + +def _shadow_state_fallback_path(package: str) -> str: + return f"{SHADOW_STATE_ROOT}/{package}/state.json" + + +def _push_json_state(state: dict, remote_path: str, device: str | None) -> bool: + remote_dir = remote_path.rsplit("/", 1)[0] + adb_shell(f"mkdir -p {remote_dir}", device=device) + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as tmp: + json.dump(state, tmp, ensure_ascii=False, indent=2) + tmp_path = tmp.name + try: + return adb_push(tmp_path, remote_path, device=device) + finally: + os.unlink(tmp_path) + def _inject_shadow_app_state( step: dict, @@ -2163,35 +2181,35 @@ def _inject_shadow_app_state( print(f" [FAIL] {app_label} fixture must be a JSON list", file=sys.stderr) return False + state = {state_key: items} + fallback_path = _shadow_state_fallback_path(package) + adb_shell(f"rm -f {fallback_path}", device=device) + if not _push_json_state(state, fallback_path, device=device): + return False + installed, package_msg = _package_installed(package, device=device) if not installed: print( - f" [FAIL] package not installed: {package}" + f" [WARN] package not installed: {package}; " + f"wrote {app_label} shadow state to {fallback_path}" + (f" ({package_msg})" if package_msg else ""), file=sys.stderr, ) - return False + return True adb_shell(f"am force-stop {package}", device=device) adb_shell(f"rm -f {state_path}", device=device) - adb_shell(f"mkdir -p $(dirname {state_path})", device=device) - - state = {state_key: items} - with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as tmp: - json.dump(state, tmp, ensure_ascii=False, indent=2) - tmp_path = tmp.name - try: - ok = adb_push(tmp_path, state_path, device=device) - finally: - os.unlink(tmp_path) - if not ok: + if not _push_json_state(state, state_path, device=device): return False launch_ok, launch_msg = _start_package_launcher(package, device=device) if not launch_ok: print(f" [FAIL] failed to relaunch {app_label}: {launch_msg}", file=sys.stderr) return False - print(f" [OK] {app_label} restarted ({len(items)} item(s) injected)") + print( + f" [OK] {app_label} restarted ({len(items)} item(s) injected; " + f"fallback state also at {fallback_path})" + ) return True diff --git a/tests/test_mobile_gui_init.py b/tests/test_mobile_gui_init.py new file mode 100644 index 0000000..f634cf9 --- /dev/null +++ b/tests/test_mobile_gui_init.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import importlib +import json + +import pytest + +gui_init = importlib.import_module("claw_anything.task.mobile_gui.init_gui_task") + + +def test_shadow_app_state_falls_back_when_package_is_missing(tmp_path, monkeypatch) -> None: + fixture = tmp_path / "products.json" + fixture.write_text( + json.dumps([{"product_id": "PROD-1", "name": "Gift"}]), + encoding="utf-8", + ) + + shells: list[str] = [] + pushed: list[tuple[str, dict]] = [] + + def fake_adb_shell(cmd: str, device=None, root_required: bool = False): + shells.append(cmd) + return True, "" + + def fake_adb_push(local: str, remote: str, device=None, root_required: bool = False) -> bool: + pushed.append((remote, json.loads(open(local, encoding="utf-8").read()))) + return True + + monkeypatch.setattr(gui_init, "adb_shell", fake_adb_shell) + monkeypatch.setattr(gui_init, "adb_push", fake_adb_push) + monkeypatch.setattr(gui_init, "_package_installed", lambda package, device=None: (False, "missing")) + monkeypatch.setattr( + gui_init, + "_start_package_launcher", + lambda *args, **kwargs: pytest.fail("missing shadow app package should not be launched"), + ) + + ok = gui_init._inject_shadow_app_state( + {"fixture": "products.json"}, + tmp_path, + None, + package="com.testmall.app", + state_path="/sdcard/Android/data/com.testmall.app/files/state.json", + state_key="products", + default_fixture="fixtures/gui/testmall_gui/products.json", + app_label="TestMall", + ) + + assert ok + assert pushed == [ + ( + "/data/local/tmp/claw_gui_state/com.testmall.app/state.json", + {"products": [{"product_id": "PROD-1", "name": "Gift"}]}, + ) + ] + assert "rm -f /data/local/tmp/claw_gui_state/com.testmall.app/state.json" in shells