ReviewOS

also looking at this

craft-native/craft

fix(android): make every Zig decline speak, and prove Zig answers

#205
Merged glennmichael123 wants to merge fix/android-zig-serves into main
8 files +616 -255
scripts/mobile-e2e/android.tsmodified+24-10
Changes to scripts/mobile-e2e/android.ts
@@ -2,7 +2,7 @@ import type { LegOutcome, RunnerOptions } from './types'
22import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
33import { join } from 'node:path'
44import { init } from '../../packages/android/src/index'
5import { evaluateRun, hasTerminated } from './protocol'
5import { androidDeclines, evaluateRun, hasTerminated } from './protocol'
66import { command, driverPage, waitForFile } from './support'
77
88/**
@@ -189,17 +189,23 @@ async function runLeg(leg: Leg, options: RunnerOptions): Promise<LegOutcome> {
189189 failures.push(`the bridge reported sdkVersion ${observed[1]} but the device says ${sdk}`)
190190 }
191191
192 // Did the Zig runtime actually load and bind? JNI_OnLoad says so itself, on
193 // the happy path as well as the failure ones — which is deliberate, because
194 // "loaded and bound" and "never shipped" are otherwise both silent, and this
195 // is also the only line that proves the log channel is working rather than
196 // merely quiet.
192 // Did the Zig runtime load, bind — and then actually answer? JNI_OnLoad
193 // reports the first two on the happy path as well as the failure ones, and
194 // every way a native can give up now says so too (android_dispatch.zig's
195 // fellThrough / failedWithoutFallback / undelivered).
197196 //
198 // There is no per-action dispatch line to count the way the iOS legs do.
199 // Binding is the gate instead: once the natives are registered, every
200 // CraftNative wrapper returns a value and the Kotlin falls through to Zig
201 // rather than the other way round.
197 // Registration alone is not enough, and the first run that loaded this
198 // library showed why: "registered 103 natives", every case passed, and
199 // getDeviceInfo threw NoSuchFieldError on every call and handed it to Kotlin.
200 // The page could not tell. So the runtime leg asks for both halves: bound,
201 // and not one decline while the page ran.
202 //
203 // Read from the full buffer, not the filtered dump: the stack trace ART
204 // prints for the underlying Java exception is under System.err, which is
205 // the part worth quoting back.
202206 const registered = logText.match(REGISTERED)
207 const fullLog = existsSync(join(evidence, 'logcat-full.txt')) ? readFileSync(join(evidence, 'logcat-full.txt'), 'utf8') : logText
208 const declines = androidDeclines(fullLog)
203209 const zigActions: string[] = []
204210
205211 if (leg.requireZig) {
@@ -212,6 +218,14 @@ async function runLeg(leg: Leg, options: RunnerOptions): Promise<LegOutcome> {
212218 else {
213219 zigActions.push(`registered:${registered[1]}`)
214220 }
221
222 for (const decline of declines) {
223 failures.push(
224 `Zig declined while the page ran — ${decline}. The case still passed because Kotlin answered instead, `
225 + `which is exactly what this leg exists to see; the Java exception, if any, is under System.err in ${label}/logcat-full.txt`,
226 )
227 }
228 if (registered && !declines.length) zigActions.push('declines:0')
215229 }
216230 else if (registered) {
217231 failures.push(`the shim leg registered ${registered[1]} natives; it was generated with no runtime`)