Commit Graph

492 Commits

Author SHA1 Message Date
pFornagiel 4a4186f103 feat: enhance DescribeNode interface and parsing logic for Android UI automation 2026-05-05 14:55:39 +02:00
Ignacy Łątka 7eca4bea76 simplify simulator-server.ts 2026-05-05 00:12:17 +02:00
Ignacy Łątka eace957876 reset package-lock to main 2026-05-04 23:59:28 +02:00
Ignacy Łątka 66ec1e84a4 make RN checklist platform agnostic 2026-05-04 23:33:52 +02:00
Ignacy Łątka a3ad857e11 isolate HID keymaps 2026-05-04 23:33:05 +02:00
Ignacy Łątka abb55104ee chore(tests): move additive tests to stacked PR
The 19 net-new test files added on this branch, plus the test-only
modifications to skills/update/auto-screenshot/native-devtools-status,
are moved to feat/android-emulator-support-tests, opened as a stacked
PR on top of this one. Tests inseparable from source changes
(describe-tool, workspace-reader{,integration}) and the boot-simulator
deletion stay here so this PR's CI keeps passing on its own.
2026-05-04 23:09:42 +02:00
Ignacy Łątka 89d8200359 boot-device: guard fast-path against wedged framebuffers; drop noWindow input
Three changes bundled here, all on the Android boot path.

1. Wedged-framebuffer bug guard on the reuse fast-path. A long-running
   emulator can drift into the same sticky-blank SurfaceFlinger state
   that `assertScreencapAlive` defends against on a hot-boot restore:
   every Android-side readiness probe still passes, but `screencap`
   returns only null bytes — meaning the caller silently gets a serial
   whose screenshots are all black, and (since `coldBoot` was removed)
   no escape hatch. The fast-path now runs the same screencap probe; on
   failure the helper kills the wedged emulator and we fall through to
   the snapshot/hot/cold pipeline so the caller still gets a usable
   serial. Labelled `BUG GUARD` in the source.

2. Cold-boot error suffix wording: `"Hot-boot was also attempted and
   failed"` was accurate only when a spawn happened and crashed; it read
   wrong on the no-snapshot/probe-rejected branches where no hot-boot
   was attempted. Now `"Hot-boot was not viable"`, which covers both.

3. `noWindow` input dropped. The window should always be visible
   (matching iOS Simulator). Removed from the zod schema, internal type
   signatures, both `-no-window` arg-push sites, and every test call
   site; added two `not.toContain("-no-window")` regression guards.

Note: real emulator spawns now require a display. Linux CI without an
X server will need `xvfb-run` (the in-repo Vitest suite mocks `spawn`
so unit tests are unaffected).

Tests: full tool-server suite passes (585/585) including a new test
covering the wedged-framebuffer fall-through.
2026-05-04 23:03:35 +02:00
Ignacy Łątka 205b8c89b0 refactor(boot-device): drop coldBoot input; auto-detection covers every case
The Android boot path already falls back to cold boot on every known
hot-boot failure mode (no snapshot on disk, -check-snapshot-loadable
rejects, ram.bin corruption, blank framebuffer post-restore). Exposing
coldBoot as an MCP input gave the agent a knob it had no signal to set
correctly — and a wrongly-passed coldBoot:true silently turns a ~30 s
boot into 2–10 min.

Drops the field from the zod schema, BootDeviceResult, internal type
signatures, the tool description, and the android-emulator-setup skill.
Removes the two tests that exercised only the removed override; the
remaining tests still cover every auto-detect branch (already-running
fast path, hot-boot success, snapshot-missing, probe-rejected, ram.bin
crash, blank-frame fallback).
2026-05-04 22:54:28 +02:00
Ignacy Łątka 531f74b45a revert: drop fixes that are out of scope for the Android branch
Reverts the following 8 commits that landed on this branch but are
unrelated to Android emulator support and bloat the PR diff:

  - 19d2b38 fix(docs): correct Codex and Windsurf config paths in README editors table
  - c239597 fix(stop-metro): only SIGTERM the LISTEN process, not connected clients
  - 4e5e874 fix(native-target-app): tolerate per-app getAppState failures via allSettled
  - 20f6802 fix(network-interceptor): stringify URLSearchParams and tag binary bodies instead of dropping them
  - eeb69ea fix(update-argent): clear the in-progress lock when the spawned update fails
  - c20b4a5 fix(native-find-views): require at least one filter at runtime, matching the docstring
  - fc4999b fix(debugger): selectTarget throws on empty deviceId filter instead of routing to wrong target
  - 90f5a2b fix(idle-timer): block onIdle while a tool call is in flight

Each of these is a real defect, but they touch pre-existing code that
predates this branch. The fix branches remain on origin (fix/*) and can
be reopened as standalone PRs off main.

In-scope fixes are kept:
  - 474d240 fix(adb): match terminal errors that include a quoted serial
  - 93f588d fix(boot-device): handle child spawn errors so tool-server doesn't crash
  - cb4ebfe fix(ios-profiler-analyze): report missing trace files instead of returning All clear (file was renamed on this branch)
  - 38e44f4 fix(describe): declare xcrun dep so missing Xcode returns 424 with install hint (describe became cross-platform on this branch)
2026-05-04 22:26:17 +02:00
Ignacy Łątka a953bf9428 fix(blueprints): reject undefined device.id before spawning helper
simulator-server and ax-service both pipe their helper's stderr through
`process.stderr.write(\`[... ${udid.slice(0, 8)}] ${data}\`)`. A wrapper
that doesn't re-validate the inner tool's schema (e.g. flow-add-step
with stringified args missing `udid`) reaches the factory with
`device.id === undefined`, the helper is spawned with `--id undefined`,
and `udid.slice(0, 8)` then throws inside the async stderr listener —
which propagates to `uncaughtException` and kills the whole tool-server.

- Validate `device.id` is a non-empty string at factory entry of both
  blueprints, throw before spawning.
- Pre-compute a `udidTag` constant in each stderr handler that falls
  back to "?" if udid is empty (defense-in-depth).

Adds blueprint tests covering missing options.device, undefined / empty
/ non-string device.id, and (for ax-service) the apple-only check
firing before the id-shape check.
2026-05-04 22:25:16 +02:00
Ignacy Łątka 90f5a2bce3 fix(idle-timer): block onIdle while a tool call is in flight
The timer was only touched at request start, so long-running tools
(xctrace export, RN builds) could exceed the idle window and trigger an
auto-shutdown mid-response. The HTTP handler now begins/ends an in-flight
counter on the idle timer; the periodic check refuses to fire onIdle while
that counter is non-zero, keeping the server alive until every parked tool
call has flushed its JSON.
2026-05-04 22:10:21 +02:00
Ignacy Łątka fc4999ba33 fix(debugger): selectTarget throws on empty deviceId filter instead of routing to wrong target
Previously, when callers passed a deviceId/deviceName that matched no CDP
targets, selectTarget silently fell back to the unfiltered list and picked
the first target — silently routing every debugger/profiler call (which
flow device_id through JsRuntimeDebugger:port:device_id) to the wrong
physical device. Throw with the requested id and the available
logicalDeviceIds so typos and stale UDIDs surface immediately.
2026-05-04 22:10:18 +02:00
Ignacy Łątka 38e44f4fcc fix(describe): declare xcrun dep so missing Xcode returns 424 with install hint
`describe` on iOS resolves the ax-service whose factory shells out to
`xcrun simctl spawn`; without xcrun on PATH the ENOENT bubbled up as a 500
with a raw "spawn xcrun ENOENT" message instead of the 424 + install hint
that every other cross-platform iOS tool already emits. Add `xcrun` to the
dispatch's iOS-branch `requires` so the preflight short-circuits cleanly.
2026-05-04 22:10:15 +02:00
Ignacy Łątka cb4ebfe1cf fix(ios-profiler-analyze): report missing trace files instead of returning All clear
The XML parsers swallow ENOENT and return [], so when ios-profiler-stop
recorded a path but the file was deleted/unreadable, analyze rendered an
"All clear" report. Pre-flight each non-null path with fs.access so the
tool surfaces a real Export warning naming the missing file.
2026-05-04 22:10:06 +02:00
Ignacy Łątka c20b4a51cd fix(native-find-views): require at least one filter at runtime, matching the docstring
The tool's docstring promises "At least one of className, identifier, label, tag,
or nativeID must be provided" but every filter was `.optional()` in zod with no
runtime guard. Empty calls slipped through to ViewHierarchy.findViews which
returned an empty matches array, leading agents to misread "no matches" as "view
does not exist". Add a runtime check at the top of execute and a vitest
contract suite covering the rejection, the no-RPC short-circuit, and the
single-filter happy path.
2026-05-04 22:08:22 +02:00
Ignacy Łątka eeb69eae26 fix(update-argent): clear the in-progress lock when the spawned update fails
The detached `argent update --yes` spawn had no error/exit listeners, so
when the binary is missing from PATH or the update exits non-zero,
`updateScheduled` stayed true for the lifetime of the tool-server and
every retry returned the "already in progress" lockout. Attach error/exit
handlers that log to stderr and reset the flag, while keeping the child
detached and unref'd so the agent still gets a fast-success response.
2026-05-04 22:08:22 +02:00
Ignacy Łątka 20f6802bed fix(network-interceptor): stringify URLSearchParams and tag binary bodies instead of dropping them
The fetch interceptor used to capture init.body only when it was already a
string, silently discarding URLSearchParams, FormData, Blob, and ArrayBuffer
payloads. Form-urlencoded POSTs and multipart uploads now serialize (or emit
a placeholder sentinel like [FormData]) so agents debugging a request can see
that a body was actually sent.
2026-05-04 22:08:22 +02:00
Ignacy Łątka 4e5e8743b1 fix(native-target-app): tolerate per-app getAppState failures via allSettled
inspectConnectedNativeApps used Promise.all, so a single mid-tear-down
app's rejected getAppState would poison the whole auto-target lookup and
describe would silently return an empty AX tree. Switch to
Promise.allSettled and drop rejected entries, mirroring the sibling
detectFrontmostBundleId in blueprints/native-devtools.ts. resolveNativeTargetApp
now also remembers the original connected count so a survivor of a
multi-app inspection is still classified as frontmost_detected rather
than as the only connected app.
2026-05-04 22:08:22 +02:00
Ignacy Łątka c239597626 fix(stop-metro): only SIGTERM the LISTEN process, not connected clients
`lsof -ti tcp:<port>` returns every pid with a socket on the port —
including connected clients (curl, browsers, debuggers). The tool was
SIGTERMing all of them. Switch to `lsof -ti -sTCP:LISTEN -i tcp:<port>`
so we only target the listener, plus add an end-to-end test that stands
up a real listener with two real client connections and asserts the
clients are spared.
2026-05-04 22:08:22 +02:00
Ignacy Łątka 93f588da41 fix(boot-device): handle child spawn errors so tool-server doesn't crash
`attemptBoot` only listened on `child.on('exit', ...)`, so a `spawn` failure
(ENOENT — emulator binary missing/EACCES, transient FS hiccup) emitted an
`error` event with no listener and Node crashed the tool-server with an
uncaught exception while leaking the in-flight Map entry. Funnel the error
into the same `earlyExitError` race so the boot promise rejects cleanly and
`bootAndroid`'s `finally` clears the map.
2026-05-04 22:08:11 +02:00
Ignacy Łątka 474d240f63 fix(adb): match terminal errors that include a quoted serial
adb's real stderr for a missing/offline device is `error: device 'emulator-5554' not found` — the serial sits between `device` and the verdict, so the previous substring check never fired. Replace the substring list with regex patterns that tolerate an optional quoted serial, restoring the fail-fast path in waitForBootCompleted instead of spinning the full timeoutMs budget.
2026-05-04 22:07:35 +02:00
Ignacy Łątka 19d2b3822b fix(docs): correct Codex and Windsurf config paths in README editors table
The Codex CLI row referenced .codex/config.yaml, but the installer reads
and writes TOML at .codex/config.toml (project) and ~/.codex/config.toml
(global). The Windsurf row pointed to a non-existent .windsurf/mcp.json;
the adapter only ever writes ~/.codeium/windsurf/mcp_config.json. Both
rows now match what the installer actually detects.
2026-05-04 22:07:31 +02:00
Ignacy Łątka 074e6698c1 cleanup native profiler skill 2026-05-04 21:57:44 +02:00
Ignacy Łątka 02cef6d525 cleanup env inspector skill 2026-05-04 21:48:54 +02:00
Ignacy Łątka 5a5731c09b rename refs in tests 2026-05-04 21:45:19 +02:00
Ignacy Łątka e34cdf022a cleanup argent md 2026-05-04 21:45:04 +02:00
Ignacy Łątka ede247670d rename ios skill to match android 2026-05-04 21:44:52 +02:00
Ignacy Łątka b135b5e63a Revert "post-review fix: wrap iOS xcrun errors with verb prefix for parity"
This reverts commit 073c2195ea.
2026-05-04 21:33:46 +02:00
Ignacy Łątka 1b9de3f9f8 Merge remote-tracking branch 'origin/main' into feat/android-emulator-support 2026-05-04 21:17:57 +02:00
Ignacy Łątka fdc3e6d3e7 fix(test): prime adb in simulator-server blueprint test
CI Linux runners don't have `adb` on PATH, so the Android paths in
`simulatorServerBlueprint.factory` hit the real `ensureDep('adb')` probe
and threw `DependencyMissingError`. Locally the tests passed because
`adb` was on PATH. Prime the dep cache in `beforeEach` (matching the
existing pattern in boot-device.test.ts and describe-tool.test.ts) so
the dep check is a no-op and the test stays focused on dispatch
behaviour. Lazy-imported via dynamic `await import(...)` so check-deps.ts
does not load before the hoisted `vi.mock("node:child_process")` factory
has its `spawnMock` initialised.
2026-05-04 21:00:44 +02:00
Ignacy Łątka 6566ddae20 prettier 2026-05-04 20:54:15 +02:00
Ignacy Łątka fa7a4af17b post-review fix: skill docs catch up with cross-platform stop/describe
- argent-react-native-app-workflow Quick Reference still labelled
  stop-simulator-server / stop-all-simulator-servers as 'iOS only'
  (superseded by the cross-platform commits) and only listed the
  iOS run / log commands. Add Android counterparts.
- rules/argent.md session-end hint claimed stop-all-simulator-servers
  is 'for iOS targets'; the tool now tears down Android servers too.
- argent-device-interact described 'describe' as 'current simulator
  screen' — Android uses uiautomator; reword to be device-neutral and
  name both backends.
2026-05-04 18:46:43 +02:00
Ignacy Łątka 75f21880e7 post-review fix: agent docs recognize Kotlin Gradle DSL
Modern AGP templates default to *.gradle.kts (Kotlin DSL); the inspector
agent's classification rule and quality-control reference only listed
the Groovy variant. Update both to call out Kotlin DSL alongside the
Groovy form so a Kotlin-only project still classifies as native Android.
2026-05-04 18:46:18 +02:00
Ignacy Łątka 4a6e2fb47f post-review fix: cover ax-service blueprint Android-rejection gate
Test file covers nativeDevtoolsBlueprint and nativeProfilerSessionBlueprint
gates but axServiceBlueprint is the third iOS-only blueprint with the
same shape and was untested. Add the Android-rejection case and the
missing-options.device case for symmetry.
2026-05-04 18:45:50 +02:00
Ignacy Łątka 073c2195ea post-review fix: wrap iOS xcrun errors with verb prefix for parity
Android branches throw 'am start failed: …', 'adb install failed: …',
'open-url failed: …' — a uniform 'verb failed: detail' shape MCP
clients can pattern-match on. iOS branches let the raw 'Command failed:
xcrun simctl …' bubble out of execFile so the same client check works
on Android but not iOS. Add a small wrapXcrunError helper in
utils/format-error.ts and apply it to launch-app, restart-app,
reinstall-app, and open-url iOS handlers.
2026-05-04 18:45:27 +02:00
Ignacy Łątka 9abb6cff7f post-review fix: alwaysLoad + searchHint on list-devices and boot-device
Both are session-start essentials in the new Android workflow yet were
missing the discoverability flags peers have (screenshot, gesture-tap,
launch-app already set both). Add alwaysLoad and a searchHint covering
ios/android, simulator/emulator, avd, serial, udid keywords so
LLM-driven harnesses surface them when the user is choosing a target.
2026-05-04 18:44:23 +02:00
Ignacy Łątka aff4c534b6 post-review fix: restore .min(1) on run-sequence steps array
A pre-PR snapshot's schema rejected steps: [] up-front; somewhere in
this PR's restructuring the .min(1) was dropped, so an empty array now
silently no-ops with { completed: 0, total: 0, steps: [] } instead of
returning a clear schema error to the caller.
2026-05-04 18:43:48 +02:00
Ignacy Łątka 49e4bca558 post-review fix: split per-platform Services generic in dispatchByPlatform
LaunchAppServices and RestartAppServices declared { nativeDevtools }
as required even though Android's services() returns {} — a documented
lie that would type-check 'services.nativeDevtools.foo()' inside the
Android handler and crash at runtime. Extend dispatchByPlatform to
accept IosServices and AndroidServices as separate generics so each
branch types against its real shape. Reinstall-app/open-url/describe
keep the same shape on both sides; launch-app/restart-app split into
*IosServices (with nativeDevtools) and *AndroidServices (Record<string,
never>).
2026-05-04 18:43:29 +02:00
Ignacy Łątka bec8fd9d64 post-review fix: restart-app forwards optional Android activity
If the user launched a non-launcher activity (e.g. .SettingsActivity)
and then called restart-app, the Android branch silently bounced them
back to the package's default LAUNCHER activity because there was no
activity param at all — the resolver always picked the launcher. Add
the optional activity field to the schema/Params (mirroring launch-app's
shape and regex), and forward it through the same component-string
construction launch-app uses. Ignored on iOS.
2026-05-04 18:41:31 +02:00
Ignacy Łątka e705e26c13 post-review fix: reinstall-app Android uninstalls first for parity with iOS
iOS reinstall-app does 'simctl uninstall' then 'simctl install' so
app data and runtime permissions are wiped. Android did only
'adb install -r -d -g' which preserves data and previously-granted
permissions. Same tool name, divergent post-state. Run 'adb uninstall
${bundleId}' first (swallowing the not-installed case the way iOS
does), then the install. Update the description to match the new
unified semantics.
2026-05-04 18:40:54 +02:00
Ignacy Łątka ca8caf5581 post-review fix: gate adb at simulator-server blueprint factory for Android
The simulator-server binary shells out to adb internally for Android
targets. Without this preflight, a host without android-platform-tools
surfaces the failure as 'Timed out waiting for simulator-server to
become ready' (READY_TIMEOUT_MS = 30s) instead of the structured 424
DependencyMissingError other Android tools emit. The simulator-server-
backed tools (gesture-*, button, keyboard, screenshot, rotate, run-
sequence) share one handler so don't go through dispatchByPlatform's
branch-aware preflight; gating once at the blueprint factory covers
the whole family in a single place.
2026-05-04 18:40:16 +02:00
Ignacy Łątka cded9850d4 post-review fix: include Android emulators in /preview/simulators
The route was filtering /preview/simulators down to platform === 'ios'
so booted Android emulators were invisible to the preview UI even
though list-devices already classifies them and the downstream
/simulator-server/:udid route accepts the serial via resolveDevice.
Map Android entries to the same { udid, name, state, runtime }
shape the iOS branch emits (state 'device' becomes 'Booted', avdName
becomes the display name, sdkLevel formats the runtime label) so the
existing UI dropdown picks them up without UI changes.
2026-05-04 18:39:43 +02:00
Ignacy Łątka 070981d963 post-review fix: coalesce concurrent boots for the same AVD
Two simultaneous bootAndroid calls for the same AVD both passed the
'already running' fast path (no emulator yet) and both spawned QEMU.
The second collided on the AVD's exclusive on-disk lock and bailed
after the boot deadline with a misleading 'Running multiple emulators'
error. Coalesce per-AVD so a duplicate caller reuses the in-flight
result (or shares its rejection).

Tests get a __resetInFlightBootsForTesting hook because some
intentionally abandon a half-started boot to assert orphan-cleanup —
without the reset that leaked promise contaminates the next test that
boots the same AVD.
2026-05-04 18:26:46 +02:00
Ignacy Łątka 45a7ff5737 post-review fix: bound listAndroidDevices enrichment getprops at 5s
Enrichment getprops used the runAdb default of 30s, so a single device
stuck mid-daemon-attach could stall every `adb devices`-driven loop
(boot-device's adb-register stage, list-devices) for 30s × 3 = up to
90s per call. boot-device's adb-register budget is 60s — so a single
hung device exhausts the whole stage.

Cap each enrichment getprop at 5s. Plenty for a healthy device; not
enough to mask the underlying issue.
2026-05-04 18:22:54 +02:00
Ignacy Łątka d52df0e35d post-review fix: waitForBootCompleted fails fast on terminal device states
The retry loop swallowed every getprop failure, so a device that came
up 'unauthorized' / 'offline' / 'not found' burned the full 2-minute
boot budget before producing a generic timeout message. The user
needs to authorise the prompt, reconnect the device, or pick a
different target — telling them only after 2 minutes is unhelpful.
Classify the rewrapped adb error and abort early on those states.
2026-05-04 18:22:15 +02:00
Ignacy Łątka 54e180ca05 post-review fix: react-profiler-status device_id description matches its peers
Every other react-profiler-* tool (start/stop/analyze/renders/fiber-tree/
cpu-summary) describes device_id as 'iOS simulator UDID or Android
logicalDeviceId'; only react-profiler-status said iOS-only. Align the
wording so agents don't conclude the status check is iOS-only.
2026-05-04 18:20:51 +02:00
Ignacy Łątka 91ede5f922 post-review fix: generalize cross-platform tool descriptions
screenshot, gesture-{tap,swipe,pinch,rotate}, button, keyboard, and
run-sequence all advertise an Android capability but their descriptions
and searchHints still said 'simulator screen' / 'simulator server is
not running' — implying iOS only. The simulator-server binary now
dispatches both iOS and Android internally, so the tools really are
cross-platform; the docs just hadn't caught up.
2026-05-04 18:20:14 +02:00
Ignacy Łątka 8d3550c134 post-review fix: argent run --help references the renamed tool
`list-simulators` was deleted and replaced by `list-devices` (which
lists both iOS simulators and Android emulators/AVDs). The CLI help
text still pointed at the old name, so anyone copy-pasting the example
got 'Tool not found.'
2026-05-04 18:18:19 +02:00
Ignacy Łątka c2f23cb853 post-review fix: open-url Android matches more failure shapes
am start can fail without an 'Error:' prefix — a deep link to a
permission-protected intent emits 'Permission Denial' /
'SecurityException' / 'requires permission' / 'denied'. Without these
in the matcher the tool returned { opened: true } while nothing
happened. Widen the regex.
2026-05-04 18:17:59 +02:00
Ignacy Łątka 132fce728d post-review fix: HTTP capability gate also reads parsedData.device_id
The gate keyed only on parsedData.udid, so debugger/profiler/network
tools (which spell the parameter `device_id`) silently bypassed
capability checks. An Android serial reaching, say, native-profiler-start
fell through to the deeper blueprint and surfaced as HTTP 500 instead
of the clean 400 the gate would have produced. Honour both names.
2026-05-04 18:17:42 +02:00