Architecture
NeriTest is one package, internally modular. It does not fake the SDK; it stands up real servers and lets the real SDK connect.
┌──────────────────────────────────────────────┐
│ Sandbox │
│ (orchestrator: clock, ids, wiring, teardown) │
└──────────────────────────────────────────────┘
│ │ │
world-side ─────┤ │ ├───── bot-side
(test author drives) │ │ │ (real nerimity.js)
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌──────────────┐
│ Facade │ │ Store │ │ BotLoader │
│ Sandbox* │──▶│ (state, │◀───│ createClient │
│ objects │ │ events) │ │ mount/loadBot│
└───────────┘ └─────┬─────┘ └──────┬───────┘
│ domain events │ wires overrides
┌────────────┴───────────┐ │
▼ ▼ ▼
┌─────────────┐ ┌──────────────────┐
│ Gateway │ │ ApiServer │
│ (socket.io) │ │ (express) │
└──────┬──────┘ └────────┬─────────┘
│ ws packets │ HTTP
└───────────┬──────────────┘
▼
┌────────────────────────┐
│ real @nerimity/*.js │
│ Client │
└────────────────────────┘
Modules
| Module | Responsibility |
|---|---|
src/model/store.ts |
Single source of truth. Holds all state as Raw* payloads; mutations emit typed domain events. |
src/model/rawTypes.ts |
Wire payload shapes mirrored 1:1 from the SDK's RawData.ts. |
src/model/enums.ts, permissions.ts |
MessageType, ChannelType, and the permission bitfield, mirrored from the SDK. |
src/gateway/ |
Real socket.io server: auth handshake, event routing, fault injection. |
src/api/ |
Real express server: every REST endpoint, faithful success/error bodies, fault injection. |
src/facade.ts |
Fluent "world" API (SandboxServer/Channel/User/Message/…). |
src/bot/ |
createClient / mount / loadBot, plus the CDN/webhook fetch shim. |
src/time/clock.ts |
Virtual clock and timers for advanceTime. |
src/recorder/ |
Record / replay of gateway + API traffic. |
src/logging/, src/debugger/ |
Structured filterable logs and a live debugger. |
src/fixtures/ |
Reusable state builders. |
src/assertions/ |
A small Jest-shaped expect. |
Data flow
World → bot. The test author calls a facade method (e.g. channel.send(alice, "!ping")).
The facade mutates the Store, which emits a domain event (messageCreated). The Gateway
forwards it to the sockets that should see it (server members / DM participants) as the real
message:created packet. The SDK parses it and fires messageCreate.
Bot → world. The bot calls the SDK (e.g. message.reply("Pong!")), which does a real
fetch to the ApiServer. The handler checks permissions, mutates the Store, and returns
the exact Raw* body the SDK expects. The store mutation also emits messageCreated, which
the gateway delivers back to the bot (the SDK does not dedupe its own messages - faithful to
production, which is why bots guard with if (message.user.id === client.user.id) return).
The audit this was built from
Everything mirrored was discovered by reading ~/nrepos/nerimity.js (the SDK) and verified
against ~/nrepos/nerimity-server (the real server). Highlights:
- Client events (
ClientEventMap):ready,messageCreate,messageUpdate,messageDelete,messageReactionAdded,messageReactionRemoved,serverMemberLeft,serverMemberJoined,serverMemberUpdated,serverJoined,serverLeft,messageButtonClick,serverChannelCreated,serverChannelUpdated,serverChannelDeleted,serverRoleCreated,serverRoleDeleted,serverRoleUpdated,serverRoleOrderUpdated. - Gateway events (server→client): the full set from the server's
ClientEventNames.ts, including ones the current SDK does not yet surface (channel:typing,user:presence_update,voice:*) - NeriTest can emit them for forward compatibility. - Client→server:
user:authenticate,user:update_activity,notification:dismiss. - REST endpoints used by the SDK:
GET/POST /channels/:id/messages,GET/PATCH/DELETE /channels/:id/messages/:mid, button callback,POST/DELETE /servers/:id/bans/:uid,DELETE /servers/:id/members/:uid/kick,GET/POST/PATCH/DELETE /posts,POST /applications/bot/commands,POST /webhooks/:id/:token. - Models:
RawUser,RawServer,RawServerRole,RawServerMember,RawChannel,RawMessage(incl.htmlEmbed,embed,buttons,reactions,mentions,replyMessages,quotedMessages),RawAttachment,RawEmbed,RawMessageButton,RawMessageReaction,RawBotCommand. - Enums:
MessageType(CONTENT=0, JOIN_SERVER=1, LEAVE_SERVER=2, KICK_USER=3, BAN_USER=4),ChannelType(DM_TEXT=0, SERVER_TEXT=1, CATEGORY=2). - Permissions: ADMIN=1, SEND_MESSAGE=2, MANAGE_ROLES=4, MANAGE_CHANNELS=8, KICK=16, BAN=32, MENTION_EVERYONE=64, NICKNAME_MEMBER=128, MENTION_ROLES=256.
- Interactions: buttons and modal-style callbacks with
text/dropdown/inputcomponents.
Known limitations
- Only faithful to the SDK you install. The emulation targets the shapes nerimity.js actually parses. Fields the SDK ignores are populated best-effort. Pin your SDK version.
- Voice, typing, presence, friends exist at the gateway/event-name level but are not surfaced by the current SDK, so there is little bot-observable behaviour to assert on yet. They are wired so that when the SDK adds support, NeriTest already emits them.
server.channelsafter a runtimeserver:joined. The SDK populates the global channel cache but notServer.channelson a runtime join (only on initial auth). NeriTest reproduces this faithfully - see the welcome-bot example, which readsclient.channels.cache.- Virtual time is opt-in. Network I/O runs on the real event loop. Time-based bot logic is
only virtualised if the bot reads the sandbox clock (inject
sandbox.now/sandbox.clock.setTimeout). Bots using bareDate.now()/setTimeoutrun in real time. - CDN uploads are acknowledged with a synthetic file id; no real file storage happens.