meshcore.js
Guides

Test without a radio

Drive the bot in Vitest on a fake radio, with simulated time.

The owner announces, the bot posts. A stranger tries, the bot refuses. Both checked in a test, without a radio.

announce.test.ts
import { createTestClient } from '@meshcorejs/testing';
import { afterEach, expect, it } from 'vitest';

const bots: Array<{ destroy(): Promise<void> }> = [];
afterEach(async () => {
  await Promise.all(bots.map((bot) => bot.destroy()));
});

it('lets an owner post on the channel', async () => {
  const bot = await createTestClient({ load: import.meta.dirname, now: '2026-09-20T18:00:00Z' });
  bots.push(bot);

  const alice = bot.fakeContact('Alice');
  bot.fakeChannel('#club');
  bot.setRoleMembers('owner', [alice]);

  expect(await bot.dm(alice, '/announce Meeting at 19:00')).toEqual(['Posted']);
  expect(bot.sentTo('#club')).toEqual(['Meeting at 19:00']);
});

it('refuses everyone else', async () => {
  const bot = await createTestClient({ load: import.meta.dirname });
  bots.push(bot);

  expect(await bot.dm(bot.fakeContact('Bob'), '/announce free beer')).toEqual(['⛔ Permission denied']);
});

What happens

createTestClient() logs a real Client in on a FakeRadio and loads the bricks of a folder. fakeContact() and fakeChannel() fill the radio's tables.

dm() sends a message as a contact and returns what the bot answered. sentTo() returns what the bot put on a channel.

Time is simulated. advanceTime('6h') fires the jobs due in between. destroy() releases everything.

Go further

On this page