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>
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→ usevi.stubGlobal('fetch', ...)(seefoundryClientRetry.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(seesessionManager.test.ts). - LLM SDKs → mock the constructor (see
litellmClient.test.ts,ollamaClient.test.ts). - Filesystem → use
mkdtempSyncfromnode:os.tmpdir()(seepersonaLoader.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. Usevi.useFakeTimers()andvi.advanceTimersByTimeAsyncto step time deterministically (seemessageRouterRunLLMTurn.test.tsfor the typing-indicator pattern).
Adding a new test
- Create
tests/unit/<name>.test.ts. - Use the closest existing test as a template —
goalRegister.test.tsfor tool plugins,foundryClientRetry.test.tsfor HTTP,relaySession.test.tsfornode:https/node:crypto,sessionManager.test.tsfor Redis. - Run
npm run test:unit -- <your-file>to iterate quickly. - When green, run the full suite:
npm run test:unit. - Optional: check
npm run test:coverageto 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;runLLMTurnis the runtime heart.src/harness/tools/— the tool plugin contracts.src/vtt/— Foundry relay;foundryClientis 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.