255 lines
12 KiB
Python
255 lines
12 KiB
Python
"""Per-profile lifecycle never stops the shared host or hides an installed profile."""
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def homes(tmp_path, monkeypatch):
|
|
from hermes_cli import profiles
|
|
root = tmp_path / '.hermes'
|
|
secondary = root / 'profiles' / 'worker'
|
|
secondary.mkdir(parents=True)
|
|
(secondary / 'config.yaml').write_text('model: {default: worker-model}\n')
|
|
(root / 'config.yaml').write_text('gateway: {multiplex_profiles: true}\n')
|
|
monkeypatch.setattr(Path, 'home', lambda: tmp_path)
|
|
monkeypatch.setenv('HERMES_HOME', str(root))
|
|
monkeypatch.setattr(profiles, 'get_active_profile_name', lambda: 'default')
|
|
return root, secondary
|
|
|
|
|
|
@pytest.mark.parametrize('include_parked', [False, True])
|
|
def test_parked_is_not_served_but_remains_installed(homes, include_parked):
|
|
from hermes_cli import profiles
|
|
root, secondary = homes
|
|
(secondary / 'gateway.parked').write_text('provisioned offline\n')
|
|
if include_parked:
|
|
actual = profiles.profiles_to_serve(True, include_parked=True)
|
|
else:
|
|
actual = profiles.profiles_to_serve(True)
|
|
assert dict(actual) == ({'default': root, 'worker': secondary} if include_parked else {'default': root})
|
|
assert profiles.parked_marker_path(secondary) == secondary / 'gateway.parked'
|
|
assert profiles.profile_is_parked(secondary)
|
|
|
|
|
|
@pytest.mark.parametrize('multiplex', [False, True])
|
|
def test_default_marker_is_ignored_with_one_warning(homes, caplog, multiplex):
|
|
from hermes_cli.profiles import profiles_to_serve
|
|
root, _ = homes
|
|
(root / 'gateway.parked').touch()
|
|
for _ in range(2):
|
|
assert 'default' in dict(profiles_to_serve(multiplex))
|
|
warnings = [r for r in caplog.records if 'gateway.parked' in r.message]
|
|
assert len(warnings) == 1
|
|
assert warnings[0].levelname == 'WARNING'
|
|
|
|
|
|
@pytest.mark.parametrize('verb', ['stop', 'start', 'restart'])
|
|
def test_cli_lifecycle_orders_marker_before_socket(homes, monkeypatch, capsys, verb):
|
|
from hermes_cli import gateway as gw
|
|
from gateway import control_socket
|
|
root, secondary = homes
|
|
marker = secondary / 'gateway.parked'
|
|
owner = SimpleNamespace(home=root, profile_label='default', profiles=('default', 'worker'),
|
|
describe=lambda: 'test host')
|
|
monkeypatch.setenv('HERMES_HOME', str(secondary))
|
|
monkeypatch.setattr(gw, '_current_profile_name', lambda: 'worker')
|
|
monkeypatch.setattr(gw, '_refuse_from_inside_gateway', lambda *a: None)
|
|
monkeypatch.setattr(gw, 'find_gateway_pids', lambda **kw: [])
|
|
monkeypatch.setattr(gw, '_served_by_another_host_gateway', lambda *a: owner)
|
|
monkeypatch.setattr(gw, 'named_profile_served_by_running_multiplexer', lambda *a: True)
|
|
monkeypatch.setattr(gw, '_host_multiplexer_for_all_verb', lambda: owner)
|
|
calls = []
|
|
|
|
def unserve(home, name):
|
|
assert home == root and name == 'worker'
|
|
assert marker.exists() is (verb == 'stop')
|
|
calls.append('unserve')
|
|
return {'unserved': name, 'served_profiles': ['default']}
|
|
|
|
def serve(home, name):
|
|
assert home == root and name == 'worker'
|
|
assert not marker.exists()
|
|
calls.append('serve')
|
|
return {'served': name, 'served_profiles': ['default', name]}
|
|
|
|
monkeypatch.setattr(control_socket, 'request_unserve_profile', unserve, raising=False)
|
|
monkeypatch.setattr(control_socket, 'request_serve_profile_hot', serve, raising=False)
|
|
if verb == 'start':
|
|
marker.touch()
|
|
# Precedence: a standalone profile is never parked, even while a stale host record lists it.
|
|
from hermes_cli.gateway_profile_lifecycle import profile_lifecycle
|
|
(secondary / 'config.yaml').write_text('gateway: {standalone: true}\n')
|
|
assert profile_lifecycle(verb, SimpleNamespace()) is False and calls == []
|
|
assert marker.exists() is (verb == 'start')
|
|
(secondary / 'config.yaml').write_text('model: {default: worker-model}\n')
|
|
getattr(gw, '_cmd_' + verb)(SimpleNamespace())
|
|
assert calls == {'stop': ['unserve'], 'start': ['serve'], 'restart': ['unserve', 'serve']}[verb]
|
|
assert marker.exists() is (verb == 'stop')
|
|
output = capsys.readouterr().out
|
|
assert {'stop': "Profile 'worker' parked; its bots and cron are stopped.",
|
|
'start': "Profile 'worker' served", 'restart': "Profile 'worker' restarted"}[verb] in output
|
|
|
|
|
|
def test_restart_after_stop_unparks_and_serves_the_profile(homes, monkeypatch, capsys):
|
|
from hermes_cli import gateway as gw
|
|
from gateway import control_socket
|
|
root, secondary = homes
|
|
marker = secondary / 'gateway.parked'
|
|
marker.touch()
|
|
owner = SimpleNamespace(home=root, profile_label='default', profiles=('default',),
|
|
describe=lambda: 'test host')
|
|
monkeypatch.setenv('HERMES_HOME', str(secondary))
|
|
monkeypatch.setattr(gw, '_current_profile_name', lambda: 'worker')
|
|
monkeypatch.setattr(gw, '_refuse_from_inside_gateway', lambda *a: None)
|
|
monkeypatch.setattr(gw, 'find_gateway_pids', lambda **kw: [])
|
|
monkeypatch.setattr(gw, '_served_by_another_host_gateway', lambda *a: None)
|
|
monkeypatch.setattr(gw, 'named_profile_served_by_running_multiplexer', lambda *a: False)
|
|
monkeypatch.setattr(gw, '_host_multiplexer_for_all_verb', lambda: owner)
|
|
calls = []
|
|
|
|
def serve(home, name):
|
|
assert home == root and name == 'worker' and not marker.exists()
|
|
calls.append('serve')
|
|
return {'served': name, 'served_profiles': ['default', name]}
|
|
|
|
def standalone(*args, **kwargs):
|
|
raise AssertionError('restart fell through to the standalone gateway path')
|
|
|
|
monkeypatch.setattr(control_socket, 'request_serve_profile_hot', serve)
|
|
monkeypatch.setattr(gw, '_guard_named_profile_under_multiplexer', standalone)
|
|
gw._cmd_restart(SimpleNamespace())
|
|
assert calls == ['serve']
|
|
assert not marker.exists()
|
|
assert "Profile 'worker' served by the host gateway." in capsys.readouterr().out
|
|
|
|
|
|
def test_force_restart_keeps_parked_profile_gateway_ownership(homes, monkeypatch):
|
|
from hermes_cli import gateway as gw
|
|
_, secondary = homes
|
|
(secondary / 'gateway.parked').touch()
|
|
monkeypatch.setenv('HERMES_HOME', str(secondary))
|
|
monkeypatch.setattr(gw, '_current_profile_name', lambda: 'worker')
|
|
|
|
def host_restart(*args, **kwargs):
|
|
raise AssertionError('forced gateway restart was routed through the host')
|
|
|
|
monkeypatch.setattr(gw, '_served_by_another_host_gateway', lambda *args: None)
|
|
monkeypatch.setattr(gw, 'named_profile_served_by_running_multiplexer', lambda: False)
|
|
monkeypatch.setattr(gw, '_host_multiplexer_for_all_verb', host_restart)
|
|
monkeypatch.setattr(gw, 'find_gateway_pids', lambda **kwargs: [4242])
|
|
from hermes_cli.gateway_profile_lifecycle import profile_lifecycle
|
|
assert profile_lifecycle('restart', SimpleNamespace()) is False
|
|
|
|
|
|
def test_parked_status_and_topology_keep_roster(homes, monkeypatch, capsys):
|
|
from hermes_cli import gateway as gw, profiles
|
|
from hermes_cli.web_server_gateway import _collect_profile_gateway_topology
|
|
root, secondary = homes
|
|
(secondary / 'gateway.parked').touch()
|
|
monkeypatch.setenv('HERMES_HOME', str(secondary))
|
|
monkeypatch.setattr(gw, '_current_profile_name', lambda: 'worker')
|
|
monkeypatch.setattr(profiles, 'get_active_profile_name', lambda: 'worker')
|
|
monkeypatch.setattr(profiles, '_check_gateway_running', lambda home: False)
|
|
gw._cmd_status(SimpleNamespace())
|
|
assert 'parked (hermes -p worker gateway start)' in capsys.readouterr().out
|
|
topology = _collect_profile_gateway_topology()
|
|
assert topology['profiles'] == ['default', 'worker']
|
|
assert topology['parked_profiles'] == ['worker']
|
|
|
|
|
|
def test_parked_profile_keeps_implicit_host_multiplexed(homes, monkeypatch):
|
|
from gateway.config import GatewayConfig
|
|
from hermes_cli import gateway_migrate
|
|
from hermes_cli.gateway_multiplex_mode import resolve_multiplex_mode
|
|
_, secondary = homes
|
|
(secondary / 'gateway.parked').touch()
|
|
monkeypatch.setattr(gateway_migrate, '_host_supports_migration', lambda: None)
|
|
monkeypatch.setattr(gateway_migrate, 'build_migration_plan', lambda: SimpleNamespace(
|
|
standalone_secondaries=[], blocked=False))
|
|
config = GatewayConfig()
|
|
assert resolve_multiplex_mode(config).enabled
|
|
|
|
|
|
def test_dashboard_exposes_parked_profile_and_start_unparks_it(homes, monkeypatch):
|
|
from types import SimpleNamespace
|
|
from fastapi.testclient import TestClient
|
|
from gateway import host_attach
|
|
from hermes_cli import web_server, profiles
|
|
from hermes_cli.web_server_gateway import multiplexed_profile_refusal
|
|
root, secondary = homes
|
|
(secondary / 'gateway.parked').touch()
|
|
monkeypatch.setattr(profiles, '_check_gateway_running', lambda home: False)
|
|
with TestClient(web_server.app) as client:
|
|
response = client.get('/api/status')
|
|
assert response.status_code == 200
|
|
assert response.json()['parked_profiles'] == ['worker']
|
|
# The Start button: refused while no host can unpark it, allowed (spawns `-p worker gateway start`)
|
|
# once the host multiplexer is live; a parked profile is not served, so this is not the served path.
|
|
monkeypatch.setattr(host_attach, 'host_gateway', lambda: None)
|
|
assert multiplexed_profile_refusal('worker', 'start')
|
|
monkeypatch.setattr(host_attach, 'host_gateway', lambda: SimpleNamespace(home=root, pid=1))
|
|
assert multiplexed_profile_refusal('worker', 'start') is None
|
|
|
|
|
|
@pytest.mark.parametrize('host_running', [False, True])
|
|
def test_start_unparks_without_host_rendezvous(homes, monkeypatch, capsys, host_running):
|
|
from hermes_cli import gateway as gw, gateway_multiplex_served as served
|
|
from gateway import control_socket
|
|
root, secondary = homes
|
|
marker = secondary / 'gateway.parked'
|
|
marker.touch()
|
|
monkeypatch.setenv('HERMES_HOME', str(secondary))
|
|
monkeypatch.setattr(gw, '_current_profile_name', lambda: 'worker')
|
|
monkeypatch.setattr(gw, '_host_multiplexer_for_all_verb', lambda: None)
|
|
monkeypatch.setattr(served, 'live_default_gateway_pid', lambda: 42 if host_running else None)
|
|
calls = []
|
|
|
|
def serve(home, name):
|
|
assert home == root and name == 'worker' and not marker.exists()
|
|
calls.append('serve')
|
|
return {'served': name}
|
|
|
|
def normal_start(**kw):
|
|
assert not marker.exists()
|
|
calls.append('normal')
|
|
|
|
monkeypatch.setattr(control_socket, 'request_serve_profile_hot', serve)
|
|
monkeypatch.setattr(gw, '_guard_named_profile_under_multiplexer', normal_start)
|
|
monkeypatch.setattr(gw, '_dispatch_via_service_manager_if_s6', lambda *a: True)
|
|
gw._cmd_start(SimpleNamespace())
|
|
assert not marker.exists()
|
|
assert calls == (['serve'] if host_running else ['normal'])
|
|
|
|
|
|
def test_default_status_distinguishes_served_and_parked(homes, monkeypatch, capsys):
|
|
from hermes_cli import gateway as gw
|
|
from hermes_cli.gateway_profile_lifecycle import print_parked_status
|
|
root, secondary = homes
|
|
(secondary / 'gateway.parked').touch()
|
|
monkeypatch.setattr(gw, '_current_profile_name', lambda: 'default')
|
|
monkeypatch.setattr(gw, 'host_multiplexer_serving', lambda: SimpleNamespace(
|
|
home=root, profiles=('default',)))
|
|
assert print_parked_status() is False
|
|
output = capsys.readouterr().out
|
|
assert 'Served profiles: default' in output
|
|
assert "Profile 'worker': parked" in output
|
|
|
|
|
|
def test_parked_status_still_reports_a_forced_gateway(homes, monkeypatch, capsys):
|
|
from hermes_cli import gateway as gw, profiles
|
|
root, secondary = homes
|
|
(secondary / 'gateway.parked').touch()
|
|
monkeypatch.setenv('HERMES_HOME', str(secondary))
|
|
monkeypatch.setattr(gw, '_current_profile_name', lambda: 'worker')
|
|
monkeypatch.setattr(profiles, 'get_active_profile_name', lambda: 'worker')
|
|
monkeypatch.setattr(gw, 'get_gateway_runtime_snapshot', lambda system=False: gw.GatewayRuntimeSnapshot(
|
|
manager='manual', gateway_pids=(4242,)))
|
|
monkeypatch.setattr(gw, 'named_profile_served_by_running_multiplexer', lambda: False)
|
|
monkeypatch.setattr(gw, '_installed_service_kind_for', lambda _check: None)
|
|
gw._cmd_status(SimpleNamespace())
|
|
output = capsys.readouterr().out
|
|
assert 'parked (hermes -p worker gateway start)' in output
|
|
assert 'Gateway is running (PID: 4242)' in output
|