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>
6.2 KiB
API Contracts
External interfaces for the Mardonar Encounter Engine. Generated 2026-06-19.
The bot has two distinct "API" surfaces: the Discord slash-command surface (player/admin) and the JSON-RPC surface used to talk to GraphMCP. The LLM's tool surface is documented in architecture.md §5.2.
1. Discord slash commands
All commands are registered via src/scripts/deploy-commands.ts (Discord REST v10). The bot responds only in channels listed in DISCORD_ALLOWED_CHANNELS (empty = none).
/dndname
| Subcommand | Args | Effect |
|---|---|---|
set |
name: string (required) |
Register or update your D&D character name |
show |
— | Echo your current registered name |
clear |
— | Remove your registration |
/character
| Subcommand | Args | Effect |
|---|---|---|
register foundry |
— | Browse and claim a Foundry VTT actor (modal-driven) |
register custom |
— | Set a custom character (modal-driven) |
show |
— | Display your current character profile |
view |
— | Fetch live character stats from Foundry VTT |
clear |
— | Delete your character profile |
admin list |
— | Show all guild character registrations |
admin remove |
user: discord user (required) |
Remove another user's registration |
admin give |
— | Give an item to a Foundry character (modal-driven) |
/encounter
| Subcommand | Args | Effect |
|---|---|---|
start |
spec: string (required, file in ./specs/) |
Load spec, open a new encounter thread |
random |
— | Start a randomly selected encounter |
status |
— | Show current encounter status (phase, players, history length) |
stats |
— | Show encounter run statistics |
audit |
— | DM the most recent encounter summary file |
end |
notes: string (optional) |
Force-resolve the encounter (admin override) |
list |
— | Show all active encounters in this server |
generate |
theme: string (required) |
LLM-generate a spec from a short description |
spec |
— | Send the YAML spec for the current encounter thread |
/encounters
Opens a select-menu + search modal flow that calls GraphMCP search_encounters and get_encounter.
/roll
| Subcommand | Args | Effect |
|---|---|---|
action |
— | Manual dice roll outside an encounter |
/actions
In-character action shortcuts.
/turn
Turn management.
/xp
| Subcommand | Args | Effect |
|---|---|---|
award |
amount: number (required) |
Award XP to a character via VTT relay |
Button / modal interactions
customId |
Type | Handler |
|---|---|---|
give_modal |
modal submit | handleGiveModal |
character_custom_modal |
modal submit | handleCustomRegisterModal |
foundry_link_modal |
modal submit | handleFoundryLinkModal |
encounters_select |
string select | handleEncounterSelect |
encounters_search_btn |
button | handleSearchButton |
encounters_search_modal |
modal submit | handleSearchModalSubmit |
| (skill check buttons) | button / modal | isSkillCheckInteraction → handleRollInteraction |
2. GraphMCP JSON-RPC
Base URL: GRAPHMCP_URL (default http://localhost:9000).
Endpoint: POST {GRAPHMCP_URL}/mcp
Content-Type: application/json
Request body (JSON-RPC 2.0):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "<tool_name>",
"arguments": { ... }
}
}
Response body:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "text": "<JSON-stringified payload>" }
]
}
}
Or on error:
{ "jsonrpc": "2.0", "id": 1, "error": { "message": "..." } }
The bot's client (src/graphmcp/client.ts) parses the inner text field as JSON.
query_as_npc
Arguments:
{ npc_name: string; question: string; limit?: number }
Returns NPCQueryResult:
{
npc: string;
tier: string;
horizon_count: number;
chunks: { text: string; score: number; source: 'message' | 'lore'; author: string; timestamp: string }[];
graph_context: {
enc_id: string; enc_title: string; enc_type: string;
enc_timestamp: string; enc_summary: string;
featured_entities: string[]; locations: string[];
}[];
}
Used for NPC memory injection at session start. Filtered by GRAPHMCP_SCORE_THRESHOLD and capped at GRAPHMCP_NPC_MEMORY_LIMIT.
semantic_search
Arguments:
{ query: string; limit?: number }
Returns SemanticSearchResult:
{ chunks: { content: string; score: number; source?: string }[] }
Used by @Zalram mention handler.
log_encounter
Arguments:
{
title: string;
participants: string;
summary: string;
location?: string; // default ''
type?: string; // default 'encounter'
}
Returns LogEncounterResult:
{
enc_id: string;
title: string;
participants: string;
location: string;
timestamp: string;
}
Called from the encounter resolve path to write a permanent encounter node.
list_encounters
Arguments:
{ limit?: number } // default 10
Returns EncounterResultItem[]:
{ id: string; title: string; location: string; timestamp: string; summary: string }[]
search_encounters
Arguments:
{ query?: string; location?: string; participant?: string; limit?: number }
Returns EncounterResultItem[].
get_encounter
Arguments:
{ id: string }
Returns EncounterDetails:
{
id: string; title: string; location: string; timestamp: string;
summary: string; type: string;
participants: string[]; featured_entities: string[];
}
3. Redis contract
The bot writes to these key patterns:
| Key | Type | TTL | Owner |
|---|---|---|---|
session:{threadId} |
string (JSON SessionState) |
SESSION_TTL_HOURS (12h) |
sessionManager |
guild_threads:{guildId} |
set of thread IDs | inherits session TTL | sessionManager |
(player registry, character registry — pattern in src/session/playerRegistry.ts and characterRegistry.ts) |
varies | varies | respective module |
SessionState JSON shape: see src/types/index.ts.
raw.messages is a Redis stream published to by graphmcp/ingest.ts (fire-and-forget per encounter message). The bot does not read from it — the GraphMCP discord-connector does.