"""C2 task → Simulation output mapping. apply_task_to_simulation() implements the full §0.11 contract: 1. execution_result — append "$ \n\n" block. 2. executed_at — set from task.completed_at when currently null. 3. commands — append task.command deduplicated line-by-line. Caller is responsible for committing the session. """ from __future__ import annotations from datetime import UTC, datetime from backend.app.models.c2_task import C2Task from backend.app.models.simulation import Simulation def apply_task_to_simulation(task: C2Task, simulation: Simulation) -> None: """Apply completed task data to simulation fields per §0.11. Idempotent: no-op when task.mapping_applied is already True. Always sets mapping_applied = True on exit so the task is never re-processed. """ if task.mapping_applied: return output = (task.output or "").strip() # 1) execution_result — "$ \n\n" block, only when output is non-empty. if output: block = f"$ {task.command}\n{output}\n" existing = simulation.execution_result or "" if existing: sep = "" if existing.endswith("\n") else "\n" simulation.execution_result = existing + sep + block else: simulation.execution_result = block # 2) executed_at — set once from the first completed task's timestamp. if simulation.executed_at is None and task.completed_at is not None: simulation.executed_at = task.completed_at # 3) commands — append deduplicated line. if task.command: existing_cmds = (simulation.commands or "").splitlines() if task.command.strip() not in (line.strip() for line in existing_cmds): if simulation.commands: simulation.commands = simulation.commands + "\n" + task.command else: simulation.commands = task.command simulation.updated_at = datetime.now(UTC) task.mapping_applied = True