Skip to content

test+refactor!: API contract test scaffold + fixes for #1202#1204

Open
msluszniak wants to merge 11 commits into
mainfrom
@ms/fix-api-inconsistencies-1202
Open

test+refactor!: API contract test scaffold + fixes for #1202#1204
msluszniak wants to merge 11 commits into
mainfrom
@ms/fix-api-inconsistencies-1202

Conversation

@msluszniak
Copy link
Copy Markdown
Member

@msluszniak msluszniak commented May 29, 2026

Description

Bundles two related changes so the API surface and its contract tests land together:

  1. API contract test scaffold (under packages/react-native-executorch/__tests__/api/) — 12 Jest suites + a CI test job. Catches drift across the public TypeScript surface (modules, hooks, types, registry, error codes, voices, URLs) without exercising native code.
  2. Source-level fixes for every actionable finding in API consistency findings surfaced by TS contract tests #1202 so the contract suite enforces every rule unconditionally — no exception sets, 0 skipped tests.

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 typecheck:tests
yarn workspace react-native-executorch test
yarn typecheck
yarn lint

Expected: 12 suites green, 980 tests passed, 0 skipped, 1 snapshot. The two demo apps that exercise the renamed APIs (apps/speech, apps/computer-vision/text_to_image) build and run with unchanged behaviour.

Screenshots

N/A — no UI changes.

Related issues

Closes #1018 (TS API tests). Closes #1202 (findings the tests surfaced). Supersedes #1203.

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

Breaking API changes:

  • ExecutorchModule and TokenizerModule now construct via static factories (fromModelSource, fromModelName); the instance load() is removed. The useModule hook helper is deleted alongside them.

Renames kept as deprecated aliases (callers don't have to migrate yet, but @deprecated JSDoc flags new usage):

  • useExecutorch is the new canonical name; useExecutorchModule is re-exported as a deprecated alias.
  • useTextToSpeech({ model, preventLoad }) is the canonical single-arg form; the previous useTextToSpeech(model, { preventLoad }) two-arg signature is preserved as a deprecated overload.
  • useTextToImage().forward is the canonical hook return field (matches TextToImageModule.forward); .generate is preserved as a deprecated alias.

Non-breaking fixes:

  • OCRModule, VerticalOCRModule, LLMModule, SpeechToTextModule, TextToSpeechModule, TokenizerModule now extend BaseModule. TokenizerModule consequently gains a working delete() (was leaking the native tokenizer handle).

Docs + skill files updated for the renames (hand-written only; docs/docs/06-api-reference/** regenerates from JSDoc on the next typedoc build).

Test suite layout

Test file Layer
moduleContracts Module → BaseModule, from* factory, useXxx hook export
modelRegistry Accessor return shape, unique modelName per category
hookContracts useXxx return type ⊇ {error, isReady, isGenerating, downloadProgress} (compile-time)
modelUrls Every URL is https:// on the software-mansion HF org
errorCodes Unique codes, working reverse lookup, non-empty default messages
ttsVoices Voice variable-name region ↔ phonemizerConfig.lang ↔ phonemizer URL paths
apiSurface Snapshot of public exports
hookPropsContract Every *Props has preventLoad?: boolean; every hook takes a single object arg (compile-time)
registryHookCompatibility Registry sample per category assignable to the hook's model prop (compile-time)
modulePrototype Every module exposes a public method; delete() reachable; BaseModule surface snapshot
moduleConstruction Mocks ResourceFetcher, constructs each module via its from* factory, asserts instance + delete() (runtime)
moduleHookSignatureAlignment Module prototype method signatures align with their hook return fields (compile-time)

Adds Jest-based scaffold and two representative tests that catch drift
across the package's public API surface. The tests intentionally do not
exercise the JSI runtime — they discover modules/hooks via the index
exports and assert shared structural contracts.

Refs #1018
Two more layers on the API contract scaffold:

- hookContracts.test.ts: compile-time assertion that every public
  useXxx hook returns at least { error, isReady, isGenerating,
  downloadProgress }. Drift in any hook surfaces as a tsc error
  naming the offending hook.

- modelUrls.test.ts: walks every accessor in the model registry,
  collects every string field that looks like a URL, and asserts
  each one is a non-empty https URL pointing at the
  software-mansion HuggingFace org.

Refs #1018, #1202.
Three more API consistency layers:

- errorCodes.test.ts: walks RnExecutorchErrorCode and asserts every
  entry is a unique non-negative integer with a working reverse
  lookup, and that constructing RnExecutorchError(code) produces a
  non-empty message.

- ttsVoices.test.ts: walks every Kokoro voice constant and asserts
  the voice variable-name region (e.g. KOKORO_FRENCH_*) matches the
  phonemizerConfig.lang, that the voiceSource URL points at the
  voices/ directory, and that every phonemizer URL lives under the
  matching /phonemizer/<lang>/ tree. Catches copy-paste bugs across
  voice configs.

- apiSurface.test.ts: snapshots the sorted list of public exports
  from src/index.ts. Accidental adds/removals show up in the diff;
  intentional changes need --updateSnapshot.

Refs #1018, #1202.
…ests

Three more API consistency layers:

- hookPropsContract.test.ts: compile-time check that every *Props
  type exposes preventLoad?: boolean, and that every public useXxx
  hook takes a single object argument. Surfaces useTextToSpeech as
  the lone two-arg outlier.

- registryHookCompatibility.test.ts: compile-time assertion that
  every category sample from the model registry is assignable to
  the matching hook's model prop type. Catches drift between the
  registry's static return shape and the hook prop shapes.

- modulePrototype.test.ts: walks each concrete module's prototype
  chain (using property descriptors so accessor getters aren't
  invoked) and asserts at least one public method is reachable and
  delete() is callable. Also snapshots BaseModule's intrinsic
  surface so silent additions/renames there fail loudly.

Surfaces TokenizerModule's missing delete() as a documented opt-out
in SKIPS_DELETE (tracked in #1202).

Refs #1018, #1202.
- moduleConstruction.test.ts: mocks the ResourceFetcher adapter and
  constructs every from*-factory-bearing module against a sample
  config from the registry. Asserts the awaited result is the
  expected instance and that delete() is callable on the stubbed
  native module.

- moduleHookSignatureAlignment.test.ts: compile-time alignment check
  between non-generic module prototype methods and the matching
  hook return field. Catches drift between e.g.
  LLMModule.prototype.generate and useLLM().generate. Surfaces the
  TextToImageModule.forward → useTextToImage().generate rename via
  a dedicated row.

- setup-globals.ts: the stubbed loadXxx now resolves to a native
  module shape with unload() and generateFromFrame() so module
  delete() and VisionModule's worklet getter work in tests.

- .github/workflows/ci.yml: adds a `test` job that runs
  typecheck:tests and `jest --ci` so the contract suite gates PRs.

Refs #1018, #1202.
Source-level fixes for every actionable finding in #1202. The contract
suite from #1203 now enforces these without exception sets.

Breaking changes:
- Renamed useExecutorchModule → useExecutorch to match the
  use<ModuleStem> convention used by every other hook.
- useTextToSpeech now takes a single object argument
  ({ model, preventLoad }) like every other hook, instead of
  (model, { preventLoad }).
- Renamed useTextToImage().generate → .forward so the hook return
  field matches the module method name.
- ExecutorchModule + TokenizerModule now construct via static
  factories (fromModelSource, fromModelName) instead of
  new + instance load(). Removed useModule helper.

Non-breaking fixes:
- OCRModule, VerticalOCRModule, LLMModule, SpeechToTextModule,
  TextToSpeechModule, TokenizerModule now extend BaseModule.
  TokenizerModule consequently gains a delete() that releases the
  native tokenizer handle (was leaking).

Demo apps (apps/speech/*, apps/computer-vision/text_to_image) and
the contract tests updated for the new shapes. apiSurface snapshot
regenerated.

Closes #1202.
@msluszniak msluszniak changed the title refactor: address API inconsistencies from #1202 refactor!: address API inconsistencies from #1202 May 29, 2026
@msluszniak msluszniak self-assigned this May 29, 2026
@msluszniak msluszniak added refactoring bug fix PRs that are fixing bugs labels May 29, 2026
@msluszniak msluszniak linked an issue May 29, 2026 that may be closed by this pull request
@msluszniak msluszniak changed the title refactor!: address API inconsistencies from #1202 test+refactor: API contract test scaffold + fixes for #1202 May 29, 2026
@msluszniak msluszniak changed the base branch from @ms/api-tests-scaffold to main May 29, 2026 10:20
@msluszniak msluszniak changed the title test+refactor: API contract test scaffold + fixes for #1202 test+refactor!: API contract test scaffold + fixes for #1202 May 29, 2026
Copy link
Copy Markdown
Member Author

@msluszniak msluszniak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also why CI workflow wasn't triggered by this PR?

Comment on lines +105 to +116
llm_generate: {
inputs: true as EqualParam<LLMModule['generate'], LLMType['generate']>,
returns: true as EqualReturn<LLMModule['generate'], LLMType['generate']>,
},
styleTransfer_forward: {
inputs: true as EqualParam<
StyleTransferModule['forward'],
StyleTransferType['forward']
>,
returns: true as EqualReturn<
StyleTransferModule['forward'],
StyleTransferType['forward']
Copy link
Copy Markdown
Member Author

@msluszniak msluszniak May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

llm has generate but the rest has forward, why? We need to think if we want to keep it this way.

EDIT:
Ok, the problem with LLM is that LLMModule exposes both generate and forward and generate from useLLM utilizes LLMModule's generate. I'm ok with both current and changed version, just want to make sure everybody is ok with the chosen version.

- Consolidate split `import type { … } from '../../src'` blocks in
  hookContracts.test.ts and moduleHookSignatureAlignment.test.ts.
- Document why LLMModule's primary method is `generate` (not
  `forward`) in the signature-alignment table — streaming HF/llama.cpp
  convention vs PyTorch single-pass forward.
@msluszniak msluszniak mentioned this pull request May 29, 2026
@msluszniak
Copy link
Copy Markdown
Member Author

Ok, tested and all functionalities that are changed worked: demo apps + custom app that covered useExectorch and useTokenizer.

* @param props - Configuration object containing `modelSource` and optional `preventLoad` flag.
* @returns Ready to use Executorch module.
*/
export const useExecutorch = ({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rename is a breaking change.
Maybe we can export an alias for the patch releases.
"export const useExecutorchModule = useExecutorch"
We could get rid of it when 1.0 rolls out

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that, but please mind the fact that this is not the only breaking change in this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, I'm still reading the changes though.
I feel like the hook and generate->forward renames have the most impact for the end user. We should handle it gracefully if possible

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in favour of making aliases for generate and useExecutorchModule and useTextToSpeech with different signature.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

llm has generate but the rest has forward, why? We need to think if we want to keep it this way.

This way we can also rename this function in llm from generate to forward if we want, similarly to textToImage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, two birds with one stone kind of situation. Let's go with the aliases til 1.0

Copy link
Copy Markdown
Member Author

@msluszniak msluszniak May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, the problem with LLM is that LLMModule exposes both generate and forward, and generate from useLLM utilizes LLMModule's generate. I'm okay with both current and changed version, just want to make sure everybody is ok with the chosen version.

…ech as deprecated aliases

Restores backward compatibility for the three external API symbols that
were renamed in 88de1f4:

- useTextToImage().generate is back as a deprecated alias for .forward.
- useExecutorchModule is back as a deprecated re-export of useExecutorch.
- useTextToSpeech still prefers the single-object-arg form, but the old
  (model, { preventLoad }) signature is restored via an overload tagged
  @deprecated.

All three are documented as deprecated and will be removed in a future
release. Contract tests updated to cover the deprecated alias hooks
alongside the canonical ones; apiSurface snapshot includes the
restored useExecutorchModule export.
Adds OCR, VerticalOCR, SpeechToText, TextToSpeech, Tokenizer, and
ExecutorchModule rows alongside the existing twelve. Closes the gap
flagged in PR review — the omission was an oversight, not deliberate.
Construction count goes from 12 to 18.
Copy link
Copy Markdown
Contributor

@benITo47 benITo47 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no remarks other than the already fixed ones.
I'm approving but someone should still take a second look

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug fix PRs that are fixing bugs refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API consistency findings surfaced by TS contract tests Add API test in TypeScript

2 participants