Test bullet parser; fix marker-less passthrough

The previous parser treated any non-empty non-header line as a bullet,
so 'plain paragraph' answers were silently rewrapped as <ul><li>. Now
we only count a line if it had a recognized bullet marker (- * • ·);
if none did, fall back to raw + <pre> as originally intended.

Tests cover dash/asterisk/unicode bullets, blank-line tolerance,
Summary:/TL;DR header stripping, HTML escaping, single-bullet output,
the no-marker passthrough path, and the IsQuestionReaction set.
This commit is contained in:
prosolis
2026-05-22 18:31:26 -07:00
parent dd324c0317
commit d41eaa6504
2 changed files with 125 additions and 8 deletions

View File

@@ -139,29 +139,36 @@ func (e *Explainer) summarize(headline, body string) (string, error) {
func formatSummary(raw string) (plain, htmlBody string) {
lines := strings.Split(raw, "\n")
var bullets []string
sawMarker := false
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Normalize common bullet markers
for _, prefix := range []string{"- ", "* ", "• ", "·"} {
if strings.HasPrefix(line, prefix) {
line = strings.TrimSpace(line[len(prefix):])
break
}
}
// Skip header-ish lines the model sometimes adds
low := strings.ToLower(line)
if strings.HasPrefix(low, "summary:") || strings.HasPrefix(low, "tl;dr") {
continue
}
// Normalize common bullet markers; only count this line if it had one.
hadMarker := false
for _, prefix := range []string{"- ", "* ", "• ", "·"} {
if strings.HasPrefix(line, prefix) {
line = strings.TrimSpace(line[len(prefix):])
hadMarker = true
break
}
}
if !hadMarker {
continue
}
sawMarker = true
if line == "" {
continue
}
bullets = append(bullets, line)
}
if len(bullets) == 0 {
if !sawMarker || len(bullets) == 0 {
// Model produced something but we couldn't parse bullets — pass through.
return raw, "<pre>" + html.EscapeString(raw) + "</pre>"
}

View File

@@ -0,0 +1,110 @@
package explainer
import (
"strings"
"testing"
)
func TestFormatSummary(t *testing.T) {
cases := []struct {
name string
in string
wantPlain string
wantHTML string
}{
{
name: "dash bullets",
in: "- First point.\n- Second point.\n- Third point.",
wantPlain: "• First point.\n• Second point.\n• Third point.",
wantHTML: "<ul><li>First point.</li><li>Second point.</li><li>Third point.</li></ul>",
},
{
name: "asterisk bullets",
in: "* alpha\n* beta",
wantPlain: "• alpha\n• beta",
wantHTML: "<ul><li>alpha</li><li>beta</li></ul>",
},
{
name: "unicode bullets",
in: "• one\n• two",
wantPlain: "• one\n• two",
wantHTML: "<ul><li>one</li><li>two</li></ul>",
},
{
name: "blank lines ignored",
in: "- a\n\n- b\n\n\n- c",
wantPlain: "• a\n• b\n• c",
wantHTML: "<ul><li>a</li><li>b</li><li>c</li></ul>",
},
{
name: "summary header stripped",
in: "Summary:\n- first\n- second",
wantPlain: "• first\n• second",
wantHTML: "<ul><li>first</li><li>second</li></ul>",
},
{
name: "tldr header stripped",
in: "TL;DR of the article\n- key point\n- other point",
wantPlain: "• key point\n• other point",
wantHTML: "<ul><li>key point</li><li>other point</li></ul>",
},
{
name: "single bullet",
in: "- just one",
wantPlain: "• just one",
wantHTML: "<ul><li>just one</li></ul>",
},
{
name: "html escaped in bullets",
in: `- A "quote" & <tag>`,
wantPlain: `• A "quote" & <tag>`,
wantHTML: "<ul><li>A &#34;quote&#34; &amp; &lt;tag&gt;</li></ul>",
},
{
name: "leading whitespace trimmed",
in: " - first\n - second ",
wantPlain: "• first\n• second",
wantHTML: "<ul><li>first</li><li>second</li></ul>",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
gotPlain, gotHTML := formatSummary(c.in)
if gotPlain != c.wantPlain {
t.Errorf("plain:\n got %q\n want %q", gotPlain, c.wantPlain)
}
if gotHTML != c.wantHTML {
t.Errorf("html:\n got %q\n want %q", gotHTML, c.wantHTML)
}
})
}
}
func TestFormatSummary_NoBulletsPassthrough(t *testing.T) {
in := "Just a single paragraph with no bullets at all."
plain, htmlBody := formatSummary(in)
if plain != in {
t.Errorf("plain should pass through raw, got %q", plain)
}
if !strings.HasPrefix(htmlBody, "<pre>") || !strings.HasSuffix(htmlBody, "</pre>") {
t.Errorf("html should wrap raw in <pre>, got %q", htmlBody)
}
if !strings.Contains(htmlBody, "single paragraph") {
t.Errorf("html should contain the body text, got %q", htmlBody)
}
}
func TestIsQuestionReaction(t *testing.T) {
want := []string{"❓", "❔", "⁉", "⁉️", "🤔", "?", ""}
for _, k := range want {
if !IsQuestionReaction(k) {
t.Errorf("expected %q to trigger explain", k)
}
}
notQuestion := []string{"👍", "👎", "🔥", "", "??", "❤️"}
for _, k := range notQuestion {
if IsQuestionReaction(k) {
t.Errorf("expected %q to NOT trigger explain", k)
}
}
}