Skip to content

[RNE Rewrite] test(ts): add API tests for the TypeScript surface - #1355

Draft
msluszniak wants to merge 7 commits into
rne-rewritefrom
@ms/api-tests
Draft

[RNE Rewrite] test(ts): add API tests for the TypeScript surface#1355
msluszniak wants to merge 7 commits into
rne-rewritefrom
@ms/api-tests

Conversation

@msluszniak

@msluszniak msluszniak commented Aug 7, 2026

Copy link
Copy Markdown
Member

Description

Adds TS API tests:

  • hooks
  • task pipelines
  • core primitives
  • the resource fetcher
  • model registry.

Also adds necessary skills.

To find details about this approach please follow: __tests__/README.md.

Three source fixes, each a case where the declared contract was violated in a way no test could express otherwise:

  • SpecMatch.dim(name) with no expected kind is typed to return the raw ConcreteDim, but unwrapped it anyway, so dims.any(...) returned numbers typed as objects. Nothing in src/ used the no-kind form.
  • getRegisteredBackends calls into the JSI global without the 'worklet' directive the architecture guide requires.
  • randomNormal's default seed is Date.now(), so two calls in the same millisecond draw the identical sequence, the opposite of its JSDoc.

One finding left unfixed and recorded. Every create<Task> that throws after loadModel abandons the native model, with no dispose to release native model. tasks/constructionFailure.test.ts asserts the behavior across factories, and hooks/taskHooks.test.ts records the app-level shape. Worth separate PR.

Introduces a breaking change?

  • Yes
  • No

Type of change

  • Bug fix (change which fixes an issue)
  • New feature (change which adds functionality)
  • Documentation update (improves or adds clarity to existing documentation)
  • Other (chores, tests, code style improvements etc.)

Tested on

  • iOS
  • Android

Testing instructions

yarn workspace react-native-executorch test
yarn typecheck
yarn lint

Expected: 23 suites, 1398 tests, 3 snapshots, 0 skipped; typecheck and lint clean. yarn prepare still emits only src/ into lib/.

Screenshots

N/A

Related issues

Closes #1352.

Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings

Additional notes

Layout

Path Contents
core/ tensor, model, runtime, and the spec matcher (symbol binding, variants, runtime constraints, authoring errors)
fetcher/ caching, forced re-download, byte-weighted progress, failures, cancellation, requests shared between concurrent callers, iOS resume, the Android DownloadManager backend, telemetry
tasks/ one suite per pipeline, plus the shared construction-failure behavior
hooks/ useModel, useResourceDownload, and the task hooks end to end
extensions/ box and point scaling, box decoding, seeded generators
api/ export snapshot, registry rules, label constants, source-level conventions
support/ the fake runtime, the mocks, the fixtures

Deliberately not covered: the numerical behavior of the native operators (that is cpp/tests/, and duplicating it here would only test the fake); the long stateful worklets, whose behavior depends on real weights — Whisper, VAD and SDXS get schema acceptance, rejection and full disposal instead; and the worklet thread hop, since worklets run inline. The 'worklet' directive convention that makes that hop possible is enforced by parsing src/ with the TypeScript compiler.

Every path through `src/` bottoms out in `__rnexecutorch_jsi__`, so stubbing
those calls per test would only ever assert against the stub. Instead of
stubbing, implement the native contract in JavaScript: typed-array-backed
tensors with the real byte semantics, JS implementations of the math/cv/speech
operators, and a `loadModel` that serves a program the test describes. Task
pipelines therefore run end to end.

Alongside it, an in-memory blob-util mock with a programmable server (status,
body, Range support, and a gate to hold a download open) and a worklets mock
that runs worklets inline.

Native memory is not garbage collected, so the setup file asserts after every
test that nothing allocated through the fake was left undisposed; each pipeline
suite gets disposal coverage for free.
- `core/`: tensor byte semantics and copy windows, model execution and
  disposal, `wrapAsync` error propagation, and the spec matcher in depth —
  symbol binding, variant selection, runtime-constraint matching, and the
  authoring errors that fire before matching starts.
- `fetcher/`: caching, forced re-download, byte-weighted progress, HTTP and
  transport failures, cancellation, requests shared between concurrent callers,
  iOS partial-file resume, and the Android DownloadManager backend. Telemetry
  gets its own suite, including the locale parsing that must not read a
  language-only tag as a country.
- `extensions/`: box and point scaling under both resize modes, box decoding,
  and the seeded generators.
One suite per pipeline: which model signatures it accepts, that a mismatch is
rejected with a message naming the mismatch, the postprocessing that is the
pipeline's own work (softmax ordering, NMS suppression, argmax colormaps,
sigmoid grayscale masks, coordinate scaling back through letterboxing), every
option and per-call override, and that `dispose()` releases everything.

The pipelines whose behavior depends on real model weights — Whisper, VAD,
SDXS, keypoints, instance segmentation — get schema acceptance, rejection and
full disposal instead, including Whisper's nested tokenizer and VAD pipeline.

`tasks/constructionFailure.test.ts` records a leak the suites surfaced: a
`create<Task>` factory that throws after `loadModel` abandons the native model,
and the caller never receives a `dispose` to release it. The tests assert the
current behavior so it flips loudly once fixed.

Hook suites cover the lifecycle apps depend on: disposal on unmount and on
config change, the create-after-unmount race, config identity by value,
download cache hits, preventLoad, and errors surfaced through the shared field.
- A snapshot of every export, so an addition, rename or removal shows up in the
  diff of the pull request that causes it rather than in a user's app.
- Registry rules that only fail on a device otherwise: https URLs on the
  software-mansion org, a pinned revision, the
  `modelname_backend_precision.pte` naming contract, a folder matching the
  backend suffix, and a default alias structurally identical to one of its own
  variants.
- Label-array invariants, including the ImageNet duplicates that must stay
  because the array mirrors the model's output vocabulary.
- Source-level conventions, parsed with the TypeScript compiler: the
  `'worklet'` directive on every JSI wrapper, and the core/extensions and
  hooks/native import boundaries from the architecture guide.
- `SpecMatch.dim(name)` with no expected kind is typed and documented to return
  the raw `ConcreteDim`, but unwrapped it anyway — so `dim('N')` handed back a
  number typed as an object, and `dims.any(...)` a tuple of them. Unwrap only
  when a kind was asked for. Nothing in `src/` used the no-kind form.
- `getRegisteredBackends` calls straight into the JSI global without the
  `'worklet'` directive the architecture guide requires, so it could not be
  called from a worklet runtime like every other native wrapper.
- `randomNormal`'s default seed is `Date.now()`, whose millisecond resolution
  means two unseeded calls in the same millisecond draw the identical sequence
  — the opposite of what its own JSDoc promises. Mix in `Math.random`; an
  explicit seed still wins, so reproducibility is unaffected.
Adds an `api-tests` job to CI — no native libraries, no simulator, no `.pte`,
so the existing TypeScript-only setup action is all it needs.

Adds an `add-api-tests` skill covering the fake runtime, the helpers, what to
cover for a new pipeline or hook, and the leak-checking contract; wires it into
the skills index, the architecture guide and the maintenance list; and adds the
test step to the verify-and-build workflow and checklist.

Also refreshes the hook example in `add-task-pipeline`, which still showed the
`localPath` shape from before the resource fetcher landed.
@msluszniak msluszniak self-assigned this Aug 7, 2026
@msluszniak msluszniak added the test Issue and PR related to tests or testing infrastructure label Aug 7, 2026
@msluszniak msluszniak linked an issue Aug 7, 2026 that may be closed by this pull request
@msluszniak
msluszniak marked this pull request as draft August 7, 2026 14:36
Rebasing onto the supertonic TTS pipeline (#1317) surfaced two gaps the suites
themselves reported:

- The registry walk assumed every task config names a single `modelPath`.
  Supertonic assembles four `.pte` files under `modelPaths`, so its category
  looked empty and its variant group went unchecked. Recognize both shapes, and
  compare variants on the whole set of files they name.
- The export snapshots record the new TTS surface. Additions only — no removal
  or rename.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test Issue and PR related to tests or testing infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RNE Rewrite] Add API tests

1 participant