Daily Bluesky bot for AT Mot. Invites players and congratulates yesterday's solvers.
1import { describe, it, expect } from 'vitest';
2import { composePost, postMarker } from '../src/compose.js';
3import { ORIGIN, type YesterdayCounts } from '../src/config.js';
4
5const y = (players: number | null, solvers: number, sampled = false): YesterdayCounts => ({
6 players,
7 solvers,
8 sampled,
9});
10
11/** Collect the tag names from a post's #tag facets, in order. */
12function tagNames(facets: ReturnType<typeof composePost>['facets']): string[] {
13 const names: string[] = [];
14 for (const f of facets) {
15 for (const feat of f.features) {
16 if (feat.$type === 'app.bsky.richtext.facet#tag') names.push(feat.tag);
17 }
18 }
19 return names;
20}
21
22describe('postMarker', () => {
23 it('is language-specific and carries the puzzle number', () => {
24 expect(postMarker('en', 5)).toBe('AT Mot #5');
25 expect(postMarker('fr', 5)).toBe('AT Mot n°5');
26 });
27});
28
29describe('composePost — EN', () => {
30 it('normal: solvers + non-solvers', () => {
31 const { text, langs, facets } = composePost({ lang: 'en', todayN: 5, yesterday: y(33, 25) });
32 expect(text).toBe(
33 `🟩 AT Mot #5 is live! Six tries to guess today's five-letter word.\n` +
34 `Congrats to the 25 of you who cracked yesterday's word. And to the 8 who didn't, better luck today!\n` +
35 `Play: ${ORIGIN}\n` +
36 `#WordGame #atproto`,
37 );
38 expect(langs).toEqual(['en']);
39 // One homepage link facet, plus a #tag facet per hashtag.
40 const links = facets.filter((f) => f.features[0]!.$type === 'app.bsky.richtext.facet#link');
41 expect(links).toHaveLength(1);
42 expect(links[0]!.features[0]).toEqual({ $type: 'app.bsky.richtext.facet#link', uri: ORIGIN });
43 expect(tagNames(facets)).toEqual(['WordGame', 'atproto']);
44 });
45
46 it('hashtags become #tag facets; the puzzle "#N" does not', () => {
47 const { facets } = composePost({ lang: 'en', todayN: 3, yesterday: y(2, 2) });
48 expect(tagNames(facets)).toEqual(['WordGame', 'atproto']);
49 expect(tagNames(facets)).not.toContain('3');
50 });
51
52 it('singular solver, no non-solvers', () => {
53 const { text } = composePost({ lang: 'en', todayN: 5, yesterday: y(1, 1) });
54 expect(text).toContain('Congrats to the one player who cracked yesterday\'s word.');
55 expect(text).not.toContain('who didn\'t');
56 });
57
58 it('singular non-solver', () => {
59 const { text } = composePost({ lang: 'en', todayN: 5, yesterday: y(5, 4) });
60 expect(text).toContain('And to the one who didn\'t, better luck today!');
61 });
62
63 it('zero solvers', () => {
64 const { text } = composePost({ lang: 'en', todayN: 5, yesterday: y(5, 0) });
65 expect(text).toContain('Nobody cracked yesterday\'s word. Fresh start today!');
66 });
67
68 it('everyone solved (no non-solver clause)', () => {
69 const { text } = composePost({ lang: 'en', todayN: 5, yesterday: y(10, 10) });
70 expect(text).toContain('Congrats to the 10 of you who cracked yesterday\'s word.');
71 expect(text).not.toContain('who didn\'t');
72 });
73
74 it('sampled: hedged floor, no exact non-solver count', () => {
75 const { text } = composePost({ lang: 'en', todayN: 5, yesterday: y(50, 20, true) });
76 expect(text).toContain('Congrats to the 20+ of you who cracked yesterday\'s word. Can you join them today?');
77 });
78
79 it('day 1 (no yesterday): invite only', () => {
80 const { text } = composePost({ lang: 'en', todayN: 1, yesterday: null });
81 expect(text).toBe(
82 `🟩 AT Mot #1 is live! Six tries to guess today's five-letter word.\n` +
83 `Play: ${ORIGIN}\n` +
84 `#WordGame #atproto`,
85 );
86 });
87
88 it('Constellation unreachable (players null): no congrats', () => {
89 const { text } = composePost({ lang: 'en', todayN: 5, yesterday: y(null, 0) });
90 expect(text).not.toContain('Congrats');
91 expect(text).not.toContain('Nobody');
92 });
93
94 it('nobody played yesterday (players 0): no congrats', () => {
95 const { text } = composePost({ lang: 'en', todayN: 5, yesterday: y(0, 0) });
96 expect(text).not.toContain('Congrats');
97 expect(text).not.toContain('Nobody');
98 });
99});
100
101describe('composePost — FR', () => {
102 it('normal: solvers + non-solvers', () => {
103 const { text, langs, facets } = composePost({ lang: 'fr', todayN: 5, yesterday: y(23, 18) });
104 expect(text).toBe(
105 `🟩 AT Mot n°5 est en ligne ! Six essais pour deviner le mot de cinq lettres du jour.\n` +
106 `Bravo aux 18 qui ont trouvé le mot d'hier. Et aux 5 qui ont séché, meilleure chance aujourd'hui !\n` +
107 `Jouez : ${ORIGIN}\n` +
108 `#JeuDeMots #atproto`,
109 );
110 expect(langs).toEqual(['fr']);
111 expect(tagNames(facets)).toEqual(['JeuDeMots', 'atproto']);
112 });
113
114 it('singular solver uses "au seul joueur" + "a trouvé"', () => {
115 const { text } = composePost({ lang: 'fr', todayN: 5, yesterday: y(1, 1) });
116 expect(text).toContain('Bravo au seul joueur qui a trouvé le mot d\'hier.');
117 });
118
119 it('singular non-solver', () => {
120 const { text } = composePost({ lang: 'fr', todayN: 5, yesterday: y(5, 4) });
121 expect(text).toContain('Et au seul qui a séché, meilleure chance aujourd\'hui !');
122 });
123
124 it('zero solvers', () => {
125 const { text } = composePost({ lang: 'fr', todayN: 5, yesterday: y(5, 0) });
126 expect(text).toContain('Personne n\'a trouvé le mot d\'hier. Nouveau départ aujourd\'hui !');
127 });
128
129 it('sampled hedge', () => {
130 const { text } = composePost({ lang: 'fr', todayN: 5, yesterday: y(50, 20, true) });
131 expect(text).toContain('Bravo aux 20+ qui ont trouvé le mot d\'hier. Saurez-vous les rejoindre aujourd\'hui ?');
132 });
133});