Files
petal/internal/vocab/plant_test.go
T
prosolis e9b8595456 Let the garden keep what she was given, not only what she sought
Two halves of the same idea, both read out of work Petal already
records.

Planting: an accepted collocation is a learnable chunk, so it becomes a
phrase card. The scheduler didn't need to know — a three-word chunk
climbs the ladder exactly like a looked-up word. What needed care was
deciding what *isn't* a chunk (single words are word choice; a
six-word-plus "collocation" is a rewritten sentence, and sentences make
miserable flashcards), and that the example must be the *corrected*
sentence — the stored draft still holds the phrasing she just left
behind. Re-accepting the same chunk leaves the existing card alone
rather than resetting a schedule it has been climbing. The whole thing
is best-effort: accepting an edit must never fail because a flashcard
couldn't be made.

The growth journal: kept this month beside kept the month before, the
phrasing that stuck, the patterns that faded. The queries were the easy
part; the honesty is the feature. "Stuck" needs the phrase in a *second*
document, because one document is just the edit where she left it.
"Faded" says nothing at all unless she has been writing lately —
otherwise a month away from Petal comes back to her as progress, which
is the one way this could lie. And a suggestion had to start recording
when she *decided* it, not when the model proposed it, so 0012 adds
resolved_at and backfills the old rows to their created_at.

It lives as a second tab in the garden, and it feeds the kitten: after
an accept she now sometimes hears something true of her alone, once per
line, half the time, never waited for.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
2026-07-27 14:16:59 -07:00

75 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package vocab
import (
"path/filepath"
"testing"
"gitea.parodia.dev/drwily/petal/internal/db"
)
func TestPhraseKey(t *testing.T) {
cases := []struct {
in, want string
}{
{"make a decision", "make a decision"},
{"Make A Decision", "make a decision"}, // shares one namespace with lookups
{" make a\ndecision ", "make a decision"}, // the editor's whitespace
{"“make a decision.”", "make a decision"}, // punctuation from the sentence around it
{"cant afford it", "cant afford it"}, // inner marks are part of the chunk
{"decision", ""}, // word choice, not a chunk — lookup's job
{"", ""}, //
{"...", ""}, //
{"12 000", ""}, // digits aren't vocabulary
{"a b c d e f g", ""}, // a clause wearing a chunk's label
{"in ones own good time again", "in ones own good time again"}, // six words is still a chunk
}
for _, tc := range cases {
if got := PhraseKey(tc.in); got != tc.want {
t.Errorf("PhraseKey(%q) = %q, want %q", tc.in, got, tc.want)
}
}
// The length cap counts runes, not bytes — otherwise a Portuguese chunk well
// inside the limit would be dropped for being accented.
accented := "ãããããã ãããããã ãããããã ãããããã ãããããã" // 34 runes, 64 bytes
if got := PhraseKey(accented); got != accented {
t.Errorf("PhraseKey(%d runes / %d bytes) = %q, want it kept", len([]rune(accented)), len(accented), got)
}
long := "ãããããããããããã ãããããããããããã ãããããããããããã ãããããããããããã ãããããããããããã ãããããããããããã"
if got := PhraseKey(long); got != "" {
t.Errorf("PhraseKey(%d runes) = %q, want \"\"", len([]rune(long)), got)
}
}
func TestPlantCreatesOnceAndReportsIt(t *testing.T) {
database, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { database.Close() })
p := Phrase{Text: "make a decision", Meaning: "why", Example: "I had to make a decision."}
created, err := Plant(database, db.LocalUserID, p)
if err != nil || !created {
t.Fatalf("first plant: created=%v err=%v", created, err)
}
created, err = Plant(database, db.LocalUserID, p)
if err != nil || created {
t.Fatalf("second plant: created=%v err=%v, want false", created, err)
}
// A card that isn't plantable is a silent no-op, not an error: planting hangs
// off accepting an edit, and that accept must never fail for a flashcard.
created, err = Plant(database, db.LocalUserID, Phrase{Text: "decision"})
if err != nil || created {
t.Fatalf("unplantable: created=%v err=%v", created, err)
}
var n int
if err := database.QueryRow(`SELECT count(*) FROM vocab_words WHERE user_id = ?`, db.LocalUserID).Scan(&n); err != nil {
t.Fatalf("count: %v", err)
}
if n != 1 {
t.Fatalf("garden has %d cards, want 1", n)
}
}