fix(issue-21): absolute brief, start-command registry, audit-log root and report paths in the delegate-job wrapper
The injected prompt, the start/success/error commands and the brief's Output
Report Path used cwd-relative `.mam/jobs/...`, and the wrapper ignored
MAM_JOBS_DIR. A creator whose cwd is a git worktree could not run them as
written, and its audit log fell back to <cwd>/.mam/delegate_job_logs.
- REGISTRY_DIR defaults to ${MAM_JOBS_DIR:-.mam/jobs} and is made absolute
right after `cd "$WORKDIR"` (a relative value still resolves against WORKDIR)
- the publish command is shell-quoted (printf %q) and prefixed with
DELEGATE_JOB_LOGS_DIR=<abs>, resolved by the same get_logs_dir() the
wrapper's subscriber uses; the heartbeat timer inherits it
- direct and loop jobs share one _instructions helper; brief and report paths
are $REGISTRY_DIR/$JOB_ID/... (absolute)
- create --onboard: absolute publish_event.py, --registry-dir and log root
Tests T1-1..T1-5 (a space in the control root, commands run from another cwd
with no workspace env).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
This commit is contained in:
1 parent
dcf2fab06e
commit
d3d3a76c83
3 files changed
+260
-9
No files matched your search
@@ -503,7 +503,20 @@ if [ -n "$DELEGATE_JOB_ID" ]; then
|
|||||||
|
|
||||||
# Construct instructions matching multi-agent-mux-delegate-job format
|
# Construct instructions matching multi-agent-mux-delegate-job format
|
||||||
py_bin="$(_delegate_py_bin)"
|
py_bin="$(_delegate_py_bin)"
|
||||||
pub="$py_bin .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --job $DELEGATE_JOB_ID"
|
# Issue #21: absolute script, registry and audit-log paths (shell-quoted), so
|
||||||
|
# an agent whose cwd is a git worktree can run the command as written.
|
||||||
|
pub_script="$WORKSPACE_ROOT/.agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py"
|
||||||
|
[ -f "$pub_script" ] || pub_script="$(_delegate_script publish_event.py)"
|
||||||
|
pub_script="$(cd -P "$(dirname "$pub_script")" && pwd -P)/$(basename "$pub_script")"
|
||||||
|
pub_reg="${MAM_JOBS_DIR:-${WORKSPACE_ROOT:+$WORKSPACE_ROOT/.mam/jobs}}"
|
||||||
|
pub_reg="${pub_reg:-.mam/jobs}"
|
||||||
|
mkdir -p "$pub_reg" && pub_reg="$(cd -P "$pub_reg" && pwd -P)"
|
||||||
|
pub_logs="$(PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" "$py_bin" -c 'import os; from lib_py.delegation.mqtt_common import get_logs_dir; print(os.path.abspath(get_logs_dir()))' 2>/dev/null || true)"
|
||||||
|
pub="$(
|
||||||
|
if [ -n "$pub_logs" ]; then printf 'DELEGATE_JOB_LOGS_DIR=%q ' "$pub_logs"; fi
|
||||||
|
printf '%q ' "$py_bin" "$pub_script" --registry-dir "$pub_reg" --job "$DELEGATE_JOB_ID"
|
||||||
|
)"
|
||||||
|
pub="${pub% }"
|
||||||
instructions="Your job_id is \"$DELEGATE_JOB_ID\" (the one just registered for THIS delegation).
|
instructions="Your job_id is \"$DELEGATE_JOB_ID\" (the one just registered for THIS delegation).
|
||||||
|
|
||||||
On start run: $pub --event started.
|
On start run: $pub --event started.
|
||||||
|
|||||||
@@ -68,7 +68,42 @@ pick_python() {
|
|||||||
echo "$py_bin"
|
echo "$py_bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
REGISTRY_DIR_DEFAULT=".mam/jobs"
|
REGISTRY_DIR_DEFAULT="${MAM_JOBS_DIR:-.mam/jobs}"
|
||||||
|
|
||||||
|
# Issue #21: the injected brief path, the start/success/error commands and the
|
||||||
|
# Output Report Path must work from any cwd - a creator in a git worktree runs
|
||||||
|
# them from there. Called right after `cd "$WORKDIR"`, so a relative registry
|
||||||
|
# still resolves against WORKDIR, as before.
|
||||||
|
_abs_registry_dir() {
|
||||||
|
mkdir -p "$REGISTRY_DIR"
|
||||||
|
REGISTRY_DIR="$(cd -P "$REGISTRY_DIR" && pwd -P)"
|
||||||
|
export SKS_REGISTRY_DIR="$REGISTRY_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The audit-log root this wrapper's own subscriber resolves (same resolver),
|
||||||
|
# pinned into the injected commands so an agent publishing from another cwd
|
||||||
|
# does not fall back to <cwd>/.mam/delegate_job_logs.
|
||||||
|
_logs_dir_abs() {
|
||||||
|
"$PY" -c 'import os, sys; sys.path.insert(0, sys.argv[1]); from lib_py.delegation.mqtt_common import get_logs_dir; print(os.path.abspath(get_logs_dir()))' "$SCRIPT_DIR/.."
|
||||||
|
}
|
||||||
|
|
||||||
|
# _pub_cmd <extra...>: the publish command as one shell-quoted string.
|
||||||
|
_pub_cmd() {
|
||||||
|
local s
|
||||||
|
s="$(
|
||||||
|
if [ -n "${LOGS_DIR_ABS:-}" ]; then printf 'DELEGATE_JOB_LOGS_DIR=%q ' "$LOGS_DIR_ABS"; fi
|
||||||
|
printf '%q ' "$PY" "$SCRIPT_DIR/scripts/publish_event.py" --registry-dir "$REGISTRY_DIR" --job "$JOB_ID" "$@"
|
||||||
|
)"
|
||||||
|
printf '%s' "${s% }"
|
||||||
|
}
|
||||||
|
|
||||||
|
# _instructions <pub> [iteration]: the prompt injected into the agent (direct
|
||||||
|
# and loop jobs share it; test18 pins parts of this wording).
|
||||||
|
_instructions() {
|
||||||
|
local pub="$1" iter="${2:-}" brief="$REGISTRY_DIR/$JOB_ID/brief.md"
|
||||||
|
printf 'Your job_id is "%s". Detailed task requirements, instructions, and target output paths%s are documented in the task brief file at: %s. Please READ and follow %s to complete your work. Commands: start='"'"'%s --event started --heartbeat %s'"'"' (run it FIRST, within %s s; it also starts an automatic heartbeat timer), success='"'"'%s --event completed --detail <summary>'"'"', error='"'"'%s --event error --detail <reason>'"'"'.' \
|
||||||
|
"$JOB_ID" "${iter:+ for iteration $iter}" "$brief" "$brief" "$pub" "$HEARTBEAT_SEC" "$ACK_TIMEOUT" "$pub" "$pub"
|
||||||
|
}
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
@@ -150,8 +185,10 @@ cmd_submit() {
|
|||||||
parse_opts "$@"
|
parse_opts "$@"
|
||||||
[[ -n "$PROMPT" ]] || { echo "submit requires --prompt" >&2; exit 1; }
|
[[ -n "$PROMPT" ]] || { echo "submit requires --prompt" >&2; exit 1; }
|
||||||
PY="$(pick_python)"
|
PY="$(pick_python)"
|
||||||
|
[[ "$PY" == */* && "$PY" != /* ]] && PY="$(pwd)/$PY"
|
||||||
cd "$WORKDIR"
|
cd "$WORKDIR"
|
||||||
mkdir -p "$REGISTRY_DIR"
|
_abs_registry_dir
|
||||||
|
LOGS_DIR_ABS="$(_logs_dir_abs 2>/dev/null || true)"
|
||||||
|
|
||||||
# 1) register job (prints the new job id)
|
# 1) register job (prints the new job id)
|
||||||
JOB_ID="$("$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" register \
|
JOB_ID="$("$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" register \
|
||||||
@@ -175,7 +212,7 @@ cmd_submit() {
|
|||||||
- **Role**: $DELEGATE_ROLE
|
- **Role**: $DELEGATE_ROLE
|
||||||
- **Timeout**: $TIMEOUT s (Idle: $IDLE_TIMEOUT s)
|
- **Timeout**: $TIMEOUT s (Idle: $IDLE_TIMEOUT s)
|
||||||
- **Liveness contract**: heartbeat every $HEARTBEAT_SEC s (automatic once you publish \`started --heartbeat $HEARTBEAT_SEC\`), unresponsive after $HEARTBEAT_MISSES missed heartbeats, \`started\` expected within $ACK_TIMEOUT s
|
- **Liveness contract**: heartbeat every $HEARTBEAT_SEC s (automatic once you publish \`started --heartbeat $HEARTBEAT_SEC\`), unresponsive after $HEARTBEAT_MISSES missed heartbeats, \`started\` expected within $ACK_TIMEOUT s
|
||||||
- **Output Report Path**: .mam/jobs/$JOB_ID/$AGENT-reports/report-final.md
|
- **Output Report Path**: $REGISTRY_DIR/$JOB_ID/$AGENT-reports/report-final.md
|
||||||
|
|
||||||
## 🔎 Task Description
|
## 🔎 Task Description
|
||||||
$PROMPT
|
$PROMPT
|
||||||
@@ -195,8 +232,9 @@ EOF
|
|||||||
local pub_extra=()
|
local pub_extra=()
|
||||||
[ -n "$_disp_tok" ] && pub_extra+=(--expect-dispatch-token "$_disp_tok")
|
[ -n "$_disp_tok" ] && pub_extra+=(--expect-dispatch-token "$_disp_tok")
|
||||||
[ -n "$_attempt_id" ] && pub_extra+=(--attempt-id "$_attempt_id")
|
[ -n "$_attempt_id" ] && pub_extra+=(--attempt-id "$_attempt_id")
|
||||||
local pub="$PY $SCRIPT_DIR/scripts/publish_event.py --registry-dir $REGISTRY_DIR --job $JOB_ID ${pub_extra[*]}"
|
local pub instructions
|
||||||
local instructions="Your job_id is \"$JOB_ID\". Detailed task requirements, instructions, and target output paths are documented in the task brief file at: .mam/jobs/$JOB_ID/brief.md. Please READ and follow .mam/jobs/$JOB_ID/brief.md to complete your work. Commands: start='$pub --event started --heartbeat $HEARTBEAT_SEC' (run it FIRST, within $ACK_TIMEOUT s; it also starts an automatic heartbeat timer), success='$pub --event completed --detail <summary>', error='$pub --event error --detail <reason>'."
|
pub="$(_pub_cmd ${pub_extra[@]+"${pub_extra[@]}"})"
|
||||||
|
instructions="$(_instructions "$pub")"
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "1" ]]; then
|
if [[ "$DRY_RUN" == "1" ]]; then
|
||||||
run_agent "$JOB_ID" "$instructions"
|
run_agent "$JOB_ID" "$instructions"
|
||||||
@@ -349,7 +387,7 @@ EOF
|
|||||||
- **Role**: $display_role
|
- **Role**: $display_role
|
||||||
- **Iteration**: $iteration
|
- **Iteration**: $iteration
|
||||||
- **Liveness contract**: heartbeat every $HEARTBEAT_SEC s (automatic once you publish \`started --heartbeat $HEARTBEAT_SEC\`), unresponsive after $HEARTBEAT_MISSES missed heartbeats, \`started\` expected within $ACK_TIMEOUT s
|
- **Liveness contract**: heartbeat every $HEARTBEAT_SEC s (automatic once you publish \`started --heartbeat $HEARTBEAT_SEC\`), unresponsive after $HEARTBEAT_MISSES missed heartbeats, \`started\` expected within $ACK_TIMEOUT s
|
||||||
- **Output Report Path**: .mam/jobs/$JOB_ID/${clean_session}-reports/report-final.md
|
- **Output Report Path**: $REGISTRY_DIR/$JOB_ID/${clean_session}-reports/report-final.md
|
||||||
|
|
||||||
## 🔎 Task Description
|
## 🔎 Task Description
|
||||||
$current_prompt
|
$current_prompt
|
||||||
@@ -451,8 +489,9 @@ EOF
|
|||||||
# Format instruction block
|
# Format instruction block
|
||||||
local loop_pub_extra=(--attempt-id "$attempt")
|
local loop_pub_extra=(--attempt-id "$attempt")
|
||||||
[ -n "$_disp_tok" ] && loop_pub_extra+=(--expect-dispatch-token "$_disp_tok")
|
[ -n "$_disp_tok" ] && loop_pub_extra+=(--expect-dispatch-token "$_disp_tok")
|
||||||
local pub="$PY $SCRIPT_DIR/scripts/publish_event.py --registry-dir $REGISTRY_DIR --job $JOB_ID ${loop_pub_extra[*]}"
|
local pub instructions
|
||||||
local instructions="Your job_id is \"$JOB_ID\". Detailed task requirements, instructions, and target output paths for iteration $iteration are documented in the task brief file at: .mam/jobs/$JOB_ID/brief.md. Please READ and follow .mam/jobs/$JOB_ID/brief.md to complete your work. Commands: start='$pub --event started --heartbeat $HEARTBEAT_SEC' (run it FIRST, within $ACK_TIMEOUT s; it also starts an automatic heartbeat timer), success='$pub --event completed --detail <summary>', error='$pub --event error --detail <reason>'."
|
pub="$(_pub_cmd "${loop_pub_extra[@]}")"
|
||||||
|
instructions="$(_instructions "$pub" "$iteration")"
|
||||||
|
|
||||||
# Trigger agent
|
# Trigger agent
|
||||||
if ! deliver_prompt "$JOB_ID" "$instructions" "$current_session" "$logf"; then
|
if ! deliver_prompt "$JOB_ID" "$instructions" "$current_session" "$logf"; then
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"""Issue #21 phase-1 prerequisites for the worktree operating model.
|
||||||
|
|
||||||
|
§1 (item 1) delegate-job wrapper: absolute brief / start-command registry /
|
||||||
|
audit-log root / Output Report Path, runnable from any cwd.
|
||||||
|
§2 (item 2) loop runner --worktree / --base / --review-only and the
|
||||||
|
fail-closed empty change-set guard; the control plane stays in the main
|
||||||
|
checkout.
|
||||||
|
§3 (item 3) control-plane binding persisted on the session row and forwarded
|
||||||
|
to the pane; status / reconcile / stop honour MAM_JOBS_DIR.
|
||||||
|
|
||||||
|
Every test writes only under tmp_path / mam_sandbox (Issue #20).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SKILLS = REPO_ROOT / ".agents" / "skills"
|
||||||
|
WRAPPER = SKILLS / "multi-agent-mux-delegate-job" / "multi-agent-mux-delegate-job"
|
||||||
|
RUN_LOOP_SH = SKILLS / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"
|
||||||
|
|
||||||
|
if str(SKILLS) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SKILLS))
|
||||||
|
|
||||||
|
UNREACHABLE = {"MQTT_BROKER": "127.0.0.1", "MQTT_PORT": "9", "MQTT_TLS": "0"}
|
||||||
|
GIT_ID = {"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@example.invalid",
|
||||||
|
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@example.invalid"}
|
||||||
|
|
||||||
|
|
||||||
|
def _env(home: Path, extra=None, drop=()) -> dict:
|
||||||
|
env = dict(os.environ)
|
||||||
|
env["DELEGATE_JOB_PYTHON"] = sys.executable
|
||||||
|
env["AGENT_PYTHON_BIN"] = sys.executable
|
||||||
|
env["HOME"] = str(home)
|
||||||
|
env["MAM_ENV_FILE"] = str(home / ".mam.env.absent")
|
||||||
|
env["PYTHONPATH"] = str(SKILLS)
|
||||||
|
for k in ("MQTT_BROKER", "MQTT_HOST", "MQTT_PORT", "MQTT_TLS", "MQTT_USERNAME", "MQTT_PASSWORD",
|
||||||
|
"MAM_JOBS_DIR", "DELEGATE_JOB_LOGS_DIR", "WORKSPACE_ROOT"):
|
||||||
|
env.pop(k, None)
|
||||||
|
env.update(UNREACHABLE)
|
||||||
|
if extra:
|
||||||
|
env.update({k: str(v) for k, v in extra.items()})
|
||||||
|
for k in drop:
|
||||||
|
env.pop(k, None)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _only_job_id(reg: Path) -> str:
|
||||||
|
ids = [p.stem for p in reg.glob("*.json")]
|
||||||
|
assert len(ids) == 1, ids
|
||||||
|
return ids[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _kill_timer(reg: Path, jid: str) -> None:
|
||||||
|
try:
|
||||||
|
pid = int((reg / f"{jid}.heartbeat.pid").read_text().strip())
|
||||||
|
os.kill(pid, signal.SIGKILL)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _dry_submit(cwd: Path, env: dict, *args) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(["bash", str(WRAPPER), "submit", "--agent", "human", "--prompt", "p21",
|
||||||
|
"--dry-run", *args],
|
||||||
|
capture_output=True, text=True, env=env, cwd=str(cwd), timeout=60)
|
||||||
|
|
||||||
|
|
||||||
|
def _injected(stdout: str, name: str) -> str:
|
||||||
|
m = re.search(name + r"='(.*?)'", stdout)
|
||||||
|
assert m, stdout
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# §1 item 1 — the wrapper's injected paths are absolute
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_t1_1_injected_paths_are_absolute(tmp_path):
|
||||||
|
a = tmp_path / "A"
|
||||||
|
a.mkdir()
|
||||||
|
real_a = os.path.realpath(a)
|
||||||
|
env = _env(tmp_path, {"WORKSPACE_ROOT": a, "DELEGATE_JOB_LOGS_DIR": a / "logs"})
|
||||||
|
res = _dry_submit(a, env, "--workdir", str(a))
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
jid = _only_job_id(a / ".mam" / "jobs")
|
||||||
|
brief = f"{real_a}/.mam/jobs/{jid}/brief.md"
|
||||||
|
assert res.stdout.count(brief) == 2, res.stdout
|
||||||
|
start = _injected(res.stdout, "start")
|
||||||
|
assert f"--registry-dir {real_a}/.mam/jobs " in start
|
||||||
|
assert start.startswith(f"DELEGATE_JOB_LOGS_DIR={real_a}/logs "), start
|
||||||
|
# no cwd-relative registry path is left anywhere in the injected text
|
||||||
|
assert res.stdout.count(f".mam/jobs/{jid}") == res.stdout.count(f"{real_a}/.mam/jobs/{jid}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_t1_2_start_and_success_commands_run_from_another_cwd(tmp_path):
|
||||||
|
"""The core regression: a registry and an audit-log root with a space in
|
||||||
|
them, the commands executed as written from a different cwd with no
|
||||||
|
workspace env - the job advances, the audit trail stays in the control
|
||||||
|
root, and nothing is written under the other cwd."""
|
||||||
|
ctl = tmp_path / "ctl root"
|
||||||
|
reg, logs, wt = ctl / "jobs", ctl / "logs", tmp_path / "wt"
|
||||||
|
for d in (reg, logs, wt):
|
||||||
|
d.mkdir(parents=True)
|
||||||
|
env = _env(tmp_path, {"WORKSPACE_ROOT": ctl, "DELEGATE_JOB_LOGS_DIR": logs})
|
||||||
|
res = _dry_submit(ctl, env, "--registry-dir", str(reg), "--workdir", str(ctl),
|
||||||
|
"--heartbeat", "7", "--ack-timeout", "11")
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
jid = _only_job_id(reg)
|
||||||
|
start = _injected(res.stdout, "start")
|
||||||
|
success = _injected(res.stdout, "success").replace("<summary>", "done")
|
||||||
|
agent_env = _env(tmp_path) # no WORKSPACE_ROOT / MAM_JOBS_DIR / DELEGATE_JOB_LOGS_DIR
|
||||||
|
try:
|
||||||
|
r1 = subprocess.run(["bash", "-c", start + " --attempts 1"], capture_output=True, text=True,
|
||||||
|
env=agent_env, cwd=str(wt), timeout=120)
|
||||||
|
# rc 2 = broker unreachable; the event is recorded locally first (test18 contract)
|
||||||
|
assert r1.returncode == 2, r1.stderr
|
||||||
|
assert json.loads((reg / f"{jid}.json").read_text())["status"] != "pending"
|
||||||
|
r2 = subprocess.run(["bash", "-c", success + " --attempts 1"], capture_output=True, text=True,
|
||||||
|
env=agent_env, cwd=str(wt), timeout=120)
|
||||||
|
assert r2.returncode == 2, r2.stderr
|
||||||
|
assert json.loads((reg / f"{jid}.json").read_text())["status"] == "completed"
|
||||||
|
finally:
|
||||||
|
_kill_timer(reg, jid)
|
||||||
|
audit = [json.loads(l) for l in (logs / jid / "events.ndjson").read_text().splitlines() if l.strip()]
|
||||||
|
published = [json.dumps(e) for e in audit if e.get("event") == "published"]
|
||||||
|
assert len(published) == 2, audit
|
||||||
|
assert '"started"' in published[0] and '"completed"' in published[1], published
|
||||||
|
assert not (wt / ".mam").exists(), "the agent's cwd must not grow a .mam"
|
||||||
|
|
||||||
|
|
||||||
|
def test_t1_3_mam_jobs_dir_is_the_default_registry(tmp_path):
|
||||||
|
x, y = tmp_path / "X", tmp_path / "Y"
|
||||||
|
env = _env(tmp_path, {"MAM_JOBS_DIR": x, "WORKSPACE_ROOT": tmp_path, "DELEGATE_JOB_LOGS_DIR": tmp_path / "logs"})
|
||||||
|
res = _dry_submit(tmp_path, env, "--workdir", str(tmp_path))
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
assert len(list(x.glob("*.json"))) == 1
|
||||||
|
assert not (tmp_path / ".mam" / "jobs").exists()
|
||||||
|
|
||||||
|
res = _dry_submit(tmp_path, env, "--workdir", str(tmp_path), "--registry-dir", str(y))
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
assert len(list(y.glob("*.json"))) == 1, "an explicit --registry-dir wins over MAM_JOBS_DIR"
|
||||||
|
assert len(list(x.glob("*.json"))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_t1_4_wrapper_source_guard():
|
||||||
|
"""The loop job type never builds its text under --dry-run (it returns
|
||||||
|
early), so both paths are pinned to the shared helpers here."""
|
||||||
|
src = WRAPPER.read_text()
|
||||||
|
assert ".mam/jobs/$JOB_ID" not in src
|
||||||
|
assert src.count('instructions="$(_instructions "$pub")"') == 1
|
||||||
|
assert src.count('instructions="$(_instructions "$pub" "$iteration")"') == 1
|
||||||
|
assert src.count('pub="$(_pub_cmd ') == 2
|
||||||
|
report_lines = [l for l in src.splitlines() if "**Output Report Path**" in l]
|
||||||
|
assert len(report_lines) == 2 and all("$REGISTRY_DIR/$JOB_ID/" in l for l in report_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _sent_texts(state) -> list:
|
||||||
|
out = []
|
||||||
|
|
||||||
|
def walk(o):
|
||||||
|
if isinstance(o, dict):
|
||||||
|
for k, v in o.items():
|
||||||
|
if k == "sent_text" and isinstance(v, str):
|
||||||
|
out.append(v)
|
||||||
|
else:
|
||||||
|
walk(v)
|
||||||
|
elif isinstance(o, list):
|
||||||
|
for v in o:
|
||||||
|
walk(v)
|
||||||
|
walk(state)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def test_t1_5_onboarding_publish_command_is_absolute(mam_sandbox, mock_herdr, mock_agents):
|
||||||
|
tmp = mam_sandbox
|
||||||
|
script = tmp / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
res = subprocess.run(["bash", str(script), "--workspace", str(tmp), "--agent", "claude",
|
||||||
|
"--role", "creator", "--onboard"],
|
||||||
|
capture_output=True, text=True, cwd=str(tmp), timeout=180)
|
||||||
|
assert res.returncode == 0, res.stdout + res.stderr
|
||||||
|
texts = [t for t in _sent_texts(json.loads(Path(mock_herdr).read_text())) if "publish_event.py" in t]
|
||||||
|
assert texts, "the onboarding instructions were not injected"
|
||||||
|
text = "".join(texts)
|
||||||
|
real = os.path.realpath(tmp)
|
||||||
|
assert f" {real}/.agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py " in text, text
|
||||||
|
assert f"--registry-dir {real}/.mam/jobs " in text, text
|
||||||
|
assert f"DELEGATE_JOB_LOGS_DIR={real}/.mam/delegate_job_logs " in text, text
|
||||||
|
assert " .agents/skills/" not in text
|
||||||
|
|
||||||
Reference in new issue
Block a user