Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions .agents/skills/qv-mobile-test-dispatch/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
---
name: qv-mobile-test-dispatch
description: Start an AWS Device Farm mobile integration test for an addon and pick the right prebuild source, so the run tests the binary the developer means rather than the published release. Covers run ids, GPR dev builds, published pins, test filters, device names, and reading the result. Use when someone asks to run mobile tests, test an addon on a device/phone, test a native change on mobile, or invokes /qv-mobile-test-dispatch.
disable-model-invocation: true
---

# mobile-test-dispatch

Mobile integration tests run on **AWS Device Farm**, which is billed per device
minute. They do not run automatically on PRs β€” someone dispatches them by hand,
choosing one platform, the device(s), and usually a test filter.

The part that goes wrong is **which binary ends up on the phone**. A dispatch
does not compile the addon; it installs a prebuilt one. Get that wrong and the
run is green against code nobody changed.

Canonical reference: [`docs/ci/MOBILE-ON-DEMAND.md`](../../../docs/ci/MOBILE-ON-DEMAND.md).
Read it once per session before answering detailed questions; this skill is the
operating procedure, that doc is the source of truth.

## When to use this skill

- "Run mobile tests for `<addon>`"
- "Test my native change on a device / on a phone"
- "Why did my mobile run test the wrong build?"
- "How do I get a run id?"

## Safety rules

- **Device Farm costs money.** Never dispatch the full suite to explore. Always
pass a `tests` filter and the smallest device set that answers the question.
- **`llm-llamacpp` is sharded** (7 Android groups, 13 iOS groups). An empty
`tests` filter fans out the whole set as separate Device Farm runs. Always
filter for LLM.
- **One platform per dispatch.** Android and iOS are separate runs.
- **A second dispatch of the same workflow on the same branch cancels the
first.** To cover both platforms, either wait, or use different addons in
parallel.
- Never dispatch on someone's behalf without telling them it bills Device Farm.

## Step 1 β€” decide which binary should be tested

| goal | input |
|---|---|
| my own PR's native change | `prebuild_run_id=<run id>` |
| a build from another branch, or a published release | `package=@tetherto/<addon>-mono@<dev>` or `package=@qvac/<addon>@<ver>` |
| just the published release | leave both empty |

`prebuild_run_id` and `package` are **mutually exclusive** β€” setting both fails
with a message telling you to clear one.

The input is named `package` on most addons but **`package_spec`** on
`asr-ggml`, `audiogen-ggml`, `tts-ggml`. `prebuild_run_id` is the same everywhere.

## Step 2 β€” get a run id (only for the `prebuild_run_id` route)

**First: the PR must have built prebuilds at all.** The prebuild stage is
label-gated by `ci-router` β€” it runs only when the PR carries `prebuilds`,
`run-desktop-addon-tests`, or `run-mobile-addon-tests`. With none of those there
is no bundle and no run id. Add the `prebuilds` label and let CI re-run.

**Then:** open the PR's Checks tab, click the run that built the prebuilds, and
take the number at the end of its URL.

Do **not** filter by the addon's own workflow name. Which workflow built the
bundle varies β€” `on-pr-nx.yml` for most addons, `on-pr-<addon>.yml` for some,
`on-merge-<addon>.yml` for a branch build. Scope by the PR's head commit:

```bash
PKG=llm-llamacpp # the package directory name, i.e. packages/<PKG>
PR=1234

SHA=$(gh pr view "$PR" --repo tetherto/qvac --json headRefOid --jq .headRefOid)
for rid in $(gh api "repos/tetherto/qvac/actions/runs?head_sha=$SHA&per_page=100" \
--jq '.workflow_runs[].id'); do
gh api "repos/tetherto/qvac/actions/runs/$rid/artifacts?per_page=100" \
--jq ".artifacts[]|select(.name==\"prebuilds-$PKG\" and .expired==false)|.name" \
2>/dev/null | grep -q . && { echo "$rid"; break; }
done

# An empty result must not be dispatched: prebuild_run_id="" is the unchanged
# path and quietly resolves @latest, which is the failure this route closes.
```

Nothing printed means either the label is missing, or β€” on the nx path β€” that run
only built the addons it considered affected and yours was not one. The dispatch
failure message lists which addons a run did build.

## Step 3 β€” pick a valid test filter

`tests` is a mocha `--grep` over runner **names**, not file names. A name that
matches nothing is rejected up front by `validate-devices`, for free, with the
list of valid names β€” so a wrong guess costs nothing but a round trip.

Read the names from the same source `validate-devices` uses:

```bash
# sharded addons (llm-llamacpp, diffusion-cpp, tts-ggml, audiogen-ggml, vla, ...)
jq -r '(.android//{})|[..|strings]|unique|.[]' packages/<PKG>/test/mobile/test-groups.json

# single-spec addons
grep -oE '\brun[A-Z][A-Za-z0-9_]*' packages/<PKG>/test/mobile/integration.auto.cjs | sort -u
```

If a name is rejected on device with
`[prestage] FATAL: tests grep /<name>/ matched no known runner`, it is in neither
the addon's `test-groups.json` nor its `integration.auto.cjs` β€” i.e. a typo. Take
a name from the commands above. (That FATAL used to fire for *valid* runners too,
because the prestage generator kept its own list; `readKnownRunners()` now reads
`test-groups.json` directly.)

## Step 4 β€” dispatch

```bash
gh workflow run integration-mobile-test-<addon>.yml --repo tetherto/qvac --ref <branch> \
-f platform=Android \
-f devices_custom="Google Pixel 9" \
-f device_model_operator=EQUALS \
-f tests=<runnerName> \
-f prebuild_run_id=<run id>
```

- `devices_custom` takes a comma-separated list and overrides the `device`
dropdown. Names are full fleet names (`Google Pixel 9`, `Apple iPhone 16 Pro`).
- `device_model_operator=EQUALS` bills exactly that model; `CONTAINS` may pick a
different variant.
- `ref` selects the JS harness, tests and app β€” **not** the native binary. It and
the prebuild source are deliberately independent.

## Step 5 β€” read the result

The build job's setup phase prints the provenance:

```
Verified: prebuilds come from run <id> β€” artifact 'prebuilds-<pkg>',
workflow '<name>', head <sha>, branch <branch> (<repo>), <conclusion>
```

Check the **head SHA** is the commit you meant β€” a run id resolves whether or not
it built the code under review.

Warnings worth acting on:

- `run <id> concluded 'failure'` β€” the source run was red. Its prebuild job may
still be the green part, but confirm.
- `run <id> built code from the FORK '<repo>'` β€” normal for a fork PR (the repo
is fork-first), but confirm you meant that contributor's code.

The run-id path **fails closed** β€” a wrong, private, unfinished or expired run id
fails the run with the reason rather than falling back to `@latest`.

## Per-addon notes

| addon | note |
|---|---|
| `llm-llamacpp` | sharded β€” always pass `tests` |
| `asr-ggml`, `audiogen-ggml` | `@qvac/*` publishes **no mobile prebuilds**, so an empty input cannot work. Use `prebuild_run_id` or the GPR `-mono` build. |
| `audiogen-ggml` | pins its composite actions to the default branch, so `prebuild_run_id` only works once that support is on `main`; it fails loudly with instructions until then |
| `vla` | package dir is `packages/vla-ggml`, workflow slug is `vla` |
| `decoder-audio` | no native prebuild of its own (rides `bare-ffmpeg` from npm). `package` has no effect; use `ref`. |
| `inference-addon-cpp` | compiles its own prebuilds in-run from the dispatched `ref`, so no prebuild input is needed or offered |

## Reading a failure β€” where the logs are

The `console-logs-*` artifact on the run is where everything lands. The
`test-results.json` in it only records the harness assertion
(`expect(received).toBe(expected)` at `app.test.js`), which is identical for
every failure and never says why. The real reason is in the app's own output,
and the file differs per platform.

| what | Android | iOS |
|---|---|---|
| JS / bare runtime, TAP lines, the failure | `logcat_full.txt`, `bare` tag | `bare_console.log` |
| **native C++ / engine output** | `logcat_full.txt`, `bare` tag, `[C++ TEST]` prefix | `bare_console.log`, `[C++ TEST]` prefix |
| app shell | `logcat_full.txt`, `ReactNativeJS` tag | `bare_console.log` |
| device/OS noise | `logcat_full.txt` (most of it) | `iOS_appium.log` |

```bash
gh run download <run-id> --repo tetherto/qvac --dir ./logs

# Android β€” the bare runtime carries BOTH the JS and the C++ output
grep -aE "E bare|I bare" logs/**/*logcat_full.txt | head -40 # test + errors
grep -a "\[C++ TEST\]" logs/**/*logcat_full.txt | head -40 # native/engine

# iOS β€” same two, one file
grep -aE "error|not ok" logs/**/*bare_console.log | head -40
grep -a "\[C++ TEST\]" logs/**/*bare_console.log | head -40
```

Traps that cost real time:

- **Use `logcat_full.txt`, not `Logcat.logcat`.** They are different files;
the latter is a smaller capture and does not carry the bare output.
- **Grep the `bare` tag, not TAP markers or the package name.** The runtime
prints through logcat, so `TAP version`/`ok 1` never appear as raw lines.
- Native C++ lines are prefixed `[C++ TEST] [INFO]: [Llama.cpp] ...` on both
platforms β€” the engine logs through the same channel, not a separate tag.
- There is **no `bare_console.log` on Android**, by construction: the app writes
it into its private data dir, which adb cannot read and `run-as` refuses on a
release-signed APK. That is expected β€” logcat is the Android channel.

A real example, the whole reason a run went red, invisible in `test-results.json`:

```
E bare: Test 'runFitStubTest' failed: AddonError: ADDON_NOT_FOUND:
Cannot find addon '.' from @qvac/model-fit/binding.js
Candidates: - linked:libqvac__model-fit.0.12.0.so
[cause]: Error: dlopen fail
```
2 changes: 2 additions & 0 deletions .agents/skills/qv-mobile-test-dispatch/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
policy:
allow_implicit_invocation: false
2 changes: 2 additions & 0 deletions .agents/skills/qv-skill-list/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ When unsure which skill fits, scan the tables below or ask: *"which qv skill sho
| [`qv-devops-pr-status`](../qv-devops-pr-status/SKILL.md) | Team DevOps PR dashboard: re-review, stale, needs-review, conflicts. | DevOps pod PR queue health. **Manual:** `/qv-devops-pr-status` |
| [`qv-devops-why-my-pr-not`](../qv-devops-why-my-pr-not/SKILL.md) | Diagnose missing CI checks or merge blockers (labels, CODEOWNERS, approvals). | "Why aren't checks running?" / "Why can't I merge?" **Manual:** `/qv-devops-why-my-pr-not` |
| [`qv-devops-daily-update`](../qv-devops-daily-update/SKILL.md) | Slack standup (Done / Planned / Blockers) from PRs, reviews, CI. | DevOps EOD or standup. **Manual:** `/qv-devops-daily-update` |
| [`qv-mobile-test-dispatch`](../qv-mobile-test-dispatch/SKILL.md) | Start an addon mobile (Device Farm) test and pick the right prebuild source; where the Android/iOS and C++ logs are. | "Run mobile tests for X" / "test my native change on a device" / reading a mobile failure. **Manual:** `/qv-mobile-test-dispatch` |

---

Expand Down Expand Up @@ -128,6 +129,7 @@ Rule nudge: `.cursor/rules/qip-triage.mdc`
| SDK team PR board | `qv-sdk-pr-status` |
| DevOps team PR board | `qv-devops-pr-status` |
| Why CI/merge is blocked | `qv-devops-why-my-pr-not` |
| Run mobile tests on a device | `qv-mobile-test-dispatch` |
| Write SDK PR body | `qv-sdk-pr-create` |
| Sync SDK models.ts from registry | `qv-sdk-update-models` |
| Write addon PR body | `qv-addon-pr-create` |
Expand Down
2 changes: 2 additions & 0 deletions .github/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Read the relevant references before changing CI:
- [`../docs/ci/SELF-HOSTED-RUNNERS.md`](../docs/ci/SELF-HOSTED-RUNNERS.md) for
persistent runners and workspace cleanup.
- [`../docs/ci/TEAMS.md`](../docs/ci/TEAMS.md) for approval ownership.
- [`../docs/ci/MOBILE-ON-DEMAND.md`](../docs/ci/MOBILE-ON-DEMAND.md) for dispatching
addon mobile (Device Farm) tests and choosing the prebuild source.
- [`../docs/agent-automation.md`](../docs/agent-automation.md) for automation safety.

When editing workflows or composite actions:
Expand Down
Loading
Loading