test(opencode): add adapter contract, TUI readiness, and lifecycle integration tests

This commit is contained in:
2026-08-29 14:59:04 +09:00
parent 94af7f6b8a
commit de2c0e6824
5 changed files with 495 additions and 15 deletions
+52 -9
View File
@@ -533,13 +533,6 @@ elif cmd1 == "agent":
sys.stderr.write(json.dumps(err_payload) + "\\n")
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
if not cwd and pane:
for p in state.get("panes", []):
@@ -550,6 +543,16 @@ elif cmd1 == "agent":
if not ws and pane and ":" in pane:
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[name] = {
"agent": agent_type,
@@ -576,7 +579,9 @@ elif cmd1 == "agent":
own_key_map = {
"claude": "claude_session_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)
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.commit()
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
save_state()
@@ -946,7 +961,35 @@ sys.exit(0)
""")
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.write_text("""#!/usr/bin/env python3
import uuid
+319 -2
View File
@@ -31,6 +31,7 @@ def test_agent_adapter_registry():
'agy': 'agy_conversation_id_own',
'hermes': 'hermes_conversation_id_own',
'grok': 'grok_session_id_own',
'opencode': 'opencode_session_id_own',
}
for agent, expected in EXPECTED_OWN_KEYS.items():
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')),
'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)),
'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():
adapter = get_adapter(agent)
@@ -93,6 +95,7 @@ def test_adapter_modal_tokens_contract():
'agy': None,
'hermes': None,
'grok': None,
'opencode': None,
}
for agent, expected in expected_modal.items():
adapter = get_adapter(agent)
@@ -106,7 +109,7 @@ def test_facts_bridge_eval_contract():
env = os.environ.copy()
skills_dir = str(Path(__file__).resolve().parent.parent / ".agents" / "skills")
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)
assert res.returncode == 0
facts_output = res.stdout
@@ -126,6 +129,72 @@ def shlex_quote(s):
import shlex
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):
import sqlite3
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(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():
claude = get_adapter('claude')
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=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):
monkeypatch.setenv("HOME_DIR", str(tmp_path))
# Claude auth runner
@@ -251,6 +384,17 @@ def test_adapter_auth_ok(tmp_path, monkeypatch):
# Hermes always 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):
import sqlite3
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')
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():
import subprocess, sys
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"
# 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)
assert res.returncode == 0
assert res.stdout.strip() == expected_key
@@ -336,6 +487,7 @@ def test_delegate_agent_resolution_and_fallback():
'agy': 'antigravity-cli',
'hermes': 'hermes-agent',
'grok': 'grok-build',
'opencode': 'opencode-cli',
}
# 1. Adapter property
for agent, expected_key in expected_map.items():
@@ -354,6 +506,7 @@ def test_delegate_agent_resolution_and_fallback():
hermes) delegate_agent="hermes-agent" ;;
agy) delegate_agent="antigravity-cli" ;;
grok) delegate_agent="grok-build" ;;
opencode) delegate_agent="opencode-cli" ;;
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
esac
fi
@@ -523,3 +676,167 @@ def test_hermes_reconcile_full_block_integration(tmp_path):
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
+5 -1
View File
@@ -488,7 +488,7 @@ def test_c10_create_session_timeout_dead_pid_kills_session(mam_sandbox, mock_her
def test_adapter_strong_weak_partition():
"""N-D: Verify strong_ready_tokens and weak_ready_tokens partition ready_tokens for all adapters."""
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)
assert adapter is not None
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 "7: $(check_sks 'claude')"
echo "9: $(check_sks 'grok-02')"
echo "10: $(check_sks 'creator-opencode-01')"
echo "11: $(check_sks 'opencode')"
"""
res = _run_lib_helpers(script)
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 "7: RESOLVED_AGENT=claude" 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():
+68 -3
View File
@@ -960,18 +960,18 @@ def test_grok_shell_and_scripts_integration(mam_sandbox):
lib_path = mam_sandbox / "skills" / "lib.sh"
lib_content = lib_path.read_text()
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
# 2. create_session.sh validation
create_path = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
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
stop_path = mam_sandbox / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
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
# 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'
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
+51
View File
@@ -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"] != "-"
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