feat(infra): mark_claude_parent — escribe parent_orchestrator en goal.json (PID->sessionId)
Helper py analogo a mark_claude_role: resuelve el sessionId de un Claude recien arrancado por su PID (sondeando sessions/<pid>.json) y escribe SOLO la clave parent_orchestrator en su goal.json, preservando el resto. Lo consume spawn_fleet_agent --parent para que el watcher de fleetview rutee los avisos del ejecutor a su orquestador padre. Tests: escribe+preserva, goal inexistente, parent vacio (ValueError), timeout sin crash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""Tests para mark_claude_parent."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from mark_claude_parent import mark_claude_parent
|
||||
|
||||
|
||||
def _write_session(sessions_dir, pid, session_id):
|
||||
"""Escribe un sessions/<pid>.json con el sessionId dado."""
|
||||
os.makedirs(sessions_dir, exist_ok=True)
|
||||
path = os.path.join(sessions_dir, f"{pid}.json")
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump({"sessionId": session_id, "cwd": "/tmp/whatever"}, fh)
|
||||
return path
|
||||
|
||||
|
||||
def _write_goal(goals_dir, session_id, goal):
|
||||
"""Escribe un goal/<sessionId>.json con el dict dado."""
|
||||
os.makedirs(goals_dir, exist_ok=True)
|
||||
path = os.path.join(goals_dir, f"{session_id}.json")
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(goal, fh)
|
||||
return path
|
||||
|
||||
|
||||
def _read_goal(goals_dir, session_id):
|
||||
path = os.path.join(goals_dir, f"{session_id}.json")
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def test_sessions_presente_resuelve_y_escribe_parent_preservando_otros_campos(tmp_path):
|
||||
sessions_dir = str(tmp_path / "sessions")
|
||||
goals_dir = str(tmp_path / "goals")
|
||||
pid = 4242
|
||||
sid = "executor-abc-123"
|
||||
parent = "orchestrator-xyz-789"
|
||||
|
||||
_write_session(sessions_dir, pid, sid)
|
||||
# Goal preexistente con campos que NO deben perderse (incluido role).
|
||||
_write_goal(
|
||||
goals_dir,
|
||||
sid,
|
||||
{
|
||||
"goal": "implementar fase 3",
|
||||
"phase": "trabajando",
|
||||
"role": "executor",
|
||||
"dod_contract": {"capa1": "mecanica"},
|
||||
},
|
||||
)
|
||||
|
||||
res = mark_claude_parent(pid, parent, wait_s=2.0,
|
||||
sessions_dir=sessions_dir, goals_dir=goals_dir)
|
||||
|
||||
assert res["ok"] is True
|
||||
assert res["pid"] == pid
|
||||
assert res["session_id"] == sid
|
||||
assert res["parent_orchestrator"] == parent
|
||||
assert res["path"] == os.path.join(goals_dir, f"{sid}.json")
|
||||
|
||||
goal = _read_goal(goals_dir, sid)
|
||||
assert goal["parent_orchestrator"] == parent
|
||||
# Todos los demas campos del goal se preservan intactos.
|
||||
assert goal["goal"] == "implementar fase 3"
|
||||
assert goal["phase"] == "trabajando"
|
||||
assert goal["role"] == "executor"
|
||||
assert goal["dod_contract"] == {"capa1": "mecanica"}
|
||||
|
||||
|
||||
def test_parent_vacio_lanza_value_error_sin_escribir(tmp_path):
|
||||
sessions_dir = str(tmp_path / "sessions")
|
||||
goals_dir = str(tmp_path / "goals")
|
||||
pid = 4242
|
||||
sid = "executor-abc-123"
|
||||
_write_session(sessions_dir, pid, sid)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
mark_claude_parent(pid, " ", wait_s=2.0,
|
||||
sessions_dir=sessions_dir, goals_dir=goals_dir)
|
||||
|
||||
# No escribio nada: el goals_dir ni siquiera deberia existir.
|
||||
assert not os.path.exists(goals_dir)
|
||||
|
||||
|
||||
def test_sessions_ausente_devuelve_timeout_sin_crash(tmp_path):
|
||||
sessions_dir = str(tmp_path / "sessions")
|
||||
goals_dir = str(tmp_path / "goals")
|
||||
pid = 9999 # sin sessions/<pid>.json escrito
|
||||
|
||||
res = mark_claude_parent(pid, "orchestrator-xyz-789", wait_s=0.5,
|
||||
sessions_dir=sessions_dir, goals_dir=goals_dir)
|
||||
|
||||
assert res["ok"] is False
|
||||
assert res["pid"] == pid
|
||||
assert "timeout" in res["error"]
|
||||
assert f"{pid}.json" in res["error"]
|
||||
# No se escribio ningun goal.
|
||||
assert not os.path.exists(goals_dir)
|
||||
|
||||
|
||||
def test_goal_inexistente_se_crea_con_solo_parent(tmp_path):
|
||||
sessions_dir = str(tmp_path / "sessions")
|
||||
goals_dir = str(tmp_path / "goals")
|
||||
pid = 7
|
||||
sid = "fresh-executor-uuid"
|
||||
parent = "orchestrator-xyz-789"
|
||||
_write_session(sessions_dir, pid, sid)
|
||||
# No existe goal previo para esta sesion.
|
||||
|
||||
res = mark_claude_parent(pid, parent, wait_s=2.0,
|
||||
sessions_dir=sessions_dir, goals_dir=goals_dir)
|
||||
|
||||
assert res["ok"] is True
|
||||
assert res["session_id"] == sid
|
||||
assert res["parent_orchestrator"] == parent
|
||||
|
||||
goal = _read_goal(goals_dir, sid)
|
||||
assert goal == {"parent_orchestrator": parent}
|
||||
Reference in New Issue
Block a user