Tests Without a Token
A real client against an in-memory Fluxer, so tests run without a token or network access

One Set of Options for the Bot and Its Tests
The testing entry point, @neontechspace/, ships with the SDK. Its createTestBot takes the same options object as runBot, except instance and transport, which the test client owns
Keep that object in a module of its own, so the bot and its tests run the same events and commands. The token can stay unset in a test
import type { BotOptions } from "@neontechspace/fluxerly"// The bot's runBot options, shared by bot.ts and the tests export const botOptions: BotOptions = { token: process.env.FLUXER_BOT_TOKEN, commands: { prefix: "!", commands: { ping: { description: "Check that the bot replies", execute: ({ reply }) => reply("Pong!"), }, }, },}A First Test with Say
The test client is a real client connected to an in-memory Fluxer. say sends a message as a fake user, waits until the bot has finished and returns what it sent
The message goes through the SDK's own decoding and handlers, as a real one would. node:test is the runner built into Node.js, so there is nothing else to install
import assert from "node:assert/strict" import { test } from "node:test"import { createTestBot } from "@neontechspace/fluxerly/testing" import { botOptions } from "./bot-options.ts" test("!ping replies Pong!", async () => { await using bot = createTestBot(botOptions) await bot.ready() const replies = await bot.say("!ping") assert.deepEqual( replies.map((message) => message.content), ["Pong!"], )})node --testA Broken Handler Fails the Test
A command that throws is recorded as commands.failed, and failures() returns it so the test can expect it
A failure that no test reads makes the shutdown reject. A broken handler fails the test even when no assertion looks at it

test("a command that throws is reported", async () => { await using bot = createTestBot({ commands: { prefix: "!", commands: { broken: { execute: () => { throw new Error("Not implemented yet") }, }, }, }, }) await bot.ready() bot.emit("MESSAGE_CREATE", bot.fixtures.message({ content: "!broken" })) await bot.idle() assert.deepEqual( bot.failures().map((record) => record.code), ["commands.failed"], )})
Answer as the API Would
rest.respond tells the in-memory Fluxer how to answer a request, and returns a route whose next() waits for the request it answered
Code that registers handlers with client.on instead of runBot is tested with createTestClient, the same test client without bot options
import assert from "node:assert/strict" import { test } from "node:test"import type { Client } from "@neontechspace/fluxerly"import { createTestClient } from "@neontechspace/fluxerly/testing" // The application passes its own client and welcome channelexport function installWelcome(client: Client, channelId: string) { return client.on("guildMemberAdd", (member) => client.messages.send(channelId, `Welcome, ${member.userId}!`)) }test("new members are welcomed", async () => { await using bot = createTestClient() const sent = bot.rest.respond("POST /channels/:id/messages", { body: bot.fixtures.message() }) installWelcome(bot.client, bot.fixtures.ids.channel) await bot.ready() bot.emit("GUILD_MEMBER_ADD", bot.fixtures.member()) const request = await sent.next() assert.match(String((request.body as { content?: unknown }).content), /^Welcome/) })The Same for Effect
Applications built with Effect, a TypeScript library for typed failures and scoped cleanup, test with @neontechspace/
It has the same createTestBot. Its controls are Effects, and the test bot shuts down when the surrounding Effect.scoped ends
import { Effect } from "effect"import { createTestBot } from "@neontechspace/fluxerly/effect/testing" export const pingTest = Effect.scoped( Effect.gen(function* () { const bot = yield* createTestBot({ token: process.env.FLUXER_BOT_TOKEN, events: { messageCreate: ({ message, reply }) => message.content !== "!ping" ? Effect.void : reply("Pong!"), }, }) yield* bot.ready() const replies = yield* bot.say("!ping") const ignored = yield* bot.say("hello") return { replies: replies.map((message) => message.content), ignored: ignored.length } }),)













