d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
258 lines
11 KiB
Plaintext
258 lines
11 KiB
Plaintext
---
|
|
description: Testing patterns with Vitest and @sim/testing
|
|
globs: ["apps/sim/**/*.test.ts", "apps/sim/**/*.test.tsx"]
|
|
---
|
|
|
|
|
|
# Testing Patterns
|
|
|
|
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
|
|
|
|
## Global Mocks (vitest.setup.ts)
|
|
|
|
These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:
|
|
|
|
- `@sim/db` → `databaseMock`
|
|
- `@sim/db/schema` → `schemaMock`
|
|
- `drizzle-orm` → `drizzleOrmMock`
|
|
- `@sim/logger` → `loggerMock`
|
|
- `@/lib/auth` → `authMock`
|
|
- `@/lib/auth/hybrid` → `hybridAuthMock` (with default session-delegating behavior)
|
|
- `@/lib/core/utils/request` → `requestUtilsMock`
|
|
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
|
|
- `@/blocks/registry`
|
|
- `@trigger.dev/sdk`
|
|
- `@sim/platform-authz/workflow` → `workflowAuthzMock`
|
|
|
|
## Structure
|
|
|
|
```typescript
|
|
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { createMockRequest } from '@sim/testing'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { mockGetSession } = vi.hoisted(() => ({
|
|
mockGetSession: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/auth', () => ({
|
|
auth: { api: { getSession: vi.fn() } },
|
|
getSession: mockGetSession,
|
|
}))
|
|
|
|
import { GET, POST } from '@/app/api/my-route/route'
|
|
|
|
describe('my route', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
|
})
|
|
|
|
it('returns data', async () => {
|
|
const req = createMockRequest('GET')
|
|
const res = await GET(req)
|
|
expect(res.status).toBe(200)
|
|
})
|
|
})
|
|
```
|
|
|
|
## Performance Rules (Critical)
|
|
|
|
### NEVER use `vi.resetModules()` + `vi.doMock()` + `await import()`
|
|
|
|
This is the #1 cause of slow tests. It forces complete module re-evaluation per test.
|
|
|
|
```typescript
|
|
// BAD — forces module re-evaluation every test (~50-100ms each)
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() }))
|
|
})
|
|
it('test', async () => {
|
|
const { GET } = await import('./route') // slow dynamic import
|
|
})
|
|
|
|
// GOOD — module loaded once, mocks reconfigured per test (~1ms each)
|
|
const { mockGetSession } = vi.hoisted(() => ({
|
|
mockGetSession: vi.fn(),
|
|
}))
|
|
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
|
|
import { GET } from '@/app/api/my-route/route'
|
|
|
|
beforeEach(() => { vi.clearAllMocks() })
|
|
it('test', () => {
|
|
mockGetSession.mockResolvedValue({ user: { id: '1' } })
|
|
})
|
|
```
|
|
|
|
**Only exception:** Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need `vi.resetModules()` + dynamic import to get a fresh instance per test.
|
|
|
|
### NEVER use `vi.importActual()`
|
|
|
|
This defeats the purpose of mocking by loading the real module and all its dependencies.
|
|
|
|
```typescript
|
|
// BAD — loads real module + all transitive deps
|
|
vi.mock('@/lib/workspaces/utils', async () => {
|
|
const actual = await vi.importActual('@/lib/workspaces/utils')
|
|
return { ...actual, myFn: vi.fn() }
|
|
})
|
|
|
|
// GOOD — mock everything, only implement what tests need
|
|
vi.mock('@/lib/workspaces/utils', () => ({
|
|
myFn: vi.fn(),
|
|
otherFn: vi.fn(),
|
|
}))
|
|
```
|
|
|
|
### Mock heavy transitive dependencies
|
|
|
|
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
|
|
|
|
```typescript
|
|
vi.mock('@/blocks', () => ({
|
|
getBlock: () => null,
|
|
getAllBlocks: () => ({}),
|
|
getAllBlockTypes: () => [],
|
|
registry: {},
|
|
}))
|
|
```
|
|
|
|
### Use `@vitest-environment node` unless DOM is needed
|
|
|
|
Only use `@vitest-environment jsdom` if the test uses `window`, `document`, `FormData`, or other browser APIs. Node environment is significantly faster.
|
|
|
|
### Avoid real timers in tests
|
|
|
|
```typescript
|
|
// BAD
|
|
await new Promise(r => setTimeout(r, 500))
|
|
|
|
// GOOD — use minimal delays or fake timers
|
|
await new Promise(r => setTimeout(r, 1))
|
|
// or
|
|
vi.useFakeTimers()
|
|
```
|
|
|
|
## Centralized Mocks (prefer over local declarations)
|
|
|
|
`@sim/testing` exports ready-to-use mock modules for common dependencies. Import and pass directly to `vi.mock()` — no `vi.hoisted()` boilerplate needed. Each paired `*MockFns` object exposes the underlying `vi.fn()`s for per-test overrides.
|
|
|
|
| Module mocked | Import | Factory form |
|
|
|---|---|---|
|
|
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
|
|
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
|
|
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
|
|
| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` |
|
|
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
|
|
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
|
|
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |
|
|
| `@/lib/core/config/env` | `envMock`, `createEnvMock(overrides)` | `vi.mock('@/lib/core/config/env', () => envMock)` |
|
|
| `@/lib/core/config/env-flags` | `featureFlagsMock` | `vi.mock('@/lib/core/config/env-flags', () => featureFlagsMock)` |
|
|
| `@/lib/core/config/redis` | `redisConfigMock`, `redisConfigMockFns` | `vi.mock('@/lib/core/config/redis', () => redisConfigMock)` |
|
|
| `@/lib/core/security/encryption` | `encryptionMock`, `encryptionMockFns` | `vi.mock('@/lib/core/security/encryption', () => encryptionMock)` |
|
|
| `@/lib/core/security/input-validation.server` | `inputValidationMock`, `inputValidationMockFns` | `vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)` |
|
|
| `@/lib/core/utils/request` | `requestUtilsMock`, `requestUtilsMockFns` | `vi.mock('@/lib/core/utils/request', () => requestUtilsMock)` |
|
|
| `@/lib/core/utils/urls` | `urlsMock`, `urlsMockFns` | `vi.mock('@/lib/core/utils/urls', () => urlsMock)` |
|
|
| `@/lib/execution/preprocessing` | `executionPreprocessingMock`, `executionPreprocessingMockFns` | `vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)` |
|
|
| `@/lib/logs/execution/logging-session` | `loggingSessionMock`, `loggingSessionMockFns`, `LoggingSessionMock` | `vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)` |
|
|
| `@/lib/workflows/orchestration` | `workflowsOrchestrationMock`, `workflowsOrchestrationMockFns` | `vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)` |
|
|
| `@/lib/workflows/persistence/utils` | `workflowsPersistenceUtilsMock`, `workflowsPersistenceUtilsMockFns` | `vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)` |
|
|
| `@/lib/workflows/utils` | `workflowsUtilsMock`, `workflowsUtilsMockFns` | `vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)` |
|
|
| `@/lib/workspaces/permissions/utils` | `permissionsMock`, `permissionsMockFns` | `vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)` |
|
|
| `@sim/db/schema` | `schemaMock` | `vi.mock('@sim/db/schema', () => schemaMock)` |
|
|
|
|
### Auth mocking (API routes)
|
|
|
|
```typescript
|
|
import { authMock, authMockFns } from '@sim/testing'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('@/lib/auth', () => authMock)
|
|
|
|
import { GET } from '@/app/api/my-route/route'
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
|
})
|
|
```
|
|
|
|
Only define a local `vi.mock('@/lib/auth', ...)` if the module under test consumes exports outside the centralized shape (e.g., `auth.api.verifyOneTimeToken`, `auth.api.resetPassword`).
|
|
|
|
### Hybrid auth mocking
|
|
|
|
```typescript
|
|
import { hybridAuthMock, hybridAuthMockFns } from '@sim/testing'
|
|
|
|
vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
|
|
|
|
// In tests:
|
|
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
|
|
success: true, userId: 'user-1', authType: 'session',
|
|
})
|
|
```
|
|
|
|
### Database chain mocking
|
|
|
|
Use the centralized `dbChainMock` + `dbChainMockFns` helpers — no `vi.hoisted()` or chain-wiring boilerplate needed.
|
|
|
|
```typescript
|
|
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
|
|
|
|
vi.mock('@sim/db', () => dbChainMock)
|
|
// Spread for custom exports: vi.mock('@sim/db', () => ({ ...dbChainMock, myTable: {...} }))
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
resetDbChainMock() // only needed if tests use permanent (non-`Once`) overrides
|
|
})
|
|
|
|
it('reads a row', async () => {
|
|
dbChainMockFns.limit.mockResolvedValueOnce([{ id: '1', name: 'test' }])
|
|
// exercise code that hits db.select().from().where().limit()
|
|
expect(dbChainMockFns.where).toHaveBeenCalled()
|
|
})
|
|
```
|
|
|
|
**Default chains supported:**
|
|
- `select()/selectDistinct()/selectDistinctOn() → from() → where()/innerJoin()/leftJoin() → where() → limit()/orderBy()/returning()/groupBy()/for()`
|
|
- `insert() → values() → returning()/onConflictDoUpdate()/onConflictDoNothing()`
|
|
- `update() → set() → where() → limit()/orderBy()/returning()/for()`
|
|
- `delete() → where() → limit()/orderBy()/returning()/for()`
|
|
- `db.execute()` resolves `[]`
|
|
- `db.transaction(cb)` calls cb with `dbChainMock.db`
|
|
|
|
`.for('update')` (Postgres row-level locking) is supported on `where` builders. It returns a thenable with `.limit` / `.orderBy` / `.returning` / `.groupBy` attached, so both `await .where().for('update')` (terminal) and `await .where().for('update').limit(1)` (chained) work. Override the terminal result with `dbChainMockFns.for.mockResolvedValueOnce([...])`; for the chained form, mock the downstream terminal (e.g. `dbChainMockFns.limit.mockResolvedValueOnce([...])`).
|
|
|
|
All terminals default to `Promise.resolve([])`. Override per-test with `dbChainMockFns.<terminal>.mockResolvedValueOnce(...)`. Use `resetDbChainMock()` in `beforeEach` only when tests replace wiring with `.mockReturnValue` / `.mockResolvedValue` (permanent); tests using only `...Once` variants don't need it.
|
|
|
|
## @sim/testing Package
|
|
|
|
Always prefer over local test data.
|
|
|
|
| Category | Utilities |
|
|
|----------|-----------|
|
|
| **Module mocks** | See "Centralized Mocks" table above |
|
|
| **Logger helpers** | `loggerMock`, `createMockLogger()`, `getLoggerCalls()`, `clearLoggerMocks()` |
|
|
| **Database helpers** | `databaseMock`, `drizzleOrmMock`, `createMockDb()`, `createMockSql()`, `createMockSqlOperators()` |
|
|
| **Fetch helpers** | `setupGlobalFetchMock()`, `createMockFetch()`, `createMockResponse()`, `mockFetchError()` |
|
|
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
|
|
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
|
|
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
|
|
| **Requests** | `createMockRequest()`, `createMockFormDataRequest()` |
|
|
|
|
## Rules Summary
|
|
|
|
1. `@vitest-environment node` unless DOM is required
|
|
2. Prefer centralized mocks from `@sim/testing` (see table above) over local `vi.hoisted()` + `vi.mock()` boilerplate
|
|
3. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
|
|
4. `vi.mock()` calls before importing mocked modules
|
|
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
|
|
6. No `vi.importActual()` — mock everything explicitly
|
|
7. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
|
|
8. Use absolute imports in test files
|
|
9. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`
|