test(mobile): state the __esModule interop rule once

It was restated at four sites with four different answers. The canonical
paragraph is in `native-module-traps.ts`; the loader and the declared tray point
at it in a line. The loader comment also said the refusal names the member the
product read, which for a default import is `.default` instead.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-15 18:24:19 -04:00
parent e5aa6bdac8
commit a70e90e65c
4 changed files with 41 additions and 29 deletions
+24 -15
View File
@@ -22,23 +22,24 @@ follows `stable-logical-rpc-client.test.ts`. Neither test exported a reusable mo
rendered: the inert primitives it chose, the copy it put on them, the labels it gave them, and the
crash instead if a reply took it down. A screen that throws is a recording, not a suite failure —
several reply partitions do exactly that, and refusing to record them would leave the shapes that
break a screen the only ones this oracle cannot see.
break a screen the only ones this oracle cannot see. The boundary also reports the crash to the
effect sink, so a hook mount, whose projection is the hook's own value and never a crash, still
carries it into a golden: an effect forces a cleanup checkpoint even when no adapter looks.
The view packages a screen imports are in `screen-native-substitutes.ts`, under the table's usual
rule: only what a screen reads is listed, the rest throws. Every element there is inert. It renders
its children and keeps its props where a projection can read them, and does nothing else: no
callback it is handed is ever invoked, nothing is measured, no gesture is recognised, no animation
runs and no navigation happens. `renderedElementProps` is the consequence — an inert list never
calls `renderItem`, so the data it was handed is the only record of what the screen would have
drawn. `screen-native-substitutes.test.ts` is the census; it renders every element with a callback
prop and a render callback as children and fails if either is called.
rule: only what a recording is known to read is listed, the rest throws. Every element there is
inert. It renders its children and keeps its props where a projection can read them, and does
nothing else: no callback it is handed is ever invoked, nothing is measured and no navigation
happens. `renderedElementProps` is the consequence — an inert list never calls `renderItem`, so the
data it was handed is the only record of what the screen would have drawn.
`screen-native-substitutes.test.ts` is the census; it renders every element with a callback prop and
a render callback as children and fails if either is called.
One member deserves naming because inertness costs something. `Alert.alert` never answers, so a
flow gated on a confirmation stops there; a recording that needs one has to drive it through the
operation's own API. `InteractionManager.runAfterInteractions` is the exception to the inert rule:
it runs its task on a microtask and returns the real handle, because a scheduler that drops its
task swallows the send the screen deferred, which is the one failure this oracle must not have.
Cancelling the handle before the task runs still prevents it.
Nothing is listed ahead of a reader, and that is a rule rather than an oversight. A member
provisioned before any recording reads it converts a refusal that would have forced a decision into
a silent stand-in, and a silent stand-in is how an inert `InteractionManager` or `Alert` swallows the
send a screen deferred behind it. The table was cut back to the members a recording actually reads;
whoever mounts the next screen adds what it needs together with the recording that reads it.
## What a scenario declares about its device
@@ -161,7 +162,15 @@ file rather than of a restatement of it; `golden-header-digest.test.ts` pins wha
buy.
Checkpoints contain ordered sender calls and serialized physical application payloads, action and
request settlements, projected state, and ordered external effects. Sender args have three
request settlements, projected state, and ordered external effects. Each effect also carries `sent`,
the number of requests sent when it was recorded: sender and effects are two independent lists, so
without it a send reordered ahead of a device write moves neither list and no golden notices.
Scheduling the journal write in `codex-reset-attempt-journal.ts` on a timer instead of awaiting it
moved none of the 520 goldens before `sent` existed and moves two now, `codex-reset-credit-consumed`
and its reply matrix, where the write's `sent` goes from 0 to 1. What `sent` cannot see is a defer
shorter than the product's own await chain: dropping that `await`, or deferring the write by one
microtask, still lands it before the send, because resolving the journal's promise chain costs more
microtask ticks than the defer saved. Sender args have three
positional slots; absent, undefined and null are distinct `$rpc` tags. Literal objects containing
`$rpc` are escaped. Only object keys are sorted; array/effect order, options, budgets, settlement
times and errors stay observable. Errors contain category, message and `isRpcDeliveryUnknown`, never
@@ -77,8 +77,8 @@ function declaredNotificationTray(
effect: DeviceEffect
): unknown {
return partialNativeModule('expo-notifications', {
// `__esModule` so a namespace import keeps reading through the trap: flattened into a fresh
// object, an unlisted member would come back undefined instead of refusing.
// Namespace-imported by every consumer, so the marker keeps reads going through the trap; see
// the `__esModule` paragraph in `native-module-traps.ts`.
__esModule: true,
// Cloned per read, so a screen that mutates a notification cannot change what the next read of
// the declaration returns.
@@ -9,12 +9,17 @@
* call instead. Whether that failure is visible depends on the caller; `host-app-version-store.ts`
* catches and degrades to its unread state, which is what it does on a device too.
*
* `__esModule` is exempt from both refusals, because it is the module system's interop marker
* rather than a native API. A store leaves it undefined: the store *is* the default export, and
* answering truthfully would bind `import X from` to the trap's own `default` instead of the trap,
* leaving the consumer holding a member-less stand-in. A partial module answers it only when it
* declares a `default` member, because `import X, { y }` compiles to `__importStar`, which
* otherwise overwrites that default with the module object.
* `__esModule` is exempt from both refusals, because it is the module system's interop marker rather
* than a native API, and what a trap answers there is the whole of the interop rule for every
* substitute in this directory — the other sites point here rather than restating it. Both emitted
* helpers short-circuit on a truthy marker: `__importDefault` returns the module instead of wrapping
* it, and `__importStar` returns it instead of copying its own keys into a fresh object.
*
* So a trap answers `true` when it has to survive being imported: the loader's refusing proxy, and
* any partial whose consumer takes a default or a namespace, because a flattened copy has no trap
* left and would answer an unlisted member with `undefined` instead of the named refusal. A store
* answers `undefined`, because the store *is* the default export — a truthy marker would bind
* `import X from` to the trap's own `default`, a throwing stub, instead of to the trap.
*/
export function partialNativeModule(module: string, members: Record<string, unknown>): unknown {
return new Proxy(members, {
@@ -55,12 +55,10 @@ export function operationModuleLoader(
return new Proxy(
{},
{
// `__esModule` is the module system's interop marker, not a native API. Answering `true`
// makes both emitted interop helpers hand this trap straight back — `__importDefault`
// returns it instead of wrapping it, `__importStar` returns it instead of copying its
// (absent) own keys — so all three import forms load and refuse at the first member read,
// naming the member. Substitute partials answer `undefined` instead, because there a real
// member object must bind as the module's default.
// Answering `__esModule` binds this trap as the module itself in every import form; the
// rule is in the `__esModule` paragraph of `native-module-traps.ts`. The refusal then
// lands on the first member the emit reads, which for a default import is `.default`
// rather than whichever member the product went on to touch.
get: (_target, key) => {
if (key === '__esModule') {
return true