# Testing

ows-coda uses **Vitest** for testing across all packages. Tests are co-located with source code in `__tests__/` directories throughout each package. Globals mode is enabled (`globals: true`), so `describe`, `it`, `expect`, `vi`, `beforeEach`, etc. are available without imports.

## Running tests

```bash
# All packages
pnpm test              # lint + vitest with coverage (all packages)
pnpm test:unit         # vitest only (no lint)

# Per-package filtering
pnpm --filter @coda/server-app test:unit         # server unit tests
pnpm --filter @coda/client-app test:unit         # client unit tests
pnpm --filter @coda/search-service test:unit  # search service unit tests
pnpm --filter @coda/common test:unit         # common unit tests
pnpm --filter @coda/platform-service test:unit  # platform service unit tests
pnpm --filter @coda/runner-service test:unit    # runner service unit tests

# Functional & integration
pnpm --filter @coda/server-app test:functional   # real DB + Redis, FakeProvider
pnpm --filter @coda/server-app test:integration  # HTTP against running server

# Docker (containerized)
pnpm docker:test:unit        # lint + test in Docker container
pnpm docker:test:functional  # with real DB + Redis (FakeProvider, no Bedrock)
pnpm docker:test:integration # HTTP against running server
pnpm docker:test:e2e         # Playwright against full stack (not yet implemented)
```

See [Docker](docker.md) for details on running containerized tests.

## Test types

```mermaid
flowchart LR
    unit["Unit<br/><i>all mocked</i>"] --> func["Functional<br/><i>real DB + Redis<br/>FakeProvider</i>"]
    func --> integ["Integration<br/><i>real services<br/>real Bedrock</i>"]
    integ --> e2e["E2E<br/><i>full stack<br/>Playwright</i>"]

    style unit fill:#d4edda
    style func fill:#fff3cd
    style integ fill:#fddede
    style e2e fill:#f8d7da
```

| Type            | Infrastructure     | AI Provider                  | Scope                                              |
| --------------- | ------------------ | ---------------------------- | -------------------------------------------------- |
| **Unit**        | All mocked         | Mocked                       | Individual functions and classes                   |
| **Functional**  | Real MySQL + Redis | FakeProvider (deterministic) | Full Express stack (routes, middleware, cache, DB) |
| **Integration** | Real services      | Real Bedrock                 | HTTP against running server                        |
| **E2E**         | Full stack         | Real Bedrock                 | Browser via Playwright                             |

## Vitest configuration

The project uses a shared base config (`vitest.shared.ts` at the repo root) that library packages import directly. App packages define their own configs per test tier.

**Shared config** (`vitest.shared.ts`):

```typescript
import { defineConfig } from "vitest/config";

export const sharedConfig = defineConfig({
  test: {
    environment: "node",
    include: ["src/**/__tests__/**/*.test.ts"],
    globals: true,
  },
});
```

**Server unit config** (`apps/server/vitest.config.ts`) adds path aliases and excludes functional/integration directories:

```typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  resolve: {
    alias: {
      "@server": path.resolve(__dirname, "src"),
      "@coda/db": path.resolve(__dirname, "../../packages/db/src/index.ts"),
      // ... other cross-package aliases
    },
  },
  test: {
    environment: "node",
    globals: true,
    include: ["src/**/__tests__/**/*.test.ts"],
    exclude: ["src/__tests__/functional/**", "src/__tests__/integration/**"],
    testTimeout: 10_000,
  },
});
```

**Functional config** (`vitest.config.functional.ts`) runs tests sequentially with a setup file:

```typescript
test: {
  include: ["src/__tests__/functional/**/*.test.ts"],
  setupFiles: ["src/__tests__/functional/helpers/setup-env.ts"],
  sequence: { sequential: true },
  fileParallelism: false,
  testTimeout: 10_000,
},
```

**Integration config** (`vitest.config.integration.ts`) uses forked processes:

```typescript
test: {
  include: ["src/__tests__/integration/**/*.test.ts"],
  pool: "forks",
  poolOptions: { forks: { singleFork: true } },
  testTimeout: 15_000,
},
```

## Test structure

Tests are co-located in `__tests__/` directories next to the code they test:

```
apps/server/src/
├── __tests__/
│   └── functional/              # Functional tests (real DB + Redis, fake AI)
│       ├── conversation-lifecycle.test.ts
│       ├── streaming-events.test.ts
│       ├── tool-execution.test.ts
│       ├── search-tools-discovery.test.ts
│       ├── edge-cases.test.ts
│       └── helpers/             # Test harness, FakeProvider, SSE parser
├── ai/
│   ├── __tests__/               # Orchestrator tests
│   ├── providers/bedrock/__tests__/  # Bedrock API + adapter tests
│   ├── tools/__tests__/         # Tool dispatch, catalog, availability tests
│   ├── tools/snowflake/__tests__/    # Schema index, search, introspect tests
│   ├── tools/graphql/__tests__/      # GraphQL handlers, search, polling tests
│   ├── tools/notion/__tests__/       # Notion tool handler tests
│   ├── skills/__tests__/        # Skill aggregation tests
│   └── utils/__tests__/         # Tokenize, hybrid search utilities
├── cache/__tests__/             # Redis conversation CRUD, memory store
├── config/__tests__/            # Zod config schema validation
├── db/snowflake/__tests__/      # Snowflake connection, pooling
├── db/coda/__tests__/           # Stream persister, repository tests
│   └── services/__tests__/      # DB service layer tests
├── middleware/__tests__/        # Auth, error handler, UUID validation
├── routes/__tests__/            # Route handler tests
├── search/__tests__/            # Vector index, glossary, hybrid search
├── services/__tests__/          # Service layer tests
└── utils/__tests__/             # Env, JSON, request context
packages/async/src/__tests__/             # Circuit breaker, retry, semaphore, throttlers
packages/collections/src/__tests__/       # Data structures, algorithms
packages/search/src/__tests__/            # BM25, HNSW, hybrid search, scoring
packages/common/src/__tests__/            # Graph, storage, crypto, utilities
packages/db/src/__tests__/                # Prisma client, crypto, barrel exports
packages/core-api/src/__tests__/          # API types, client
packages/api-common/src/__tests__/        # RPC utilities (callRpc, mapConnectError)
packages/admin-api/src/__tests__/         # Access + platform client tests
packages/extensions/src/__tests__/        # Glossary loaders
packages/sandbox/src/__tests__/           # V8 isolate, sandbox engine tests
packages/search-api/src/__tests__/        # Search ConnectRPC client tests
packages/runner-api/src/__tests__/        # Runner ConnectRPC client tests
packages/admin-api/src/__tests__/      # Platform proto definitions
apps/client/src/
├── admin/__tests__/             # Admin panel tests
├── components/__tests__/        # UI component tests
├── data/__tests__/              # Data layer tests
├── hooks/__tests__/             # Custom hook tests
├── lib/__tests__/               # Client utilities
├── offline/__tests__/           # IndexedDB, sync engine tests
└── providers/__tests__/         # Provider/context tests
apps/search/src/
├── __tests__/                   # Search pipeline, handler, config tests
├── config/__tests__/            # Search config tests
├── embedding/__tests__/         # Embedding strategy tests
├── engine/__tests__/            # Engine tests
├── events/__tests__/            # Event handling tests
├── graphql/__tests__/           # GraphQL integration tests
├── handlers/__tests__/          # Handler tests
├── middleware/__tests__/        # Search middleware tests
├── pipeline/__tests__/          # Pipeline tests
├── snapshot/__tests__/          # Snapshot tests
└── snowflake/__tests__/         # Snowflake tests
```

## Mocking patterns

The test suite mocks external dependencies so no real HTTP calls or Redis connections are made. With `globals: true`, `vi` is available in every test file without importing it.

### Module-level mocks with `vi.mock()`

Vitest hoists `vi.mock()` calls to the top of the file automatically, so they execute before any imports. This is the standard pattern for mocking modules:

```typescript
vi.mock("@server/utils/logger", () => ({
  __esModule: true,
  default: {
    info: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
  },
}));

vi.mock("@server/config", () => ({
  __esModule: true,
  default: {
    auth0: { domain: "test.auth0.com", audience: "test-api" },
  },
}));

import { requireAuth } from "../auth";
```

### Mock factories with `vi.fn()`

Build reusable mock factories that return objects with `vi.fn()` stubs:

```typescript
/** Build a mock CacheStore with vi.fn() stubs for all methods. */
const makeMockStore = (
  overrides: Partial<Record<keyof CacheStore, Mock>> = {},
): CacheStore => ({
  get: vi.fn().mockResolvedValue(null),
  set: vi.fn().mockResolvedValue(undefined),
  del: vi.fn().mockResolvedValue(undefined),
  hget: vi.fn().mockResolvedValue(null),
  hset: vi.fn().mockResolvedValue(undefined),
  hgetall: vi.fn().mockResolvedValue({}),
  hdel: vi.fn().mockResolvedValue(undefined),
  expire: vi.fn().mockResolvedValue(undefined),
  appendToList: vi.fn().mockResolvedValue(0),
  checkRateLimit: vi.fn().mockResolvedValue(false),
  disconnect: vi.fn().mockResolvedValue(undefined),
  ...overrides,
});
```

### Prisma mocking via path aliases

Instead of `vi.mock()`, the Vitest config uses `resolve.alias` to redirect Prisma imports to mock files in `src/__mocks__/`:

```typescript
// vitest.config.ts — resolve.alias entries
"../generated/prisma/client": path.resolve(__dirname, "src/__mocks__/prisma-client.ts"),
"@prisma/adapter-mariadb": path.resolve(__dirname, "src/__mocks__/prisma-adapter-mariadb.ts"),
```

The mock files use `vi.fn()` to stub the Prisma client:

```typescript
// src/__mocks__/prisma-client.ts
/// <reference types="vitest/globals" />
export const PrismaClient = vi.fn().mockImplementation(() => ({}));
export const Prisma = {
  sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({
    strings,
    values,
  }),
  join: (values: unknown[], separator = ", ") => values.join(separator),
  raw: (value: string) => value,
  empty: "",
  DbNull: null,
  JsonNull: null,
  AnyNull: null,
};
```

### Per-test response control

Use `mockResolvedValueOnce` / `mockRejectedValueOnce` to control responses per test:

```typescript
it("parses and returns stored messages", async () => {
  const { cache } = makeCache({
    get: vi.fn().mockResolvedValue(JSON.stringify(sampleMessages)),
  });
  await expect(cache.getConversation(USER_ID, CONV_ID)).resolves.toEqual(
    sampleMessages,
  );
});
```

### Clearing mocks between tests

Use `vi.clearAllMocks()` in `beforeEach` to reset call counts and return values:

```typescript
beforeEach(() => {
  vi.clearAllMocks();
});
```

## Writing new tests

1. Create a `__tests__/` directory next to the source file if one doesn't exist
2. Name test files `<module>.test.ts` matching the source file
3. Mock external dependencies (logger, config, Redis, Snowflake) at the module level with `vi.mock()`
4. Use `mockResolvedValueOnce` / `mockRejectedValueOnce` per test case
5. For tools with `enabled()` predicates, test both enabled and disabled states
6. Tests must pass with `pnpm test:unit` before committing -- husky pre-commit runs lint on staged files

## Functional tests

Functional tests exercise the full Express stack with real MySQL and Redis but a deterministic `FakeProvider` instead of Bedrock. They run via Docker Compose:

```bash
pnpm docker:test:functional
```

The `FakeProvider` implements the same `AIProvider` interface as the real Bedrock provider but returns queue-based, deterministic responses. This lets you test:

- Conversation CRUD lifecycle
- SSE streaming event serialization
- Tool execution round-trips
- Pagination, identity isolation, auth
- Error handling, degradation, health checks

Test helpers (`apps/server/src/__tests__/functional/helpers/`) provide:

- **`TestHarness`** -- app lifecycle, DB/Redis cleanup between tests
- **`FakeProvider`** -- deterministic AI responses with static factory methods
- **`SSEParser`** -- parses SSE event streams into typed objects

See the [Functional Tests TRD](../decisions/trds/functional-tests.md) for the full design.

## Code quality

- **ESLint** -- `@typescript-eslint/no-explicit-any` is an error (zero-any policy)
- **Prettier** -- enforced via pre-commit hook on staged `.ts`/`.tsx` files across all packages
- **TypeScript strict mode** -- `strictNullChecks`, `noImplicitAny`, `noUncheckedIndexedAccess`
- **Unused vars** -- prefix with `_` to intentionally ignore

## CI

Jenkins runs `docker compose up lint-and-test` which builds a Docker image with all dev dependencies and runs `pnpm test` (lint + Vitest with coverage). Tests must pass before a PR can merge.
