ReMe/tests/unit/test_docker_runtime.py

146 lines
6.8 KiB
Python

"""Container configuration and health checks without Docker or model calls."""
import json
from io import BytesIO
from unittest.mock import Mock
import pytest
from deploy.docker import reme_container
from reme.config import parse_kwargs, resolve_app_config
def test_container_defaults_and_cli_override_precedence(tmp_path):
"""CLI overrides win over container settings, including structured service values."""
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps({"service": {"backend": "http", "port": 8001}, "language": "zh"}))
command = reme_container.start_command(
['service={"port":8003,"web_enabled":false}', "log_to_file=true"],
{
"REME_CONFIG": str(config_file),
"REME_WORKSPACE_DIR": str(tmp_path / "data"),
"REME_HOST": "0.0.0.0",
"REME_PORT": "8002",
"REME_TIMEZONE": "UTC",
},
)
config = resolve_app_config(log_config=False, **parse_kwargs(*command[2:]))
assert config["service"] == {"backend": "http", "host": "0.0.0.0", "port": 8003, "web_enabled": False}
assert config["workspace_dir"] == str(tmp_path / "data")
assert config["timezone"] == "UTC"
assert config["language"] == "zh"
assert config["log_to_file"] is True
def test_container_preserves_file_config_port_and_quoted_paths(tmp_path):
"""Unset environment options preserve the file; paths survive CLI serialization."""
config_file = tmp_path / 'configuration "with spaces".json'
config_file.write_text(json.dumps({"service": {"backend": "http", "host": "0.0.0.0", "port": 8234}}))
command = reme_container.start_command([], {"REME_CONFIG": str(config_file)})
state_path = tmp_path / "health.json"
reme_container.write_health_state(command, state_path)
assert json.loads(state_path.read_text()) == {"url": "http://127.0.0.1:8234/health_check"}
def test_health_state_contains_no_config_credentials(tmp_path):
"""The ephemeral probe address follows effective CLI settings and stores no secrets."""
command = reme_container.start_command(
["service.port=8901", "components.as_llm.default.credential.api_key=not-a-real-key"],
{"REME_PORT": "8900", "REME_HOST": "0.0.0.0"},
)
state_path = tmp_path / "health.json"
reme_container.write_health_state(command, state_path)
assert json.loads(state_path.read_text()) == {"url": "http://127.0.0.1:8901/health_check"}
assert state_path.stat().st_mode & 0o777 == 0o600
def test_one_shot_job_does_not_register_http_health_state(tmp_path):
"""One-shot jobs use the ordinary CLI lifecycle without pretending to serve HTTP."""
state_path = tmp_path / "health.json"
command = reme_container.start_command(["job=version"], {})
reme_container.write_health_state(command, state_path)
assert not state_path.exists()
@pytest.mark.parametrize("port", ["0", "65536", "invalid"])
def test_container_rejects_invalid_port(port, tmp_path):
"""Invalid addresses fail before the server starts."""
with pytest.raises(ValueError):
command = reme_container.start_command([], {"REME_PORT": port})
reme_container.write_health_state(command, tmp_path / "health.json")
@pytest.mark.parametrize(
"payload,expected",
[
({"success": True, "metadata": {"health": {"healthy": True}}}, 0),
({"success": True, "metadata": {"health": {"healthy": False}}}, 1),
({"success": False, "metadata": {"health": {"healthy": True}}}, 1),
({"success": True, "metadata": {}}, 1),
({"success": True, "metadata": None}, 1),
(["not a Job response"], 1),
],
)
def test_healthcheck_validates_response_body(payload, expected, tmp_path, monkeypatch):
"""HTTP 200 alone cannot turn an unhealthy or malformed response into success."""
state_path = tmp_path / "health.json"
state_path.write_text(json.dumps({"url": "http://127.0.0.1:2333/health_check"}))
opener = Mock()
opener.open.return_value = BytesIO(json.dumps(payload).encode())
monkeypatch.setattr(reme_container, "build_opener", Mock(return_value=opener))
assert reme_container.healthcheck(state_path) == expected
request = opener.open.call_args.args[0]
assert request.get_method() == "POST"
assert request.data == b"{}"
def test_healthcheck_handles_missing_state_and_unreachable_server(tmp_path, monkeypatch):
"""Missing initialization and connection errors fail the probe cleanly."""
state_path = tmp_path / "health.json"
assert reme_container.healthcheck(state_path) == 1
state_path.write_text(json.dumps({"url": "http://127.0.0.1:2333/health_check"}))
opener = Mock()
opener.open.side_effect = OSError("connection refused")
monkeypatch.setattr(reme_container, "build_opener", Mock(return_value=opener))
assert reme_container.healthcheck(state_path) == 1
@pytest.mark.parametrize("action", [["start"], ["--start"], ["reme", "-start"], ["reme", "start"]])
def test_entrypoint_executes_start_and_preserves_cli_action_syntax(action, tmp_path, monkeypatch):
"""All supported start spellings apply defaults and replace the supervisor child."""
monkeypatch.setattr(reme_container.sys, "argv", ["entrypoint", *action, "service.port=8901"])
monkeypatch.setattr(reme_container, "HEALTH_STATE_PATH", tmp_path / "health.json")
monkeypatch.setenv("HOME", str(tmp_path / "home"))
monkeypatch.setenv("REME_WORKSPACE_DIR", str(tmp_path / "data"))
monkeypatch.setattr("reme.utils.load_env", Mock())
state_writer = Mock()
monkeypatch.setattr(reme_container, "write_health_state", state_writer)
exec_command = Mock(side_effect=RuntimeError("exec called"))
monkeypatch.setattr(reme_container.os, "execvp", exec_command)
with pytest.raises(RuntimeError, match="exec called"):
reme_container.main()
executable, arguments = exec_command.call_args.args
assert executable == "reme"
assert arguments[:2] == ["reme", "start"]
assert parse_kwargs(*arguments[2:])["service"]["port"] == 8901
state_writer.assert_called_once_with(arguments)
def test_entrypoint_passes_other_commands_without_shell_interpolation(tmp_path, monkeypatch):
"""Diagnostic commands retain literal arguments and discard stale HTTP probe state."""
command = ["python", "-c", "print('$HOME and `literal text`')"]
monkeypatch.setattr(reme_container.sys, "argv", ["entrypoint", *command])
state_path = tmp_path / "health.json"
state_path.write_text("stale")
monkeypatch.setattr(reme_container, "HEALTH_STATE_PATH", state_path)
monkeypatch.setenv("HOME", str(tmp_path / "home"))
exec_command = Mock(side_effect=RuntimeError("exec called"))
monkeypatch.setattr(reme_container.os, "execvp", exec_command)
with pytest.raises(RuntimeError, match="exec called"):
reme_container.main()
exec_command.assert_called_once_with("python", command)
assert not state_path.exists()