@happyvertical/smrt-vitest
Vitest plugin for SMRT projects -- required for every SMRT test. Auto-generates the build-time manifest, loads cross-package class metadata, and provides transaction-isolated test database utilities.
Overview
@happyvertical/smrt-vitest ships the smrtVitestPlugin() Vite
plugin that every SMRT project must include in its vitest.config.ts. Without it, tests fail with "No field metadata found" or "unregistered class" errors.
At test startup, the plugin scans src/**/*.ts for @smrt() classes,
discovers every @happyvertical/smrt-* dependency in your package.json, loads their manifests via ManifestManager, and
registers all classes in ObjectRegistry. The package also provides transaction-isolated test database
utilities with automatic adapter detection (PostgreSQL via DATABASE_URL,
otherwise SQLite temp files).
Installation
pnpm add -D @happyvertical/smrt-vitestRequired Plugin Setup
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
export default defineConfig({
plugins: [smrtVitestPlugin()],
test: {
setupFiles: ['@happyvertical/smrt-vitest/setup'] // optional: globalThis isolation
}
});What the Plugin Does
- Scans
src/**/*.tsfor SMRT classes via ManifestBuilder - Discovers
@happyvertical/smrt-*dependencies from package.json - Loads external manifests via ManifestManager
- Registers all classes in ObjectRegistry
Test Database Utilities
Adapter auto-detection: if DATABASE_URL is set, tests use PostgreSQL;
otherwise they use SQLite temp files.
| Function | Use Case |
|---|---|
createIsolatedTestDbFromManifest() | Multi-table tests -- auto-creates schema from manifest with FK ordering and STI dedup (recommended) |
createIsolatedTestDb({ schema }) | Single-table tests -- pass raw DDL with transaction isolation |
createTestDb() | No transaction isolation (legacy) |
getTestDbConfig() | Get DB config for current environment |
Manifest-Driven Schema (recommended)
import { createIsolatedTestDbFromManifest } from '@happyvertical/smrt-vitest';
let db, cleanup;
beforeEach(async () => {
// Filter to just the classes this test needs
({ db, cleanup } = await createIsolatedTestDbFromManifest({
includeObjects: ['Product', 'Category'],
}));
});
afterEach(async () => {
await cleanup(); // rolls back the transaction
});
it('should insert and query', async () => {
await db.insert('products', { id: '1', name: 'Widget' });
const product = await db.get('products', { id: '1' });
expect(product?.name).toBe('Widget');
});Raw-Schema Variant
import { createIsolatedTestDb } from '@happyvertical/smrt-vitest';
let db, cleanup;
beforeEach(async () => {
({ db, cleanup } = await createIsolatedTestDb({
schema: `CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL)`
}));
});
afterEach(async () => { await cleanup(); });Imperative Setup
For non-Vite setups (e.g., a globalSetup file or a custom bootstrap), use setupSmrtManifests() directly. It loads existing manifests but does not auto-generate
them:
import { setupSmrtManifests } from '@happyvertical/smrt-vitest';
await setupSmrtManifests({ verbose: true });Testing Rules
- Use real in-memory SQLite for
SmrtObject/SmrtCollectiontests. Don't mock the database -- it's fast enough. - Mock only external API calls (
@happyvertical/ai, HTTP fetches). Never mockAgentinstances,SmrtObject,SmrtCollection, or business logic. - Use
createIsolatedTestDb*for transaction-isolated tests -- cleanup rolls the transaction back, so tests are independent. - Test generators, not generated output. Assert the produced manifest / routes / commands, not snapshots of the rendered code.
- File naming:
*.test.tsfor unit,*.spec.tsfor integration,*.optional.test.tsfor tests that require external APIs (skipped in CI).
Singleton Cache Gotcha
Module-level singleton caches (common in SMRT collections) persist across tests and ignore new
mocks. The fix is to vi.resetModules() in beforeEach and use a
dynamic await import(...) inside each test instead of a top-level import:
import { beforeEach, vi, it, expect } from 'vitest';
beforeEach(() => {
vi.resetModules();
});
it('reads fresh module each time', async () => {
const { getCollection } = await import('./my-module');
// ...
});Key Files
src/index.ts-- Vite plugin, manifest generation, all exportssrc/setup.ts-- globalThis isolation setup filesrc/test-db.ts-- createIsolatedTestDb, createIsolatedTestDbFromManifest, createTestDb