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
119 lines
3.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|