Files
petal/internal/lexicon/lexicon_test.go
prosolis 60eba25fee Phase 9: ESL superpowers — Chinese gloss + tone-rewrite
Inline Chinese gloss (offline) and a "say it more naturally" / tone-rewrite,
the two ESL features for the Mandarin-speaking writer.

Gloss: embedded English→Chinese dictionary (gloss.json.gz, 57k common words
built from ECDICT via scripts/build_gloss.py). lexicon gains Gloss()/Result.Gloss
and a lightweight GET /api/gloss/{word}; the right-click WordCard leads with the
中文; GlossTip shows it on a 350ms hover (reuses wordAt, so CJK is never glossed).
Offline + instant, works with the LLM down.

Rewrite: selecting text pops a SelectionBubble (更自然 + the tone vocabulary);
picking a style calls POST /api/docs/:id/rewrite (llm.RunRewrite, stateless,
owner-scoped) and shows a RewritePreview (original→rewrite, accept/cancel/retry).
Accept applies it in-editor.

Tests added in lexicon and suggestions. go build/vet/test, tsc, vite all clean;
live smoke vs a fake vLLM verified gloss + rewrite + 400/404/502 paths.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-26 00:07:26 -07:00

119 lines
3.0 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 TestGloss(t *testing.T) {
l := New()
// A common word carries a Chinese gloss...
res, err := l.Gloss("river")
if err != nil {
t.Fatalf("Gloss: %v", err)
}
if res.Word != "river" {
t.Errorf("Word = %q, want %q", res.Word, "river")
}
if res.Gloss == "" {
t.Errorf("expected a Chinese gloss for %q, got none", "river")
}
// ...and the inflected form resolves via the same de-inflection.
inflected, err := l.Gloss("rivers")
if err != nil {
t.Fatalf("Gloss(rivers): %v", err)
}
if inflected.Gloss == "" {
t.Errorf("expected a gloss for inflected %q, got none", "rivers")
}
// A nonsense word is a clean empty (not an error).
miss, err := l.Gloss("zzzxqqq")
if err != nil {
t.Fatalf("Gloss(miss): %v", err)
}
if miss.Gloss != "" {
t.Errorf("expected empty gloss for nonsense word, got %q", miss.Gloss)
}
}
func TestLookupIncludesGloss(t *testing.T) {
l := New()
res, err := l.Lookup("happy")
if err != nil {
t.Fatalf("Lookup: %v", err)
}
if res.Gloss == "" {
t.Errorf("expected Lookup to include a Chinese gloss for %q", "happy")
}
}
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)
}
}
}