Add #help command — short single-message list of all commands (v0.21.6)
CI / build + unit (pull_request) Failing after 21s

Owner request: 'now add #help with short list of all command'. Registered in
the CommandService constructor next to #ping; returns a SINGLE multi-line
string (one packet, ~203 chars) — not an array, which would drip-feed one
spaced packet per line through the v0.21.5 MESH_MULTI_SEND_DELAY_MS loop.

Also hardened the v0.21.5 multi-send-spacing spec: its wall-clock assertion
('first send < 8 ms after t0') flaked under full-suite load — replaced with
fake timers, which assert deterministically that line 1 is out before any time
elapses and lines 2-3 land exactly one delay apart.
This commit is contained in:
2026-09-26 10:52:18 +00:00
parent 847c3d47d0
commit 70be8bdaa7
5 changed files with 43 additions and 12 deletions
+25 -10
View File
@@ -41,6 +41,15 @@ describe('CommandService.onRx', () => {
expect(mesh.sendText).not.toHaveBeenCalled();
});
it('replies #help with a short single-message list of all commands', async () => {
const [svc, mesh] = makeService();
await svc.onRx(2, '#help', 42, 7);
expect(mesh.respond).toHaveBeenCalledTimes(1); // one packet — not one per line
const text = mesh.respond.mock.calls[0][1];
expect(text).toContain('#ping');
expect(text).toContain('#rss');
});
it('falls back to a directed sendText on COMMAND_CHANNEL when the trigger has no packet id (DM)', async () => {
const [svc, mesh] = makeService();
await svc.onRx(2, '#ping', 42); // from present but no id — defensive path
@@ -126,26 +135,32 @@ describe('CommandService.onRx', () => {
});
it('spaces consecutive multi-line sends by MESH_MULTI_SEND_DELAY_MS — no delay before the first line', async () => {
process.env.MESH_MULTI_SEND_DELAY_MS = '10'; // short so the test stays fast
const sentAt: number[] = [];
process.env.MESH_MULTI_SEND_DELAY_MS = '50'; // short so the test stays fast
let sends = 0;
const respond = vi.fn(async () => {
sentAt.push(Date.now());
sends++;
return { id: 1 };
});
vi.useFakeTimers();
try {
const [svc, mesh] = makeService(undefined, respond);
svc.register('multi3', () => ['a: one', 'b: two', 'c: three']);
const t0 = Date.now();
await svc.onRx(2, '#multi3', 42, 9);
const p = svc.onRx(2, '#multi3', 42, 9);
// flush microtasks without advancing time — the first line must already be out (no delay before it)
await vi.advanceTimersByTimeAsync(0);
expect(sends).toBe(1);
// each subsequent line waits exactly MESH_MULTI_SEND_DELAY_MS after the previous one landed
await vi.advanceTimersByTimeAsync(50);
expect(sends).toBe(2);
await vi.advanceTimersByTimeAsync(50);
expect(sends).toBe(3);
await p;
expect(mesh.respond).toHaveBeenCalledTimes(3);
// the first line goes out immediately (no delay before it) — only microtask-scale overhead between t0 and the first send
expect(sentAt[0] - t0).toBeLessThan(8);
// each subsequent line waits at least MESH_MULTI_SEND_DELAY_MS after the previous one landed
expect(sentAt[1] - sentAt[0]).toBeGreaterThanOrEqual(10);
expect(sentAt[2] - sentAt[1]).toBeGreaterThanOrEqual(10);
} finally {
vi.useRealTimers();
delete process.env.MESH_MULTI_SEND_DELAY_MS;
}
});
+11
View File
@@ -43,6 +43,17 @@ export class CommandService {
rss?: RssService,
) {
this.register('ping', () => '#pong!');
// single multi-line string = ONE packet — not an array, which would drip-feed one spaced packet per line
this.register('help', () =>
[
'#ping - alive check',
'#rss help - rss subcommands',
'#rss add <url> <name> - add feed with AI tags',
'#rss remove <name> - drop feed',
'#rss get <name|#tag> - news summary',
'#rss sources / #rss tags - list feeds/tags',
].join('\n'),
);
if (rss) this.registerPrefix('rss', (ctx) => rss.handle(ctx));
}