Testing
SMRT objects are plain classes, so they test like any TypeScript code. The one thing they need
is a manifest — the field metadata produced by the build-time scanner. The smrtVitestPlugin() generates it for you, so a Vitest run has everything it needs without
a separate build step.
Why the plugin is required
SMRT does not read field types from the live class at runtime. Schema generation, queries, and AI methods all read from a manifest built by the AST scanner at build time. In a test run, there is no build, so the manifest is missing — and the first database operation throws:
Error: No field metadata found for class 'Task'. The class is registered
(decorator ran) but has no field definitions. This usually means the
manifest file is missing or stale.The plugin closes that gap. It builds the local manifest once at Vitest startup and loads the
manifests shipped by every @happyvertical/smrt-* dependency, so cross-package classes
are registered too.
Setup
Add the plugin to vitest.config.ts. With the default generateManifest: true, the manifest is generated at startup — you do not need to
run a build or smrt test first.
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
export default defineConfig({
plugins: [smrtVitestPlugin()],
test: {
globals: true,
environment: 'node'
}
});pnpm add -D @happyvertical/smrt-vitest vitestThat is the whole setup. Write tests as you normally would:
// src/models/Task.test.ts
import { describe, it, expect } from 'vitest';
import { TaskCollection } from './TaskCollection.js';
describe('Task', () => {
it('creates and reads a task', async () => {
const tasks = await TaskCollection.create({ db: ':memory:' });
const task = await tasks.create({ title: 'Write tests' });
expect(task.id).toBeTruthy();
expect(task.title).toBe('Write tests');
const found = await tasks.get(task.id);
expect(found?.title).toBe('Write tests');
});
});Plugin options
The defaults work for a typical single-package project. Override these when you need to tune manifest generation or widen the scan.
| Option | Type | Default | Purpose |
|---|---|---|---|
generateManifest | boolean | true | Generate the local manifest at startup. Set false to use an existing manifest only. |
packages | string[] | [] | Extra @happyvertical/smrt-* packages to load beyond those auto-discovered from
package.json. |
verbose | boolean | false | Log each manifest discovered, loaded, or skipped — useful when debugging "No field metadata". |
root | string | process.cwd() | Project root used to locate package.json and resolve manifest paths. |
Isolated test databases
For the simplest unit tests, point a collection at an in-memory SQLite database — fast, and discarded when the process exits:
const tasks = await TaskCollection.create({ db: ':memory:' });For suites that need transaction isolation between tests, @happyvertical/smrt-vitest ships createIsolatedTestDbFromManifest(). It derives the schema from the manifest the
plugin already generated, runs each test inside a transaction, and rolls back on cleanup() — so no data leaks between tests and tables are never re-created.
import { beforeEach, afterEach, describe, it, expect } from 'vitest';
import { createIsolatedTestDbFromManifest } from '@happyvertical/smrt-vitest';
import { TaskCollection } from './TaskCollection.js';
describe('Task isolation', () => {
let db, cleanup;
beforeEach(async () => {
// Restrict schema creation to the objects this suite touches
({ db, cleanup } = await createIsolatedTestDbFromManifest({
includeObjects: ['Task']
}));
});
afterEach(async () => {
await cleanup(); // Rolls back the transaction — nothing persists
});
it('does not leak rows between tests', async () => {
const tasks = await TaskCollection.create({ db });
await tasks.create({ title: 'isolated' });
expect(await tasks.count({})).toBe(1);
});
});Other helpers exported from the same package:
| Export | Purpose |
|---|---|
createIsolatedTestDbFromManifest() | Transaction-isolated database whose schema is built from the generated manifest.
Returns db, config, and cleanup(). |
createIsolatedTestDb() | Transaction-isolated database from raw schema DDL you pass in. Use when you
are testing SQL directly, not SMRT objects. |
createTestDb() | Non-transactional test database (file is deleted on cleanup). |
isPostgresAvailable() | Gate Postgres-only suites so they skip cleanly on machines without a database. |
Testing component packages
For Svelte component tests, add @happyvertical/smrt-vitest/svelte-setup to your setupFiles. It wires up jest-dom matchers, Testing Library auto-cleanup, and a <dialog> polyfill for jsdom. It is safe to list alongside node tests — the
DOM pieces only load when a document is present.
// vitest.config.ts (component package)
import { defineConfig } from 'vitest/config';
import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
export default defineConfig({
plugins: [smrtVitestPlugin()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['@happyvertical/smrt-vitest/svelte-setup']
}
});Accessibility assertions are available from the /a11y subpath via expectNoA11yViolations().
Alternative: smrt test
If you prefer to run tests through the SMRT CLI instead of wiring the plugin, the smrt test command generates the manifest and then invokes Vitest for you. The
Vitest plugin is the better default for app projects because plain vitest and vitest --watch just work once it is installed.
Related
- @happyvertical/smrt-vitest — the module reference for the plugin and database helpers.
- Configuration — where the database type used by tests is resolved.
- Objects — what the manifest describes.
Verified against SMRT v0.29.34.