Code-review fixes for collocation coach + vocab garden
Correctness: - useCheckpoint: clear the busy flag unconditionally so overlapping explicit passes don't strand each other's spinner; explicit actions now also supersede a queued auto-check and clear the stranded "checking" dot. Deduped runVoice/runCollocation into runExplicitPass. - EditorCore: token-guard the auto-capture so a late capture can't resurrect a removed word; move toggleSaveWord side effects out of the setWordInfo updater (StrictMode double-fire); fix sentenceAround offset desync via shared exampleAt (textBetween + parentOffset, single resolve); optimistic saved state so the heart doesn't flash unsaved. - vocab capture: normalize word to lower+trim (matches lexicon) so "Apple"/"apple" don't make duplicate cards; check rows.Err() in queryList. - GardenPanel: Promise.allSettled so a /due failure doesn't blank the whole garden; scrim click during review ends the review (mirrors Esc); gate footer on !error; O(1) due lookup via a Set. Features requested in review: - Definition-only review card: add vocab_words.definition (migration 0007) as an English fallback meaning, threaded through capture and used by review/garden when there's no Chinese gloss. - Scheduler caps: maxEase 3.0 + maxInterval 365d so "easy" growth can't push a word out of rotation for years. Tests: TestCaptureCaseInsensitive, TestCaptureStoresDefinitionFallback, TestCapsBoundGrowth. go build/vet/test, tsc, vitest 51/51, vite build clean. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -141,6 +141,48 @@ func TestCaptureValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCaptureStoresDefinitionFallback proves a word with an English definition
|
||||
// but no Chinese gloss still round-trips its definition, so review has a meaning
|
||||
// to reveal instead of a blank card.
|
||||
func TestCaptureStoresDefinitionFallback(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
rec := do(t, srv, http.MethodPost, "/vocab",
|
||||
`{"word":"ineffable","definition":"too great to be expressed in words"}`)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("capture: code=%d body=%s", rec.Code, rec.Body)
|
||||
}
|
||||
var w Word
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &w)
|
||||
if w.Gloss != "" {
|
||||
t.Fatalf("expected no gloss, got %q", w.Gloss)
|
||||
}
|
||||
if w.Definition != "too great to be expressed in words" {
|
||||
t.Fatalf("definition not stored, got %q", w.Definition)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCaptureCaseInsensitive proves "Apple" (sentence start) and "apple"
|
||||
// (mid-line) land in the SAME garden card — capture normalizes to lower+trim
|
||||
// like the lexicon, so the idempotent upsert isn't defeated by casing.
|
||||
func TestCaptureCaseInsensitive(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
if rec := do(t, srv, http.MethodPost, "/vocab", `{"word":"Apple","gloss":"苹果"}`); rec.Code != http.StatusCreated {
|
||||
t.Fatalf("capture Apple: code=%d body=%s", rec.Code, rec.Body)
|
||||
}
|
||||
if rec := do(t, srv, http.MethodPost, "/vocab", `{"word":" apple ","gloss":"苹果"}`); rec.Code != http.StatusCreated {
|
||||
t.Fatalf("capture apple: code=%d body=%s", rec.Code, rec.Body)
|
||||
}
|
||||
rec := do(t, srv, http.MethodGet, "/vocab", "")
|
||||
var all []Word
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &all)
|
||||
if len(all) != 1 {
|
||||
t.Fatalf("Apple/apple should be one card, got %d", len(all))
|
||||
}
|
||||
if all[0].Word != "apple" {
|
||||
t.Fatalf("word should be normalized to lowercase, got %q", all[0].Word)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDocLinkSurvivesDocDelete proves the ON DELETE SET NULL keeps a word in the
|
||||
// garden when its source document is removed.
|
||||
func TestDocLinkSurvivesDocDelete(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user