NeriTest

Testing cookbook

Recipes for common bot patterns. All of these are exercised by the runnable examples/bots.test.ts and test/integration suites.

A small helper used throughout:

TypeScript
const waitFor = async (fn: () => unknown, timeout = 3000) => {
  const start = Date.now();
  while (!fn()) {
    if (Date.now() - start > timeout) throw new Error("timed out");
    await new Promise((r) => setTimeout(r, 10));
  }
};
const ready = (c: any) => new Promise((r) => c.on("ready", r));
const syncServer = (c: any, id: string) => waitFor(() => c.servers.cache.get(id));

Ping / pong and edits

TypeScript
channel.send(alice, "!ping");
await waitFor(() => channel.lastMessage?.content?.startsWith("Pong!"));

HTML content

Nerimity's HTML support is the message's htmlEmbed field, distinct from plain content.

TypeScript
channel.send(bob, { content: "hi", html: "<b>Hello!</b>" });
client.on("messageCreate", (m: any) => {
  m.content;         // "hi"
  m.raw.htmlEmbed;   // "<b>Hello!</b>"
});
// assert with the HTML matcher:
expect(channel.lastMessage?.htmlEmbed).toContainHtml("<b>");

Reactions

TypeScript
const msg = channel.send(alice, "vote!");
msg.react(bob, "thumbsup");
client.on("messageReactionAdded", (reaction, user) => {
  reaction.name; // "thumbsup"
  user?.id;      // bob.id
});

Buttons and modals

TypeScript
const msg = channel.send(alice, { content: "click", buttons: [{ id: "hi", label: "Hi" }] });
msg.clickButton(alice, "hi");
client.on("messageButtonClick", async (button) => {
  if (button.id === "hi") await button.respond({ title: "Hey", content: `Hi ${button.user?.username}!` });
});

Moderation with permission enforcement

TypeScript
const server = sandbox.createServer({ name: "Guild", owner, botPermissions: permissions("BAN") });
const sdkServer = await syncServer(client, server.id);
await sdkServer.banMember(troll.id, { reason: "spam" });
expect(server.isBanned(troll)).toBe(true);
expect(troll.wasBanned(server)).toBe(true);

Give the bot no permission and assert the failure path:

TypeScript
const server = sandbox.createServer({ name: "Locked", owner, botPermissions: 0 });
const sdkServer = await syncServer(client, server.id);
await expect(sdkServer.banMember(victim.id)).rejects.toThrow(/not allowed to ban/);

Automod (delete + warn)

TypeScript
const bad = channel.send(spammer, "buy cheap followers at spamlink.example");
await waitFor(() => bad.deleted);
expect(bad.deleted).toBe(true);

Reaction roles

TypeScript
attachReactionRoles(client, {
  messageId: pinned.id,
  map: { gamepad: gamerRoleId },
  onAssign: (userId, roleId) => server.giveRole(sandbox.user(userId)!, roleId),
});
pinned.react(alice, "gamepad");
await waitFor(() => server.member(alice)?.hasRole(gamerRoleId));

Cooldowns and scheduled jobs (virtual time)

Inject the sandbox clock so you can fast-forward instead of waiting:

TypeScript
attachEconomy(client, { now: () => sandbox.now() });

channel.send(user, "!daily");
await waitFor(() => channel.lastMessage?.content?.includes("claimed 100"));
channel.send(user, "!daily");
await waitFor(() => channel.lastMessage?.content?.includes("Already claimed"));

sandbox.advanceTime("24h");          // no real waiting
channel.send(user, "!daily");
await waitFor(() => channel.lastMessage?.content?.includes("Balance: 200"));
TypeScript
attachReminder(client, { schedule: (cb, ms) => sandbox.clock.setTimeout(cb, ms) });
channel.send(user, "!remind 60 water the plants");
sandbox.advanceTime("60s");
await waitFor(() => channel.lastMessage?.content?.includes("water the plants"));

Rate limits, failures, latency

TypeScript
sandbox.api.rateLimit({ requests: 1, retryAfterMs: 1000 });
await expect(sdkChannel.send("hi")).rejects.toThrow(/rate limited/i);
await sdkChannel.send("ok"); // succeeds - one-shot

sandbox.api.failNext(400, { message: "Custom validation error." });
await expect(sdkChannel.send("x")).rejects.toThrow(/Custom validation/);

sandbox.api.setLatency(200);   // every REST call now takes ≥200ms
sandbox.api.hangNext(5000);    // next call hangs 5s (test your timeouts)

Gateway faults and reconnection

TypeScript
sandbox.gateway.setLatency(50);          // add ws latency
sandbox.gateway.setPacketLoss(0.1);      // drop 10% of packets
sandbox.gateway.setDuplication(0.05);    // duplicate 5%
sandbox.gateway.rejectNextAuth();        // fail the next authenticate
sandbox.gateway.disconnect();            // drop all sockets; the SDK reconnects

Slash commands

TypeScript
await register(client, sandbox.token);   // POST /applications/bot/commands
// the SDK parses "/help:botId 2" into message.command = { name: "help", args: ["2"] }

Performance / load

TypeScript
const { server, users, channels } = sandbox.loadFixture("large-server");
// 200 users, 25 channels, ~1000 messages by default - tune via a custom builder.