Two commits. The first corrects docs/api-37-emulator-crash.md with what a CI
investigation measured; the second splits the API 37 leg so the part that works can gate.
1. Measure the API 36 control and record what CI cannot measure
Documentation only, plus one wrong figure in api37-debug.yml's own comments.
A third measured bullet: API 36 against API 37, back to back, same two tests, same
renderer, same SystemUI-disable path. 37.0 fails both on c2.goldfish.h264.decoder
(32660148155);
36 passes both in 4.603 s with the same decoder in its logcat
(32660152961).
That falsifies "the stripped configuration is what breaks these tests". It carries its two
uncontrolled variables rather than dropping them.
The decoder-mechanism bullet is unchanged and still labelled inference. This adds a
measurement next to it; it retracts nothing.
The intact-SystemUI counterfactual is unmeasurable on a GitHub runner. Seven dispatches,
zero verdicts, with a mechanism rather than bad luck: while the framework crash-loops the
guest cannot reliably create per-user private directories, so an app installed during the
loop has no cache dir and the fixture copy dies in @Before before any codec exists. The
result XML masks this behind an UninitializedPropertyAccessException in tearDown, which
reads as a defect in this repository and is not one.
Abort cadence corrected. "Roughly every 20 s" was the watchdog's sampling interval:
measured gaps are 20–90 s, median 60–70 s, three to five per run.
2. Split the API 37 leg so the part that works can gate
CI has never run the API level this app targets, because two tests fail on the emulator image
and one row would be permanently red or permanently allow-listed.
job
runs
gates
E2E API 37
55 of 57 (notAnnotation)
yes, must be green
E2E API 37 Media3 hardware transcode (advisory)
the other 2 (annotation)
no, continue-on-error
One marker, @FailsOnEmulatorApi37, drives both. Two lists would drift, and drift is
silent in both directions — a test in neither job reads as green. Excluding by class was
not an option: Media3EngineTest has four tests and two of them pass here.
The advisory job is named for what it runs. Both its tests drive a full H.264 → H.265
hardware transcode, which is what distinguishes them from the two Media3EngineTest cases
that pass — those never decode video. The goldfish-decoder theory is in a comment inside the
job, where it can be corrected without renaming a check people have learned to look for.
The SystemUI disable moves into .github/scripts/e2e-run.sh behind E2E_DISABLE_SYSTEM_UI, unset on the other four legs, which therefore run byte-identical
commands. It runs before the streamed logcat starts: adb shell stop would end that
logcat and nothing restarts it. The body is probe v2 (three rounds, waits for system_server to actually be gone, verifies pm list packages -d, requires a zero-abort
window) — the weaker one-round probe reported success on a run that then started SystemUI
eight more times.
The caveat is next to the row: this leg runs with SystemUI disabled and the framework
restarted under it, a configuration no other leg and no Pixel run uses. Anything touching
system UI must not trust it.
What to check on this run
The count is the discriminator, not the failure count. If notAnnotation silently did not
apply you get 57 tests with 2 failures, which reads as "expected red" and is easy to wave
through.
E2E API 37 → tests="55" failures="0" errors="0" skipped="2"
(grep -rho '@Test' app/src/androidTest | wc -l = 57 on this branch, minus the 2 marked)
advisory → tests="2" failures="2"
API 33–36 → unchanged.
Follow-up, deliberately NOT done here
E2E API 37 is not yet a required check. Adding a required context that does not exist on main blocks every PR, so the ruleset change has to come after this merges:
The advisory job must not be added — it is continue-on-error by design.
CLAUDE.md is untouched. Its "CI's matrix therefore stops at API 36" clause is now false;
that correction is parked in the doc's existing "Correction owed to CLAUDE.md" section
alongside two others already waiting.
Two commits. The first corrects `docs/api-37-emulator-crash.md` with what a CI
investigation measured; the second splits the API 37 leg so the part that works can gate.
## 1. `Measure the API 36 control and record what CI cannot measure`
Documentation only, plus one wrong figure in `api37-debug.yml`'s own comments.
- **A third measured bullet**: API 36 against API 37, back to back, same two tests, same
renderer, same SystemUI-disable path. 37.0 fails both on `c2.goldfish.h264.decoder`
([32660148155](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32660148155));
36 passes both in 4.603 s with the same decoder in its logcat
([32660152961](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32660152961)).
That falsifies "the stripped configuration is what breaks these tests". It carries its two
uncontrolled variables rather than dropping them.
- The decoder-mechanism bullet is **unchanged** and still labelled inference. This adds a
measurement next to it; it retracts nothing.
- **The intact-SystemUI counterfactual is unmeasurable on a GitHub runner.** Seven dispatches,
zero verdicts, with a mechanism rather than bad luck: while the framework crash-loops the
guest cannot reliably create per-user private directories, so an app installed during the
loop has no cache dir and the fixture copy dies in `@Before` before any codec exists. The
result XML masks this behind an `UninitializedPropertyAccessException` in `tearDown`, which
reads as a defect in this repository and is not one.
- **Abort cadence corrected.** "Roughly every 20 s" was the watchdog's *sampling* interval:
measured gaps are 20–90 s, median 60–70 s, three to five per run.
## 2. `Split the API 37 leg so the part that works can gate`
CI has never run the API level this app targets, because two tests fail on the emulator image
and one row would be permanently red or permanently allow-listed.
| job | runs | gates |
|---|---|---|
| `E2E API 37` | 55 of 57 (`notAnnotation`) | yes, must be green |
| `E2E API 37 Media3 hardware transcode (advisory)` | the other 2 (`annotation`) | no, `continue-on-error` |
- **One marker, `@FailsOnEmulatorApi37`**, drives both. Two lists would drift, and drift is
silent in both directions — a test in neither job reads as green. Excluding by *class* was
not an option: `Media3EngineTest` has four tests and two of them pass here.
- **The advisory job is named for what it runs.** Both its tests drive a full H.264 → H.265
hardware transcode, which is what distinguishes them from the two `Media3EngineTest` cases
that pass — those never decode video. The goldfish-decoder theory is in a comment inside the
job, where it can be corrected without renaming a check people have learned to look for.
- **The SystemUI disable moves into `.github/scripts/e2e-run.sh`** behind
`E2E_DISABLE_SYSTEM_UI`, unset on the other four legs, which therefore run byte-identical
commands. It runs **before** the streamed logcat starts: `adb shell stop` would end that
logcat and nothing restarts it. The body is probe v2 (three rounds, waits for
`system_server` to actually be gone, verifies `pm list packages -d`, requires a zero-abort
window) — the weaker one-round probe reported success on a run that then started SystemUI
eight more times.
- **The caveat is next to the row**: this leg runs with SystemUI disabled and the framework
restarted under it, a configuration no other leg and no Pixel run uses. Anything touching
system UI must not trust it.
## What to check on this run
The count is the discriminator, not the failure count. If `notAnnotation` silently did not
apply you get 57 tests with 2 failures, which reads as "expected red" and is easy to wave
through.
- `E2E API 37` → **`tests="55" failures="0" errors="0" skipped="2"`**
(`grep -rho '@Test' app/src/androidTest | wc -l` = 57 on this branch, minus the 2 marked)
- advisory → **`tests="2" failures="2"`**
- API 33–36 → unchanged.
## Follow-up, deliberately NOT done here
`E2E API 37` is not yet a *required* check. Adding a required context that does not exist on
`main` blocks every PR, so the ruleset change has to come **after** this merges:
```
ruleset 21117412 (JMR-dev/LibreMediaConverter, "main")
rules[].type == required_status_checks
→ add { "context": "E2E API 37", "integration_id": 15368 }
```
The advisory job must **not** be added — it is `continue-on-error` by design.
`CLAUDE.md` is untouched. Its "CI's matrix therefore stops at API 36" clause is now false;
that correction is parked in the doc's existing "Correction owed to `CLAUDE.md`" section
alongside two others already waiting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Starting 57 tests each — the filter is inert there
success
Unit tests, Static analysis, FFmpeg binary
—
success
55 = 57 − 2, so notAnnotation applied. Had it silently not applied, this would have been 57
tests with 2 failures, which reads as "expected red".
continue-on-error behaves as intended: the advisory check run stays visibly failure while
the workflow run conclusion is success, so it reports without blocking.
Both advisory failures are the known signature and nothing new — name=c2.goldfish.h264.decoder, dequeueOutputBuffer(MediaCodec.java:4274).
The relocated disable works, and the hardening earned its keep immediately
E2E API 37, round 1:
--- SystemUI disable, round 1 ---
pm attempt 1: Package com.android.systemui new state: disabled-user
system_server down after ~16 s
services back after ~10 s
verified: com.android.systemui is in pm list packages -d
abort rate, SystemUI disabled: 0 new in 45 s (total 2)
final state: SystemUI disabled
Starting 55 tests on test(AVD) - 17
BUILD SUCCESSFUL in 2m 54s
The advisory job needed two rounds on this very run — the first did not verify — which is
exactly the case the one-round probe used to report as success. Cost: ~7 min for the whole job,
against ~6 at API 36.
The logcat-api37.txt artifact is 10.5 MB and its last entry is 22:06:14, one second after the
test XML's timestamp, with 208 TestRunner lines. That confirms the reason the disable runs before the stream starts: it still covers the Gradle window rather than stopping at adb shell stop.
Independent second measurement
Dispatch 32669193446 ran
the same notAnnotation filter at API 37 through api37-debug.yml's own probe path instead of the
new e2e-run.sh one: Starting 55 tests, BUILD SUCCESSFUL. So the filter and the relocated
disable are each confirmed by a run that does not depend on the other.
One flake, not this change
E2E API 35 failed on the first attempt with Instrumentation run failed due to Process crashed
after 8 tests. Its log shows Starting 57 tests, i.e. no filter reached it, and nothing in this
diff touches app/src/main or that leg's runtime path. It passed on rerun.
Tooling run against this diff
actionlint 1.7.12 (all three workflows), shellcheck 0.10.0 (e2e-run.sh, clean), PyYAML
structural check, and locally compileDebugAndroidTestKotlin + ktlintCheck + detekt + lintDebug — all green.
## CI result — [run 32669190757](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32669190757), conclusion **success**
The count was the thing to check, and it is right:
| check | result XML | conclusion |
|---|---|---|
| `E2E API 37` | `tests="55" failures="0" errors="0" skipped="2" time="24.261"` | success |
| `E2E API 37 Media3 hardware transcode (advisory)` | `tests="2" failures="2" errors="0" skipped="0"` | failure, `continue-on-error` |
| `E2E API 33` / `34` / `35` / `36` | `Starting 57 tests` each — the filter is inert there | success |
| `Unit tests`, `Static analysis`, `FFmpeg binary` | — | success |
55 = 57 − 2, so `notAnnotation` applied. Had it silently not applied, this would have been 57
tests with 2 failures, which reads as "expected red".
**`continue-on-error` behaves as intended**: the advisory check run stays visibly `failure` while
the workflow run conclusion is `success`, so it reports without blocking.
Both advisory failures are the known signature and nothing new — `name=c2.goldfish.h264.decoder`,
`dequeueOutputBuffer(MediaCodec.java:4274)`.
### The relocated disable works, and the hardening earned its keep immediately
`E2E API 37`, round 1:
```
--- SystemUI disable, round 1 ---
pm attempt 1: Package com.android.systemui new state: disabled-user
system_server down after ~16 s
services back after ~10 s
verified: com.android.systemui is in pm list packages -d
abort rate, SystemUI disabled: 0 new in 45 s (total 2)
final state: SystemUI disabled
Starting 55 tests on test(AVD) - 17
BUILD SUCCESSFUL in 2m 54s
```
The advisory job needed **two** rounds on this very run — the first did not verify — which is
exactly the case the one-round probe used to report as success. Cost: ~7 min for the whole job,
against ~6 at API 36.
The `logcat-api37.txt` artifact is 10.5 MB and its last entry is `22:06:14`, one second after the
test XML's timestamp, with 208 `TestRunner` lines. That confirms the reason the disable runs
*before* the stream starts: it still covers the Gradle window rather than stopping at
`adb shell stop`.
### Independent second measurement
[Dispatch 32669193446](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32669193446) ran
the same `notAnnotation` filter at API 37 through `api37-debug.yml`'s own probe path instead of the
new `e2e-run.sh` one: `Starting 55 tests`, `BUILD SUCCESSFUL`. So the filter and the relocated
disable are each confirmed by a run that does not depend on the other.
### One flake, not this change
`E2E API 35` failed on the first attempt with `Instrumentation run failed due to Process crashed`
after 8 tests. Its log shows `Starting 57 tests`, i.e. no filter reached it, and nothing in this
diff touches `app/src/main` or that leg's runtime path. It passed on rerun.
### Tooling run against this diff
`actionlint 1.7.12` (all three workflows), `shellcheck 0.10.0` (`e2e-run.sh`, clean), PyYAML
structural check, and locally `compileDebugAndroidTestKotlin` + `ktlintCheck` + `detekt` +
`lintDebug` — all green.
Force-pushed: three prose over-claims corrected, then re-run clean
A review pass caught three places where the new text outran its evidence — the exact failure mode
commit 1 exists to stop, and one this file has already been corrected for three times
(792286a, 775a447, 961cfa7). Corrected in the commit that introduced each, so both commits
still stand alone. No behavioural change: the tree is otherwise byte-identical to the one that
produced the green run above.
The CI environment line claimed one system image for a table whose control row is API 36.
c2 is the API 36 control and runs that level's own image — which is the point of it. Now stated
per-row instead of blanket.
"the suite runs under either renderer once SystemUI is gone" had no row behind it. The
swangle+disabled cell is c5 (32645543238, 57/2/0/2); it had been trimmed out of the table
while the claim it supported stayed. c5 is back.
"the same SystemUI-disable path" was claimed across API 33–36, which never ran it. E2E_DISABLE_SYSTEM_UI is empty on those legs and run-e2e.sh gates its local equivalent on 37 | 37.*. Only the API 36 control (32660152961) ran that path. Both the doc bullet and the
advisory job's comment now say the accurate — and stronger — thing: pass at API 36 under the
same renderer and the same disable path, and pass at 33–36 without needing that path at all,
because nothing below 37 has the bug it works around.
Also added, not blocking: E2E API 37 Media3 hardware transcode (advisory) going green is now a "When to revisit" trigger in the doc. continue-on-error means nothing announces it — the
image fixing itself would look exactly like a check nobody reads quietly ceasing to be red.
--- SystemUI disable, round 1 ---
verified: com.android.systemui is in pm list packages -d
abort rate, SystemUI disabled: 0 new in 45 s (total 2)
final state: SystemUI disabled
Starting 55 tests on test(AVD) - 17
BUILD SUCCESSFUL in 2m 57s
All eight other checks green including E2E API 35, which confirms its earlier failure was the
flake it looked like. The advisory check is red by design and does not block: the run conclusion
is success.
## Force-pushed: three prose over-claims corrected, then re-run clean
A review pass caught three places where the new text outran its evidence — the exact failure mode
commit 1 exists to stop, and one this file has already been corrected for three times
(`792286a`, `775a447`, `961cfa7`). Corrected in the commit that introduced each, so both commits
still stand alone. **No behavioural change: the tree is otherwise byte-identical to the one that
produced the green run above.**
1. **The CI environment line claimed one system image for a table whose control row is API 36.**
c2 is the API 36 control and runs that level's own image — which is the point of it. Now stated
per-row instead of blanket.
2. **"the suite runs under either renderer once SystemUI is gone" had no row behind it.** The
swangle+disabled cell is c5 (`32645543238`, 57/2/0/2); it had been trimmed out of the table
while the claim it supported stayed. c5 is back.
3. **"the same SystemUI-disable path" was claimed across API 33–36, which never ran it.**
`E2E_DISABLE_SYSTEM_UI` is empty on those legs and `run-e2e.sh` gates its local equivalent on
`37 | 37.*`. Only the API 36 control (`32660152961`) ran that path. Both the doc bullet and the
advisory job's comment now say the accurate — and stronger — thing: *pass at API 36 under the
same renderer and the same disable path, and pass at 33–36 without needing that path at all,
because nothing below 37 has the bug it works around.*
Also added, not blocking: `E2E API 37 Media3 hardware transcode (advisory)` going green is now a
**"When to revisit"** trigger in the doc. `continue-on-error` means nothing announces it — the
image fixing itself would look exactly like a check nobody reads quietly ceasing to be red.
### [Run 32670321869](https://github.com/JMR-dev/LibreMediaConverter/actions/runs/32670321869) — conclusion **success**, no reruns
```
--- SystemUI disable, round 1 ---
verified: com.android.systemui is in pm list packages -d
abort rate, SystemUI disabled: 0 new in 45 s (total 2)
final state: SystemUI disabled
Starting 55 tests on test(AVD) - 17
BUILD SUCCESSFUL in 2m 57s
```
All eight other checks green including `E2E API 35`, which confirms its earlier failure was the
flake it looked like. The advisory check is red by design and does not block: the run conclusion
is `success`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Two commits. The first corrects
docs/api-37-emulator-crash.mdwith what a CIinvestigation measured; the second splits the API 37 leg so the part that works can gate.
1.
Measure the API 36 control and record what CI cannot measureDocumentation only, plus one wrong figure in
api37-debug.yml's own comments.renderer, same SystemUI-disable path. 37.0 fails both on
c2.goldfish.h264.decoder(32660148155);
36 passes both in 4.603 s with the same decoder in its logcat
(32660152961).
That falsifies "the stripped configuration is what breaks these tests". It carries its two
uncontrolled variables rather than dropping them.
measurement next to it; it retracts nothing.
zero verdicts, with a mechanism rather than bad luck: while the framework crash-loops the
guest cannot reliably create per-user private directories, so an app installed during the
loop has no cache dir and the fixture copy dies in
@Beforebefore any codec exists. Theresult XML masks this behind an
UninitializedPropertyAccessExceptionintearDown, whichreads as a defect in this repository and is not one.
measured gaps are 20–90 s, median 60–70 s, three to five per run.
2.
Split the API 37 leg so the part that works can gateCI has never run the API level this app targets, because two tests fail on the emulator image
and one row would be permanently red or permanently allow-listed.
E2E API 37notAnnotation)E2E API 37 Media3 hardware transcode (advisory)annotation)continue-on-error@FailsOnEmulatorApi37, drives both. Two lists would drift, and drift issilent in both directions — a test in neither job reads as green. Excluding by class was
not an option:
Media3EngineTesthas four tests and two of them pass here.hardware transcode, which is what distinguishes them from the two
Media3EngineTestcasesthat pass — those never decode video. The goldfish-decoder theory is in a comment inside the
job, where it can be corrected without renaming a check people have learned to look for.
.github/scripts/e2e-run.shbehindE2E_DISABLE_SYSTEM_UI, unset on the other four legs, which therefore run byte-identicalcommands. It runs before the streamed logcat starts:
adb shell stopwould end thatlogcat and nothing restarts it. The body is probe v2 (three rounds, waits for
system_serverto actually be gone, verifiespm list packages -d, requires a zero-abortwindow) — the weaker one-round probe reported success on a run that then started SystemUI
eight more times.
restarted under it, a configuration no other leg and no Pixel run uses. Anything touching
system UI must not trust it.
What to check on this run
The count is the discriminator, not the failure count. If
notAnnotationsilently did notapply you get 57 tests with 2 failures, which reads as "expected red" and is easy to wave
through.
E2E API 37→tests="55" failures="0" errors="0" skipped="2"(
grep -rho '@Test' app/src/androidTest | wc -l= 57 on this branch, minus the 2 marked)tests="2" failures="2"Follow-up, deliberately NOT done here
E2E API 37is not yet a required check. Adding a required context that does not exist onmainblocks every PR, so the ruleset change has to come after this merges:The advisory job must not be added — it is
continue-on-errorby design.CLAUDE.mdis untouched. Its "CI's matrix therefore stops at API 36" clause is now false;that correction is parked in the doc's existing "Correction owed to
CLAUDE.md" sectionalongside two others already waiting.
🤖 Generated with Claude Code
CI result — run 32669190757, conclusion success
The count was the thing to check, and it is right:
E2E API 37tests="55" failures="0" errors="0" skipped="2" time="24.261"E2E API 37 Media3 hardware transcode (advisory)tests="2" failures="2" errors="0" skipped="0"continue-on-errorE2E API 33/34/35/36Starting 57 testseach — the filter is inert thereUnit tests,Static analysis,FFmpeg binary55 = 57 − 2, so
notAnnotationapplied. Had it silently not applied, this would have been 57tests with 2 failures, which reads as "expected red".
continue-on-errorbehaves as intended: the advisory check run stays visiblyfailurewhilethe workflow run conclusion is
success, so it reports without blocking.Both advisory failures are the known signature and nothing new —
name=c2.goldfish.h264.decoder,dequeueOutputBuffer(MediaCodec.java:4274).The relocated disable works, and the hardening earned its keep immediately
E2E API 37, round 1:The advisory job needed two rounds on this very run — the first did not verify — which is
exactly the case the one-round probe used to report as success. Cost: ~7 min for the whole job,
against ~6 at API 36.
The
logcat-api37.txtartifact is 10.5 MB and its last entry is22:06:14, one second after thetest XML's timestamp, with 208
TestRunnerlines. That confirms the reason the disable runsbefore the stream starts: it still covers the Gradle window rather than stopping at
adb shell stop.Independent second measurement
Dispatch 32669193446 ran
the same
notAnnotationfilter at API 37 throughapi37-debug.yml's own probe path instead of thenew
e2e-run.shone:Starting 55 tests,BUILD SUCCESSFUL. So the filter and the relocateddisable are each confirmed by a run that does not depend on the other.
One flake, not this change
E2E API 35failed on the first attempt withInstrumentation run failed due to Process crashedafter 8 tests. Its log shows
Starting 57 tests, i.e. no filter reached it, and nothing in thisdiff touches
app/src/mainor that leg's runtime path. It passed on rerun.Tooling run against this diff
actionlint 1.7.12(all three workflows),shellcheck 0.10.0(e2e-run.sh, clean), PyYAMLstructural check, and locally
compileDebugAndroidTestKotlin+ktlintCheck+detekt+lintDebug— all green.Force-pushed: three prose over-claims corrected, then re-run clean
A review pass caught three places where the new text outran its evidence — the exact failure mode
commit 1 exists to stop, and one this file has already been corrected for three times
(
792286a,775a447,961cfa7). Corrected in the commit that introduced each, so both commitsstill stand alone. No behavioural change: the tree is otherwise byte-identical to the one that
produced the green run above.
c2 is the API 36 control and runs that level's own image — which is the point of it. Now stated
per-row instead of blanket.
swangle+disabled cell is c5 (
32645543238, 57/2/0/2); it had been trimmed out of the tablewhile the claim it supported stayed. c5 is back.
E2E_DISABLE_SYSTEM_UIis empty on those legs andrun-e2e.shgates its local equivalent on37 | 37.*. Only the API 36 control (32660152961) ran that path. Both the doc bullet and theadvisory job's comment now say the accurate — and stronger — thing: pass at API 36 under the
same renderer and the same disable path, and pass at 33–36 without needing that path at all,
because nothing below 37 has the bug it works around.
Also added, not blocking:
E2E API 37 Media3 hardware transcode (advisory)going green is now a"When to revisit" trigger in the doc.
continue-on-errormeans nothing announces it — theimage fixing itself would look exactly like a check nobody reads quietly ceasing to be red.
Run 32670321869 — conclusion success, no reruns
All eight other checks green including
E2E API 35, which confirms its earlier failure was theflake it looked like. The advisory check is red by design and does not block: the run conclusion
is
success.