Use when adding or changing anything under src/ — a task pipeline, a hook, a native op wrapper, a registry entry — and you need to cover it with the TypeScript API test suites.
Every change under src/ belongs in the Jest suites at
packages/react-native-executorch/__tests__/.
They run on a laptop or a CI runner — no simulator, no device, no .pte — and
finish in a few seconds.
yarn workspace react-native-executorch test
yarn workspace react-native-executorch test __tests__/tasks # one directory
yarn workspace react-native-executorch test -u # update snapshots
Types come along for free: yarn typecheck already covers __tests__/.
🧩 The Fake Native Runtime
There is no stubbing of individual JSI calls. __tests__/support/fakeJsi.ts
implements the whole __rnexecutorch_jsi__ contract in JavaScript — tensors
hold real data, math/cv/speech operators compute real values — so a task
pipeline runs end to end and its own logic is what the assertions measure.
A test describes the model it wants, then drives the real pipeline:
Make loadModel(path) succeed with a given schema and execute
fakeJsi.registerTokenizer(path, vocabulary)
Same for loadTokenizer
fakeJsi.registerLLMRunner(path, program)
Same for createLLMRunner: a context window and the responses to generate, one per generate call
fakePhonemizer.serve(text, phonemes)
Script the native grapheme-to-phoneme converter
fakeFs.write(path, contents)
Put a file where a pipeline will read it (a charset, a tokenizer_config.json, a voice matrix)
exported(spec)
Reinterpret a spec built with method/f32/i64 as an exported one (it verifies no symbolic dims are left)
writesOutputs(...), copiesInputToOutput()
Ready-made execute implementations
tracked(pipeline)
Auto-dispose at the end of the test
imageBuffer(w, h, format)
A deterministic input image
cachePathFor(url)
Where the fetcher will download a URL, so a hook test can register its model up front
fakeNet.serve(url, route)
Script the server: status, body, Range support, and a gate to hold a download open
🧠 What to Cover for a New Task Pipeline
Schema acceptance — one test per variant the pipeline declares
(batched, unbatched, ...), asserting the factory resolves.
Schema rejection — a model that matches no variant, asserting the error
names the mismatch (Rank mismatch, inconsistent bindings, ...). A caller
should learn what is wrong from the message.
Configuration mismatch — e.g. a labels array that disagrees with the
model's output dimension.
Postprocessing — the part that is yours: sorting, thresholding,
suppression, colormaps, coordinate scaling. Choose fixture values that make
the expected output obvious in the test.
Options — every default in modelOpts, and every per-call override.
Disposal — dispose() leaves fakeJsi.liveTensors() at 0 and
fakeJsi.liveModels() empty, and repeated calls do not accumulate scratch
tensors. The same has to hold when construction fails: allocate through a
createResourceScope() and wrap the factory body in try/catch so a
schema mismatch releases the model instead of stranding it. Add the factory
to __tests__/tasks/constructionFailure.test.ts, which drives every one of
them.
Only the weights are out of scope, not the pipeline that runs on them. Before
settling for schema-acceptance-and-disposal, check what is actually TypeScript:
a decode loop, a sliding window, a chunker, an argument check, a streaming
generator and a disposal path all run fine over a scripted execute. Reach for
the minimal treatment only when the assertion would be measuring the fixture.
For a new hook, add a case to __tests__/hooks/: not-ready before the download
lands, methods exposed after, errors surfaced through the shared error field,
and every native handle released on unmount.
🔒 Leak Checking
Native memory is not garbage collected, so the setup file asserts after every
test that nothing allocated through the fake was left undisposed. That gives
each pipeline suite disposal coverage for free.
Wrap construction in tracked() — it disposes at the end of the test and
stops a failing assertion from cascading into a second, misleading error.
A test that deliberately leaks calls allowNativeLeaks() with a comment
saying why.
📐 Source-Level Conventions
__tests__/api/workletDirective.test.ts parses src/ with the TypeScript
compiler and enforces the conventions no type can express:
every exported function that calls into rnexecutorchJsi starts with
'worklet';
no async function is marked as a worklet
only src/native/bridge.ts names the __rnexecutorch_jsi__ global
core/ never imports from extensions/, and hooks/ never imports from
native/
If you add a new native wrapper without the directive, that suite fails — add
the directive rather than the exception.
📋 Verification Checklist
When adding or changing code under src/, verify that:
yarn workspace react-native-executorch test passes.
A new task pipeline has a suite covering acceptance, rejection,
postprocessing, options and disposal.
A new hook has a lifecycle case in __tests__/hooks/.
A new registry entry passes __tests__/api/modelRegistry.test.ts without
the rules being loosened (https URL, pinned revision,
modelname_backend_precision.pte, a folder naming that backend, and a
DEFAULT that is one of the configs the group offers).
A new error code is in VALID_ERROR_CODES, so isRnExecuTorchError does
not reject the library's own error — __tests__/core/error.test.ts reads
every code src/ raises out of the source and checks it is listed.
A new export is reflected in the api/apiSurface snapshot, and the change
is intentional (a removal or rename is a breaking change).
Any new fake behavior in __tests__/support/ is faithful where fidelity
changes an assertion, and its simplifications are commented.
A new create<Task> allocates through createResourceScope() and is
listed in __tests__/tasks/constructionFailure.test.ts.
No test was made to pass by calling allowNativeLeaks(). Nothing in the
suite needs it today, so reach for it only when a leak is genuinely the
point of the test, and say why.
1---2name: add-api-tests3description: Use when adding or changing anything under src/ — a task pipeline, a hook, a native op wrapper, a registry entry — and you need to cover it with the TypeScript API test suites.4---56# Skill: Add TypeScript API Tests78Every change under `src/` belongs in the Jest suites at9[`packages/react-native-executorch/__tests__/`](../../../packages/react-native-executorch/__tests__/README.md).10They run on a laptop or a CI runner — no simulator, no device, no `.pte` — and11finish in a few seconds.1213```bash14yarn workspace react-native-executorch test15yarn workspace react-native-executorch test __tests__/tasks # one directory16yarn workspace react-native-executorch test -u # update snapshots17```1819Types come along for free: `yarn typecheck` already covers `__tests__/`.2021---2223## 🧩 The Fake Native Runtime2425There is no stubbing of individual JSI calls. `__tests__/support/fakeJsi.ts`26implements the whole `__rnexecutorch_jsi__` contract in JavaScript — tensors27hold real data, `math`/`cv`/`speech` operators compute real values — so a task28pipeline runs end to end and its own logic is what the assertions measure.2930A test describes the model it wants, then drives the real pipeline:3132```typescript33import { f32, method } from '../../src/core/schema';34import { fakeJsi } from '../support/fakeJsi';35import { tracked } from '../support/lifetime';36import { STRETCH_PREPROCESSING, exported, imageBuffer, writesOutputs } from '../support/fixtures';3738fakeJsi.registerModel('/models/task.pte', {39 schema: exported(method('forward', [f32(1, 3, 4, 4)], [f32(1, 3)])),40 execute: writesOutputs([1, 0, 2]),41});4243const runner = tracked(await createMyTask({ modelPath: '/models/task.pte', modelOpts }));44expect(await runner.runTask(imageBuffer(8, 8))).toEqual(/* ... */);45```4647Key helpers:4849| Helper | Use |50| :--- | :--- |51| `fakeJsi.registerModel(path, program)` | Make `loadModel(path)` succeed with a given schema and `execute` |52| `fakeJsi.registerTokenizer(path, vocabulary)` | Same for `loadTokenizer` |53| `fakeJsi.registerLLMRunner(path, program)` | Same for `createLLMRunner`: a context window and the responses to generate, one per `generate` call |54| `fakePhonemizer.serve(text, phonemes)` | Script the native grapheme-to-phoneme converter |55| `fakeFs.write(path, contents)` | Put a file where a pipeline will read it (a charset, a `tokenizer_config.json`, a voice matrix) |56| `exported(spec)` | Reinterpret a spec built with `method`/`f32`/`i64` as an *exported* one (it verifies no symbolic dims are left) |57| `writesOutputs(...)`, `copiesInputToOutput()` | Ready-made `execute` implementations |58| `tracked(pipeline)` | Auto-dispose at the end of the test |59| `imageBuffer(w, h, format)` | A deterministic input image |60| `cachePathFor(url)` | Where the fetcher will download a URL, so a hook test can register its model up front |61| `fakeNet.serve(url, route)` | Script the server: status, body, `Range` support, and a `gate` to hold a download open |6263---6465## 🧠 What to Cover for a New Task Pipeline66671. **Schema acceptance** — one test per variant the pipeline declares68 (`batched`, `unbatched`, ...), asserting the factory resolves.692. **Schema rejection** — a model that matches no variant, asserting the error70 names the mismatch (`Rank mismatch`, `inconsistent bindings`, ...). A caller71 should learn what is wrong from the message.723. **Configuration mismatch** — e.g. a `labels` array that disagrees with the73 model's output dimension.744. **Postprocessing** — the part that is yours: sorting, thresholding,75 suppression, colormaps, coordinate scaling. Choose fixture values that make76 the expected output obvious in the test.775. **Options** — every default in `modelOpts`, and every per-call override.786. **Disposal** — `dispose()` leaves `fakeJsi.liveTensors()` at 0 and79 `fakeJsi.liveModels()` empty, and repeated calls do not accumulate scratch80 tensors. The same has to hold when construction *fails*: allocate through a81 `createResourceScope()` and wrap the factory body in `try`/`catch` so a82 schema mismatch releases the model instead of stranding it. Add the factory83 to `__tests__/tasks/constructionFailure.test.ts`, which drives every one of84 them.857. **Sync/async parity** — `runTaskWorklet(x)` equals `await runTask(x)`.8687Only the *weights* are out of scope, not the pipeline that runs on them. Before88settling for schema-acceptance-and-disposal, check what is actually TypeScript:89a decode loop, a sliding window, a chunker, an argument check, a streaming90generator and a disposal path all run fine over a scripted `execute`. Reach for91the minimal treatment only when the assertion would be measuring the fixture.9293For a new hook, add a case to `__tests__/hooks/`: not-ready before the download94lands, methods exposed after, errors surfaced through the shared `error` field,95and every native handle released on unmount.9697---9899## 🔒 Leak Checking100101Native memory is not garbage collected, so the setup file asserts after **every102test** that nothing allocated through the fake was left undisposed. That gives103each pipeline suite disposal coverage for free.104105- Wrap construction in `tracked()` — it disposes at the end of the test and106 stops a failing assertion from cascading into a second, misleading error.107- A test that deliberately leaks calls `allowNativeLeaks()` with a comment108 saying why.109110---111112## 📐 Source-Level Conventions113114`__tests__/api/workletDirective.test.ts` parses `src/` with the TypeScript115compiler and enforces the conventions no type can express:116117- every exported function that calls into `rnexecutorchJsi` starts with118 `'worklet';`119- no `async` function is marked as a worklet120- only `src/native/bridge.ts` names the `__rnexecutorch_jsi__` global121- `core/` never imports from `extensions/`, and `hooks/` never imports from122 `native/`123124If you add a new native wrapper without the directive, that suite fails — add125the directive rather than the exception.126127---128129## 📋 Verification Checklist130131When adding or changing code under `src/`, verify that:132133- [ ] `yarn workspace react-native-executorch test` passes.134- [ ] A new task pipeline has a suite covering acceptance, rejection,135 postprocessing, options and disposal.136- [ ] A new hook has a lifecycle case in `__tests__/hooks/`.137- [ ] A new registry entry passes `__tests__/api/modelRegistry.test.ts` without138 the rules being loosened (https URL, pinned revision,139 `modelname_backend_precision.pte`, a folder naming that backend, and a140 `DEFAULT` that is one of the configs the group offers).141- [ ] A new error code is in `VALID_ERROR_CODES`, so `isRnExecuTorchError` does142 not reject the library's own error — `__tests__/core/error.test.ts` reads143 every code `src/` raises out of the source and checks it is listed.144- [ ] A new export is reflected in the `api/apiSurface` snapshot, and the change145 is intentional (a removal or rename is a breaking change).146- [ ] Any new fake behavior in `__tests__/support/` is faithful where fidelity147 changes an assertion, and its simplifications are commented.148- [ ] A new `create<Task>` allocates through `createResourceScope()` and is149 listed in `__tests__/tasks/constructionFailure.test.ts`.150- [ ] No test was made to pass by calling `allowNativeLeaks()`. Nothing in the151 suite needs it today, so reach for it only when a leak is genuinely the152 point of the test, and say why.
Run npx skillmds@latest add software-mansion/add-api-tests in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Use when adding or changing anything under src/ — a task pipeline, a hook, a native op wrapper, a registry entry — and you need to cover it with the TypeScript API test suites. It is listed under DevOps & Infra on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
software-mansion (@software-mansion) published this skill. Their other Agent Skills are listed on their SkillMD profile.