Some checks failed
Release / semantic-release (push) Successful in 42s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / build-linux (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
tests / Unit tests (Linux, Python 3.11) (push) Has been cancelled
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 <noreply@anthropic.com>
120 lines
5.0 KiB
Python
120 lines
5.0 KiB
Python
"""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
|