fix(sessions): recover home-scoped list after fresh connect
* fix(sessions): use active connection client directly, remove roots filter Root cause A: loadSessions was calling clientForDirectory(serverHome) which scoped the session list to /home/azureuser — a different project than the server's active CWD. Sessions in the current project (e.g. opencode-mobile) were never returned. Root cause B: roots:true filtered out sessions that have a parentID (sub-task / AUTO-REVIEW sessions), hiding valid sessions from the list. Fix: use connState.client directly (the connection's active directory) and drop the roots filter so all sessions for that project are visible. Also adds a verify_session_list CUA smoke scenario that navigates back to the sessions tab after creating a session and asserts the list is non-empty — covering the regression path that was previously untested. * fix(sessions): fetch serverHome in addConnection so loadSessions shows correct sessions Root cause: addConnection() built the HTTP client but never fetched serverHome (only loadConnections and setActiveConnection did). When the user adds a new connection (fresh install / first sign-in), serverHome = null, so loadSessions fell through to connState.client (the server's CWD). On this dev server the CWD is the deploy directory — 11 old May-19 sessions that are not the user's recent work sessions. Fix: addConnection now fetches currentProject + serverHome via the same Promise.all as setActiveConnection, before calling set(). This ensures loadSessions immediately uses clientForDirectory(serverHome) → the global project → the user's actual recent parent sessions. Also adds --opencode-url flag to the CUA smoke script, which appends a connect_and_verify_sessions scenario that reproduces the regression: python scripts/android-cua-smoke.py --opencode-url http://100.108.64.76:4096 * fix(sessions): recover home scope after fresh connect Resolve stale deploy-only session list by recovering server home during first load and keeping regression coverage in default Android CUA smoke and CI. * chore(release): bump version to 0.4.0
This commit is contained in:
@@ -50,6 +50,8 @@ import sys
|
||||
import tempfile
|
||||
import time
|
||||
import threading
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
@@ -76,6 +78,107 @@ def adb(*args: str) -> str:
|
||||
|
||||
|
||||
_step_counter = 0
|
||||
APP_PACKAGE = "ai.opencode.mobile"
|
||||
|
||||
|
||||
def _bounds_center(bounds: str) -> tuple[int, int] | None:
|
||||
match = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds or "")
|
||||
if not match:
|
||||
return None
|
||||
x1, y1, x2, y2 = map(int, match.groups())
|
||||
return ((x1 + x2) // 2, (y1 + y2) // 2)
|
||||
|
||||
|
||||
def current_foreground_package() -> str:
|
||||
"""Return resumed foreground package name when available."""
|
||||
out = adb("shell", "dumpsys", "activity", "activities")
|
||||
for line in out.splitlines():
|
||||
if "mResumedActivity" not in line:
|
||||
continue
|
||||
match = re.search(r"\s([a-zA-Z0-9_\.]+)/", line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def ensure_app_foreground(package: str = APP_PACKAGE, retries: int = 3,
|
||||
verbose: bool = True) -> bool:
|
||||
"""Bring app to foreground before scenario start."""
|
||||
for attempt in range(retries):
|
||||
current = current_foreground_package()
|
||||
if current == package:
|
||||
return True
|
||||
|
||||
adb("shell", "monkey", "-p", package, "-c", "android.intent.category.LAUNCHER", "1")
|
||||
time.sleep(2.0)
|
||||
|
||||
if verbose:
|
||||
seen = current or "unknown"
|
||||
print(f" [prep] foreground package was '{seen}', launched '{package}' (attempt {attempt + 1}/{retries})")
|
||||
|
||||
return current_foreground_package() == package
|
||||
|
||||
|
||||
def maybe_dismiss_telemetry_consent(package: str = APP_PACKAGE,
|
||||
verbose: bool = True) -> bool:
|
||||
"""Dismiss first-launch telemetry consent modal when present."""
|
||||
xml = ui_dump()
|
||||
if not xml:
|
||||
return False
|
||||
|
||||
try:
|
||||
root = ET.fromstring(xml)
|
||||
except ET.ParseError:
|
||||
return False
|
||||
|
||||
consent_markers = (
|
||||
"help improve opencode",
|
||||
"share anonymous crash reports",
|
||||
)
|
||||
dismiss_markers = (
|
||||
"not now", "no thanks", "decline", "skip", "later",
|
||||
"don't allow", "dont allow", "deny", "continue without",
|
||||
"opt out", "cancel",
|
||||
)
|
||||
|
||||
page_text = " ".join(
|
||||
" ".join(filter(None, [
|
||||
node.attrib.get("text", ""),
|
||||
node.attrib.get("content-desc", ""),
|
||||
])).lower()
|
||||
for node in root.iter()
|
||||
)
|
||||
|
||||
if not any(marker in page_text for marker in consent_markers):
|
||||
return False
|
||||
|
||||
candidates = []
|
||||
for node in root.iter():
|
||||
clickable = node.attrib.get("clickable") == "true"
|
||||
if not clickable:
|
||||
continue
|
||||
|
||||
label = " ".join(filter(None, [
|
||||
node.attrib.get("text", ""),
|
||||
node.attrib.get("content-desc", ""),
|
||||
node.attrib.get("resource-id", ""),
|
||||
])).strip().lower()
|
||||
center = _bounds_center(node.attrib.get("bounds", ""))
|
||||
if not center:
|
||||
continue
|
||||
candidates.append((label, center))
|
||||
|
||||
for label, (x, y) in candidates:
|
||||
if any(marker in label for marker in dismiss_markers):
|
||||
adb("shell", "input", "tap", str(x), str(y))
|
||||
time.sleep(1.0)
|
||||
if verbose:
|
||||
print(f" [prep] dismissed telemetry consent via '{label or 'button'}' at ({x}, {y})")
|
||||
return True
|
||||
|
||||
if verbose:
|
||||
print(" [prep] telemetry consent detected but dismiss button was not found")
|
||||
return False
|
||||
|
||||
|
||||
def screenshot_b64() -> str:
|
||||
@@ -255,7 +358,6 @@ def execute_action(action: dict) -> str:
|
||||
|
||||
elif act == "send":
|
||||
# Auto-locate send button: rightmost clickable ViewGroup in the bottom input bar
|
||||
import re
|
||||
xml = ui_dump()
|
||||
# Find the EditText (message input) and the clickable element immediately after it
|
||||
# The send button is the last clickable ViewGroup in the input row
|
||||
@@ -476,8 +578,35 @@ SMOKE_SCENARIOS = [
|
||||
"Report success if you see two assistant reply bubbles."
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "verify_session_list",
|
||||
"goal": (
|
||||
"You see the OpenCode mobile app. Tap the '+' button (top-right) to create a new session. "
|
||||
"Wait 2 seconds for the session to be created. "
|
||||
"Navigate back to the sessions list by tapping the bottom-left 'Sessions' tab or pressing the back button. "
|
||||
"Wait 3 seconds for the session list to load. "
|
||||
"Report success if you can see at least one session entry in the list. "
|
||||
"Report failure if the sessions list appears empty or shows an error message."
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
# Extended scenarios requiring an external OpenCode server.
|
||||
# Run with: python scripts/android-cua-smoke.py --opencode-url http://<host>:<port>
|
||||
def _connect_and_verify_sessions_goal(url: str) -> str:
|
||||
return (
|
||||
f"You see the OpenCode mobile app. "
|
||||
"Go to the Connections tab (bottom navigation bar). "
|
||||
"If a connection to the server already exists, tap it to make it active and skip to the next step. "
|
||||
"Otherwise tap '+' or 'Add Connection', "
|
||||
f"enter the URL '{url}', leave username/password blank, tap Save or Connect. "
|
||||
"Wait 3 seconds. "
|
||||
"Now navigate to the Sessions tab (bottom navigation bar). "
|
||||
"Wait 5 seconds for sessions to load. "
|
||||
"Report SUCCESS if you see at least one session listed (a session title is visible). "
|
||||
"Report FAILURE if the sessions list is empty, shows 'No sessions yet', or shows an error."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Android CUA smoke test")
|
||||
@@ -486,6 +615,16 @@ def main():
|
||||
parser.add_argument("--max-steps", type=int, default=30)
|
||||
parser.add_argument("--include-xml", action="store_true", help="Include UI XML in context")
|
||||
parser.add_argument("--quiet", action="store_true")
|
||||
parser.add_argument(
|
||||
"--opencode-url",
|
||||
help="OpenCode server URL (e.g. http://100.108.64.76:4096). "
|
||||
"Used by the default connect-and-verify regression scenario.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-connect-scenario",
|
||||
action="store_true",
|
||||
help="Skip the default connect-and-verify regression scenario.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Verify ADB
|
||||
@@ -496,7 +635,15 @@ def main():
|
||||
except FileNotFoundError:
|
||||
sys.exit("adb not found in PATH")
|
||||
|
||||
scenarios = [{"name": "custom", "goal": args.goal}] if args.goal else SMOKE_SCENARIOS
|
||||
scenarios = [{"name": "custom", "goal": args.goal}] if args.goal else list(SMOKE_SCENARIOS)
|
||||
|
||||
# Keep connect-and-verify in the default smoke path so regressions are exercised.
|
||||
if not args.goal and not args.skip_connect_scenario:
|
||||
connect_url = args.opencode_url or os.environ.get("OPENCODE_URL") or "http://100.108.64.76:4096"
|
||||
scenarios.append({
|
||||
"name": "connect_and_verify_sessions",
|
||||
"goal": _connect_and_verify_sessions_goal(connect_url),
|
||||
})
|
||||
|
||||
results = []
|
||||
for scenario in scenarios:
|
||||
@@ -511,6 +658,11 @@ def main():
|
||||
local_video = f"/tmp/cua_{scenario['name']}.mp4"
|
||||
|
||||
try:
|
||||
if not ensure_app_foreground(verbose=not args.quiet):
|
||||
print(f" [prep] warning: could not confirm {APP_PACKAGE} in foreground")
|
||||
maybe_dismiss_telemetry_consent(verbose=not args.quiet)
|
||||
ensure_app_foreground(verbose=not args.quiet)
|
||||
|
||||
result = run_cua(
|
||||
goal=scenario["goal"],
|
||||
max_steps=args.max_steps,
|
||||
|
||||
Reference in New Issue
Block a user