From ccddbd64485232fd6a144d8fb0c2c8ec70a4e6aa Mon Sep 17 00:00:00 2001 From: javis-bot Date: Tue, 16 Jun 2026 19:56:13 +0900 Subject: [PATCH] =?UTF-8?q?test:=20settings=20output=5Flanguage=20survives?= =?UTF-8?q?=20save=E2=86=92apply=E2=86=92recreate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration test driving the real bridge _save() and engine _resolve_output_language(): a language chosen in the settings UI is written to both the persistent volume and the runtime config, applies immediately (config wins over the OUTPUT_LANGUAGE env), and survives a simulated container recreate (entrypoint re-renders the config then merges the persistent override). Also asserts the persona and reply directive both follow the persisted language. Co-Authored-By: Claude Opus 4.7 --- ...st_settings_output_language_persistence.py | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/test_settings_output_language_persistence.py diff --git a/tests/test_settings_output_language_persistence.py b/tests/test_settings_output_language_persistence.py new file mode 100644 index 0000000..b55e262 --- /dev/null +++ b/tests/test_settings_output_language_persistence.py @@ -0,0 +1,119 @@ +"""End-to-end persistence of the output_language settings change. + +Closes the loop the reviewer flagged: a language chosen in the settings web UI +must (1) take effect immediately for the reply engine and (2) survive a +container recreate. The pieces: + + bridge._save() -> writes BOTH /data/jarvis-settings.json (persistent) + and JARVIS_CONFIG_PATH (live runtime config) + entrypoint merge -> on recreate, re-renders config from the env template + then merges the persistent overrides back on top + engine._resolve_output_language() -> reads JARVIS_CONFIG_PATH, config wins + over the OUTPUT_LANGUAGE env + +This test drives the REAL bridge save function and the REAL engine resolver +(the resolver is loaded standalone because the full engine import needs the +mcp package, which isn't installed in CI here). It simulates the env default +disagreeing with the chosen language, which is exactly the bug condition. +""" + +import ast +import json +import os +from pathlib import Path + +import pytest + +# bridge.settings_web imports only stdlib at module load (flask is imported +# lazily inside register()), so it is safe to import directly. +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "bridge")) +import settings_web # noqa: E402 + + +def _load_resolver(): + """Load engine._resolve_output_language + _extra_config without importing + the heavy jarvis package (which pulls in the optional mcp dependency).""" + src = ( + Path(__file__).resolve().parents[1] + / "src/jarvis/reply/engine.py" + ).read_text("utf-8") + tree = ast.parse(src) + wanted = {"_extra_config", "_resolve_output_language"} + mod = ast.Module( + body=[ + n + for n in tree.body + if isinstance(n, ast.FunctionDef) and n.name in wanted + ], + type_ignores=[], + ) + ns = {"os": os, "Optional": __import__("typing").Optional} + exec(compile(mod, "engine_subset", "exec"), ns) # noqa: S102 + return ns["_resolve_output_language"] + + +def _simulate_recreate_merge(template_lang: str, config_path: Path, persist_path: Path): + """Mirror docker/entrypoint.sh: re-render the runtime config from the env + template, then merge the persistent overrides on top.""" + config_path.write_text(json.dumps({"output_language": template_lang}), "utf-8") + if persist_path.exists(): + base = json.loads(config_path.read_text("utf-8")) + ov = json.loads(persist_path.read_text("utf-8")) + base.update(ov) + config_path.write_text(json.dumps(base, ensure_ascii=False, indent=2), "utf-8") + + +@pytest.mark.integration +def test_settings_save_applies_and_survives_recreate(monkeypatch, tmp_path): + config_path = tmp_path / "jarvis.json" + persist_path = tmp_path / "data" / "jarvis-settings.json" + # The compose env default is the "old" language that must be overridden. + monkeypatch.setenv("OUTPUT_LANGUAGE", "English") + monkeypatch.setenv("JARVIS_CONFIG_PATH", str(config_path)) + monkeypatch.setenv("JARVIS_SETTINGS_PATH", str(persist_path)) + # Start from the env-rendered config (as entrypoint would produce). + config_path.write_text(json.dumps({"output_language": "English"}), "utf-8") + + resolve = _load_resolver() + # Before the change: the env default wins. + assert resolve() == "English" + + # 1) User saves Korean in the settings UI. + settings_web._save({"output_language": "Korean"}) + + # Both targets are written. + assert json.loads(config_path.read_text("utf-8"))["output_language"] == "Korean" + assert json.loads(persist_path.read_text("utf-8"))["output_language"] == "Korean" + + # 2) Applies immediately: the resolver now returns Korean (config > env). + assert resolve() == "Korean" + + # 3) Survives a container recreate: entrypoint re-renders the config from the + # env template (still English) then merges the persistent override. + _simulate_recreate_merge("English", config_path, persist_path) + assert json.loads(config_path.read_text("utf-8"))["output_language"] == "Korean" + assert resolve() == "Korean" + + +@pytest.mark.integration +def test_persona_and_directive_follow_persisted_language(monkeypatch, tmp_path): + """After persistence, the persona and the reply directive both lock to the + saved language, not the env default.""" + from jarvis.system_prompt import build_system_prompt, reply_language_directive + + config_path = tmp_path / "jarvis.json" + persist_path = tmp_path / "data" / "jarvis-settings.json" + monkeypatch.setenv("OUTPUT_LANGUAGE", "English") + monkeypatch.setenv("JARVIS_CONFIG_PATH", str(config_path)) + monkeypatch.setenv("JARVIS_SETTINGS_PATH", str(persist_path)) + config_path.write_text(json.dumps({"output_language": "English"}), "utf-8") + + settings_web._save({"output_language": "Korean"}) + lang = _load_resolver()() + + persona = build_system_prompt("Jarvis", lang) + directive = reply_language_directive(lang, "melo") + assert "in Korean" in persona and "in English" not in persona + assert directive is not None and "Korean" in directive