Files
zalbot/tests/README.md
Kaysser Kayyali e2c92e854f
Some checks failed
tests / Unit tests (Node 22) (push) Failing after 2m13s
Add unit tests for LLM clients, persona loader, and XP/Foundry rewards
Expands the unit test suite from 320 to 380 tests (+60) and adds a
Gitea Actions CI workflow. Closes all six follow-up recommendations
from the test-architecture validation report.

New tests (tests/unit/):
  - ollamaClient.test.ts          — Ollama SDK wrapper, options passthrough
  - litellmClient.test.ts         — OpenAI SDK wrapper, model fallback
  - personaLoader.test.ts         — Zod validation + cache invalidation
  - foundryReward.test.ts         — Tool plugin: lookup, errors, partial grants
  - xpAwarder.test.ts             — Bulk XP awards + per-player skip reasons
  - redisErrorPath.test.ts        — Singleton error handler does not crash
  - messageRouterRunLLMTurn.test.ts — 18 cases for the runtime heart:
    narrative-only path, tool dispatch, filter correction, retry loop
    guard, missed-skill-check heuristic, typing indicator interval,
    LLM error fallback, archive on resolve.

Coverage (line %):
  - harness/litellmClient.ts      0 → 100
  - harness/ollamaClient.ts       0 → 100
  - harness/tools/foundryReward.ts 0 → 100
  - session/xpAwarder.ts          0 → 100
  - persona/loader.ts             0 → 100
  - db/redis.ts                   0 → 100
  - bot/handlers/messageRouter.ts 0 → 39.86 (runLLMTurn now covered)

Tooling:
  - package.json: + test:coverage, test:watch scripts
  - devDep: @vitest/coverage-v8@^3.1.0
  - tests/README.md: conventions, anti-patterns, template map
  - .gitignore: exclude coverage/
  - .gitea/workflows/test.yml: Node 22, npm cache, tsc --noEmit gate

Documentation (from earlier /bmad-document-project run, now committed):
  - docs/index.md
  - docs/project-overview.md
  - docs/architecture.md
  - docs/deployment-guide.md
  - docs/api-contracts.md
  - docs/data-models.md
  - docs/source-tree-analysis.md
  - docs/component-inventory.md
  - docs/development-guide.md
  - _bmad-output/test-artifacts/automate-validation-report.md

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-19 05:59:13 +00:00

4.9 KiB

Tests

This directory holds the project's automated test suite.

Layout

tests/
├── fixtures/        Shared test fixtures (spec, session, etc.)
├── integration/     Integration tests (require live infrastructure)
├── unit/            Unit tests (default CI gate)
└── README.md        You are here
  • unit/ — fast, isolated tests for individual modules. No network, no Redis, no Discord gateway. The CI default runs only this directory.
  • integration/ — slower tests that exercise real services (or mocks close to the wire). Run explicitly; not part of the default test command.
  • fixtures/ — reusable mocks (mockSession, mockSpec) shared by multiple unit tests.

Running

npm test              # alias for `npm run test:unit` + runs once (not watch)
npm run test:unit     # run all tests in tests/unit
npm run test:int      # run all tests in tests/integration
npm run test:coverage # run unit tests with v8 coverage report
npm run test:watch    # vitest in watch mode

Conventions

1. One module per file

A test file covers one source module. File name: <moduleName>.test.ts, placed under tests/unit/. If a source module exports multiple functions worth testing, group them with describe blocks in the same file.

2. Mock before import — always

vi.mock calls must appear before the import of the module under test, otherwise the unmocked module is already cached. The pattern:

import { vi, describe, it, expect, beforeEach } from 'vitest';

const { mockFn } = vi.hoisted(() => ({ mockFn: vi.fn() }));

vi.mock('../../src/lib/logger.js', () => ({
  log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}));

// ...more mocks...

import { myFunction } from '../../src/some/module.js'; // AFTER mocks

vi.hoisted lets you share mock state between a vi.mock factory and the test body — both run in the same scope.

3. vi.clearAllMocks() in beforeEach

Prevents test bleed-through. If you also mutate config or module-level state, reset it explicitly in beforeEach.

4. Reuse mockSession and mockSpec

Import from ../fixtures/spec.js. Don't redefine session shape per file — schema drift is one of the easier ways for tests to silently rot.

5. Test the behavior, not the implementation

Assert outcomes (return values, side effects on real collaborators, error messages) rather than calling patterns. When a test would only pass with a specific internal implementation, ask whether the contract is what's documented in the source's doc comment.

6. Don't hit the network

  • fetch → use vi.stubGlobal('fetch', ...) (see foundryClientRetry.test.ts).
  • Discord client → pass a hand-rolled mock with only the methods the code uses (e.g. messages.fetch, send, sendTyping, setArchived).
  • Redis → use ioredis-mock (see sessionManager.test.ts).
  • LLM SDKs → mock the constructor (see litellmClient.test.ts, ollamaClient.test.ts).
  • Filesystem → use mkdtempSync from node:os.tmpdir() (see personaLoader.test.ts).

7. Player-facing strings

When a test asserts on a string the bot would say to a player, prefer in-world language over utility terms. (Same rule that applies to production code — see feedback-in-world-voice memory.)

Anti-patterns

  • Asserting private state. Reach for behaviour-side assertions first.
  • Resetting state with vi.resetModules() for the sake of it. It breaks shared mock state. Use it only when a module-scoped cache (e.g. a lazy client) needs to be re-constructed.
  • Catching all errors in a test. If a test passes by accident because an unhandled rejection was swallowed, it's not testing anything.
  • Mocking the module under test. If you have to mock the file you're testing, the test is asserting nothing.
  • Timeouts in it() callbacks. Use vi.useFakeTimers() and vi.advanceTimersByTimeAsync to step time deterministically (see messageRouterRunLLMTurn.test.ts for the typing-indicator pattern).

Adding a new test

  1. Create tests/unit/<name>.test.ts.
  2. Use the closest existing test as a template — goalRegister.test.ts for tool plugins, foundryClientRetry.test.ts for HTTP, relaySession.test.ts for node:https / node:crypto, sessionManager.test.ts for Redis.
  3. Run npm run test:unit -- <your-file> to iterate quickly.
  4. When green, run the full suite: npm run test:unit.
  5. Optional: check npm run test:coverage to confirm the file's coverage.

Coverage

npm run test:coverage produces a v8 coverage report in the terminal. Directories worth watching:

  • src/bot/handlers/ — message routing; runLLMTurn is the runtime heart.
  • src/harness/tools/ — the tool plugin contracts.
  • src/vtt/ — Foundry relay; foundryClient is the biggest single file.

Coverage is informational, not a gate. The goal is to grow the unit test surface for the modules that own irreversible or user-facing behavior.