@metamesh/testkit
v0.20.0
Published
Runner-neutral MetaMesh model and Generator testing helpers, assertions, and local font fixtures.
Readme
@metamesh/testkit
Check that your MetaMesh models produce the geometry you expect. Run models and Generators with the production engine, check their measurements, and test definitions and presets with your preferred test runner.
Preview:
0.xminor releases may break compatibility. Patches remain compatible within the same minor line.
Installation
Install matching SDK, Modeling, and Testkit releases, with Testkit as a development dependency:
pnpm add @metamesh/generator-sdk @metamesh/modeling
pnpm add -D @metamesh/testkitThe package publishes ES modules only. Use Node.js 24 or newer. TypeScript consumers need
TypeScript 6.0 or newer, strict checking, and NodeNext or Bundler module resolution.
The assertions work without a test framework and don't register globals. Testkit has no compiler, renderer, or browser dependency. Use the CLI to check source files, save captured runs, or render images.
Test a model
Build a plate, check its volume and shell count, then check that its meshes reconstruct correctly:
import { box } from "@metamesh/modeling";
import { assertMeshMeasurements, assertValidParts, runModel } from "@metamesh/testkit";
const result = await runModel<{ width: number }>(
({ params, part }) => part("plate", box({ size: [params.width, 20, 4] })),
{ width: 30 }
);
assertMeshMeasurements(
result.part("plate"),
{ signedVolume: 2400, shellCount: 1 },
{ signedVolume: 0.05 }
);
await assertValidParts(result.parts);runModel returns immutable parts, measurements, diagnostics, and dependencies. Results need no
cleanup. Use result.part(id) to find a required part by its ID, or iterate over result.parts.
Always await assertValidParts(parts). It checks the world-space buffers for closed topology,
then reconstructs each part from its local geometry and rigid transform and compares the positions
and indices exactly. It resolves to the same parts or rejects on failure. Pass an engine as the
second argument to reuse one you own; otherwise, the shared test engine initializes when needed.
You can pass an existing Modeling program: await runModel(program, parameters) uses the same
program and parameter types as engine.run. For a small test, pass a synchronous callback
directly. Without parameters, no type argument or wrapper is needed:
await runModel(({ part }) => part("cube", box({ size: 2 }))).
Parameters keep the program or callback's declared type. Use
GenerateCallback<Parameters, Services> for a reusable callback or supply
runModel<Parameters, Services> inline. Options stay in the third argument; use undefined for
omitted parameters when supplying an engine, limits, or prepared services.
Assertions
| Helper | Checks |
| ------------------------ | -------------------------------------------------------------- |
| assertValidDefinition | Strict parsing and a canonical JSON round trip. |
| assertValidDefaults | Strict decoding and agreement with definition defaults. |
| assertValidPresets | Complete, normalized preset metadata. |
| assertValidParts | Part metadata, closed topology, and native reconstruction. |
| assertMeshMeasurements | Bounds, surface area, signed volume, and surface shell count. |
| assertEqualBuffer | Exact ordered numeric buffer contents without copying storage. |
| assertEqualMesh | Exact positions and indices, independently of mesh metadata. |
assertValidDefinition requires strict SDK parsing and a canonical JSON round trip; it does not
execute the Generator. Check model assertions and domain constraints through the Generator runner.
Measurement tolerances are absolute: millimetres for bounds, mm² for area, and mm³ for volume. They default to zero; shell counts are exact. Matching measurements do not prove geometric equivalence or rule out self-intersections. Use native reconstruction checks alongside them.
Use assertEqualBuffer(actual, expected) or assertEqualMesh(actual, expected) for exact content
comparisons. They read length and at(index) without copying storage and throw
ContentMismatchError with the first differing index or length. Malformed representations throw
TypeError; reader failures propagate. Numeric equality uses Object.is: NaN equals NaN and
signed zero differs. Mesh comparisons ignore metadata and require identical position and index
contents. Geometrically equivalent meshes with different triangulation or ordering can fail.
Register contentMatchers with your runner's expect.extend(contentMatchers) to use
toEqualBuffer and toEqualMesh, including .not. Only ContentMismatchError becomes a failed
matcher result; invalid inputs and unexpected errors still throw. Follow your runner's
matcher extension guidance for TypeScript declarations.
Test Generators and repeated edits
For a Generator, import its definition and default runtime, then call
runModel({ definition, runtime }, parameters). This validates the inputs and prepares services
before generation. Omit parameters or pass undefined to use defaults. Supplied inputs use the
definition's inferred types and must form a complete canonical map.
To change one value, spread getDefaultParameters(definition) and apply your override. The runner
doesn't merge defaults into supplied values. Use SDK normalizeParameters when recovering saved
values. Plain model parameters pass through directly.
Import the actual Generator entry point in integration tests so they exercise its input handling
and model together. Direct callbacks are useful for testing construction helpers or mesh reconstruction.
For a deliberate schema bypass, runModel(runtime, parameters, { services }) executes
the Generator's model behavior without claiming those values pass its parameter schema.
See the SDK example.
Use createGeneratorTestRunner(definition, runtime) for repeated parameter cases. Its run method
returns a generated result or a structured validation failure. Call runner.run(record) to test
unknown keys, missing fields, or wrong value types. Use check to test declarations and reached
assertions without finalizing parts; geometry observations can still require the engine.
Dispose the runner in finally; returned results remain usable.
Use check(parameters, { validateInput: false }) only to probe model assertions with deliberately
invalid typed values, bypassing the parameter schema.
Check cache reuse
For models using memo, include repeated-edit tests with { verifyCache: true } on the runner.
Change geometry inputs and placement inputs independently. Verification reruns cached calculations
and rejects changed results for unchanged inputs. Keep pure helpers stable and pass changing values
as arguments; pass the current params/context to track fields read inside the helper.
Verification has bounded comparison work. For outputs it can't compare, use { cache: false }
on a separate runner and compare the resulting geometry. Neither check proves arbitrary JavaScript pure.
Keep prepared service identity stable for unchanged assets; replacing services creates a new session
and prevents testing cache reuse. Isolated runModel calls do not verify repeated-edit correctness.
For plain models, use engine.createSession(program, options) for repeated edits or cache tests
and dispose the session in finally. Session results remain usable after later runs and disposal.
Both runModel and Generator runners can reuse a test-owned engine and lower its resource ceilings.
They accept the same physical resolution, meshValidation, and meshTopologyMode policy.
Fonts and entry points
| Import | Responsibility |
| --------------------------------- | ------------------------------------------------------------------------------ |
| @metamesh/testkit | Model execution, Generator runners, assertions, and optional content matchers. |
| @metamesh/testkit/fonts | Prepared font services from supplied fixture bytes. |
| @metamesh/testkit/font-fixtures | Node-only JSON fixture loading with SHA-256 verification. |
For a Generator with Font parameters, supply prepareServices to prepare its font services before
generation. Tests use checked-in bytes without CDN access; retain their licenses. Font preparation
needs Modeling's optional fontkit peer, plus woff2-encoder for WOFF2 fixtures.
Documentation
Resolve @metamesh/testkit/docs/manifest.json for exact-version Markdown guidance and annotated
API references. With CLI installed, read pnpm exec metamesh docs show testkit/guide/testing.
See Modeling
for geometry and Contributing
for package development.
