OverviewFilesCommitsIssuesDocsReleasesPeers

favela-android / test / first-run.test.mjs

62 lines · javascript ref:
// Which screen a fresh install shows, and whether it can ever leave it.
// Run: bare test/first-run.test.mjs
//
// Every case below is a state a real handset reaches. The one that mattered on 2026-08-25 is the third:
// a phone with no identity could not get past the pairing modal, because there was no rule that let it.
import { initialScreen } from '../lib/first-run.js'

let pass = 0
// ⛔ NOT `process.exit(1)`. There is no `process` global under bare, so the sibling suites' helper dies
// with "ReferenceError: process is not defined" the moment an assertion fails — the failure is reported,
// then buried under a stack about the reporter. That trap cost three wrong measurements on 2026-08-25
// (hyper-social #54 §9.1). Throwing exits non-zero and says only the thing that broke.
const ok = (c, m) => { if (!c) throw new Error('FAIL: ' + m); console.log('  ok -', m); pass++ }

// ── a fresh install ──────────────────────────────────────────────────────────────────────────────
// Auto-showing the ceremony STAYS. A fresh install has no settings page to reach a button on, and the
// thing the phone most needs is an identity — so the modal is still the first thing, unprompted.
ok(initialScreen({ signing: false }) === 'pair', 'no identity => the pairing screen, unprompted')
ok(initialScreen({}) === 'pair', 'nothing known at all => the pairing screen (never a blank app)')

// ⛔ THE BUG. The user declines to pair. Pair.jsx's footer says "you can keep using the app without
// pairing — you will see posts, but you will not be able to write", and the node behind it is serving
// those posts right now. Before this rule existed the answer here was 'pair', for ever.
ok(initialScreen({ signing: false, readOnly: true }) === 'web', 'declined pairing => the read-only app, not a wall')

// ── a paired device ──────────────────────────────────────────────────────────────────────────────
ok(initialScreen({ signing: true }) === 'web', 'has an identity => straight to the app')

// Re-pairing is reached from favela-web's settings. HAVING AN IDENTITY IS NOT BEING A MEMBER: a handset
// holding the DID and its key, but absent from the room's __members__, has every write refused and
// re-pairing is the fix. App.jsx's own note records that the first cut of this shell removed that route.
ok(initialScreen({ signing: true, pairing: true }) === 'pair', 'asked to pair while paired => the ceremony (the re-pair case)')

// ── the interaction that is easy to get wrong ────────────────────────────────────────────────────
// An explicit request must never be swallowed by a skip made earlier in the same run. Both of these are
// reachable: skip on launch, then open pairing from settings.
ok(initialScreen({ signing: false, pairing: true, readOnly: true }) === 'pair', 'explicit pair beats an earlier skip (unpaired)')
ok(initialScreen({ signing: true, pairing: true, readOnly: true }) === 'pair', 'explicit pair beats an earlier skip (paired)')

// A skip does not survive gaining an identity — nothing to suppress once signing is true.
ok(initialScreen({ signing: true, readOnly: true }) === 'web', 'a stale skip does not hide the app from a paired device')

// ── creating an identity ─────────────────────────────────────────────────────────────────────────
// The other half of the intro modal. A phone that has never been paired has no identity to copy, so
// pairing alone left a fresh device with no route to being a full node at all (hyper-social #54 §8).
ok(initialScreen({ signing: false, creating: true }) === 'create', 'asked to create => the creation ceremony')

// Both ceremonies are reachable from the same modal, and each beats a skip — the same rule as pairing,
// for the same reason: an explicit choice is never swallowed by an earlier decline.
ok(initialScreen({ signing: false, creating: true, readOnly: true }) === 'create', 'explicit create beats an earlier skip')

// ⛔ CREATING AND PAIRING ARE MUTUALLY EXCLUSIVE and must resolve deterministically rather than by
// whichever branch happens to be written first. Pairing wins: it joins an identity that ALREADY EXISTS,
// while creating mints a new one — and minting is the irreversible half (a PLC genesis is a permanent
// append to a global log). If the state is ever ambiguous, the recoverable route is the safe one.
ok(initialScreen({ signing: false, pairing: true, creating: true }) === 'pair', 'ambiguous => pair, because minting is the irreversible one')

// A device that already signs is not offered creation — it has an identity.
ok(initialScreen({ signing: true, creating: true }) === 'create', 'a paired device that explicitly asks can still create')

console.log('\n' + pass + ' assertions passed')