Files
javis_bot/tests/test_system_prompt.py
javis-bot 8a2a109d5e feat(brain): make OUTPUT_LANGUAGE lock robust on small models
Harden the reply-language lock so qwen2.5:3b reliably stays in the locked
language instead of leaking the query language back in:

- reply_language_directive(): single resolver with clear precedence —
  explicit OUTPUT_LANGUAGE lock wins over the Piper/Chatterbox English-only
  fallback (this deployment's actual TTS is Korean MeloTTS, so the legacy
  English lock was both wrong and contradicting the Korean lock).
- Stronger, override-explicit directive wording, inserted near the FRONT of
  the system prompt so a small model gives it primacy over the persona.
- build_system_prompt(output_language=...): rewrite the persona's "in the
  user's language" clause to the locked language so the persona stops
  fighting the lock.
- docs/llm_contexts.md: document the resolver, precedence, and placement.

Live-verified on the running brain (qwen2.5:3b): Korean voice-style input
and a cold English query both return fully Korean replies with no CJK/Hanja
leak. Tests cover unset/set/agnostic/whitespace + precedence + persona rewrite.
2026-06-12 21:18:47 +09:00

111 lines
4.5 KiB
Python

"""Tests for the unified persona system prompt.
The persona should match the user's configured wake word so renaming the
wake word to e.g. "Friday" produces a butler named Friday, not one still
hardcoded to Jarvis.
"""
from jarvis.system_prompt import (
build_system_prompt,
output_language_directive,
reply_language_directive,
ENGLISH_ONLY_DIRECTIVE,
)
class TestBuildSystemPrompt:
def test_default_name_is_jarvis(self):
prompt = build_system_prompt()
assert "named Jarvis" in prompt
def test_custom_name_replaces_jarvis(self):
prompt = build_system_prompt("Friday")
assert "named Friday" in prompt
assert "named Jarvis" not in prompt
def test_lowercase_wake_word_is_capitalised(self):
prompt = build_system_prompt("friday".capitalize())
assert "named Friday" in prompt
def test_blank_name_falls_back_to_jarvis(self):
assert "named Jarvis" in build_system_prompt("")
assert "named Jarvis" in build_system_prompt(" ")
assert "named Jarvis" in build_system_prompt(None) # type: ignore[arg-type]
def test_default_keeps_user_language_clause(self):
# Without a lock, the persona still mirrors the user's language.
assert "in the user's language" in build_system_prompt("Jarvis")
def test_language_lock_rewrites_user_language_clause(self):
# With a lock, the contradicting "user's language" clause is rewritten
# so the persona does not fight the OUTPUT_LANGUAGE directive.
prompt = build_system_prompt("Jarvis", "Korean")
assert "in the user's language" not in prompt
assert "in Korean" in prompt
class TestOutputLanguageDirective:
"""A deployment may lock replies to a single language via OUTPUT_LANGUAGE.
Unset (the default) must keep the assistant's multilingual behaviour of
replying in the user's own language, so the helper returns None and no
directive is injected.
"""
def test_unset_returns_none(self):
assert output_language_directive(None) is None
assert output_language_directive("") is None
assert output_language_directive(" ") is None
def test_set_language_is_named_and_exclusive(self):
directive = output_language_directive("Korean")
assert directive is not None
assert "Korean" in directive
# Must force exclusivity, not merely prefer the language.
assert "only" in directive.lower()
def test_language_agnostic(self):
# The helper takes any language string — no hardcoded single language.
assert "French" in (output_language_directive("French") or "")
assert "日本語" in (output_language_directive("日本語") or "")
def test_strips_surrounding_whitespace(self):
directive = output_language_directive(" Korean ")
assert directive is not None
assert "Korean" in directive
assert " Korean" not in directive
class TestReplyLanguageDirective:
"""Precedence: explicit OUTPUT_LANGUAGE lock > English-only TTS > free.
The lock must override the Piper/Chatterbox English fallback, because a
deployment that sets OUTPUT_LANGUAGE (e.g. Korean) also runs a TTS voice
that can speak it. Without this, the English lock and the Korean lock
contradict each other and the model reverts to English.
"""
def test_lock_overrides_english_only_tts(self):
directive = reply_language_directive("Korean", "piper")
assert directive is not None
assert "Korean" in directive
assert directive != ENGLISH_ONLY_DIRECTIVE
def test_english_only_tts_forces_english_without_lock(self):
assert reply_language_directive(None, "piper") == ENGLISH_ONLY_DIRECTIVE
assert reply_language_directive("", "chatterbox") == ENGLISH_ONLY_DIRECTIVE
def test_no_lock_multilingual_tts_is_free(self):
# A non-English-only engine (e.g. melo) with no lock → reply in the
# user's own language, so no directive.
assert reply_language_directive(None, "melo") is None
def test_unknown_tts_defaults_to_english_only(self):
# Preserves the original getattr(cfg, 'tts_engine', 'piper') default:
# an unknown/missing engine is treated conservatively as English-only.
assert reply_language_directive(None, None) == ENGLISH_ONLY_DIRECTIVE
def test_lock_wins_even_with_multilingual_tts(self):
directive = reply_language_directive("Korean", "melo")
assert directive is not None and "Korean" in directive