Commit Graph

7 Commits

Author SHA1 Message Date
Ignacy Łątka 1706fb5c2a fix(boot-device): don't tear down a healthy-but-slow emulator on the final boot attempt (#305)
## Problem

On Linux (headless, software-GPU) I hit `boot-device` repeatedly failing
with
`PackageManager did not respond ... Emulator has been terminated`, even
though
the emulator had booted fine. Root cause: **Stage 5** of `attemptBoot`
ran a
single `pm path android` probe with a 10 s timeout and, on failure,
killed the
emulator and threw.

On a loaded host (or a freshly `-wipe-data`'d image still finishing its
first-boot package scan), `pm` can take well over 10 s to answer while
the guest
is perfectly healthy — it already reached `sys.boot_completed` and
registered
with adb. On the **final cold attempt** there is no fallback, so a
momentarily
slow PackageManager turned a usable emulator into a hard boot failure
(and
destroyed it, so the next try started from a cold boot again).

## Fix

- Stage 5 now **retries** `pm path android` within a budget instead of a
single
  10 s window.
- Threads a `tearDownIfUnready` flag through `attemptBoot`:
- **hot-boot attempt** — tight ~10 s budget, tear down on failure so we
still
    fall through to the cold boot.
- **final cold attempt** — retry up to 45 s and, if PM is still slow,
**return
the emulator as booted rather than destroying it**. gRPC
screenshots/gestures
work without PM, and killing it just guarantees failure with nothing to
fall
back to. A confirmed QEMU crash (mid-probe or via the early-exit racer)
still
    tears down and rethrows the real cause.

Also adds a note to the `argent-android-emulator-setup` skill:
hard-killing qemu
(`pkill -9`) leaves the userdata image dirty — the usual cause of a cold
boot
that hangs for minutes with runaway writes — and `-wipe-data` resets it.

## Tests

`packages/tool-server/test/boot-device-hotboot.test.ts` — added a
regression
test asserting the final cold attempt returns the emulator as `booted`
(and does
**not** issue `adb emu kill`) when PM stays slow. All 28 boot-device
tests pass.

## Out of scope / follow-ups (found during the same Linux session)

- `simulator-server` (Rust) **panics** (`android_emulator.rs:104 "should
be
running"`) when asked to attach to a pre-existing emulator launched with
`-read-only` that never registered gRPC discovery. Better to return a
clear
"can't attach, reboot via Argent" error. Separate (cross-language)
change.
- `screenshot-diff` with `captureCurrent: true` fails reproducibly with
  `"server response missing url or path"` while plain `screenshot` and
  file-vs-file diff both work.
- `react-profiler-component-source` returns `found: false` for valid
top-level
  `export function` components.
2026-06-03 22:23:29 +02:00
Ignacy Łątka 2be58a5cf1 fix(boot-device): five correctness fixes from post-merge Linux review (#296)
## Summary

Five issues found in the post-merge code review of #249 (Linux support).
All confirmed present on main; none fixed by #194.

### H1 — `awaitFirstRealFrame` timeout leaves emulator running

`awaitFirstRealFrame` is called after the cold-boot `try/catch` closes.
On a SurfaceFlinger timeout the emulator process stayed alive:
`inFlightBoots.finally` cleared the map entry, so the next `boot-device`
call found the stale serial via the `alreadyRunning` fast path, had to
run `assertScreencapAlive`, killed it there, and then fell through to
another full cold boot — doubling latency and surfacing a confusing
"wedged framebuffer" error instead of the original timeout.

**Fix:** wrap the `awaitFirstRealFrame` call in a try/catch that calls
`killEmulatorQuietly` before re-throwing.

### H2 — `awaitFirstRealFrame` budget not clamped against
`overallDeadline`

Every `attemptBoot` stage clamps via `Math.min(params.attemptDeadline,
…)`. `awaitFirstRealFrame` received the fixed
`STAGE_BUDGET.firstRealFrame` (90 s) with no knowledge of
`overallDeadline`, allowing total wall time to silently exceed
`bootTimeoutMs` by up to 90 s — violating the documented contract and
the Zod schema's `max(900_000)`.

**Fix:** pass `Math.min(STAGE_BUDGET.firstRealFrame, Math.max(0,
overallDeadline - Date.now()))`.

### H3 — `ARGENT_SIMULATOR_SERVER_DIR` semantic changed without
migration hint

The env var previously pointed directly at the directory containing
`simulator-server`; it now points at the root of the per-platform tree.
A user with the old flat layout gets a bare "binary not found" with no
explanation of what changed.

**Fix:** detect the old flat layout and include a migration hint in the
error message; update the env var's doc comment.

### M4 — `ARGENT_EMULATOR_NO_WINDOW=false` silently enables headless
mode

`selectExtraEmulatorArgs` used a deny-list (`!== "0"`) so `"false"` and
`"no"` — natural shell idioms for "disabled" — both mapped to
`-no-window`. Test suite covered `""`, `" "`, `"0"` but not `"false"`.

**Fix:** switch to an allowlist `["1", "true", "yes"]`; add `"false"`
and `"no"` to the parameterized off-test.

### M5 — `bootstrapDylibPathTcp` / `nativeDevtoolsDylibPathTcp` missing
`requireDarwin`

All six non-TCP dylib/binary exports gained explicit `requireDarwin()`
guards in #249. The two TCP dylib exports did not. A Linux caller gets
"dylib not found" instead of "requires macOS host".

**Fix:** add `requireDarwin()` as the first statement in both TCP dylib
functions; add a test.

### S7 (bonus) — `selectGpuMode` validation fires mid-function after
slow I/O

`selectGpuMode()` and `selectExtraEmulatorArgs()` were called inside
deep nested code after several seconds of I/O. A typo in
`ARGENT_EMULATOR_GPU_MODE` surfaced late with a spurious "emulator has
been terminated" suffix appended.

**Fix:** call both once at the top of `bootAndroidImpl`, just after
`ensureDep`, and reuse the captured values — eliminates the late error
and the redundant double-call.

## Test plan

- `npm run test -w @argent/tool-server` — **847 passed** (was 845; +2
for `false`/`no` NO_WINDOW cases)
- `npm run test -w @argent/native-devtools-ios` — **10 passed** (was 8;
+2 for TCP dylib darwin gate and migration hint)
2026-06-03 17:32:28 +02:00
Ignacy Łątka 80661fad8f feat: Linux support (#249)
This PR makes Linux work.

# AI Summary 

<details>
Brings Argent up on Linux as a host: per-platform npm bundle, renderer
selection that actually works on hybrid-GPU / Wayland / containerized
Linux, hot-boot guarding that survives the SwiftShader composite gap, a
real Linux preflight, and a Wayland-headless E2E job in CI. macOS stays
on
the path it was on before (no `-gpu` flag was passed; AVD config
decided);
nothing about the iOS Simulator behavior changes.

The diff is broader than "ship the Linux artifact" — calling it out
below
because several pieces affect macOS too and reviewers should be primed
for
that rather than skim past expecting "just packaging."

### What's actually in here

**npm packaging (the original framing — still load-bearing)**

| File | Change |
|---|---|
| `packages/argent/scripts/argent-simulator-server.cjs` (new) | Node
dispatcher exposed as the `argent-simulator-server` bin entry. Joins
`process.platform` into the bin path and execs. Forwards
SIGTERM/SIGINT/SIGHUP parent→child so non-TTY supervisors can't orphan
the binary. |
| `packages/argent/scripts/bundle-tools.cjs` | Copies per-platform
binaries from `packages/native-devtools-ios/bin/<platform>/` into
`packages/argent/bin/<platform>/`. darwin required, linux best-effort. |
| `packages/argent/package.json` | `bin.argent-simulator-server` points
at the dispatcher. |
| `packages/native-devtools-ios/src/index.ts` | Resolver joins
`process.platform` into the simulator-server bin path. iOS-only dylibs
and `ax-service` throw `"requires a macOS host"` on Linux callers so
they get the root cause, not "file not found". |
| `scripts/download-simulator-server.sh` | Downloads
`simulator-server-argent-{macos,linux}` into platform-keyed subdirs.
**Purges each platform dir before its download attempt** so a failed
download can't leave stale binaries in place from a prior run. |
| `scripts/dev.cjs` | Local dev copies the current host's binary into
`bin/<process.platform>/` to match the resolver layout. |
| `.github/workflows/publish{,-next}.yml` | Replaces single `test -x
darwin/...` with a per-platform inventory loop. darwin always required;
Linux gated by `ARGENT_REQUIRE_LINUX_BINARY=1` (flip once radon#118
lands). |

**Linux runtime — emulator boot path**

| File | Change |
|---|---|
| `packages/tool-server/src/tools/devices/boot-device.ts` |
`selectGpuMode()`: Linux defaults to `-gpu swiftshader` (`auto` lands on
lavapipe with ~10× cold-boot slowdown; `-gpu host` produces silent
black-window failures on Optimus / NVIDIA+libglvnd / Wayland-on-hybrid /
containers). macOS keeps `auto` (ANGLE→Metal).
`ARGENT_EMULATOR_GPU_MODE` overrides, **validated against `emulator
-help-gpu`'s whitelist** so a typoed value fails fast instead of burning
the whole boot budget. `ARGENT_EMULATOR_NO_WINDOW=1` appends
`-no-window` for headless contexts (CI / containers / Wayland sessions
with no Qt wayland plugin). |
| `packages/tool-server/src/tools/devices/boot-device.ts` (cont.) |
**Cold-boot:** `awaitFirstRealFrame` polls SwiftShader's lockscreen
composite (PNG byte-count via `screencap -p`); blocks `booted:true`
until a real frame shows up. Without this, callers chaining `boot-device
→ screenshot` get an all-black PNG because `sys.boot_completed=1` fires
5–60 s before the first composite. |
| `packages/tool-server/src/tools/devices/boot-device.ts` (cont.) |
**Hot-boot:** `assertScreencapAlive` polls within an 8 s budget instead
of single-probing. The earlier single-probe caught the sticky-blank
state but also killed the emulator on every transient snapshot-restore
blank, forcing "every boot is cold" cycles. Sticky vs transient is
discriminated by whether the budget exhausts. |
| `packages/tool-server/src/utils/linux-preflight.ts` (new) |
`linuxBootDiagnostics()` checks `/dev/kvm` access. `diagnoseAvdSizing()`
warns when a target AVD's `hw.ramSize`/`vm.heapSize` is below the floor
an aggressive RN/Expo app needs (2G/228M avdmanager defaults wedge under
load). |
| `packages/tool-server/src/utils/android-binary.ts` |
`defaultAndroidRoots()` probes typical SDK install locations when
`ANDROID_HOME`/`ANDROID_SDK_ROOT` are unset — **cross-platform**,
benefits macOS launches from GUI-launched MCP hosts (Claude Code Desktop
etc.) where shell rc env doesn't propagate. |

**Cross-platform fixes that landed alongside**

| File | Change |
|---|---|
| `packages/argent-installer/src/utils.ts` | `getLatestVersion()` now
redirects `npm view`'s stderr to `ignore` so `EBADDEVENGINES` warnings
from a consumer project's `package.json` (e.g. Bluesky's pnpm pin) don't
leak to the user's terminal. **Cross-platform fix**, not Linux-specific.
|

**CI**

| File | Change |
|---|---|
| `.github/workflows/wayland-e2e.yml` (new) | Boots an AVD through the
tool-server under headless Weston; asserts `hw.gpu.mode = swiftshader`
and that screenshots + gesture-tap work without a display. Currently
runs on `pull_request` for the affected paths + `workflow_dispatch`.
Pinned to `radon-daily` for the simulator-server until radon#118 ships a
Linux asset to `radon-main`. |

### Tests added in this PR

- `packages/argent/test/dispatcher.test.ts` — e2e against tmpdir-staged
dispatcher + fake `<platform>/simulator-server`: missing-binary error
path, exit-code passthrough, argv passthrough, SIGTERM forwarding.
- `packages/tool-server/test/native-devtools-ios-resolver.test.ts` —
`requireDarwin` gates for dylibs and `ax-service` on Linux, per-platform
`simulator-server` path resolution, missing-binary error,
`ARGENT_NATIVE_DEVTOOLS_DIR` override.
- `packages/tool-server/test/linux-preflight.test.ts` — KVM diagnostic
shape + `diagnoseAvdSizing` (ram low, heap low, both, neither, missing
keys).
- `packages/tool-server/test/boot-device-hotboot.test.ts` — extended
with `it.each([linux→swiftshader, darwin→auto])` so a single CI runner
exercises both GPU branches (was runner-platform-dependent, regressions
could pass on both runners individually). New cases for the wedged-frame
fallback (under fake timers because the polling budget is short but
real-time enough to exceed vitest's 5 s default), the GPU-mode whitelist
rejection, and the cold-boot composite-wait timeout.
- `packages/tool-server/test/android-binary.test.ts` — extended for
`defaultAndroidRoots` probe ordering and HOME pinning.

### Verification

- `tsc --build`: clean apart from pre-existing
`@argent/native-devtools-android` import errors unrelated to this
branch.
- `vitest run` in `packages/tool-server` and `packages/argent`: 689 + 11
= 700 tests passing; the 4 pre-existing test-file load failures
(native-devtools-android missing) reproduce on `main` and are out of
scope.
- `npm pack -w @swmansion/argent`: produces `swmansion-argent-0.8.0.tgz`
with the per-platform layout.
- Wayland-e2e: triggered on this branch — link will be in a PR comment
once the run completes.
- Manually verified end-to-end on a Wayland-on-hybrid-GPU developer
laptop: AVD boots through the tool-server with `-gpu swiftshader`,
screencap returns real frames, gesture tools work.

### Dependencies / what still gates this

- **radon#118** must land before the Linux artifact ships through the
default channel. The download script tolerates the asset being absent
today (best-effort), so this PR is technically safe to merge before —
Linux installs just won't get a working `simulator-server` until the
upstream artifact is published. Once it lands:
  - Drop the `radon-daily` arg in `.github/workflows/wayland-e2e.yml`.
  - Flip `ARGENT_REQUIRE_LINUX_BINARY` to `1` in both publish workflows.
- **wayland-e2e is not a required status check** today (no branch
protection on `main` requires it). Worth turning on before merging
anything else that touches the Linux boot path.

### Related

- radon#118 — produces the `simulator-server-argent-linux` artifact this
PR consumes.
- argent-private#11 — cross-builds the iOS Simulator dylibs on Linux.
Strictly orthogonal: those dylibs are macOS-runtime-only regardless of
where they're built, and this PR keeps them under `dylibs/` with a clean
error path on Linux callers.

</details>

Fixes #246
2026-06-03 15:55:39 +02:00
Ignacy Łątka cb8db6d4ac feat(boot-device): pass canonical launch-hardening flags (#269)
## Summary

argent's boot-device currently spawns `emulator -avd <name> -gpu auto`
plus the snapshot flags — nothing else. Five flags every MCP-driven boot
benefits from were missing; add them as `LAUNCH_HARDENING_ARGS` and
spread into the snapshot probe, hot boot, and cold boot.

### Performance (3 flags)

- `-noaudio` — guest skips pulseaudio init; saves one thread and ~50 MB
RSS.
- `-no-boot-anim` — skips the Pixel boot animation, a major CPU spike on
software-rendered GPU modes for ~30 s.
- `-netfast` — disables qemu network shaping; pure overhead for MCP use.

Measured on a 4-core i5-6300U with a 4096 MB / 228 MB-heap AVD,
warm-cache cold boot, no other workload, `-gpu swiftshader`:

| | Boot time | Δ |
|---|---|---|
| Baseline (no extras) | 66 s | — |
| + 3 perf flags | 49 s | −26% |
| + all 5 (incl. dialog suppression) | 47 s | −29% |

qemu RSS at +20 s post-boot drops ~190 MB (5%) with the perf flags.
`android-emulator-runner` (the canonical CI launcher) defaults to the
same three.

### Dialog suppression (2 flags)

- `-crash-report-mode never` — keeps emulator crashes from popping a Qt
consent dialog that blocks the next boot until a human dismisses it.
Crash dumps still land in `/tmp/android-unknown/emu-crash-*.db`; only
the modal popup is suppressed.
- `-no-metrics` — same for the metrics-collection consent dialog.
Unrelated to any argent profiler tool: those run guest-side via
Perfetto/simpleperf or via Metro CDP, none of which depend on the
emulator's host-side Google telemetry.

### Snapshot parity

`-noaudio` and `-netfast` change qemu device topology. The snapshot's
saved state has to match the runtime config the next boot uses or the
snapshot validator rejects perfectly loadable snapshots — the same class
of bug as the prior `-gpu auto` parity work that introduced
`RENDERER_ARGS`. Plumb the hardening args into the same three call sites
(probe `extraArgs`, hot spawn, cold spawn) so the probe agrees with both
boot paths.

## Test plan

- [x] Unit tests: extended `boot-device-hotboot.test.ts` with two new
cases asserting the flags reach the hot-boot and cold-boot spawn argv;
tightened the "same renderer args" parity test to also require
`-noaudio`/`-netfast` on the probe. **712 tests pass.**
- [x] Live-emulator verification on Linux: actual emulator boot with all
5 flags completes in 47 s, no flag-rejection warnings in the emulator
log.
- [ ] Confirm no regression on a macOS test host (please run locally
before merging — I can only verify Linux here).
- [ ] Confirm CI green.

Pre-existing test failure in `android-binary.test.ts` on `origin/main`
reproduces without this change — host-side `$PATH` leakage in the test
setup, not in scope for this PR.
2026-06-01 17:15:47 +02:00
Ignacy Łątka 324d8c6049 fix(android): stop forcing cold boot on every emulator launch (#251)
## Summary

`hasDefaultBootSnapshot` enforced a 60s mtime-skew guard between
`snapshot.pb` and `ram.bin`. In practice the emulator updates
`snapshot.pb` metadata on every load (load count, last-loaded timestamp)
even with `-no-snapshot-save`, so the two mtimes drift apart by hours or
days after a few hot-boot sessions and the guard rejects every valid
snapshot — forcing a cold boot every time.

The bug hits macOS too; Linux just makes it painful because cold boot is
slow there. Confirmed against my own `Pixel_6_API_34`, whose
`snapshot.pb` is 4 days newer than `ram.bin` while the snapshot itself
is fully usable.

## What changed

- **Drop the mtime-skew guard.** `-check-snapshot-loadable` + the
hot-boot spawn's `-force-snapshot-load` already make truncated / corrupt
`ram.bin` fail loudly via `attemptBoot`'s `earlyExitError` race. The
on-disk pre-check only needs to confirm both files exist and `ram.bin`
is non-empty.
- **Resolve the `.avd` folder via the AVD's `.ini` `path=` line**,
matching what the emulator binary itself does
(`external/qemu/android/android-emu/android/avd/util.cpp`).
Snap-installed Studio on Linux and users who relocate AVDs to faster
disks both put `.avd` outside the convention root, where the old
`<root>/<name>.avd` assumption silently failed.
- **Honor `$ANDROID_USER_HOME`, `$XDG_CONFIG_HOME`,
`$ANDROID_SDK_HOME`** in priority order alongside `$ANDROID_AVD_HOME`
and `$HOME/.android/avd`, mirroring the emulator binary's own lookup
precedence.
- **Reject whitespace-only and relative `path=` values.** The regex's
non-greedy `(.+?)` still captures a single space for `path= `, which
would have leaked past `if (!avdPath)` as truthy; a relative `path=`
would have resolved against `process.cwd()` and silently mis-located
snapshots.

## Test plan

- [x] 18 new unit tests in `avd-snapshot.test.ts` covering: priority
order across all five env-var candidates, relocated AVDs, drifted mtimes
(4-day skew explicitly regression-tested), zero-length / missing /
directory-shaped files, whitespace-only paths, relative paths,
bad-then-good fallthrough.
- [x] Full `npm run test -w @argent/tool-server` — 703 passing.
- [x] `npm run typecheck:tests -w @argent/tool-server` — clean.
- [x] `npm run build` from workspace root — clean.
- [x] `npx prettier --check` on changed files — clean.
- [x] End-to-end repro against a real local AVD: drifted
`Pixel_3a_API_34`'s `snapshot.pb` mtime to 5 days newer than `ram.bin`
(the exact pathology in the wild), drove `boot-device` through this
branch's built code, observed `qemu-system-aarch64 ...
-force-snapshot-load -no-snapshot-save` in the spawned process — i.e.
the hot-boot path was taken with a snapshot the old check would have
rejected.

## Risk

Pre-check is now more lenient on disk shape. If `ram.bin` is non-empty
but truncated/corrupt, the hot-boot spawn still catches it via
`-force-snapshot-load` (loud early child-exit) and `attemptBoot` falls
back to cold boot. So the worst case has not regressed; it just surfaces
at spawn-time instead of pre-spawn.
2026-05-27 16:24:31 +02:00
Ignacy Łątka 11df817616 test: drop real-time timeouts from the slowest unit tests (#236)
The slowest unit tests were waiting on real wall-clock timers, not doing
work. This makes them deterministic and fast with **no production
behavior change and no production test seams**.

## Speedup

**Suite-level**

| Scope | Before | After | Faster |
|---|---|---|---|
| tool-server — wall clock | 2.72s | ~1.3s | ~2× |
| tool-server — aggregate test time | 9.70s | 4.03s | 2.4× |
| argent-mcp — aggregate test time | 0.90s | 0.28s | 3.2× |

**Per file**

| File | Before | After | Faster |
|---|---|---|---|
| simulator-server-blueprint.test.ts | 2.55s | 0.05s | ~50× |
| boot-device-hardening.test.ts | 2.17s | 0.18s | 12× |
| boot-device-spawn-error.test.ts | 1.11s | 0.005s | ~200× |
| boot-device-hotboot.test.ts | 1.01s | 0.06s | 16× |
| fetch-timeout.test.ts | 0.81s | 0.18s | 4.5× |
| flow-tools.test.ts (delayMs test) | 0.31s | 0.04s | ~7× |

**Worst individual tests, before → after**

| Test | Before | After |
|---|---|---|
| rejects the boot promise…when spawn emits error | 1.06s | ~1ms |
| reports the emulator crash error… | 1.00s | ~2ms |
| falls back to cold boot when hot-boot child exits early | 1.00s | ~1ms
|
| reports a signal-terminated emulator (SIGSEGV)… | 1.00s | ~1ms |
| 5× simulator-server-blueprint tests | ~0.50s ea | ~0.001–0.02s ea |
| throws after retries when POST never responds | 0.60s | 0.14s |
| sleeps the step's delayMs | 0.31s | 0.04s |

No test exceeds ~0.7s anymore (the slowest,
`workspace-reader-integration`, is genuine subprocess I/O — `node
--version` etc. — left as-is on purpose; it is not a timeout).

## What changed

- **`boot-device.ts` (only production change):** the three hardcoded
poll cadences are extracted into a single immutable
`BOOT_POLL_INTERVALS_MS` constant (values unchanged: 1500/1000/500ms)
and referenced at the four `setTimeout` sites. Pure readability
extraction — **no mutable state, no exported test hooks, no runtime
behavior change.**
- **Boot-device tests:** the timing-sensitive tests now use vitest fake
timers (`vi.useFakeTimers()` + `vi.advanceTimersByTimeAsync`).
`boot-device-hardening` / `-spawn-error` add the same partial
`vi.mock("../src/utils/adb")` already used by
`boot-device-hotboot.test.ts`, so pre-flight does no real filesystem I/O
and the timers drive deterministically.
- **`simulator-server-blueprint.test.ts`:** `signalReady` now emits
`stream_ready` then `api_ready` (matching the real streaming server's
ordering) so the blueprint resolves immediately instead of waiting out
the 500ms `STREAM_GRACE_MS` fallback. Adds a dedicated fake-timer test
that explicitly covers the non-streaming grace path (coverage gained,
not lost).
- **`fetch-timeout.test.ts` / `flow-tools.test.ts`:** smaller timing
knobs; same behaviors asserted.

## No regressions

- 1011/1011 tests pass (tool-server 656, argent-mcp 53, argent 6,
registry 64, tools-client 1, installer 231); `tsc --build` and Prettier
clean. The converted boot-device, simulator-server-blueprint, and
flow-tools tests are deterministic under fake timers (verified across
stress runs).
- No production behavior change; `simulator-server.ts` is byte-identical
to `main`; net +1 test.
- Heads-up: `argent-mcp/test/fetch-timeout.test.ts` has a
**pre-existing** macOS flake (~10–20% rate; `connect EADDRNOTAVAIL
127.0.0.1:<port> - Local (0.0.0.0:0)` — kernel source-port exhaustion
under rapid-fire loopback fetches). Stress-testing showed it on `main`'s
pristine file too (2/10 failures vs 1/10 on this branch), so this PR
does not introduce or worsen it. Fixing it (e.g., EADDRNOTAVAIL retry,
or moving the test off real loopback fetch) is worth a follow-up.
2026-05-27 16:24:07 +02:00
Ignacy Łątka 837d088744 test: cover #148 (android) (#186)
This PR adds tests for #148.

## Summary

Moves the Android emulator branch's test coverage out of #148 so that PR
can be reviewed without the test churn. This branch adds the test files
back on top of the (now test-stripped) base branch.

Tests inseparable from source changes (`describe-tool`,
`workspace-reader`, `workspace-reader-integration`) and the
`boot-simulator.test.ts` deletion stayed on #148 - base PR's CI passes
on its own.

## Test plan

- [x] `npx tsc --build` — clean
- [x] `npx prettier --check .` — clean
- [x] `npm test --workspaces --if-present` — 585/585 tool-server tests
pass; all other workspaces green

---------

Co-authored-by: Paweł Fornagiel <pawel.fornagiel2@gmail.com>
2026-05-12 14:21:09 +02:00