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

v0.29.34TestingVitestRequired

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

bash
pnpm add -D @happyvertical/smrt-vitest

Required Plugin Setup

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

  1. Scans src/**/*.ts for SMRT classes via ManifestBuilder
  2. Discovers @happyvertical/smrt-* dependencies from package.json
  3. Loads external manifests via ManifestManager
  4. 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.

FunctionUse 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)

typescript
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

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

typescript
import { setupSmrtManifests } from '@happyvertical/smrt-vitest';

await setupSmrtManifests({ verbose: true });

Testing Rules

  • Use real in-memory SQLite for SmrtObject / SmrtCollection tests. Don't mock the database -- it's fast enough.
  • Mock only external API calls (@happyvertical/ai, HTTP fetches). Never mock Agent instances, 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.ts for unit, *.spec.ts for integration, *.optional.test.ts for 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:

typescript
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 exports
  • src/setup.ts -- globalThis isolation setup file
  • src/test-db.ts -- createIsolatedTestDb, createIsolatedTestDbFromManifest, createTestDb