// The APK must ship every library the libraries it ships link against.
//
// ⛔ THE BUG THIS PINS. app/build.gradle collected the addons to exclude into ONE FLAT SET OF FILE
// NAMES across all four staged ABIs, then excluded each as `**/<name>` — a pattern that matches every
// ABI. The keep-list is computed PER ABI, because a library's ELF NEEDED closure differs between them,
// so a library arm64 keeps and x86 does not was excluded from arm64 as well.
//
// Measured on 0.0.33: the keep-list kept libbare-os.3.8.7.so and libbare-url.2.4.1.so for arm64, the
// other three ABIs did not keep them, and the APK shipped neither — so libbare-signals.4.2.0.so and
// libbare-fs.4.7.2.so could not dlopen. The handset crash-looped on its OWN bundle with
//
// ADDON_NOT_FOUND: Cannot find addon '.' imported from …/hyper-proto-peer/node_modules/bare-signals/binding.js
// Candidates:
// - linked:libbare-signals.4.2.0.so
//
// naming a library that was present, because bare reports a failed dlopen as a failed resolution. There
// is no adb on a user's phone and no UI left when the node cannot boot.
//
// ⚠ AND IT WAS INTERMITTENT, which is the real argument for the artifact-level gate in CI. Whether the
// leak fires depends on the four staging dirs DISAGREEING, and they only disagree when the unshipped
// ABIs hold stale leftovers — restaging them made all four agree and the same source built a working
// APK. A build that passes is not evidence the packaging is right.
//
// ⚠ SOURCE-LEVEL, for the reason announce.test.js and boundary.test.js give: the defect lives in Groovy
// that only runs inside a gradle build, and a test that has to assemble a 160 MB APK is not a test
// anyone runs. scripts/verify-apk-addons.mjs is the behavioural check and it runs in CI on the real
// artifact; this one pins the shape so the regression cannot return quietly.
import test from 'brittle'
import fs from 'bare-fs'
import { join } from 'bare-path'
const ROOT = new URL('..', import.meta.url).pathname
test('jniLibs exclusions are qualified by ABI', (t) => {
const src = fs.readFileSync(join(ROOT, 'android/app/build.gradle'), 'utf8')
t.absent(/excludes \+= undemandedAddons\.collect \{ "\*\*\/" \+ it \}/.test(src),
'the exclude list is not re-prefixed with a bare `**/` — that is what crossed ABIs')
t.ok(/excludes \+= undemandedAddons\b/.test(src),
'it takes the patterns as built')
// Both branches that can populate it — the keep-list one and the header-only fallback — must name the
// ABI, or the shape reaching `excludes` depends on which ran.
const qualified = src.match(/excess << \("\*\*\/" \+ abi\.name \+ "\/" \+ so\.name\)/g) || []
t.is(qualified.length, 2,
'both the keep-list branch and the fallback build `**/<abi>/<name>` patterns')
t.absent(/excess << so\.name/.test(src),
'and neither collects a bare file name, which is shared across every ABI')
})
test('the artifact gate exists and refuses to pass when it cannot check', (t) => {
const p = join(ROOT, 'scripts/verify-apk-addons.mjs')
t.ok(fs.existsSync(p), 'scripts/verify-apk-addons.mjs is present')
const src = fs.readFileSync(p, 'utf8')
// ⛔ THE OPPOSITE DIRECTION FROM stage-addons.mjs. That script keeps everything when readelf is
// missing, because over-shipping is safe. A verifier that cannot read the artifact has proven
// nothing, and passing would be a false green on the last step before a phone.
t.ok(/readelf unavailable[\s\S]{0,200}?process\.exit\(1\)/.test(src),
'a missing readelf fails the build rather than passing unverified')
t.ok(/NEEDED\\\)/.test(src), 'it reads ELF NEEDED entries')
const ci = fs.readFileSync(join(ROOT, '.pear-git/ci.yaml'), 'utf8')
t.ok(/node scripts\/verify-apk-addons\.mjs favela-surface-android\.apk/.test(ci),
'and CI runs it on the built APK, which is the only place it can catch this')
})