Files
prosolis 7fa98d03c7 Both sections of the advice arrived in the language she is not learning
Reported from the phone: "I have my main language set as Portuguese and I say
I'm learning English but yet Petal presents the Ask Petal advice in both
sections as Portuguese."

Nothing was wrong with targetFor again. A Portuguese document by a Portuguese
writer is explained in Portuguese, which is the whole point of Phase 28. The
card was right. What was wrong was the tap underneath it: /suggestions/{id}
/translate answered "" for exactly that case, on the reasoning that an
unasked-for English rendering of an explanation she can already read is not a
seed but noise. That reasoning had the writer facing the wrong way. She is
learning English. The half she is *practising* is the half worth a tap, and the
bubble sits directly beneath the explanation inside the same card, so answering
"" left her with Portuguese, the same Portuguese again, and no English anywhere
on the card. targetFor's own comment promised the other language stays one tap
away in both directions; only one direction had ever been built.

So the endpoint keeps the one rule it always claimed: render into whichever
half the explanation is not already in. English explanation into her language,
as before; her language into the English she is learning, which is new. Both
ends of that are now parameters — TranslateMessages took the source language
for granted as English because until Phase 28 it always was. The zh prompt is
unchanged byte for byte, which its test still pins.

The client fallback was the same symptom from a different cause and would have
survived the server fix: an empty answer, or an unreachable model, seeded the
bubble with the explanation itself — a verbatim repeat of the line two above
it, which reads as Petal replying in the language the tap was pressed to
escape. With the endpoint always having somewhere to go, empty now means only
that the model didn't answer, so the panel opens with no bubble at all and the
input where she can ask. AskPetal no longer takes the explanation as a prop; it
never needed anything but the id.

The test that pinned the refusal now pins the rendering, and carries the report.

Claude-Session: https://claude.ai/code/session_01KGACAtTPjvZ2PipDZ5qD99
2026-08-02 12:03:19 -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", EnglishTarget(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.", English, 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", "", EnglishTarget(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", English, 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)
}
}
}