Files
petal/internal/lexicon/lexicon_test.go
prosolis 4c288834c0 Editor: document tone, right-click word lookup, expanded stats
Four enhancements to make the editor fit real school usage:

- Per-document tone (academic/professional/casual/humorous/creative/
  persuasive/general): new documents.tone column (migration 0002), threaded
  through the docs API, a bilingual ToneSelect dropdown on the title row, and
  injected into the grammar-checkpoint LLM prompt so advice fits the register.
  The voice pass stays tone-agnostic.

- Right-click word lookup: a new offline `lexicon` package serves definitions
  (Wordset, modern ESL-friendly glosses) and synonyms (WordNet synsets first,
  then frequency+stopword-ranked Moby for breadth) from gzipped embedded data,
  behind /api/word/{word} with light morphology. The WordCard popover shows the
  definition and tappable synonym pills that swap the word in place.

- Expanded writing stats: clicking the word count opens a StatsPanel with page
  count, sentences, paragraphs, reading time, average word length, word variety,
  and Flesch-Kincaid reading level — all computed client-side.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 23:22:55 -07:00

77 lines
1.9 KiB
Go

package lexicon
import "testing"
func TestLookupKnownWord(t *testing.T) {
l := New()
res, err := l.Lookup("happy")
if err != nil {
t.Fatalf("Lookup: %v", err)
}
if len(res.Definitions) == 0 {
t.Errorf("expected definitions for %q, got none", "happy")
}
if len(res.Synonyms) == 0 {
t.Errorf("expected synonyms for %q, got none", "happy")
}
if len(res.Synonyms) > maxSynonyms {
t.Errorf("synonyms not capped: got %d, want <= %d", len(res.Synonyms), maxSynonyms)
}
if len(res.Definitions) > maxDefinitions {
t.Errorf("definitions not capped: got %d, want <= %d", len(res.Definitions), maxDefinitions)
}
}
func TestLookupMorphology(t *testing.T) {
l := New()
// Inflected forms should resolve to their base entry via candidates().
for _, w := range []string{"running", "studies", "boxes", "quickly", "stopped"} {
res, err := l.Lookup(w)
if err != nil {
t.Fatalf("Lookup(%q): %v", w, err)
}
if len(res.Definitions) == 0 && len(res.Synonyms) == 0 {
t.Errorf("expected some result for inflected %q, got nothing", w)
}
}
}
func TestLookupUnknownWord(t *testing.T) {
l := New()
res, err := l.Lookup("zzzxqqq")
if err != nil {
t.Fatalf("Lookup: %v", err)
}
if len(res.Definitions) != 0 || len(res.Synonyms) != 0 {
t.Errorf("expected empty result for nonsense word, got %+v", res)
}
// Empty result must still serialize as [] not null for the frontend.
if res.Definitions == nil || res.Synonyms == nil {
t.Errorf("empty slices must be non-nil for JSON []: %+v", res)
}
}
func TestCandidates(t *testing.T) {
cases := map[string]string{
"cats": "cat",
"studies": "study",
"running": "run",
"hoped": "hope",
"quickly": "quick",
"happily": "happy",
}
for word, want := range cases {
got := candidates(word)
found := false
for _, c := range got {
if c == want {
found = true
break
}
}
if !found {
t.Errorf("candidates(%q) = %v, missing expected base %q", word, got, want)
}
}
}