mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-13 04:01:35 +00:00
146 lines
6.8 KiB
Python
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()
|