feat(mutator): route mutate_decky through emit_decky_mutated with trigger
Mutator now emits one decky_mutated event (RFC 5424 + bus) per successful mutation instead of the inline decky.<id>.state bus publish. The previous state topic published new_services only; mutation events carry old/new/trigger, which is what the correlation engine needs to interleave substrate-change markers into attacker traversals. - mutate_decky gains trigger: MutationTrigger = "operator" and captures old_services before the shuffle; replaces the inline _publish_safely(decky.<id>.state) with emit_decky_mutated(...). - mutate_all derives trigger internally: operator when force or only-filter is set (CLI --all, API mutate-now, UI bus request); scheduled on interval ticks. Passed through to each mutate_decky call. - Tests updated: the old decky.<id>.state assertion is replaced with decky.<id>.mutation topic + mutation payload shape; 3 new tests cover trigger derivation for scheduled / force / only paths. 26 tests in test_mutator.py green; 116 across mutator + topology + bus.
This commit is contained in:
@@ -29,6 +29,7 @@ from decnet.bus.publish import (
|
||||
publish_safely as _publish_safely,
|
||||
run_health_heartbeat as _run_health_heartbeat,
|
||||
)
|
||||
from decnet.mutator.events import MutationTrigger, emit_decky_mutated
|
||||
from decnet.web.db.repository import BaseRepository
|
||||
|
||||
log = get_logger("mutator")
|
||||
@@ -40,6 +41,7 @@ async def mutate_decky(
|
||||
decky_name: str,
|
||||
repo: BaseRepository,
|
||||
bus: BaseBus | None = None,
|
||||
trigger: MutationTrigger = "operator",
|
||||
) -> bool:
|
||||
"""
|
||||
Perform an Intra-Archetype Shuffle for a specific decky.
|
||||
@@ -73,6 +75,7 @@ async def mutate_decky(
|
||||
console.print(f"[yellow]No services available for mutating '{decky_name}'.[/]")
|
||||
return False
|
||||
|
||||
old_services = list(decky.services)
|
||||
current_services = set(decky.services)
|
||||
|
||||
attempts = 0
|
||||
@@ -103,15 +106,12 @@ async def mutate_decky(
|
||||
console.print(f"[red]Failed to mutate '{decky_name}': {e}[/]")
|
||||
return False
|
||||
|
||||
await _publish_safely(
|
||||
await emit_decky_mutated(
|
||||
bus,
|
||||
_topics.decky(decky_name, _topics.DECKY_STATE),
|
||||
{
|
||||
"name": decky_name,
|
||||
"services": list(decky.services),
|
||||
"last_mutated": decky.last_mutated,
|
||||
},
|
||||
event_type=_topics.DECKY_STATE,
|
||||
decky=decky_name,
|
||||
old_services=old_services,
|
||||
new_services=list(decky.services),
|
||||
trigger=trigger,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -143,6 +143,11 @@ async def mutate_all(
|
||||
config = DecnetConfig(**state_dict["config"])
|
||||
now = time.time()
|
||||
|
||||
# Trigger derivation: explicit force / targeted only-list come from
|
||||
# an operator action (CLI --all, API mutate-now, UI bus request).
|
||||
# Scheduled-interval ticks carry trigger=scheduled.
|
||||
trigger: MutationTrigger = "operator" if (force or only is not None) else "scheduled"
|
||||
|
||||
mutated_count = 0
|
||||
next_due_in: float | None = None
|
||||
for decky in config.deckies:
|
||||
@@ -162,7 +167,9 @@ async def mutate_all(
|
||||
next_due_in = remaining
|
||||
|
||||
if due:
|
||||
success = await mutate_decky(decky.name, repo=repo, bus=bus)
|
||||
success = await mutate_decky(
|
||||
decky.name, repo=repo, bus=bus, trigger=trigger,
|
||||
)
|
||||
if success:
|
||||
mutated_count += 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user