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:

text
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.

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';

export default defineConfig({
  plugins: [smrtVitestPlugin()],
  test: {
    globals: true,
    environment: 'node'
  }
});
bash
pnpm add -D @happyvertical/smrt-vitest vitest

That is the whole setup. Write tests as you normally would:

typescript
// 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.

OptionTypeDefaultPurpose
generateManifestbooleantrueGenerate the local manifest at startup. Set false to use an existing manifest only.
packagesstring[][]Extra @happyvertical/smrt-* packages to load beyond those auto-discovered from package.json.
verbosebooleanfalseLog each manifest discovered, loaded, or skipped — useful when debugging "No field metadata".
rootstringprocess.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:

typescript
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.

typescript
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:

ExportPurpose
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.

typescript
// 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

Verified against SMRT v0.29.34.