test(opencode): add adapter contract, TUI readiness, and lifecycle integration tests
This commit is contained in:
+52
-9
@@ -533,13 +533,6 @@ elif cmd1 == "agent":
|
|||||||
sys.stderr.write(json.dumps(err_payload) + "\\n")
|
sys.stderr.write(json.dumps(err_payload) + "\\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# TUI Welcome Tokens definition to prevent TUI readiness check timeout
|
|
||||||
buffer_content = {
|
|
||||||
"claude": "Anthropic Claude Ready",
|
|
||||||
"agy": "Antigravity Ready",
|
|
||||||
"hermes": "Hermes Ready"
|
|
||||||
}.get(agent_type, "Ready")
|
|
||||||
|
|
||||||
# Look up cwd from target pane in state if not explicitly passed
|
# Look up cwd from target pane in state if not explicitly passed
|
||||||
if not cwd and pane:
|
if not cwd and pane:
|
||||||
for p in state.get("panes", []):
|
for p in state.get("panes", []):
|
||||||
@@ -550,6 +543,16 @@ elif cmd1 == "agent":
|
|||||||
if not ws and pane and ":" in pane:
|
if not ws and pane and ":" in pane:
|
||||||
ws = pane.split(":")[0]
|
ws = pane.split(":")[0]
|
||||||
|
|
||||||
|
# TUI Welcome Tokens definition to prevent TUI readiness check timeout
|
||||||
|
resolved_cwd = cwd or "TMP_PATH_PLACEHOLDER"
|
||||||
|
buffer_content = {
|
||||||
|
"claude": f"Anthropic Claude Ready in {resolved_cwd}",
|
||||||
|
"agy": f"Antigravity Ready in {resolved_cwd}",
|
||||||
|
"hermes": f"Hermes Ready in {resolved_cwd}",
|
||||||
|
"grok": f"Grok Ready in {resolved_cwd}",
|
||||||
|
"opencode": f"OpenCode Ready in {resolved_cwd}"
|
||||||
|
}.get(agent_type, f"Ready in {resolved_cwd}")
|
||||||
|
|
||||||
agents = state.get("agents", {})
|
agents = state.get("agents", {})
|
||||||
agents[name] = {
|
agents[name] = {
|
||||||
"agent": agent_type,
|
"agent": agent_type,
|
||||||
@@ -576,7 +579,9 @@ elif cmd1 == "agent":
|
|||||||
own_key_map = {
|
own_key_map = {
|
||||||
"claude": "claude_session_id_own",
|
"claude": "claude_session_id_own",
|
||||||
"agy": "agy_conversation_id_own",
|
"agy": "agy_conversation_id_own",
|
||||||
"hermes": "hermes_conversation_id_own"
|
"hermes": "hermes_conversation_id_own",
|
||||||
|
"grok": "grok_session_id_own",
|
||||||
|
"opencode": "opencode_session_id_own"
|
||||||
}
|
}
|
||||||
own_key = own_key_map.get(agent_type)
|
own_key = own_key_map.get(agent_type)
|
||||||
if own_key:
|
if own_key:
|
||||||
@@ -627,6 +632,16 @@ elif cmd1 == "agent":
|
|||||||
conn.execute("INSERT OR REPLACE INTO sessions (id, cwd) VALUES (?, ?)", (session_uuid, ws_abs))
|
conn.execute("INSERT OR REPLACE INTO sessions (id, cwd) VALUES (?, ?)", (session_uuid, ws_abs))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
elif agent_type == "opencode":
|
||||||
|
opencode_dir = os.path.join(home_dir, ".local", "share", "opencode")
|
||||||
|
os.makedirs(opencode_dir, exist_ok=True)
|
||||||
|
odb = os.path.join(opencode_dir, "opencode.db")
|
||||||
|
conn = sqlite3.connect(odb)
|
||||||
|
conn.execute("CREATE TABLE IF NOT EXISTS session (id TEXT PRIMARY KEY, directory TEXT, time_created INTEGER, cwd TEXT, created_at REAL)")
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
|
conn.execute("INSERT OR REPLACE INTO session (id, directory, time_created, cwd, created_at) VALUES (?, ?, ?, ?, ?)", (session_uuid, ws_abs, now_ms, ws_abs, time.time()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
state["agents"] = agents
|
state["agents"] = agents
|
||||||
save_state()
|
save_state()
|
||||||
@@ -946,7 +961,35 @@ sys.exit(0)
|
|||||||
""")
|
""")
|
||||||
hermes_bin.chmod(0o755)
|
hermes_bin.chmod(0o755)
|
||||||
|
|
||||||
# 4. uuidgen mock to guarantee isolated creation UUIDs
|
# 4. grok mock
|
||||||
|
grok_bin = bin_dir / "grok"
|
||||||
|
grok_bin.write_text("""#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
sys.exit(0)
|
||||||
|
""")
|
||||||
|
grok_bin.chmod(0o755)
|
||||||
|
|
||||||
|
# 5. opencode mock
|
||||||
|
opencode_bin = bin_dir / "opencode"
|
||||||
|
opencode_bin.write_text("""#!/usr/bin/env python3
|
||||||
|
import sys, os
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if len(args) >= 2 and args[0] == "db" and args[1] == "path":
|
||||||
|
home = os.environ.get("HOME", "")
|
||||||
|
db_path = f"{home}/.local/share/opencode/opencode.db"
|
||||||
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.execute("CREATE TABLE IF NOT EXISTS session (id TEXT PRIMARY KEY, directory TEXT, time_created INTEGER)")
|
||||||
|
conn.close()
|
||||||
|
print(db_path)
|
||||||
|
sys.exit(0)
|
||||||
|
sys.exit(0)
|
||||||
|
""")
|
||||||
|
opencode_bin.chmod(0o755)
|
||||||
|
|
||||||
|
# 6. uuidgen mock to guarantee isolated creation UUIDs
|
||||||
uuidgen_bin = bin_dir / "uuidgen"
|
uuidgen_bin = bin_dir / "uuidgen"
|
||||||
uuidgen_bin.write_text("""#!/usr/bin/env python3
|
uuidgen_bin.write_text("""#!/usr/bin/env python3
|
||||||
import uuid
|
import uuid
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ def test_agent_adapter_registry():
|
|||||||
'agy': 'agy_conversation_id_own',
|
'agy': 'agy_conversation_id_own',
|
||||||
'hermes': 'hermes_conversation_id_own',
|
'hermes': 'hermes_conversation_id_own',
|
||||||
'grok': 'grok_session_id_own',
|
'grok': 'grok_session_id_own',
|
||||||
|
'opencode': 'opencode_session_id_own',
|
||||||
}
|
}
|
||||||
for agent, expected in EXPECTED_OWN_KEYS.items():
|
for agent, expected in EXPECTED_OWN_KEYS.items():
|
||||||
adapter = get_adapter(agent)
|
adapter = get_adapter(agent)
|
||||||
@@ -73,6 +74,7 @@ def test_adapter_required_properties():
|
|||||||
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
|
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
|
||||||
'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)),
|
'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)),
|
||||||
'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
|
'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
|
||||||
|
'opencode': ('OpenCode|Chat', '/exit', 'opencode-cli', ('session_id',)),
|
||||||
}
|
}
|
||||||
for agent, (toks, exitk, delk, cache_f) in expected.items():
|
for agent, (toks, exitk, delk, cache_f) in expected.items():
|
||||||
adapter = get_adapter(agent)
|
adapter = get_adapter(agent)
|
||||||
@@ -93,6 +95,7 @@ def test_adapter_modal_tokens_contract():
|
|||||||
'agy': None,
|
'agy': None,
|
||||||
'hermes': None,
|
'hermes': None,
|
||||||
'grok': None,
|
'grok': None,
|
||||||
|
'opencode': None,
|
||||||
}
|
}
|
||||||
for agent, expected in expected_modal.items():
|
for agent, expected in expected_modal.items():
|
||||||
adapter = get_adapter(agent)
|
adapter = get_adapter(agent)
|
||||||
@@ -106,7 +109,7 @@ def test_facts_bridge_eval_contract():
|
|||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
skills_dir = str(Path(__file__).resolve().parent.parent / ".agents" / "skills")
|
skills_dir = str(Path(__file__).resolve().parent.parent / ".agents" / "skills")
|
||||||
env["PYTHONPATH"] = f"{skills_dir}:{env.get('PYTHONPATH', '')}"
|
env["PYTHONPATH"] = f"{skills_dir}:{env.get('PYTHONPATH', '')}"
|
||||||
for agent in ('claude', 'agy', 'hermes', 'grok'):
|
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||||
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "facts", agent], capture_output=True, text=True, env=env)
|
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "facts", agent], capture_output=True, text=True, env=env)
|
||||||
assert res.returncode == 0
|
assert res.returncode == 0
|
||||||
facts_output = res.stdout
|
facts_output = res.stdout
|
||||||
@@ -126,6 +129,72 @@ def shlex_quote(s):
|
|||||||
import shlex
|
import shlex
|
||||||
return shlex.quote(s)
|
return shlex.quote(s)
|
||||||
|
|
||||||
|
def _seed_opencode_session(db_path, session_id, ws_path, epoch_sec):
|
||||||
|
import sqlite3
|
||||||
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("PRAGMA foreign_keys = OFF")
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||||
|
tables = {row[0] for row in cursor.fetchall()}
|
||||||
|
if "session" not in tables:
|
||||||
|
conn.execute("CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT, time_created INTEGER, cwd TEXT, created_at REAL)")
|
||||||
|
if "project" in tables:
|
||||||
|
cursor.execute("PRAGMA table_info(project)")
|
||||||
|
p_cols = [col[1] for col in cursor.fetchall()]
|
||||||
|
p_ins = ["id"]
|
||||||
|
p_vals = ["default"]
|
||||||
|
if "worktree" in p_cols:
|
||||||
|
p_ins.append("worktree")
|
||||||
|
p_vals.append(ws_path)
|
||||||
|
if "time_created" in p_cols:
|
||||||
|
p_ins.append("time_created")
|
||||||
|
p_vals.append(0)
|
||||||
|
if "time_updated" in p_cols:
|
||||||
|
p_ins.append("time_updated")
|
||||||
|
p_vals.append(0)
|
||||||
|
conn.execute(f"INSERT OR IGNORE INTO project ({', '.join(p_ins)}) VALUES ({', '.join(['?']*len(p_vals))})", p_vals)
|
||||||
|
|
||||||
|
cursor.execute("PRAGMA table_info(session)")
|
||||||
|
cols = [col[1] for col in cursor.fetchall()]
|
||||||
|
|
||||||
|
insert_cols = ["id"]
|
||||||
|
vals = [session_id]
|
||||||
|
if "project_id" in cols:
|
||||||
|
insert_cols.append("project_id")
|
||||||
|
vals.append("default")
|
||||||
|
if "slug" in cols:
|
||||||
|
insert_cols.append("slug")
|
||||||
|
vals.append("default")
|
||||||
|
if "title" in cols:
|
||||||
|
insert_cols.append("title")
|
||||||
|
vals.append("default")
|
||||||
|
if "version" in cols:
|
||||||
|
insert_cols.append("version")
|
||||||
|
vals.append("1.0.0")
|
||||||
|
if "directory" in cols:
|
||||||
|
insert_cols.append("directory")
|
||||||
|
vals.append(ws_path)
|
||||||
|
if "cwd" in cols:
|
||||||
|
insert_cols.append("cwd")
|
||||||
|
vals.append(ws_path)
|
||||||
|
if "time_created" in cols:
|
||||||
|
insert_cols.append("time_created")
|
||||||
|
vals.append(int(float(epoch_sec) * 1000))
|
||||||
|
if "time_updated" in cols:
|
||||||
|
insert_cols.append("time_updated")
|
||||||
|
vals.append(int(float(epoch_sec) * 1000))
|
||||||
|
if "created_at" in cols:
|
||||||
|
insert_cols.append("created_at")
|
||||||
|
vals.append(float(epoch_sec))
|
||||||
|
|
||||||
|
placeholders = ", ".join(["?"] * len(vals))
|
||||||
|
col_str = ", ".join(insert_cols)
|
||||||
|
conn.execute(f"INSERT OR REPLACE INTO session ({col_str}) VALUES ({placeholders})", vals)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def test_purge_artifacts_composite(tmp_path):
|
def test_purge_artifacts_composite(tmp_path):
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from lib_py.agents.base import DiscoveryContext
|
from lib_py.agents.base import DiscoveryContext
|
||||||
@@ -200,6 +269,64 @@ def test_purge_artifacts_composite(tmp_path):
|
|||||||
assert not os.path.exists(g_path)
|
assert not os.path.exists(g_path)
|
||||||
assert not os.path.exists(os.path.dirname(g_path))
|
assert not os.path.exists(os.path.dirname(g_path))
|
||||||
|
|
||||||
|
# 5. OpenCode (SQLite tables session, message, part)
|
||||||
|
opencode_adapter = get_adapter('opencode')
|
||||||
|
opencode_ctx = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
|
||||||
|
o_db = opencode_adapter.artifact_path('uuid-o', opencode_ctx)
|
||||||
|
_seed_opencode_session(o_db, 'uuid-o', ws, 2000.0)
|
||||||
|
conn = sqlite3.connect(o_db)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("PRAGMA foreign_keys = OFF")
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||||
|
tables = {row[0] for row in cursor.fetchall()}
|
||||||
|
if "message" not in tables:
|
||||||
|
conn.execute("CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT)")
|
||||||
|
if "part" not in tables:
|
||||||
|
conn.execute("CREATE TABLE part (id TEXT PRIMARY KEY, session_id TEXT)")
|
||||||
|
|
||||||
|
cursor.execute("PRAGMA table_info(message)")
|
||||||
|
m_cols = [c[1] for c in cursor.fetchall()]
|
||||||
|
m_ins = ["id", "session_id"] if "id" in m_cols else ["session_id"]
|
||||||
|
m_vals = ["m1", "uuid-o"] if "id" in m_cols else ["uuid-o"]
|
||||||
|
if "time_created" in m_cols:
|
||||||
|
m_ins.append("time_created")
|
||||||
|
m_vals.append(0)
|
||||||
|
if "time_updated" in m_cols:
|
||||||
|
m_ins.append("time_updated")
|
||||||
|
m_vals.append(0)
|
||||||
|
if "data" in m_cols:
|
||||||
|
m_ins.append("data")
|
||||||
|
m_vals.append("{}")
|
||||||
|
conn.execute(f"INSERT OR REPLACE INTO message ({', '.join(m_ins)}) VALUES ({', '.join(['?']*len(m_vals))})", m_vals)
|
||||||
|
|
||||||
|
cursor.execute("PRAGMA table_info(part)")
|
||||||
|
p_cols = [c[1] for c in cursor.fetchall()]
|
||||||
|
p_ins = ["id", "session_id"] if "id" in p_cols else ["session_id"]
|
||||||
|
p_vals = ["p1", "uuid-o"] if "id" in p_cols else ["uuid-o"]
|
||||||
|
if "message_id" in p_cols:
|
||||||
|
p_ins.append("message_id")
|
||||||
|
p_vals.append("m1")
|
||||||
|
if "time_created" in p_cols:
|
||||||
|
p_ins.append("time_created")
|
||||||
|
p_vals.append(0)
|
||||||
|
if "time_updated" in p_cols:
|
||||||
|
p_ins.append("time_updated")
|
||||||
|
p_vals.append(0)
|
||||||
|
if "data" in p_cols:
|
||||||
|
p_ins.append("data")
|
||||||
|
p_vals.append("{}")
|
||||||
|
conn.execute(f"INSERT OR REPLACE INTO part ({', '.join(p_ins)}) VALUES ({', '.join(['?']*len(p_vals))})", p_vals)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
purged_o = opencode_adapter.purge_artifacts('uuid-o', opencode_ctx)
|
||||||
|
assert len(purged_o) == 1
|
||||||
|
conn = sqlite3.connect(o_db)
|
||||||
|
assert conn.execute("SELECT count(*) FROM session WHERE id='uuid-o'").fetchone()[0] == 0
|
||||||
|
assert conn.execute("SELECT count(*) FROM message WHERE session_id='uuid-o'").fetchone()[0] == 0
|
||||||
|
assert conn.execute("SELECT count(*) FROM part WHERE session_id='uuid-o'").fetchone()[0] == 0
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def test_adapter_spawn_and_resume_specs():
|
def test_adapter_spawn_and_resume_specs():
|
||||||
claude = get_adapter('claude')
|
claude = get_adapter('claude')
|
||||||
assert claude.spawn_spec('claude', 'u1') == 'claude --dangerously-skip-permissions --session-id u1'
|
assert claude.spawn_spec('claude', 'u1') == 'claude --dangerously-skip-permissions --session-id u1'
|
||||||
@@ -222,6 +349,12 @@ def test_adapter_spawn_and_resume_specs():
|
|||||||
assert grok.resume_spec('grok', 'u1', materialized=True) == 'grok --resume u1 --permission-mode bypassPermissions'
|
assert grok.resume_spec('grok', 'u1', materialized=True) == 'grok --resume u1 --permission-mode bypassPermissions'
|
||||||
assert grok.resume_spec('grok', 'u1', materialized=False) == 'grok --session-id u1 --permission-mode bypassPermissions'
|
assert grok.resume_spec('grok', 'u1', materialized=False) == 'grok --session-id u1 --permission-mode bypassPermissions'
|
||||||
|
|
||||||
|
opencode = get_adapter('opencode')
|
||||||
|
assert opencode.spawn_spec('opencode', 'u1') == 'opencode --auto --agent build'
|
||||||
|
assert opencode.spawn_spec('opencode', '') == 'opencode --auto --agent build'
|
||||||
|
assert opencode.resume_spec('opencode', 'u1', materialized=True) == 'opencode --session u1 --auto --agent build'
|
||||||
|
assert opencode.resume_spec('opencode', 'u1', materialized=False) == 'opencode --auto --agent build'
|
||||||
|
|
||||||
def test_adapter_auth_ok(tmp_path, monkeypatch):
|
def test_adapter_auth_ok(tmp_path, monkeypatch):
|
||||||
monkeypatch.setenv("HOME_DIR", str(tmp_path))
|
monkeypatch.setenv("HOME_DIR", str(tmp_path))
|
||||||
# Claude auth runner
|
# Claude auth runner
|
||||||
@@ -251,6 +384,17 @@ def test_adapter_auth_ok(tmp_path, monkeypatch):
|
|||||||
# Hermes always True
|
# Hermes always True
|
||||||
assert get_adapter('hermes').auth_ok() is True
|
assert get_adapter('hermes').auth_ok() is True
|
||||||
|
|
||||||
|
# OpenCode auth check (provider env var or auth.json file)
|
||||||
|
opencode = get_adapter('opencode')
|
||||||
|
assert opencode.auth_ok() is False
|
||||||
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
||||||
|
assert opencode.auth_ok() is True
|
||||||
|
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||||
|
opencode_auth = tmp_path / ".local" / "share" / "opencode" / "auth.json"
|
||||||
|
opencode_auth.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
opencode_auth.write_text("{}")
|
||||||
|
assert opencode.auth_ok() is True
|
||||||
|
|
||||||
def test_adapter_discover(tmp_path):
|
def test_adapter_discover(tmp_path):
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from lib_py.agents.base import DiscoveryContext
|
from lib_py.agents.base import DiscoveryContext
|
||||||
@@ -306,6 +450,13 @@ def test_adapter_discover(tmp_path):
|
|||||||
f.write('{"session_id": "u-g1", "cwd": "' + ws + '"}\n')
|
f.write('{"session_id": "u-g1", "cwd": "' + ws + '"}\n')
|
||||||
assert grok.discover(ctx_g) == ['u-g1']
|
assert grok.discover(ctx_g) == ['u-g1']
|
||||||
|
|
||||||
|
# 5. OpenCode
|
||||||
|
opencode = get_adapter('opencode')
|
||||||
|
ctx_o = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
|
||||||
|
o_db = opencode.artifact_path('u-o1', ctx_o)
|
||||||
|
_seed_opencode_session(o_db, 'u-o1', ws, 2000.0)
|
||||||
|
assert opencode.discover(ctx_o) == ['u-o1']
|
||||||
|
|
||||||
def test_cli_bridge_subcommands_and_quote_safety():
|
def test_cli_bridge_subcommands_and_quote_safety():
|
||||||
import subprocess, sys
|
import subprocess, sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -324,7 +475,7 @@ def test_cli_bridge_subcommands_and_quote_safety():
|
|||||||
assert res.stdout.strip() == "/bin/claude --dangerously-skip-permissions --session-id uuid-test"
|
assert res.stdout.strip() == "/bin/claude --dangerously-skip-permissions --session-id uuid-test"
|
||||||
|
|
||||||
# 3. exit-key
|
# 3. exit-key
|
||||||
for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('grok', '/exit')]:
|
for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('grok', '/exit'), ('opencode', '/exit')]:
|
||||||
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "exit-key", agent], capture_output=True, text=True, env=env)
|
res = subprocess.run([sys.executable, "-m", "lib_py.agents", "exit-key", agent], capture_output=True, text=True, env=env)
|
||||||
assert res.returncode == 0
|
assert res.returncode == 0
|
||||||
assert res.stdout.strip() == expected_key
|
assert res.stdout.strip() == expected_key
|
||||||
@@ -336,6 +487,7 @@ def test_delegate_agent_resolution_and_fallback():
|
|||||||
'agy': 'antigravity-cli',
|
'agy': 'antigravity-cli',
|
||||||
'hermes': 'hermes-agent',
|
'hermes': 'hermes-agent',
|
||||||
'grok': 'grok-build',
|
'grok': 'grok-build',
|
||||||
|
'opencode': 'opencode-cli',
|
||||||
}
|
}
|
||||||
# 1. Adapter property
|
# 1. Adapter property
|
||||||
for agent, expected_key in expected_map.items():
|
for agent, expected_key in expected_map.items():
|
||||||
@@ -354,6 +506,7 @@ def test_delegate_agent_resolution_and_fallback():
|
|||||||
hermes) delegate_agent="hermes-agent" ;;
|
hermes) delegate_agent="hermes-agent" ;;
|
||||||
agy) delegate_agent="antigravity-cli" ;;
|
agy) delegate_agent="antigravity-cli" ;;
|
||||||
grok) delegate_agent="grok-build" ;;
|
grok) delegate_agent="grok-build" ;;
|
||||||
|
opencode) delegate_agent="opencode-cli" ;;
|
||||||
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
|
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
@@ -523,3 +676,167 @@ def test_hermes_reconcile_full_block_integration(tmp_path):
|
|||||||
assert valid_candidates == ['u-target']
|
assert valid_candidates == ['u-target']
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_input_region_properties():
|
||||||
|
adapter = get_adapter('opencode')
|
||||||
|
assert adapter.input_prompt == '❯'
|
||||||
|
assert adapter.input_placeholder == ''
|
||||||
|
assert adapter.input_rule_pattern == '─{10,}'
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_verify_artifact_created_at_epoch_filtering(tmp_path):
|
||||||
|
import sqlite3
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
ws = str(tmp_path / "ws")
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
adapter = get_adapter('opencode')
|
||||||
|
ctx_old = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home, epoch=1500)
|
||||||
|
odb = adapter.artifact_path('u-old', ctx_old)
|
||||||
|
_seed_opencode_session(odb, 'u-old', ws, 1000.0)
|
||||||
|
_seed_opencode_session(odb, 'u-new', ws, 2000.0)
|
||||||
|
|
||||||
|
assert adapter.verify_artifact('u-old', ctx_old) is False
|
||||||
|
assert adapter.verify_artifact('u-new', ctx_old) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_reconcile_dual_direction_epoch_guards(tmp_path):
|
||||||
|
import sqlite3
|
||||||
|
from lib_py.agents.registry import get_adapter
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
from lib_py.verify_session import verify_session_uuid
|
||||||
|
ws = str(tmp_path / "ws")
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
os.makedirs(ws, exist_ok=True)
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
|
||||||
|
adapter = get_adapter('opencode')
|
||||||
|
ctx = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
|
||||||
|
odb = adapter.artifact_path('u-hist', ctx)
|
||||||
|
_seed_opencode_session(odb, 'u-hist', ws, 1000.0)
|
||||||
|
_seed_opencode_session(odb, 'u-fresh1', ws, 2000.0)
|
||||||
|
|
||||||
|
epoch = 1500
|
||||||
|
s_row = {'name': 'sess1', 'pane': {'cwd': ws}, 'herdr_session_epoch': epoch}
|
||||||
|
|
||||||
|
valid = [u for u in adapter.discover(DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home, epoch=epoch, row=s_row)) if verify_session_uuid(ws, 'opencode', u, s_row, home_dir=home, mode="discover")]
|
||||||
|
assert valid == ['u-fresh1']
|
||||||
|
assert len(valid) == 1
|
||||||
|
|
||||||
|
_seed_opencode_session(odb, 'u-fresh2', ws, 2100.0)
|
||||||
|
|
||||||
|
valid2 = [u for u in adapter.discover(DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home, epoch=epoch, row=s_row)) if verify_session_uuid(ws, 'opencode', u, s_row, home_dir=home, mode="discover")]
|
||||||
|
assert len(valid2) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_reconcile_full_block_integration(tmp_path):
|
||||||
|
"""Verify opencode drift-C block behavior with sibling_claimed exclusion."""
|
||||||
|
import sqlite3
|
||||||
|
from lib_py.agents.registry import get_adapter
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
from lib_py.verify_session import verify_session_uuid
|
||||||
|
ws = str(tmp_path / "ws")
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
os.makedirs(ws, exist_ok=True)
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
|
||||||
|
adapter = get_adapter('opencode')
|
||||||
|
ctx = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
|
||||||
|
odb = adapter.artifact_path('u-target', ctx)
|
||||||
|
_seed_opencode_session(odb, 'u-claimed', ws, 2000.0)
|
||||||
|
_seed_opencode_session(odb, 'u-target', ws, 2001.0)
|
||||||
|
|
||||||
|
epoch = 1900
|
||||||
|
s_eval = {
|
||||||
|
'name': 'opencode-target-01',
|
||||||
|
'pane': {'cwd': ws},
|
||||||
|
'herdr_session_epoch': epoch,
|
||||||
|
'_sibling_claimed_uuids': ['u-claimed']
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates = adapter.discover(DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home, epoch=epoch, row=s_eval))
|
||||||
|
|
||||||
|
valid_candidates = []
|
||||||
|
for uuid in candidates:
|
||||||
|
if uuid in s_eval['_sibling_claimed_uuids']:
|
||||||
|
continue
|
||||||
|
if verify_session_uuid(ws, 'opencode', uuid, s_eval, home_dir=home, mode="discover"):
|
||||||
|
valid_candidates.append(uuid)
|
||||||
|
|
||||||
|
assert valid_candidates == ['u-target']
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_real_schema_directory_and_time_created_ms(tmp_path):
|
||||||
|
"""Verify opencode adapter behavior with the real live schema (directory, time_created in ms)."""
|
||||||
|
import sqlite3
|
||||||
|
from lib_py.agents.registry import get_adapter
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
|
||||||
|
ws1 = str(tmp_path / "ws1")
|
||||||
|
ws2 = str(tmp_path / "ws2")
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
os.makedirs(ws1, exist_ok=True)
|
||||||
|
os.makedirs(ws2, exist_ok=True)
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
|
||||||
|
odb = f"{home}/.local/share/opencode/opencode.db"
|
||||||
|
os.makedirs(os.path.dirname(odb), exist_ok=True)
|
||||||
|
conn = sqlite3.connect(odb)
|
||||||
|
conn.execute("CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT NOT NULL, time_created INTEGER NOT NULL)")
|
||||||
|
|
||||||
|
epoch_floor_sec = 1724920000
|
||||||
|
conn.execute("INSERT INTO session VALUES ('u-target-fresh', ?, ?)", (ws1, int((epoch_floor_sec + 100) * 1000)))
|
||||||
|
conn.execute("INSERT INTO session VALUES ('u-target-old', ?, ?)", (ws1, int((epoch_floor_sec - 100) * 1000)))
|
||||||
|
conn.execute("INSERT INTO session VALUES ('u-foreign', ?, ?)", (ws2, int((epoch_floor_sec + 200) * 1000)))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
adapter = get_adapter('opencode')
|
||||||
|
ctx_ws1 = DiscoveryContext(workspace=ws1, agent_name='opencode', home_dir=home, epoch=epoch_floor_sec)
|
||||||
|
|
||||||
|
# verify_artifact assertions
|
||||||
|
assert adapter.verify_artifact('u-target-fresh', ctx_ws1) is True
|
||||||
|
assert adapter.verify_artifact('u-target-old', ctx_ws1) is False # Stale epoch rejected
|
||||||
|
assert adapter.verify_artifact('u-foreign', ctx_ws1) is False # Foreign workspace rejected
|
||||||
|
|
||||||
|
# discover assertions
|
||||||
|
discovered = adapter.discover(ctx_ws1)
|
||||||
|
assert discovered == ['u-target-fresh']
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_resolve_db_cli_path(tmp_path, monkeypatch):
|
||||||
|
"""Verify _resolve_db correctly executes opencode db path and falls back to candidate paths."""
|
||||||
|
import stat
|
||||||
|
from lib_py.agents.registry import get_adapter
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
|
||||||
|
adapter = get_adapter('opencode')
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
ws = str(tmp_path / "ws")
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
os.makedirs(ws, exist_ok=True)
|
||||||
|
|
||||||
|
expected_db = f"{home}/.local/share/opencode/custom_opencode.db"
|
||||||
|
os.makedirs(os.path.dirname(expected_db), exist_ok=True)
|
||||||
|
with open(expected_db, "w") as f:
|
||||||
|
f.write("mock db")
|
||||||
|
|
||||||
|
# Create mock opencode binary that outputs custom DB path
|
||||||
|
bin_dir = tmp_path / "bin"
|
||||||
|
os.makedirs(bin_dir, exist_ok=True)
|
||||||
|
mock_bin = bin_dir / "opencode"
|
||||||
|
mock_bin.write_text(f"""#!/bin/sh
|
||||||
|
if [ "$1" = "db" ] && [ "$2" = "path" ]; then
|
||||||
|
echo "{expected_db}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
""")
|
||||||
|
mock_bin.chmod(mock_bin.stat().st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
|
monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ.get('PATH', '')}")
|
||||||
|
ctx = DiscoveryContext(workspace=ws, agent_name='opencode', home_dir=home)
|
||||||
|
assert adapter._resolve_db(ctx) == expected_db
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -488,7 +488,7 @@ def test_c10_create_session_timeout_dead_pid_kills_session(mam_sandbox, mock_her
|
|||||||
def test_adapter_strong_weak_partition():
|
def test_adapter_strong_weak_partition():
|
||||||
"""N-D: Verify strong_ready_tokens and weak_ready_tokens partition ready_tokens for all adapters."""
|
"""N-D: Verify strong_ready_tokens and weak_ready_tokens partition ready_tokens for all adapters."""
|
||||||
from lib_py.agents.registry import get_adapter
|
from lib_py.agents.registry import get_adapter
|
||||||
for agent in ('claude', 'agy', 'hermes', 'grok'):
|
for agent in ('claude', 'agy', 'hermes', 'grok', 'opencode'):
|
||||||
adapter = get_adapter(agent)
|
adapter = get_adapter(agent)
|
||||||
assert adapter is not None
|
assert adapter is not None
|
||||||
ready_set = set(t for t in adapter.ready_tokens.split('|') if t)
|
ready_set = set(t for t in adapter.ready_tokens.split('|') if t)
|
||||||
@@ -525,6 +525,8 @@ echo "5: $(check_sks 'test-worker-hermes-02')"
|
|||||||
echo "6: $(check_sks 'workspace-creator-agy-05')"
|
echo "6: $(check_sks 'workspace-creator-agy-05')"
|
||||||
echo "7: $(check_sks 'claude')"
|
echo "7: $(check_sks 'claude')"
|
||||||
echo "9: $(check_sks 'grok-02')"
|
echo "9: $(check_sks 'grok-02')"
|
||||||
|
echo "10: $(check_sks 'creator-opencode-01')"
|
||||||
|
echo "11: $(check_sks 'opencode')"
|
||||||
"""
|
"""
|
||||||
res = _run_lib_helpers(script)
|
res = _run_lib_helpers(script)
|
||||||
assert res.returncode == 0, res.stderr + res.stdout
|
assert res.returncode == 0, res.stderr + res.stdout
|
||||||
@@ -534,6 +536,8 @@ echo "9: $(check_sks 'grok-02')"
|
|||||||
assert "6: RESOLVED_AGENT=agy" in res.stdout
|
assert "6: RESOLVED_AGENT=agy" in res.stdout
|
||||||
assert "7: RESOLVED_AGENT=claude" in res.stdout
|
assert "7: RESOLVED_AGENT=claude" in res.stdout
|
||||||
assert "9: RESOLVED_AGENT=grok" in res.stdout
|
assert "9: RESOLVED_AGENT=grok" in res.stdout
|
||||||
|
assert "10: RESOLVED_AGENT=opencode" in res.stdout
|
||||||
|
assert "11: RESOLVED_AGENT=opencode" in res.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_f2_upsell_modal_defense_with_real_facts_and_banner():
|
def test_f2_upsell_modal_defense_with_real_facts_and_banner():
|
||||||
|
|||||||
@@ -960,18 +960,18 @@ def test_grok_shell_and_scripts_integration(mam_sandbox):
|
|||||||
lib_path = mam_sandbox / "skills" / "lib.sh"
|
lib_path = mam_sandbox / "skills" / "lib.sh"
|
||||||
lib_content = lib_path.read_text()
|
lib_content = lib_path.read_text()
|
||||||
assert '*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok"' in lib_content
|
assert '*-creator-grok|*-planner-grok|*-reviewer-grok) kind="grok"' in lib_content
|
||||||
assert "'claude', 'agy', 'hermes', 'grok'" in lib_content
|
assert "'claude', 'agy', 'hermes', 'grok', 'opencode'" in lib_content
|
||||||
assert '[[ "$sess" =~ "grok" ]]' in lib_content
|
assert '[[ "$sess" =~ "grok" ]]' in lib_content
|
||||||
|
|
||||||
# 2. create_session.sh validation
|
# 2. create_session.sh validation
|
||||||
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
create_content = create_path.read_text()
|
create_content = create_path.read_text()
|
||||||
assert 'claude|agy|hermes|grok)' in create_content
|
assert 'claude|agy|hermes|grok|opencode)' in create_content
|
||||||
|
|
||||||
# 3. stop_session.sh validation & state capture
|
# 3. stop_session.sh validation & state capture
|
||||||
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
|
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
|
||||||
stop_content = stop_path.read_text()
|
stop_content = stop_path.read_text()
|
||||||
assert 'claude|agy|hermes|grok)' in stop_content
|
assert 'claude|agy|hermes|grok|opencode)' in stop_content
|
||||||
assert "target['grok_session_id_own'] = captured" in stop_content
|
assert "target['grok_session_id_own'] = captured" in stop_content
|
||||||
|
|
||||||
# 4. workspace_uuid OWN_KEY
|
# 4. workspace_uuid OWN_KEY
|
||||||
@@ -979,6 +979,71 @@ def test_grok_shell_and_scripts_integration(mam_sandbox):
|
|||||||
assert OWN_KEY.get('grok') == 'grok_session_id_own'
|
assert OWN_KEY.get('grok') == 'grok_session_id_own'
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_shell_and_scripts_integration(mam_sandbox):
|
||||||
|
"""Verify lib.sh, create_session.sh, stop_session.sh, and workspace_uuid contain opencode."""
|
||||||
|
import json, subprocess
|
||||||
|
# 1. lib.sh kind mapping & binaries
|
||||||
|
lib_path = mam_sandbox / "skills" / "lib.sh"
|
||||||
|
lib_content = lib_path.read_text()
|
||||||
|
assert '*-creator-opencode|*-planner-opencode|*-reviewer-opencode) kind="opencode"' in lib_content
|
||||||
|
assert "'claude', 'agy', 'hermes', 'grok', 'opencode'" in lib_content
|
||||||
|
|
||||||
|
# 2. create_session.sh validation & OPENCODE_PERMISSION behavior (unset & preset)
|
||||||
|
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
create_content = create_path.read_text()
|
||||||
|
assert 'claude|agy|hermes|grok|opencode)' in create_content
|
||||||
|
# Test unset path
|
||||||
|
env_unset = {k: v for k, v in os.environ.items() if k != "OPENCODE_PERMISSION"}
|
||||||
|
res_c_unset = subprocess.run(
|
||||||
|
["bash", "-c", 'if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION=\'{"*":"allow"}\'; fi; echo "$OPENCODE_PERMISSION"'],
|
||||||
|
env=env_unset,
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
assert json.loads(res_c_unset.stdout.strip()) == {"*": "allow"}
|
||||||
|
# Test preset path (user-supplied override preserved intact)
|
||||||
|
res_c_preset = subprocess.run(
|
||||||
|
["bash", "-c", 'if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION=\'{"*":"allow"}\'; fi; echo "$OPENCODE_PERMISSION"'],
|
||||||
|
env={**os.environ, "OPENCODE_PERMISSION": '{"*":"deny"}'},
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
assert json.loads(res_c_preset.stdout.strip()) == {"*": "deny"}
|
||||||
|
|
||||||
|
# 3. resume_session.sh OPENCODE_PERMISSION behavior (unset & preset)
|
||||||
|
resume_path = mam_sandbox / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
|
||||||
|
resume_content = resume_path.read_text()
|
||||||
|
assert 'export OPENCODE_PERMISSION=\'{"*":"allow"}\'' in resume_content
|
||||||
|
# Test unset path
|
||||||
|
res_r_unset = subprocess.run(
|
||||||
|
["bash", "-c", 'if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION=\'{"*":"allow"}\'; fi; echo "$OPENCODE_PERMISSION"'],
|
||||||
|
env=env_unset,
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
assert json.loads(res_r_unset.stdout.strip()) == {"*": "allow"}
|
||||||
|
# Test preset path (user-supplied override preserved intact)
|
||||||
|
res_r_preset = subprocess.run(
|
||||||
|
["bash", "-c", 'if [ -z "${OPENCODE_PERMISSION:-}" ]; then export OPENCODE_PERMISSION=\'{"*":"allow"}\'; fi; echo "$OPENCODE_PERMISSION"'],
|
||||||
|
env={**os.environ, "OPENCODE_PERMISSION": '{"*":"deny"}'},
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
assert json.loads(res_r_preset.stdout.strip()) == {"*": "deny"}
|
||||||
|
|
||||||
|
# 4. stop_session.sh validation & state capture
|
||||||
|
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
|
||||||
|
stop_content = stop_path.read_text()
|
||||||
|
assert 'claude|agy|hermes|grok|opencode)' in stop_content
|
||||||
|
assert "target['opencode_session_id_own'] = captured" in stop_content
|
||||||
|
|
||||||
|
# 5. workspace_uuid OWN_KEY
|
||||||
|
from lib_py.workspace_uuid import OWN_KEY
|
||||||
|
assert OWN_KEY.get('opencode') == 'opencode_session_id_own'
|
||||||
|
|
||||||
|
# 6. reconcile.sh RECON_SRC imports shutil and get_adapter
|
||||||
|
recon_path = mam_sandbox / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
|
||||||
|
recon_content = recon_path.read_text()
|
||||||
|
assert 'import os, json, glob, subprocess, time, sqlite3, re, shutil' in recon_content
|
||||||
|
assert 'op_adapter = get_adapter(\'opencode\')' in recon_content
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1225,3 +1225,54 @@ def test_comp_reconcile_drift_b_populates_workspace_and_server(mam_sandbox, mock
|
|||||||
assert s["herdr_workspace"] != ""
|
assert s["herdr_workspace"] != ""
|
||||||
assert s["herdr_workspace"] != "-"
|
assert s["herdr_workspace"] != "-"
|
||||||
|
|
||||||
|
|
||||||
|
def test_comp_opencode_create_and_reconcile_and_stop(mam_sandbox, mock_herdr, mock_agents):
|
||||||
|
"""Verify create_session -> reconcile drift-C materialization -> stop flow for opencode."""
|
||||||
|
create_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
res = subprocess.run([
|
||||||
|
"bash", str(create_script),
|
||||||
|
"--workspace", str(mam_sandbox),
|
||||||
|
"--agent", "opencode",
|
||||||
|
"--role", "creator"
|
||||||
|
], capture_output=True, text=True, cwd=str(mam_sandbox))
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
|
||||||
|
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||||
|
import yaml
|
||||||
|
with open(yaml_path) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
sessions = data.get("herdr_sessions", [])
|
||||||
|
assert len(sessions) == 1
|
||||||
|
s = sessions[0]
|
||||||
|
session_name = s["name"]
|
||||||
|
assert s["status"] == "running"
|
||||||
|
assert s["opencode_session_id_own"] is None
|
||||||
|
assert s["session_id_source"] == "pending-discovery"
|
||||||
|
|
||||||
|
# Run reconcile.sh to trigger drift-C materialization
|
||||||
|
reconcile_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
|
||||||
|
res_rec = subprocess.run(["bash", str(reconcile_script), "--once", "--emit-diff"], capture_output=True, text=True, cwd=str(mam_sandbox), env={**os.environ, "WORKSPACE_ROOT": str(mam_sandbox)})
|
||||||
|
assert res_rec.returncode == 0, res_rec.stderr
|
||||||
|
|
||||||
|
with open(yaml_path) as f:
|
||||||
|
data_after = yaml.safe_load(f)
|
||||||
|
s_after = [x for x in data_after.get("herdr_sessions", []) if x.get("name") == session_name][0]
|
||||||
|
assert s_after.get("opencode_session_id_own") is not None
|
||||||
|
assert s_after.get("last_visible_status") in ("pinned", "resume_verified")
|
||||||
|
|
||||||
|
# Stop session
|
||||||
|
stop_script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
|
||||||
|
res_stop = subprocess.run([
|
||||||
|
"bash", str(stop_script),
|
||||||
|
"--session", session_name,
|
||||||
|
"--agent", "opencode"
|
||||||
|
], capture_output=True, text=True, cwd=str(mam_sandbox))
|
||||||
|
assert res_stop.returncode == 0, res_stop.stderr
|
||||||
|
|
||||||
|
with open(yaml_path) as f:
|
||||||
|
data_stopped = yaml.safe_load(f)
|
||||||
|
s_stopped = [x for x in data_stopped.get("herdr_sessions", []) if x.get("name") == session_name][0]
|
||||||
|
assert s_stopped["status"] == "stopped"
|
||||||
|
assert s_stopped["resumable"] is True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user