// The staged bundle is chosen BY NAME, never by position in the tree.
//
// ⛔ THIS IS THE TRAP THE CI FILE ALREADY NAMED. materialize looped every file entry and kept whichever
// it wrote LAST as the bundle to boot. That was safe only while the tree had exactly one entry, and the
// assemble step's own comment says what happens when it does not: "a two-entry tree stages a 154 MB APK
// as the worklet bundle. The OTA client on a handset is exactly what cannot be fixed remotely."
//
// It stopped being hypothetical when CI began publishing to the release registry: `pgh publish` REQUIRES
// a package.json, so the published tree is { package.json, worklet.bundle } and the winner depended on
// entry order — an ordering nothing in this codebase controls or asserts.
//
// Selecting by name also stops the phone DOWNLOADING what it will not use. The old loop fetched every
// entry's content before discarding all but the last.
//
// Run: bare test/bundle-by-name.test.mjs
import fs from 'bare-fs'
import os from 'bare-os'
import path from 'bare-path'
import { KIND_FILE } from 'hyper-overlay/tree'
import { bundleStore } from '../lib/host/ota.js'
let pass = 0
const ok = (c, m) => { if (!c) throw new Error('FAIL: ' + m); console.log(' ok -', m); pass++ }
const tmp = (n) => {
const d = path.join(os.tmpdir(), 'favela-byname-' + n + '-' + Date.now())
fs.mkdirSync(d, { recursive: true })
return d
}
const BUNDLE = Buffer.from('THE REAL WORKLET BUNDLE')
const MANIFEST = Buffer.from('{"name":"favela-android"}')
const h = (b) => require_hash(b)
// hash the way the store keys files: content hash from the fetcher's own map
function require_hash (b) { return 'h' + b.length + '_' + b.toString('hex').slice(0, 8) }
const entry = (name, bytes) => ({ kind: KIND_FILE, exec: 0, name, childHash: h(bytes) })
async function stage (entries, bytesByHash) {
const dir = tmp('s')
const store = bundleStore({ dir })
const fetched = []
await store.materialize('root-' + Date.now(), entries, {
fetchContent: async (hash) => { fetched.push(hash); return bytesByHash[hash] }
})
return { dir, store, fetched }
}
// ── the published shape: manifest FIRST, bundle second ───────────────────────────────────────────
{
const es = [entry('package.json', MANIFEST), entry('worklet.bundle', BUNDLE)]
const { dir, fetched } = await stage(es, { [h(MANIFEST)]: MANIFEST, [h(BUNDLE)]: BUNDLE })
const cur = JSON.parse(fs.readFileSync(path.join(dir, 'current.json'), 'utf8'))
const staged = fs.readFileSync(path.join(dir, cur.file))
ok(staged.equals(BUNDLE), 'the WORKLET BUNDLE is staged, not the manifest')
ok(!fetched.includes(h(MANIFEST)), 'and the manifest was never even downloaded')
}
// ── the order that would have passed by luck ─────────────────────────────────────────────────────
// With last-entry-wins this arrangement staged the manifest. Nothing orders a published tree, so a
// change in pgh's entry ordering would have silently bricked every phone that took the update.
{
const es = [entry('worklet.bundle', BUNDLE), entry('package.json', MANIFEST)]
const { dir } = await stage(es, { [h(MANIFEST)]: MANIFEST, [h(BUNDLE)]: BUNDLE })
const cur = JSON.parse(fs.readFileSync(path.join(dir, 'current.json'), 'utf8'))
ok(fs.readFileSync(path.join(dir, cur.file)).equals(BUNDLE), 'bundle first: still the bundle')
}
// ── the single-entry tree the forge publishes must keep working ──────────────────────────────────
// tree.json (npm run gen:tree) is one entry, and phones in the field consume it. This must not regress.
{
const es = [entry('worklet.bundle', BUNDLE)]
const { dir } = await stage(es, { [h(BUNDLE)]: BUNDLE })
const cur = JSON.parse(fs.readFileSync(path.join(dir, 'current.json'), 'utf8'))
ok(fs.readFileSync(path.join(dir, cur.file)).equals(BUNDLE), 'a one-entry tree still stages')
}
// ── a tree with NO bundle is an error, not a silently-staged something-else ──────────────────────
{
let threw = null
try { await stage([entry('package.json', MANIFEST)], { [h(MANIFEST)]: MANIFEST }) } catch (e) { threw = e }
ok(threw, 'a tree with no worklet.bundle throws rather than staging the wrong file')
ok(/worklet\.bundle|no file/i.test(threw.message), 'and says what was missing: ' + threw.message)
}
console.log('\n' + pass + ' assertions passed')
// ── the staged marker must carry a build time, whatever the release calls it ──────────────────────
// chooseBundle compares this against the APK's stamp. A staged bundle with no time is one an APK
// install cannot supersede — the failure that left a handset running 0.1.6's JS through four installs.
// A forge release says `builtAt`; the pgh ReleaseRegistry says ISO `publishedAt`. Both must land.
{
const es = [entry('worklet.bundle', BUNDLE)]
for (const [label, release, want] of [
['forge builtAt', { version: 'v', builtAt: 1787708000000 }, 1787708000000],
['registry publishedAt', { version: 'v', publishedAt: '2026-08-26T00:34:47.151Z' }, Date.parse('2026-08-26T00:34:47.151Z')],
['neither', { version: 'v' }, null]
]) {
const dir = tmp('mark')
const store = bundleStore({ dir })
store.noteRelease(release)
await store.materialize('root-' + label, es, { fetchContent: async () => BUNDLE })
const cur = JSON.parse(fs.readFileSync(path.join(dir, 'current.json'), 'utf8'))
ok(cur.builtAt === want, label + ' -> builtAt ' + cur.builtAt)
}
}
console.log('\n(marker provenance checked)')