Files
petal/internal/llm/lang_test.go
prosolis 978cb80642 Ask Petal answers in both languages, with room to read
The tutor prompt said "never mix languages in a single response" and
mirrored the language of the question, so asking in English — which she
does, because she is practising — returned the one explanation surface
that gives nothing in her own language. It now answers in both, pair
language first, halves separated by a blank line.

Which half is the safety net and which is the lesson depends on who is
writing: the pair is (English + X) and Petal is used from both ends, so
the prompt asks for both and says it doesn't know which way round.

The split is a rendering nicety, never a parse the reply depends on: a
half-streamed reply is all one half, a model that ignores the
instruction renders as one block, and nothing is ever dropped.

For the height, the first attempt clamped the box to the room left below
the anchored card so it could never overhang — measured, that gave 176px
against a 442px answer, worse than the 220px it replaced. The card's own
chrome spends ~290px of an 810px window, so "fits below the word" and
"room to read" are not both available. The ceiling is now a flat 50vh and
the overhang is made navigable instead, per item 4: the card reports its
reach like the rail already does, the column grows, and the page can
scroll to the actions below it.
2026-07-28 07:04:16 -07:00

123 lines
5.3 KiB
Go

package llm
import (
"strings"
"testing"
)
func TestLangForFallsBackToDefault(t *testing.T) {
if got := LangFor("zh"); got.Code != "zh" {
t.Fatalf("LangFor(zh) = %+v", got)
}
if got := LangFor("pt-PT"); got.Code != "pt-PT" {
t.Fatalf("LangFor(pt-PT) = %+v", got)
}
// A blank column, a stray value, and stray whitespace all resolve rather
// than erroring — a prompt is the wrong place to discover a config problem.
for _, in := range []string{"", " ", "klingon", "ZH"} {
if got := LangFor(in); got.Code != DefaultLang.Code {
t.Fatalf("LangFor(%q) = %q, want the default %q", in, got.Code, DefaultLang.Code)
}
}
if got := LangFor(" zh "); got.Code != "zh" {
t.Fatalf("LangFor with padding = %+v", got)
}
}
// The three prompts that name the writer's language must actually name *hers*.
// Before Phase 19 all three said "Simplified Chinese" outright, which is the
// bug this guards: a pt-PT writer asking "porquê" would have been answered in
// Mandarin.
func TestPromptsNameTheWritersLanguage(t *testing.T) {
pt := LangFor("pt-PT")
collocation := CollocationMessages("The rain was strong.", "casual", pt)[0].Content
if !strings.Contains(collocation, "European Portuguese") {
t.Fatalf("collocation prompt doesn't ask for a pt-PT gloss:\n%s", collocation)
}
if strings.Contains(collocation, "Simplified Chinese") {
t.Fatalf("collocation prompt still hardcodes Chinese:\n%s", collocation)
}
// The tone steering must survive alongside the language — they share one
// format string, and getting the verbs in the wrong order silently drops one.
if !strings.Contains(collocation, "relaxed, friendly, and conversational") {
t.Fatalf("collocation prompt lost its tone guidance:\n%s", collocation)
}
translate := TranslateMessages("Try a shorter sentence here.", pt)[0].Content
if !strings.Contains(translate, "European Portuguese") || strings.Contains(translate, "Chinese") {
t.Fatalf("translate prompt targets the wrong language:\n%s", translate)
}
ask := AskPetalSystemPrompt("origin", "replacement", "grammar", "explanation", "paragraph", pt)
if !strings.Contains(ask, "European Portuguese") || strings.Contains(ask, "Mandarin") {
t.Fatalf("ask-petal prompt targets the wrong language:\n%s", ask)
}
if !strings.Contains(ask, "porquê") {
t.Fatalf("ask-petal prompt doesn't recognise her word for \"why\":\n%s", ask)
}
// The suggestion context is positional in that template; a mis-numbered
// verb would quietly blank one of these fields.
for _, want := range []string{"origin", "replacement", "grammar", "explanation", "paragraph"} {
if !strings.Contains(ask, want) {
t.Fatalf("ask-petal prompt dropped %q:\n%s", want, ask)
}
}
if strings.Contains(ask, "%!") {
t.Fatalf("ask-petal prompt has a formatting error:\n%s", ask)
}
}
// The zh pair is in daily use and must be untouched by the extraction: its
// prompts should read exactly as they did when they were hardcoded.
func TestDefaultPairStillReadsAsBefore(t *testing.T) {
zh := LangFor("zh")
if got := CollocationMessages("x", "", zh)[0].Content; !strings.Contains(got, "Simplified Chinese (Mandarin) gloss in parentheses") {
t.Fatalf("zh collocation gloss changed:\n%s", got)
}
if got := TranslateMessages("x", zh)[0].Content; !strings.Contains(got, "natural, friendly Simplified Chinese (Mandarin)") {
t.Fatalf("zh translate target changed:\n%s", got)
}
if got := AskPetalSystemPrompt("a", "b", "c", "d", "e", zh); !strings.Contains(got, "为什么") {
t.Fatalf("zh ask-petal lost its Mandarin \"why\":\n%s", got)
}
}
// UX item 6: the Ask Petal answer is bilingual, pair language first, halves
// separated by one blank line. That separator is not a stylistic preference —
// AskPetal.tsx splits on it to render the two halves the way the companion
// renders its two lines — so the instruction has to survive prompt edits.
//
// The direction the writer is learning in is deliberately not encoded: the pair
// is (English + X), and an English speaker learning French needs the same two
// halves a Mandarin speaker learning English does. The prompt asks for both and
// lets the reader choose, so there is nothing here that names one half the
// answer and the other a courtesy.
func TestAskPetalAnswersInBothLanguages(t *testing.T) {
for _, code := range []string{"zh", "pt-PT", "fr", "es"} {
lang := LangFor(code)
ask := AskPetalSystemPrompt("a", "b", "grammar", "d", "e", lang)
if !strings.Contains(ask, "BOTH languages") {
t.Fatalf("%s: ask-petal no longer asks for both languages:\n%s", code, ask)
}
if !strings.Contains(ask, "single blank line") {
t.Fatalf("%s: ask-petal lost the blank-line separator the client splits on:\n%s", code, ask)
}
// Order matters to the rendering: the pair language is the prominent
// half, English the muted one beneath it.
if !strings.Contains(ask, "first the whole answer in "+lang.Name) {
t.Fatalf("%s: ask-petal doesn't put %s first:\n%s", code, lang.Name, ask)
}
// The instruction it replaced. Left in place it directly contradicts the
// new one, and a model given both will pick one at random.
if strings.Contains(ask, "Never mix languages") {
t.Fatalf("%s: ask-petal still forbids the bilingual reply it now asks for:\n%s", code, ask)
}
if strings.Contains(ask, "%!") {
t.Fatalf("%s: ask-petal prompt has a formatting error:\n%s", code, ask)
}
}
}