Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b81f99732 | ||
|
|
7cf63f4858 | ||
|
|
c0e9f5c8a5 | ||
|
|
3fd2416a30 | ||
|
|
5305cf21f9 |
No files matched your search
@@ -1,6 +1,7 @@
|
||||
# __main__.py — CLI bridge for lib_py.agents facts and resolution (N4)
|
||||
|
||||
import os, sys, json, shlex, time
|
||||
from lib_py.agents.base import POST_DISCOVERED
|
||||
from lib_py.agents.registry import get_adapter, own_key, agent_of_row
|
||||
|
||||
# `purge auto` probes adapters in this order; opencode last because its
|
||||
@@ -71,6 +72,7 @@ def main():
|
||||
print(f"MAM_AGENT_STATUS_RELIABLE={1 if adapter.agent_status_reliable else 0}")
|
||||
print(f"MAM_EXIT_KEY={q(adapter.exit_key)}")
|
||||
print(f"MAM_DELEGATE_AGENT_KEY={q(adapter.delegate_agent_key)}")
|
||||
print(f"MAM_ID_POST_DISCOVERED={1 if adapter.id_lifecycle == POST_DISCOVERED else 0}")
|
||||
|
||||
elif cmd == 'resolve':
|
||||
name = sys.argv[2] if len(sys.argv) > 2 else ''
|
||||
|
||||
@@ -170,7 +170,7 @@ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace
|
||||
| `--herdr-session` (`--herdr-server`) | Isolated herdr server name |
|
||||
| `--herdr-workspace` | herdr workspace label recorded in the registry (a label only, never a socket) |
|
||||
| `--submit-job PROMPT` | Register a delegated job with this prompt and send it to the new agent |
|
||||
| `--onboard` / `--no-onboard` | Submit the onboarding job (default: on) |
|
||||
| `--onboard` / `--no-onboard` | Submit the onboarding job (default: on); with `--no-onboard` an agent whose id appears only after its first turn (adapter `id_lifecycle`) records none until then and blocks a same-folder create of its type (#63) - create warns |
|
||||
| `--auto-trust` / `--no-auto-trust` | Answer a start-up directory-trust dialog or leave it to a human (`MAM_AUTO_TRUST`; default: answer) |
|
||||
| `--isolate` / `--no-isolate` | Legacy, no-op |
|
||||
|
||||
@@ -202,15 +202,15 @@ Run from the control root's skills tree (lib.sh's `WORKSPACE_ROOT` = the main ch
|
||||
```bash
|
||||
bash .agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh \
|
||||
--path ../multi-agent-mux-wt/issue-42 --branch fix/issue-42 [--base <ref>] [--submodules] \
|
||||
[--creator-agent claude [--session <name>] [--role creator] [--herdr-session <s>] [--herdr-workspace <w>] [--onboard]] [--dry-run]
|
||||
[--creator-agent claude [--session <name>] [--role creator] [--herdr-session <s>] [--herdr-workspace <w>] [--no-onboard]] [--dry-run]
|
||||
```
|
||||
|
||||
- Creates the worktree next to the control root (a path **inside** it is refused), idempotently; creates the worktree's **own** `.venv` from the control root's `.venv/bin/python` and installs the delegate-job skill's `requirements.txt` (paho-mqtt, PyYAML - what `deploy/install.sh` installs) plus `requirements.txt` / `requirements-dev.txt` when present (needs network access to the package index; a failed install removes the half-made venv and exits 1 so a re-run starts over; never a link - a leftover link is refused with exit 1; no control-root interpreter is a warning and `VENV=skipped`); adds `/.venv` and `.pytest_cache/` to the shared `info/exclude` (a linked worktree's exclude file is `$GIT_COMMON_DIR/info/exclude`, so it applies to the main checkout too).
|
||||
- **Submodules are opt-in** (`--submodules`). Initialisation writes `submodule.<name>.url` to the shared `.git/config`, and any attempt - even a failed one - leaves a `modules/` directory that makes a plain `git worktree remove` refuse. A failed `--submodules` undoes only what that attempt created - new submodule checkouts are deinitialised before their git dirs are deleted, so the worktree stays a usable checkout; submodules initialised earlier and pre-existing `submodule.*` values are kept - and exits 1; re-run without `--submodules` to finish.
|
||||
- `--creator-agent` creates the creator through this skill with the control root's binding and `MAM_NO_SUBSCRIBER_DAEMON=1`; the row's `pane.cwd` is the worktree.
|
||||
- `--creator-agent` creates the creator through this skill with the control root's binding and `MAM_NO_SUBSCRIBER_DAEMON=1` (the row's `pane.cwd` is the worktree) and onboards it (the onboarding job is sent; `--no-onboard` skips it, `--onboard` is the default and still accepted).
|
||||
- Prints `BASE=<sha>`: record it **before** delegating the implementation, then review with `run_loop.sh --worktree <WT> --base "$BASE" --review-only`.
|
||||
- A submodule path that is not initialised yet but already holds files refuses `--submodules` up front (exit 3, nothing changed): the clone would fail on it and a rollback could not tell those files from its own.
|
||||
- Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule, 3 refused to protect existing content (nothing changed); 44/45 are create's (onboarding error / timeout with `--onboard`; session and worktree kept).
|
||||
- Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule, 3 refused to protect existing content (nothing changed); 44/45 are create's (onboarding error / timeout; onboarding is the default, `--no-onboard` skips it; session and worktree kept).
|
||||
- create itself: a control-plane binding that cannot be computed is refused (exit 2) for a `--workspace` other than the control root and warned about for the control root; the immediate reconcile and the watchdog run the control root's code.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
@@ -205,6 +205,10 @@ fi
|
||||
|
||||
# Retrieve agent facts once
|
||||
eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$AGENT" 2>/dev/null || true)"
|
||||
# Issue #68: warn when --no-onboard is used with a post-discovered-id agent (adapter id_lifecycle via facts).
|
||||
if [ "$ONBOARD" = 0 ] && [ "${MAM_ID_POST_DISCOVERED:-0}" = 1 ]; then
|
||||
echo "WARNING: --no-onboard: $AGENT records its conversation id only after its first turn; until then it is not in the registry, and another $AGENT session created in this folder waits for it (Issue #63, MAM_ID_ORDER_WAIT_S)." >&2
|
||||
fi
|
||||
|
||||
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "$SESSION_UUID" "${USE_WRAPPER:-0}" 2>/dev/null || true)"
|
||||
if [ -z "$CMD_FULL" ]; then
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
# Usage:
|
||||
# worktree_prepare.sh --path <WT> --branch <name> [--base <ref>] [--submodules]
|
||||
# [--creator-agent <agent> [--session <name>] [--role <role>]
|
||||
# [--herdr-session <s>] [--herdr-workspace <w>] [--onboard]] [--dry-run]
|
||||
# [--herdr-session <s>] [--herdr-workspace <w>] [--no-onboard]] [--dry-run]
|
||||
#
|
||||
# Steps: git worktree (idempotent) -> the worktree's own .venv (control root
|
||||
# interpreter + requirements*.txt) -> /.venv and .pytest_cache/ in the shared info/exclude -> submodules only with
|
||||
# --submodules (a failed init is rolled back: it would otherwise leave a
|
||||
# modules/ trace that makes a plain `git worktree remove` refuse) -> optional
|
||||
# creator session registered in the control root's .mam -> summary with BASE=.
|
||||
# creator session (onboarded unless --no-onboard) registered in the control root's .mam -> summary with BASE=.
|
||||
#
|
||||
# Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule,
|
||||
# 3 refused to protect existing content (nothing changed); 44/45 are create's
|
||||
# (onboarding error / timeout; session and worktree kept, Issue #50).
|
||||
# (onboarding error / timeout - onboarding is the default, --no-onboard skips it; session and worktree kept, Issue #50).
|
||||
set -euo pipefail
|
||||
|
||||
_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -27,7 +27,7 @@ usage() {
|
||||
}
|
||||
|
||||
WT=""; BRANCH=""; BASE_REF=""; SUBMODULES=0; DRY_RUN=0
|
||||
CREATOR_AGENT=""; SESSION=""; ROLE="creator"; HS=""; HW=""; ONBOARD=0
|
||||
CREATOR_AGENT=""; SESSION=""; ROLE="creator"; HS=""; HW=""; ONBOARD=1
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--path) WT="${2:-}"; shift 2 ;;
|
||||
@@ -39,7 +39,8 @@ while [ $# -gt 0 ]; do
|
||||
--role) ROLE="${2:-}"; shift 2 ;;
|
||||
--herdr-session) HS="${2:-}"; shift 2 ;;
|
||||
--herdr-workspace) HW="${2:-}"; shift 2 ;;
|
||||
--onboard) ONBOARD=1; shift ;;
|
||||
--onboard) ONBOARD=1; shift ;; # the default; still accepted
|
||||
--no-onboard) ONBOARD=0; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; usage >&2; exit 2 ;;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"kind": "example",
|
||||
"evidence": [
|
||||
"multi-agent-mux-create/scripts/create_session.sh:99",
|
||||
"multi-agent-mux-create/scripts/create_session.sh:360"
|
||||
"multi-agent-mux-create/scripts/create_session.sh:364"
|
||||
],
|
||||
"mismatch": null,
|
||||
"note": "HERDR_SESSION_NAME environment example"
|
||||
@@ -67,7 +67,7 @@
|
||||
"class": "c",
|
||||
"kind": "example",
|
||||
"evidence": [
|
||||
"multi-agent-mux-create/scripts/create_session.sh:518-545"
|
||||
"multi-agent-mux-create/scripts/create_session.sh:522-549"
|
||||
],
|
||||
"mismatch": null,
|
||||
"note": "Output format: the row create_session.sh writes"
|
||||
@@ -91,12 +91,12 @@
|
||||
"start": 202,
|
||||
"end": 206,
|
||||
"lang": "bash",
|
||||
"sha": "38836d31bf38",
|
||||
"sha": "8bfabafb0328",
|
||||
"class": "c",
|
||||
"kind": "invocation",
|
||||
"evidence": [
|
||||
"multi-agent-mux-create/scripts/worktree_prepare.sh:24",
|
||||
"multi-agent-mux-create/scripts/worktree_prepare.sh:250"
|
||||
"multi-agent-mux-create/scripts/worktree_prepare.sh:251"
|
||||
],
|
||||
"mismatch": null,
|
||||
"note": "worktree_prepare.sh call"
|
||||
@@ -175,7 +175,7 @@
|
||||
"kind": "invocation",
|
||||
"evidence": [
|
||||
"lib_py/loop/runner.py:155",
|
||||
"multi-agent-mux-create/scripts/worktree_prepare.sh:250"
|
||||
"multi-agent-mux-create/scripts/worktree_prepare.sh:251"
|
||||
],
|
||||
"mismatch": null,
|
||||
"note": "--worktree/--base --review-only call pattern"
|
||||
@@ -560,9 +560,9 @@
|
||||
"evidence": [
|
||||
"multi-agent-mux-create/scripts/create_session.sh:166-168",
|
||||
"multi-agent-mux-create/scripts/create_session.sh:207",
|
||||
"multi-agent-mux-create/scripts/create_session.sh:259-274",
|
||||
"multi-agent-mux-create/scripts/create_session.sh:365-402",
|
||||
"multi-agent-mux-create/scripts/create_session.sh:414-416"
|
||||
"multi-agent-mux-create/scripts/create_session.sh:263-278",
|
||||
"multi-agent-mux-create/scripts/create_session.sh:369-406",
|
||||
"multi-agent-mux-create/scripts/create_session.sh:418-420"
|
||||
],
|
||||
"mismatch": [
|
||||
"C1: an existing session is exit 1 in the doc, exit 3 in the script",
|
||||
@@ -582,7 +582,7 @@
|
||||
"base_sha": "f71be0f26fb8",
|
||||
"class": "a",
|
||||
"evidence": [
|
||||
"multi-agent-mux-create/scripts/create_session.sh:490-608"
|
||||
"multi-agent-mux-create/scripts/create_session.sh:494-612"
|
||||
],
|
||||
"mismatch": [],
|
||||
"replacement": "bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace <path> --agent <agent> --role <role>",
|
||||
@@ -633,7 +633,7 @@
|
||||
"other"
|
||||
],
|
||||
"passthrough": {
|
||||
"4": "multi-agent-mux-create/scripts/create_session.sh:550"
|
||||
"4": "multi-agent-mux-create/scripts/create_session.sh:554"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ def _sibling(main: Path, state_file):
|
||||
before = _rows(main)
|
||||
since = len(_raw_calls(state_file))
|
||||
wt = _wt(main)
|
||||
res = _prepare(main, wt, "--creator-agent", "claude", "--herdr-session", HS)
|
||||
res = _prepare(main, wt, "--creator-agent", "claude", "--herdr-session", HS, "--no-onboard")
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
return wt, since, before
|
||||
|
||||
@@ -204,7 +204,7 @@ def test_a5b_a_stale_last_round_is_not_accepted(wt_repo, monkeypatch, tmp_path,
|
||||
def test_a6_row_recovery_and_resume_only_at_the_original_cwd(ctl, mock_herdr, mock_agents):
|
||||
main = ctl
|
||||
wt = _wt(main)
|
||||
res = _prepare(main, wt, "--creator-agent", "claude")
|
||||
res = _prepare(main, wt, "--creator-agent", "claude", "--no-onboard")
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
(row,) = _rows(main)
|
||||
name, binding = row["name"], row["control_plane"]
|
||||
|
||||
@@ -419,7 +419,7 @@ def test_w6_remove_plain_and_with_stop(ctl, mock_herdr, mock_agents):
|
||||
assert os.path.realpath(wt1) not in _worktrees(main)
|
||||
assert _git(main, "show-ref", "--verify", "--quiet", f"refs/heads/b-{wt1.name}", check=False).returncode == 0
|
||||
|
||||
res = _prepare(main, wt2, "--creator-agent", "claude")
|
||||
res = _prepare(main, wt2, "--creator-agent", "claude", "--no-onboard")
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
name = [r for r in _rows(main) if os.path.realpath(r["pane"]["cwd"]) == os.path.realpath(wt2)][0]["name"]
|
||||
res = _run(main, REM, "--path", str(wt2), "--stop")
|
||||
|
||||
@@ -108,6 +108,7 @@ NOT_ARITHMETIC = {
|
||||
# sleep's fractional seconds are its meaning here (default 0.5): mam_int would refuse them
|
||||
"SKS_QUIESCENT_INTERVAL": "sleep, fractional by default (0.5) - left raw",
|
||||
"DELEGATE_JOB_ACK_S": "create_session.sh -> delegate_wait_started, which runs mam_int on it",
|
||||
"MAM_ID_POST_DISCOVERED": "string compare (adapter fact from lib_py.agents facts)",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
"""Issue #68: worktree_prepare.sh onboards by default (--no-onboard skips), and
|
||||
create_session.sh --no-onboard warns for post-discovered-id agents (adapter
|
||||
id_lifecycle == POST_DISCOVERED).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SKILLS = REPO_ROOT / ".agents" / "skills"
|
||||
if str(SKILLS) not in sys.path:
|
||||
sys.path.insert(0, str(SKILLS))
|
||||
|
||||
from lib_py.agents.base import POST_DISCOVERED # noqa: E402
|
||||
from lib_py.agents.registry import get_adapter # noqa: E402
|
||||
from test_issue21_phase2_loop_a import _prepare, _rows, _wt, ctl # noqa: E402,F401
|
||||
from test_issue63_same_cwd_ids import _mock_herdr_agent, _read_rows, _row, _write_rows # noqa: E402
|
||||
|
||||
ALL_AGENTS = ("claude", "agy", "hermes", "grok", "opencode", "codex")
|
||||
EXPECTED_POST_DISCOVERED = {
|
||||
"agy": 1,
|
||||
"codex": 1,
|
||||
"hermes": 1,
|
||||
"opencode": 1,
|
||||
"claude": 0,
|
||||
"grok": 0,
|
||||
}
|
||||
UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.I)
|
||||
|
||||
|
||||
def _norm_uuid(text: str) -> str:
|
||||
return UUID_RE.sub("<UUID>", text)
|
||||
|
||||
|
||||
def _warn_lines(stderr: str) -> list[str]:
|
||||
return [l for l in stderr.splitlines() if l.startswith("WARNING: --no-onboard:")]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent", ALL_AGENTS)
|
||||
def test_facts_expose_post_discovered(agent):
|
||||
env = dict(os.environ, PYTHONPATH=f"{SKILLS}:{os.environ.get('PYTHONPATH', '')}")
|
||||
res = subprocess.run(
|
||||
[sys.executable, "-m", "lib_py.agents", "facts", agent],
|
||||
capture_output=True, text=True, check=True, env=env,
|
||||
)
|
||||
eval_res = subprocess.run(
|
||||
["bash", "-c", f"set -euo pipefail; eval {shlex.quote(res.stdout)}; echo \"$MAM_ID_POST_DISCOVERED\""],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
val = int(eval_res.stdout.strip())
|
||||
adapter = get_adapter(agent)
|
||||
assert val == (1 if adapter.id_lifecycle == POST_DISCOVERED else 0)
|
||||
assert val == EXPECTED_POST_DISCOVERED[agent]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent", ALL_AGENTS)
|
||||
def test_no_onboard_warning_dry_run(mam_sandbox, mock_herdr, mock_agents, monkeypatch, agent):
|
||||
monkeypatch.setenv("XAI_API_KEY", "test-key")
|
||||
ws = mam_sandbox / f"ws-dry-{agent}"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
create_sh = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
|
||||
res = subprocess.run(
|
||||
["bash", str(create_sh), "--dry-run", "--no-onboard", "--workspace", str(ws),
|
||||
"--agent", agent, "--role", "creator", "--session", f"t68-{agent}"],
|
||||
capture_output=True, text=True, cwd=str(mam_sandbox), timeout=60,
|
||||
)
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
warns = _warn_lines(res.stderr)
|
||||
assert len(warns) == EXPECTED_POST_DISCOVERED[agent], res.stderr
|
||||
if warns:
|
||||
assert "Issue #63" in warns[0] and agent in warns[0]
|
||||
|
||||
# Control: --dry-run with default onboarding emits 0 warning lines for all agents
|
||||
ctl_res = subprocess.run(
|
||||
["bash", str(create_sh), "--dry-run", "--workspace", str(ws),
|
||||
"--agent", agent, "--role", "creator", "--session", f"t68-ctl-{agent}"],
|
||||
capture_output=True, text=True, cwd=str(mam_sandbox), timeout=60,
|
||||
)
|
||||
assert ctl_res.returncode == 0, ctl_res.stdout + ctl_res.stderr
|
||||
assert _warn_lines(ctl_res.stderr) == [], ctl_res.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent", ALL_AGENTS)
|
||||
def test_no_onboard_warning_keeps_exit_and_row(mam_sandbox, mock_herdr, mock_agents, monkeypatch, agent):
|
||||
monkeypatch.setenv("XAI_API_KEY", "test-key")
|
||||
ws = mam_sandbox / f"ws-real-{agent}"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
session_name = f"t68-{agent}"
|
||||
create_sh = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
lib_sh = mam_sandbox / ".agents" / "skills" / "lib.sh"
|
||||
ws_slug = subprocess.run(
|
||||
["bash", "-c", f'source "{lib_sh}" && derive_workspace_slug "{ws}"'],
|
||||
capture_output=True, text=True, check=True, cwd=str(mam_sandbox),
|
||||
).stdout.strip()
|
||||
|
||||
res = subprocess.run(
|
||||
["bash", str(create_sh), "--no-onboard", "--workspace", str(ws),
|
||||
"--agent", agent, "--role", "creator", "--session", session_name],
|
||||
capture_output=True, text=True, cwd=str(mam_sandbox), timeout=120,
|
||||
)
|
||||
# 1. Exit code
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
|
||||
# 2. Stable row fields (checked BEFORE warning assertion so base b9c828f fails only at step 3)
|
||||
rows = _read_rows(mam_sandbox)
|
||||
assert session_name in rows
|
||||
row = rows[session_name]
|
||||
pre_assigned = agent in ("claude", "grok")
|
||||
expected_cmd_full = _norm_uuid(
|
||||
get_adapter(agent).spawn_spec(
|
||||
str(mock_agents / agent),
|
||||
"00000000-0000-0000-0000-000000000000" if pre_assigned else "",
|
||||
)
|
||||
)
|
||||
expected_source = "assigned" if pre_assigned else ("pending-discovery" if agent in ("opencode", "codex") else None)
|
||||
expected_verified = False if agent in ("claude", "grok", "opencode", "codex") else None
|
||||
expected_vis = "assigned (awaiting first message)" if pre_assigned else "unverified"
|
||||
expected_cp = {
|
||||
"workspace_root": os.path.realpath(mam_sandbox),
|
||||
"agent_sessions_yaml": os.path.realpath(mam_sandbox / ".mam" / "agent-sessions.yaml"),
|
||||
"mam_jobs_dir": os.path.realpath(mam_sandbox / ".mam" / "jobs"),
|
||||
"delegate_job_logs_dir": os.path.realpath(mam_sandbox / ".mam" / "delegate_job_logs"),
|
||||
}
|
||||
|
||||
assert row["name"] == session_name
|
||||
assert row["status"] == "running"
|
||||
assert row["role"] == "creator"
|
||||
assert row["delegate_job_id"] is None
|
||||
assert row["herdr_server"] == ws_slug
|
||||
assert row["herdr_workspace"] == ws_slug.removeprefix("mam-")
|
||||
assert row["pane"]["cmd"] == agent
|
||||
assert _norm_uuid(row["pane"]["cmd_full"]) == expected_cmd_full
|
||||
assert os.path.realpath(row["pane"]["cwd"]) == os.path.realpath(ws)
|
||||
assert row.get("session_id_source") == expected_source
|
||||
assert row.get("session_id_verified") == expected_verified
|
||||
assert row["last_visible_status"] == expected_vis
|
||||
assert row["control_plane"] == expected_cp
|
||||
|
||||
# 3. Warning line count
|
||||
warns = _warn_lines(res.stderr)
|
||||
assert len(warns) == EXPECTED_POST_DISCOVERED[agent], res.stderr
|
||||
if warns:
|
||||
assert "Issue #63" in warns[0] and agent in warns[0]
|
||||
|
||||
|
||||
def test_no_onboard_warning_precedes_a_later_refusal(mam_sandbox, mock_herdr, mock_agents):
|
||||
ws = mam_sandbox / "c68-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
epoch_a = int(time.time()) - 100
|
||||
name_a, name_b = "c68-ws-creator-agy", "c68-ws-reviewer-agy"
|
||||
create_sh = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
|
||||
holder = subprocess.Popen(["sleep", "120"])
|
||||
try:
|
||||
_write_rows(mam_sandbox, [_row(name_a, "agy", ws, epoch_a, pid=holder.pid)])
|
||||
_mock_herdr_agent(mock_herdr, name_a, ws, holder.pid, epoch_a, pane_id="w1:p_a")
|
||||
|
||||
env0 = dict(os.environ, MAM_ID_ORDER_WAIT_S="0")
|
||||
res = subprocess.run(
|
||||
["bash", str(create_sh), "--workspace", str(ws), "--agent", "agy",
|
||||
"--role", "reviewer", "--session", name_b, "--no-onboard"],
|
||||
capture_output=True, text=True, cwd=str(mam_sandbox), env=env0, timeout=120,
|
||||
)
|
||||
assert res.returncode == 1, res.stdout + res.stderr
|
||||
warns = _warn_lines(res.stderr)
|
||||
assert len(warns) == 1, res.stderr
|
||||
assert "Issue #63" in warns[0]
|
||||
assert name_a in res.stderr
|
||||
assert res.stderr.index("WARNING: --no-onboard:") < res.stderr.index(name_a), res.stderr
|
||||
finally:
|
||||
holder.kill()
|
||||
holder.wait()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"opts,expect_onboard",
|
||||
[
|
||||
([], True),
|
||||
(["--onboard"], True),
|
||||
(["--no-onboard"], False),
|
||||
(["--no-onboard", "--onboard"], True),
|
||||
],
|
||||
)
|
||||
def test_prepare_forwards_onboarding_choice(ctl, opts, expect_onboard):
|
||||
main = ctl
|
||||
args_file = main / "captured_create_args.txt"
|
||||
stub = main / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||
stub.write_text(f'#!/usr/bin/env bash\nprintf "%s\\n" "$*" > "{args_file}"\nexit 0\n')
|
||||
wt = _wt(main, "fwd")
|
||||
try:
|
||||
res = _prepare(main, wt, "--creator-agent", "claude", *opts)
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
tokens = args_file.read_text().split()
|
||||
if expect_onboard:
|
||||
assert "--onboard" in tokens, tokens
|
||||
assert "--no-onboard" not in tokens, tokens
|
||||
else:
|
||||
assert "--no-onboard" in tokens, tokens
|
||||
assert "--onboard" not in tokens, tokens
|
||||
finally:
|
||||
subprocess.run(["git", "worktree", "remove", "--force", str(wt)], cwd=str(main), capture_output=True)
|
||||
|
||||
|
||||
def test_prepare_onboards_by_default_end_to_end(ctl, mock_herdr, mock_agents):
|
||||
main = ctl
|
||||
wt = _wt(main, "onboard-def")
|
||||
try:
|
||||
res = _prepare(main, wt, "--creator-agent", "claude")
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
(row,) = _rows(main)
|
||||
jid = row["delegate_job_id"]
|
||||
assert jid, row
|
||||
job_path = main / ".mam" / "jobs" / f"{jid}.json"
|
||||
assert job_path.is_file(), f"missing job record {job_path}"
|
||||
rec = json.loads(job_path.read_text())
|
||||
assert rec["status"] == "completed", rec
|
||||
assert f"onboarding completed: job {jid}" in res.stdout, res.stdout
|
||||
finally:
|
||||
subprocess.run(["git", "worktree", "remove", "--force", str(wt)], cwd=str(main), capture_output=True)
|
||||
|
||||
|
||||
def test_prepare_no_onboard_sends_none(ctl, mock_herdr, mock_agents):
|
||||
main = ctl
|
||||
wt = _wt(main, "no-onboard")
|
||||
try:
|
||||
res = _prepare(main, wt, "--creator-agent", "claude", "--no-onboard")
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
(row,) = _rows(main)
|
||||
assert row["delegate_job_id"] is None, row
|
||||
jobs_dir = main / ".mam" / "jobs"
|
||||
if jobs_dir.is_dir():
|
||||
for jf in jobs_dir.glob("*.json"):
|
||||
rec = json.loads(jf.read_text())
|
||||
assert rec.get("agent_session") != f"herdr:{row['name']}", rec
|
||||
finally:
|
||||
subprocess.run(["git", "worktree", "remove", "--force", str(wt)], cwd=str(main), capture_output=True)
|
||||
Reference in new issue
Block a user