package phraser import ( "errors" "strings" "testing" ) // A reply that starts a JSON object and never finishes it is a failed // generation, not a reply. Before this, the parser returned ("", "") for these // and every caller then shipped the raw fragment as the thing Maven said. A // real run produced replies of literally "{" and "{\n \"". func TestParseResponseMoodRejectsUnfinishedJSON(t *testing.T) { for _, raw := range []string{ `{`, "{\n \"", `{"response": "неполн`, `{"response": "текст", "mood":`, } { text, mood, err := parseResponseMood(raw) if !errors.Is(err, errBrokenJSON) { t.Errorf("parseResponseMood(%q) err = %v, want errBrokenJSON", raw, err) } if text != "" || mood != "" { t.Errorf("parseResponseMood(%q) leaked %q/%q — a fragment must never come back as a reply", raw, text, mood) } } } // Bare prose is still fine. Small models sometimes answer without any JSON at // all, and that reply is usable — so the new error must not swallow it. func TestParseResponseMoodAllowsBareProse(t *testing.T) { for _, raw := range []string{ "норм, а ты как?", "вот что я нашла: ключ у соседа", } { text, mood, err := parseResponseMood(raw) if err != nil { t.Errorf("parseResponseMood(%q) err = %v, want nil", raw, err) } // No JSON means no fields; the caller ships raw as-is. if text != "" || mood != "" { t.Errorf("parseResponseMood(%q) = %q/%q, want empty", raw, text, mood) } } } // The measured failure: the model wants more than 400 characters and the old // grammar cut it off mid-word. Guards the bound against being tightened back. func TestGrammarStringBoundHasRoomForARealAnswer(t *testing.T) { if !strings.Contains(responseGrammar, "{0,1000}") { t.Error("grammar string bound is not 1000; 400 truncated real replies mid-word (see the comment on responseGrammar)") } }