OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface-android / test / bundle-by-name.test.js

109 lines · javascript ref:
// 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)')