1- // Package main provides unit tests for the ash Matrix bot functionality.
21package main
32
43import (
4+ "context"
5+ "database/sql"
6+ "fmt"
7+ "strings"
58 "testing"
9+ "time"
10+
11+ _ "github.com/mattn/go-sqlite3"
12+ "maunium.net/go/mautrix/event"
13+ "maunium.net/go/mautrix/id"
614)
715
8- // TestExtractLinks tests the URL extraction from text messages.
916func TestExtractLinks (t * testing.T ) {
1017 tests := []struct {
1118 name string
@@ -37,7 +44,6 @@ func TestExtractLinks(t *testing.T) {
3744 }
3845}
3946
40- // TestIsBlacklisted tests URL blacklisting functionality.
4147func TestIsBlacklisted (t * testing.T ) {
4248 blacklist , err := LoadBlacklist ("blacklist.json" )
4349 if err != nil {
@@ -47,7 +53,6 @@ func TestIsBlacklisted(t *testing.T) {
4753 _ = IsBlacklisted ("https://example.com" , blacklist )
4854}
4955
50- // TestExtractJSONPath tests JSON path extraction for bot commands.
5156func TestExtractJSONPath (t * testing.T ) {
5257 root := map [string ]interface {}{
5358 "a" : map [string ]interface {}{
@@ -84,7 +89,6 @@ func TestExtractJSONPath(t *testing.T) {
8489 }
8590}
8691
87- // TestFormatPosts tests formatting of post arrays for bot responses.
8892func TestFormatPosts (t * testing.T ) {
8993 posts := []interface {}{
9094 map [string ]interface {}{"title" : "Post 1" , "url" : "https://a.com" },
@@ -102,7 +106,6 @@ func TestFormatPosts(t *testing.T) {
102106 }
103107}
104108
105- // TestFormatPostsLimit tests that formatPosts limits output to 5 posts.
106109func TestFormatPostsLimit (t * testing.T ) {
107110 // More than 5 posts should be capped
108111 posts := make ([]interface {}, 10 )
@@ -125,7 +128,6 @@ func TestFormatPostsLimit(t *testing.T) {
125128 }
126129}
127130
128- // TestTruncateText tests text truncation for AI prompts.
129131func TestTruncateText (t * testing.T ) {
130132 tests := []struct {
131133 name string
@@ -147,7 +149,6 @@ func TestTruncateText(t *testing.T) {
147149 }
148150}
149151
150- // TestStripCommandPrefix tests removal of bot command prefixes.
151152func TestStripCommandPrefix (t * testing.T ) {
152153 tests := []struct {
153154 input string
@@ -170,7 +171,6 @@ func TestStripCommandPrefix(t *testing.T) {
170171 }
171172}
172173
173- // TestResolveReplyLabel tests reply label resolution logic.
174174func TestResolveReplyLabel (t * testing.T ) {
175175 tests := []struct {
176176 name string
@@ -194,7 +194,6 @@ func TestResolveReplyLabel(t *testing.T) {
194194 }
195195}
196196
197- // TestInSlice tests string slice membership checking.
198197func TestInSlice (t * testing.T ) {
199198 slice := []string {"a" , "b" , "c" }
200199 if ! inSlice (slice , "b" ) {
@@ -208,7 +207,6 @@ func TestInSlice(t *testing.T) {
208207 }
209208}
210209
211- // TestTruncate tests string truncation utility.
212210func TestTruncate (t * testing.T ) {
213211 if truncate ("hello" , 10 ) != "hello" {
214212 t .Error ("truncate should not truncate short string" )
@@ -219,7 +217,6 @@ func TestTruncate(t *testing.T) {
219217 }
220218}
221219
222- // TestGenerateHelpMessage tests help message generation for bot commands.
223220func TestGenerateHelpMessage (t * testing.T ) {
224221 botCfg := & BotConfig {
225222 Commands : map [string ]BotCommand {
@@ -245,7 +242,6 @@ func TestGenerateHelpMessage(t *testing.T) {
245242 }
246243}
247244
248- // TestLoadBotConfig tests loading of bot configuration from file.
249245func TestLoadBotConfig (t * testing.T ) {
250246 cfg , err := LoadBotConfig ("bot.json" )
251247 if err != nil {
@@ -275,12 +271,10 @@ func TestLoadBotConfig(t *testing.T) {
275271
276272// helpers
277273
278- // contains checks if a substring is present in a string.
279274func contains (s , substr string ) bool {
280275 return len (s ) >= len (substr ) && searchString (s , substr )
281276}
282277
283- // searchString performs a simple substring search.
284278func searchString (s , substr string ) bool {
285279 for i := 0 ; i <= len (s )- len (substr ); i ++ {
286280 if s [i :i + len (substr )] == substr {
@@ -290,7 +284,6 @@ func searchString(s, substr string) bool {
290284 return false
291285}
292286
293- // splitLines splits a string into lines by newline characters.
294287func splitLines (s string ) []string {
295288 var lines []string
296289 start := 0
@@ -305,3 +298,150 @@ func splitLines(s string) []string {
305298 }
306299 return lines
307300}
301+
302+ func TestUwuify (t * testing.T ) {
303+ tests := []struct {
304+ name string
305+ input string
306+ check func (string ) bool
307+ desc string
308+ }{
309+ {
310+ "replaces r/l with w" ,
311+ "really cool" ,
312+ func (s string ) bool { return strings .Contains (s , "w" ) },
313+ "should replace r and l with w" ,
314+ },
315+ {
316+ "replaces th with d" ,
317+ "the weather" ,
318+ func (s string ) bool { return strings .Contains (s , "da" ) && strings .Contains (s , "wead" ) },
319+ "should replace 'the ' with 'da ' and 'th' with 'd'" ,
320+ },
321+ {
322+ "replaces love with wuv" ,
323+ "I love you" ,
324+ func (s string ) bool { return strings .Contains (s , "wuv" ) },
325+ "should replace love with wuv" ,
326+ },
327+ {
328+ "appends kaomoji" ,
329+ "hello world" ,
330+ func (s string ) bool {
331+ faces := []string {"uwu" , "owo" , ">w<" , "^w^" , "◕ᴗ◕✿" , "✧w✧" , "~nyaa" }
332+ for _ , f := range faces {
333+ if strings .HasSuffix (s , f ) {
334+ return true
335+ }
336+ }
337+ return false
338+ },
339+ "should end with a kaomoji" ,
340+ },
341+ }
342+ for _ , tt := range tests {
343+ t .Run (tt .name , func (t * testing.T ) {
344+ got := uwuify (tt .input )
345+ if ! tt .check (got ) {
346+ t .Errorf ("uwuify(%q) = %q: %s" , tt .input , got , tt .desc )
347+ }
348+ })
349+ }
350+ }
351+
352+ func TestQueryTopYappers (t * testing.T ) {
353+ db , err := sql .Open ("sqlite3" , ":memory:" )
354+ if err != nil {
355+ t .Fatalf ("open db: %v" , err )
356+ }
357+ defer db .Close ()
358+
359+ _ , err = db .Exec (`CREATE TABLE IF NOT EXISTS messages (
360+ id TEXT PRIMARY KEY,
361+ room_id TEXT,
362+ sender TEXT,
363+ ts_ms INTEGER,
364+ body TEXT,
365+ msgtype TEXT,
366+ raw_json TEXT
367+ )` )
368+ if err != nil {
369+ t .Fatalf ("create table: %v" , err )
370+ }
371+
372+ now := time .Now ().UnixMilli ()
373+ room := "!testroom:example.com"
374+
375+ // Insert test messages: alice=5, bob=3, carol=1, plus some bot messages that should be excluded.
376+ for i := 0 ; i < 5 ; i ++ {
377+ _ , _ = db .Exec (`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)` ,
378+ fmt .Sprintf ("alice-%d" , i ), room , "@alice:example.com" , now - int64 (i * 1000 ), fmt .Sprintf ("hello %d" , i ), "m.text" )
379+ }
380+ for i := 0 ; i < 3 ; i ++ {
381+ _ , _ = db .Exec (`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)` ,
382+ fmt .Sprintf ("bob-%d" , i ), room , "@bob:example.com" , now - int64 (i * 1000 ), fmt .Sprintf ("hey %d" , i ), "m.text" )
383+ }
384+ _ , _ = db .Exec (`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)` ,
385+ "carol-0" , room , "@carol:example.com" , now , "sup" , "m.text" )
386+
387+ // Bot messages — should be excluded.
388+ _ , _ = db .Exec (`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)` ,
389+ "bot-1" , room , "@bot:example.com" , now , "[BOT] hello" , "m.text" )
390+ _ , _ = db .Exec (`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)` ,
391+ "bot-2" , room , "@bot:example.com" , now , "/bot help" , "m.text" )
392+
393+ // Old message — should be excluded (>24h ago).
394+ _ , _ = db .Exec (`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)` ,
395+ "old-1" , room , "@old:example.com" , now - 100000000 , "ancient msg" , "m.text" )
396+
397+ // Different room — should be excluded.
398+ _ , _ = db .Exec (`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)` ,
399+ "other-1" , "!otherroom:example.com" , "@other:example.com" , now , "wrong room" , "m.text" )
400+
401+ ev := & event.Event {
402+ RoomID : id .RoomID (room ),
403+ }
404+
405+ ctx := context .Background ()
406+
407+ // Test default (top 5).
408+ result , err := queryTopYappers (ctx , db , nil , ev , "" )
409+ if err != nil {
410+ t .Fatalf ("queryTopYappers: %v" , err )
411+ }
412+ if ! strings .Contains (result , "alice" ) {
413+ t .Errorf ("expected alice in result, got: %s" , result )
414+ }
415+ if ! strings .Contains (result , "5 msgs" ) {
416+ t .Errorf ("expected '5 msgs' for alice, got: %s" , result )
417+ }
418+ if ! strings .Contains (result , "bob" ) {
419+ t .Errorf ("expected bob in result, got: %s" , result )
420+ }
421+ // alice should be ranked #1.
422+ if ! strings .Contains (result , "1. alice" ) {
423+ t .Errorf ("expected alice at rank 1, got: %s" , result )
424+ }
425+
426+ // Test with limit.
427+ result2 , err := queryTopYappers (ctx , db , nil , ev , "2" )
428+ if err != nil {
429+ t .Fatalf ("queryTopYappers with limit: %v" , err )
430+ }
431+ lines := strings .Split (strings .TrimSpace (result2 ), "\n " )
432+ // Header line + 2 results.
433+ if len (lines ) != 3 {
434+ t .Errorf ("expected 3 lines (header + 2 results), got %d: %s" , len (lines ), result2 )
435+ }
436+
437+ // Bot and old messages should not appear.
438+ if strings .Contains (result , "bot" ) {
439+ t .Errorf ("bot messages should be excluded, got: %s" , result )
440+ }
441+ if strings .Contains (result , "old" ) {
442+ t .Errorf ("old messages should be excluded, got: %s" , result )
443+ }
444+ if strings .Contains (result , "other" ) {
445+ t .Errorf ("messages from other rooms should be excluded, got: %s" , result )
446+ }
447+ }
0 commit comments