feat(engine): emit creation/retirement mutation events on deploy/teardown
Close the lifecycle loop for the correlation graph: every decky now enters the substrate with an explicit `trigger=creation` event (old_services=[] ⇒ new_services=<initial>) and leaves it with `trigger=retirement` (old=<current> ⇒ new=[]). With scheduled/operator mutations already flowing through emit_decky_mutated, the entire decky lifecycle is now a well-formed sequence of mutation events — the correlator can fold substrate_state(t) at any T by replaying them. Lazy-imports mutator.events to dodge the engine↔mutator circular dependency. Bus is None at CLI sites; the syslog write is what the correlator consumes. Emission is soft-failing so a broken log path never aborts a deploy.
This commit is contained in:
@@ -168,6 +168,57 @@ class TestDeploy:
|
||||
mock_save.assert_called_once()
|
||||
mock_retry.assert_called()
|
||||
|
||||
@patch("decnet.engine.deployer._emit_lifecycle_event")
|
||||
@patch("decnet.engine.deployer._print_status")
|
||||
@patch("decnet.engine.deployer._compose_with_retry")
|
||||
@patch("decnet.engine.deployer.save_state")
|
||||
@patch("decnet.engine.deployer.write_compose", return_value=Path("test.yml"))
|
||||
@patch("decnet.engine.deployer._sync_logging_helper")
|
||||
@patch("decnet.engine.deployer.setup_host_macvlan")
|
||||
@patch("decnet.engine.deployer.create_macvlan_network")
|
||||
@patch("decnet.engine.deployer.get_host_ip", return_value="192.168.1.2")
|
||||
@patch("decnet.engine.deployer.ips_to_range", return_value="192.168.1.10/32")
|
||||
@patch("decnet.engine.deployer.docker.from_env")
|
||||
def test_emits_creation_event_per_decky(
|
||||
self, mock_docker, mock_range, mock_hip, mock_create, mock_setup,
|
||||
mock_sync, mock_compose, mock_save, mock_retry, mock_print, mock_emit,
|
||||
):
|
||||
from decnet.engine.deployer import deploy
|
||||
deckies = [
|
||||
_decky(name="decky-01", ip="192.168.1.10", services=["ssh"]),
|
||||
_decky(name="decky-02", ip="192.168.1.11", services=["http", "ftp"]),
|
||||
]
|
||||
deploy(_config(deckies=deckies))
|
||||
assert mock_emit.call_count == 2
|
||||
triggers = [c.kwargs["trigger"] for c in mock_emit.call_args_list]
|
||||
assert triggers == ["creation", "creation"]
|
||||
names = [c.kwargs["decky_name"] for c in mock_emit.call_args_list]
|
||||
assert names == ["decky-01", "decky-02"]
|
||||
# empty-set symmetry: creation has old=[] ⇒ new=<initial>
|
||||
for call in mock_emit.call_args_list:
|
||||
assert call.kwargs["old_services"] == []
|
||||
assert mock_emit.call_args_list[0].kwargs["new_services"] == ["ssh"]
|
||||
assert mock_emit.call_args_list[1].kwargs["new_services"] == ["http", "ftp"]
|
||||
|
||||
@patch("decnet.engine.deployer._emit_lifecycle_event")
|
||||
@patch("decnet.engine.deployer._print_status")
|
||||
@patch("decnet.engine.deployer._compose_with_retry")
|
||||
@patch("decnet.engine.deployer.save_state")
|
||||
@patch("decnet.engine.deployer.write_compose", return_value=Path("test.yml"))
|
||||
@patch("decnet.engine.deployer._sync_logging_helper")
|
||||
@patch("decnet.engine.deployer.setup_host_macvlan")
|
||||
@patch("decnet.engine.deployer.create_macvlan_network")
|
||||
@patch("decnet.engine.deployer.get_host_ip", return_value="192.168.1.2")
|
||||
@patch("decnet.engine.deployer.ips_to_range", return_value="192.168.1.10/32")
|
||||
@patch("decnet.engine.deployer.docker.from_env")
|
||||
def test_dry_run_skips_creation_events(
|
||||
self, mock_docker, mock_range, mock_hip, mock_create, mock_setup,
|
||||
mock_sync, mock_compose, mock_save, mock_retry, mock_print, mock_emit,
|
||||
):
|
||||
from decnet.engine.deployer import deploy
|
||||
deploy(_config(), dry_run=True)
|
||||
mock_emit.assert_not_called()
|
||||
|
||||
@patch("decnet.engine.deployer._print_status")
|
||||
@patch("decnet.engine.deployer._compose_with_retry")
|
||||
@patch("decnet.engine.deployer.save_state")
|
||||
@@ -296,6 +347,53 @@ class TestTeardown:
|
||||
for name in svc_names:
|
||||
assert "[" not in name and "'" not in name
|
||||
|
||||
@patch("decnet.engine.deployer._emit_lifecycle_event")
|
||||
@patch("decnet.engine.deployer.clear_state")
|
||||
@patch("decnet.engine.deployer.remove_macvlan_network")
|
||||
@patch("decnet.engine.deployer.teardown_host_macvlan")
|
||||
@patch("decnet.engine.deployer._compose")
|
||||
@patch("decnet.engine.deployer.ips_to_range", return_value="192.168.1.10/32")
|
||||
@patch("decnet.engine.deployer.docker.from_env")
|
||||
@patch("decnet.engine.deployer.load_state")
|
||||
def test_full_teardown_emits_retirement_per_decky(
|
||||
self, mock_load, mock_docker, mock_range, mock_compose,
|
||||
mock_td_macvlan, mock_rm_net, mock_clear, mock_emit,
|
||||
):
|
||||
deckies = [
|
||||
_decky(name="decky-01", ip="192.168.1.10", services=["ssh"]),
|
||||
_decky(name="decky-02", ip="192.168.1.11", services=["http"]),
|
||||
]
|
||||
mock_load.return_value = (_config(deckies=deckies), Path("test.yml"))
|
||||
from decnet.engine.deployer import teardown
|
||||
teardown()
|
||||
assert mock_emit.call_count == 2
|
||||
for call in mock_emit.call_args_list:
|
||||
assert call.kwargs["trigger"] == "retirement"
|
||||
assert call.kwargs["new_services"] == []
|
||||
assert mock_emit.call_args_list[0].kwargs["old_services"] == ["ssh"]
|
||||
assert mock_emit.call_args_list[1].kwargs["old_services"] == ["http"]
|
||||
|
||||
@patch("decnet.engine.deployer._emit_lifecycle_event")
|
||||
@patch("decnet.engine.deployer._compose")
|
||||
@patch("decnet.engine.deployer.docker.from_env")
|
||||
@patch("decnet.engine.deployer.load_state")
|
||||
def test_single_decky_teardown_emits_one_retirement(
|
||||
self, mock_load, mock_docker, mock_compose, mock_emit,
|
||||
):
|
||||
deckies = [
|
||||
_decky(name="decky-01", ip="192.168.1.10", services=["ssh", "ftp"]),
|
||||
_decky(name="decky-02", ip="192.168.1.11", services=["http"]),
|
||||
]
|
||||
mock_load.return_value = (_config(deckies=deckies), Path("test.yml"))
|
||||
from decnet.engine.deployer import teardown
|
||||
teardown(decky_id="decky-01")
|
||||
assert mock_emit.call_count == 1
|
||||
call = mock_emit.call_args_list[0]
|
||||
assert call.kwargs["decky_name"] == "decky-01"
|
||||
assert call.kwargs["trigger"] == "retirement"
|
||||
assert call.kwargs["old_services"] == ["ssh", "ftp"]
|
||||
assert call.kwargs["new_services"] == []
|
||||
|
||||
@patch("decnet.engine.deployer._compose")
|
||||
@patch("decnet.engine.deployer.docker.from_env")
|
||||
@patch("decnet.engine.deployer.load_state")
|
||||
|
||||
Reference in New Issue
Block a user