158 lines
5.7 KiB
TypeScript
158 lines
5.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createSend } from '../src/chat';
|
|
import { explainerEnabled, ExplainerPoster } from '../src/explainer';
|
|
import { EXPLAINER_TEMPLATE_IDS } from '../src/templates';
|
|
import { FakeClock } from '../src/ratelimit';
|
|
import { FakeChatSender } from './helpers';
|
|
|
|
const INTERVAL_MS = 20 * 60 * 1000;
|
|
const SILENCE_MS = 20 * 60 * 1000;
|
|
// Both default to the same 20-minute window (docs/design/stage-bridge.md B2), so a tick that is
|
|
// exactly due is also exactly at the silence boundary if chat activity and the last post
|
|
// coincide. Tests that want "due, but recently active" need a clear margin between the two
|
|
// anchors — this is that margin, well under either window.
|
|
const SLACK_MS = 5 * 60 * 1000;
|
|
|
|
void test('does not post before intervalMs has elapsed', async () => {
|
|
const clock = new FakeClock();
|
|
const sender = new FakeChatSender();
|
|
const poster = new ExplainerPoster({
|
|
send: createSend(sender),
|
|
config: { gameTitle: 'Pokemon Red' },
|
|
intervalMs: INTERVAL_MS,
|
|
silenceMs: SILENCE_MS,
|
|
clock,
|
|
});
|
|
clock.advance(INTERVAL_MS - 1);
|
|
const posted = await poster.tick();
|
|
assert.equal(posted, false);
|
|
assert.equal(sender.sent.length, 0);
|
|
});
|
|
|
|
void test('posts the first rotation card once intervalMs has elapsed, with recent chat activity', async () => {
|
|
const clock = new FakeClock();
|
|
const sender = new FakeChatSender();
|
|
const poster = new ExplainerPoster({
|
|
send: createSend(sender),
|
|
config: { gameTitle: 'Pokemon Red' },
|
|
intervalMs: INTERVAL_MS,
|
|
silenceMs: SILENCE_MS,
|
|
clock,
|
|
});
|
|
clock.advance(SLACK_MS);
|
|
poster.noteChatActivity();
|
|
clock.advance(INTERVAL_MS - SLACK_MS + 1); // total elapsed since start: INTERVAL_MS + 1 (due)
|
|
const posted = await poster.tick();
|
|
assert.equal(posted, true);
|
|
assert.equal(sender.sent.length, 1);
|
|
});
|
|
|
|
void test('skips posting when chat has been silent for silenceMs, so it does not shout into an empty room', async () => {
|
|
const clock = new FakeClock();
|
|
const sender = new FakeChatSender();
|
|
const poster = new ExplainerPoster({
|
|
send: createSend(sender),
|
|
config: { gameTitle: 'Pokemon Red' },
|
|
intervalMs: INTERVAL_MS,
|
|
silenceMs: SILENCE_MS,
|
|
clock,
|
|
});
|
|
// No noteChatActivity() calls after construction; chat goes silent from t=0.
|
|
clock.advance(INTERVAL_MS);
|
|
const posted = await poster.tick();
|
|
assert.equal(posted, false);
|
|
assert.equal(sender.sent.length, 0);
|
|
});
|
|
|
|
void test('resumes posting once chat activity returns after a silent stretch', async () => {
|
|
const clock = new FakeClock();
|
|
const sender = new FakeChatSender();
|
|
const poster = new ExplainerPoster({
|
|
send: createSend(sender),
|
|
config: { gameTitle: 'Pokemon Red' },
|
|
intervalMs: INTERVAL_MS,
|
|
silenceMs: SILENCE_MS,
|
|
clock,
|
|
});
|
|
|
|
clock.advance(INTERVAL_MS + SLACK_MS);
|
|
assert.equal(await poster.tick(), false); // silent since t=0, skipped, lastPostAt reset here
|
|
|
|
clock.advance(SLACK_MS); // still silent, some time passes before chat picks back up
|
|
poster.noteChatActivity();
|
|
clock.advance(INTERVAL_MS - SLACK_MS + 1); // due since the reset above, not silent since activity
|
|
assert.equal(await poster.tick(), true);
|
|
assert.equal(sender.sent.length, 1);
|
|
});
|
|
|
|
void test('rotates through all six explainer cards in order before repeating', async () => {
|
|
const clock = new FakeClock();
|
|
const sender = new FakeChatSender();
|
|
const poster = new ExplainerPoster({
|
|
send: createSend(sender),
|
|
config: { gameTitle: 'Pokemon Red' },
|
|
intervalMs: INTERVAL_MS,
|
|
silenceMs: SILENCE_MS,
|
|
clock,
|
|
});
|
|
|
|
for (let i = 0; i < EXPLAINER_TEMPLATE_IDS.length + 2; i++) {
|
|
clock.advance(SLACK_MS);
|
|
poster.noteChatActivity();
|
|
clock.advance(INTERVAL_MS - SLACK_MS + 1);
|
|
const posted = await poster.tick();
|
|
assert.equal(posted, true, `expected card ${i} to post`);
|
|
}
|
|
|
|
assert.equal(sender.sent.length, EXPLAINER_TEMPLATE_IDS.length + 2);
|
|
// The 7th post (index 6) should repeat the 1st card's text.
|
|
assert.equal(sender.sent[0], sender.sent[EXPLAINER_TEMPLATE_IDS.length]);
|
|
});
|
|
|
|
// -- Off switches: EXPLAINER_INTERVAL_MS=0 and FEATURE_QUIET ------------------------------------
|
|
|
|
void test('explainerEnabled is false in quiet mode and false at interval 0, independently', () => {
|
|
assert.equal(explainerEnabled({ featureQuiet: false, explainerIntervalMs: INTERVAL_MS }), true);
|
|
assert.equal(explainerEnabled({ featureQuiet: true, explainerIntervalMs: INTERVAL_MS }), false);
|
|
assert.equal(explainerEnabled({ featureQuiet: false, explainerIntervalMs: 0 }), false);
|
|
assert.equal(explainerEnabled({ featureQuiet: true, explainerIntervalMs: 0 }), false);
|
|
});
|
|
|
|
void test('an interval of 0 turns the rotation off, however long the clock runs', async () => {
|
|
const clock = new FakeClock();
|
|
const sender = new FakeChatSender();
|
|
const poster = new ExplainerPoster({
|
|
send: createSend(sender),
|
|
config: { gameTitle: 'Pokemon Red' },
|
|
intervalMs: 0,
|
|
silenceMs: SILENCE_MS,
|
|
clock,
|
|
});
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
poster.noteChatActivity(); // busy chat: the silence rule is not what is stopping it
|
|
clock.advance(INTERVAL_MS);
|
|
assert.equal(await poster.tick(), false);
|
|
}
|
|
assert.equal(sender.sent.length, 0);
|
|
});
|
|
|
|
void test('an explicitly disabled poster (quiet mode) never posts, even when due with live chat', async () => {
|
|
const clock = new FakeClock();
|
|
const sender = new FakeChatSender();
|
|
const poster = new ExplainerPoster({
|
|
send: createSend(sender),
|
|
config: { gameTitle: 'Pokemon Red' },
|
|
intervalMs: INTERVAL_MS,
|
|
silenceMs: SILENCE_MS,
|
|
clock,
|
|
enabled: false,
|
|
});
|
|
|
|
clock.advance(SLACK_MS);
|
|
poster.noteChatActivity();
|
|
clock.advance(INTERVAL_MS);
|
|
assert.equal(await poster.tick(), false);
|
|
assert.equal(sender.sent.length, 0);
|
|
});
|