package tldrbench import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "github.com/bwmarrin/discordgo" ) func write(t *testing.T, name, content string) string { t.Helper() p := filepath.Join(t.TempDir(), name) if err := os.WriteFile(p, []byte(content), 0o600); err != nil { t.Fatal(err) } return p } func texts(msgs []*discordgo.Message) []string { var out []string for _, m := range msgs { out = append(out, m.Author.Username+"|"+m.Content) } return out } func TestLoadAuthorLines(t *testing.T) { msgs, err := LoadChat(write(t, "chat.txt", "Ana: olá a todos\nRui: bom dia\nsegunda linha do Rui\n\nAna: http://x.pt/a?b=1: link\n")) if err != nil { t.Fatal(err) } want := []string{"Ana|olá a todos", "Rui|bom dia\nsegunda linha do Rui", "Ana|http://x.pt/a?b=1: link"} if got := texts(msgs); strings.Join(got, ";") != strings.Join(want, ";") { t.Errorf("got %q, want %q", got, want) } } func TestLoadClientCopy(t *testing.T) { chat := "Ana — Today at 14:32\nolá pessoal\nvamos ao jantar?\n[14:33]\nque horas?\nRui — Today at 14:35\nsexta às 20h\nAna — 12/10/2026 14:40\nboa\n" msgs, err := LoadChat(write(t, "copy.txt", chat)) if err != nil { t.Fatal(err) } want := []string{"Ana|olá pessoal", "Ana|vamos ao jantar?", "Ana|que horas?", "Rui|sexta às 20h", "Ana|boa"} if got := texts(msgs); strings.Join(got, ";") != strings.Join(want, ";") { t.Errorf("got %q, want %q", got, want) } } func TestLoadExport(t *testing.T) { js := `{"messages":[ {"id":"1","type":"Default","content":"olha isto https://youtu.be/x","author":{"id":"9","name":"ana","nickname":"Ana","isBot":false},"embeds":[{"title":"Um vídeo"}]}, {"id":"2","type":"Reply","content":"vi","author":{"id":"8","name":"rui","isBot":false},"reference":{"messageId":"1"},"attachments":[{"fileName":"a.png"}]}, {"id":"3","type":"GuildMemberJoin","content":"","author":{"id":"7","name":"x"}}, {"id":"4","type":"Default","content":"beep","author":{"id":"6","name":"bot","isBot":true}}]}` msgs, err := LoadChat(write(t, "export.json", js)) if err != nil { t.Fatal(err) } if len(msgs) != 4 { t.Fatalf("got %d messages", len(msgs)) } if msgs[0].Author.Username != "Ana" || len(msgs[0].Embeds) != 1 { t.Errorf("first message wrong: %+v", msgs[0]) } if msgs[1].Type != discordgo.MessageTypeReply || msgs[1].ReferencedMessage != msgs[0] || len(msgs[1].Attachments) != 1 { t.Errorf("reply wrong: %+v", msgs[1]) } if msgs[2].Type == discordgo.MessageTypeDefault || !msgs[3].Author.Bot { t.Error("system and bot messages should be marked so the bot's own filters drop them") } } func TestLoadErrors(t *testing.T) { if _, err := LoadChat(filepath.Join(t.TempDir(), "missing.txt")); err == nil { t.Error("missing file should fail") } if _, err := LoadChat(write(t, "empty.txt", "\n\n")); err == nil { t.Error("empty chat should fail") } if _, err := LoadChat(write(t, "bad.json", "{nope")); err == nil { t.Error("broken json should fail") } } func TestEvaluate(t *testing.T) { facts := writeFacts(t, `[ {"name":"dinner","all":["jantar","20h"]}, {"name":"meeting","all":["reuni","terça"]}, {"name":"hosting","all":["120","80"]}]`) chat := "Ana: jantar sexta às 20h\nRui: o hosting subiu de 80€ para 120€\nRita: reunião para terça às 11h\n" out := "- Jantar de equipa na sexta às 20h, 9 pessoas.\n- A reunião passa para terça às 11h.\n mais uma linha\n- Hosting subiu para 120€ por mês, 300 pedidos" sc := Evaluate(out, chat, facts) if sc.FactsHit != 2 || sc.FactsTotal != 3 || len(sc.Missed) != 1 || sc.Missed[0] != "hosting" { t.Errorf("facts: %+v", sc) } // "9" and "300" are not in the chat; "20", "11", "120" are. if strings.Join(sc.Invented, ",") != "9,300" { t.Errorf("invented = %v", sc.Invented) } if sc.Bullets != 3 || !sc.CutOff || sc.Copied || sc.EnglishIntro { t.Errorf("flags: %+v", sc) } if !Evaluate("Here is an overview of the chat:\n- ok.", "", nil).EnglishIntro { t.Error("English intro not detected") } if !Evaluate("- "+strings.Repeat("palavra ", 120)+".", "", nil).Copied { t.Error("one giant bullet should count as copied") } if Evaluate("- Tudo bem.", "", nil).CutOff { t.Error("a finished sentence is not cut off") } // A fact needs all its patterns in one bullet. split := Evaluate("- Jantar sexta.\n- Às 20h há reunião.", "", facts[:1]) if split.FactsHit != 0 { t.Error("pattern spread over two bullets must not pass") } } func writeFacts(t *testing.T, js string) []Fact { t.Helper() f, err := LoadFacts(write(t, "facts.json", js)) if err != nil { t.Fatal(err) } return f } func TestLoadFactsErrors(t *testing.T) { for name, js := range map[string]string{ "no patterns": `[{"name":"x","all":[]}]`, "bad regex": `[{"name":"x","all":["("]}]`, "bad json": `nope`, } { if _, err := LoadFacts(write(t, "f.json", js)); err == nil { t.Errorf("%s should fail", name) } } } func TestReportRender(t *testing.T) { rep := &Report{ When: time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC), Chat: ChatInfo{Source: "chat.txt", Messages: 100, Kept: 60, Chunks: 1, InputChars: 3000}, Threads: 8, Facts: []string{"dinner", "meeting"}, Models: []string{"m1", "m2"}, Variants: []string{"current", "alt"}, Runs: []Run{ {Model: "m1", Variant: "current", N: 1, Seconds: 60, FirstText: 20, Output: "- x & y", Score: Score{FactsHit: 2, FactsTotal: 2, Hit: []string{"dinner", "meeting"}}}, {Model: "m1", Variant: "current", N: 2, Seconds: 70, Output: "- ok", Score: Score{FactsHit: 1, FactsTotal: 2, Hit: []string{"dinner"}, Missed: []string{"meeting"}, Invented: []string{"7"}}}, {Model: "m2", Variant: "current", N: 1, Error: "ollama unreachable"}, {Model: "m1", Variant: "alt", N: 1, Seconds: 40, Output: "- ok", Score: Score{FactsHit: 0, FactsTotal: 2, Missed: []string{"dinner", "meeting"}}}, }, } var buf bytes.Buffer if err := rep.WriteHTML(&buf); err != nil { t.Fatal(err) } html := buf.String() for _, want := range []string{"2 models, 2 prompts", "1.5 of 2", "failed", "not run", "<b>x</b> & y", "Best on this chat", "m1", "missed: meeting", "numbers not in the chat: 7"} { if !strings.Contains(html, want) { t.Errorf("html missing %q", want) } } if strings.Contains(html, "x") { t.Error("output must be escaped") } if strings.Contains(html, "http://") || strings.Contains(html, "https://") || strings.Contains(html, " 3 { t.Errorf("want 1 to 3 patterns, got %v", f.All) } for _, p := range f.All { if strings.Contains(strings.ToLower(p), "ana") && !strings.Contains(strings.ToLower(f.Note[strings.Index(f.Note, ":"):]), "ana") { t.Errorf("author name leaked into pattern %q", p) } } } if got := SuggestFacts(lines, 2); len(got) != 2 { t.Errorf("max not applied: %d", len(got)) } if got := SuggestFacts([]string{"Ana: oi", "Rui: sim"}, 5); len(got) != 0 { t.Errorf("small talk should give no candidates: %+v", got) } }