feat(agent): add Grok Build TUI adapter, runtime dispatch, and skill support
This commit is contained in:
@@ -26,11 +26,18 @@ def test_resolve_home_contract():
|
||||
assert resolve_home() == home_val
|
||||
|
||||
def test_agent_adapter_registry():
|
||||
for agent in ('claude', 'agy', 'hermes', 'cline'):
|
||||
EXPECTED_OWN_KEYS = {
|
||||
'claude': 'claude_session_id_own',
|
||||
'agy': 'agy_conversation_id_own',
|
||||
'hermes': 'hermes_conversation_id_own',
|
||||
'cline': 'cline_conversation_id_own',
|
||||
'grok': 'grok_session_id_own',
|
||||
}
|
||||
for agent, expected in EXPECTED_OWN_KEYS.items():
|
||||
adapter = get_adapter(agent)
|
||||
assert adapter is not None
|
||||
assert adapter.name == agent
|
||||
assert adapter.own_key == f"{agent}_{'session' if agent == 'claude' else 'conversation'}_id_own"
|
||||
assert adapter.own_key == expected
|
||||
assert own_key(agent) == adapter.own_key
|
||||
|
||||
def test_agent_of_row_priority():
|
||||
@@ -67,6 +74,7 @@ def test_adapter_required_properties():
|
||||
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
|
||||
'hermes': ('Hermes', '/exit', 'hermes-agent', ('session_id',)),
|
||||
'cline': ('Cline|history|Chat|What can I do|slash commands', '/exit', 'cline-agent', ('session_id',)),
|
||||
'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
|
||||
}
|
||||
for agent, (toks, exitk, delk, cache_f) in expected.items():
|
||||
adapter = get_adapter(agent)
|
||||
@@ -82,7 +90,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', 'cline'):
|
||||
for agent in ('claude', 'agy', 'hermes', 'cline', 'grok'):
|
||||
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
|
||||
@@ -174,6 +182,19 @@ def test_purge_artifacts_composite(tmp_path):
|
||||
assert len(purged_cl) == 1
|
||||
assert not os.path.exists(cline_dir)
|
||||
|
||||
# 5. Grok (session directory containing chat_history.jsonl)
|
||||
grok_adapter = get_adapter('grok')
|
||||
grok_ctx = DiscoveryContext(workspace=ws, agent_name='grok', home_dir=home)
|
||||
g_path = grok_adapter.artifact_path('uuid-g', grok_ctx)
|
||||
os.makedirs(os.path.dirname(g_path), exist_ok=True)
|
||||
with open(g_path, 'w') as f:
|
||||
f.write('{"session_id": "uuid-g"}')
|
||||
assert os.path.exists(g_path)
|
||||
purged_g = grok_adapter.purge_artifacts('uuid-g', grok_ctx)
|
||||
assert len(purged_g) == 1
|
||||
assert not os.path.exists(g_path)
|
||||
assert not os.path.exists(os.path.dirname(g_path))
|
||||
|
||||
def test_adapter_spawn_and_resume_specs():
|
||||
claude = get_adapter('claude')
|
||||
assert claude.spawn_spec('claude', 'u1') == 'claude --dangerously-skip-permissions --session-id u1'
|
||||
@@ -194,6 +215,12 @@ def test_adapter_spawn_and_resume_specs():
|
||||
assert cline.resume_spec('cline', 'u1', materialized=True) == 'cline -i --id u1'
|
||||
assert cline.resume_spec('cline', 'u1', materialized=False) == 'cline -i'
|
||||
|
||||
grok = get_adapter('grok')
|
||||
assert grok.spawn_spec('grok', 'u1') == 'grok --session-id u1 --permission-mode bypassPermissions'
|
||||
assert grok.spawn_spec('grok', '') == 'grok --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'
|
||||
|
||||
def test_adapter_auth_ok(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HOME_DIR", str(tmp_path))
|
||||
# Claude auth runner
|
||||
@@ -209,6 +236,17 @@ def test_adapter_auth_ok(tmp_path, monkeypatch):
|
||||
oauth_file.write_text("{}")
|
||||
assert agy.auth_ok() is True
|
||||
|
||||
# Grok auth check (env var or auth.json file)
|
||||
grok = get_adapter('grok')
|
||||
assert grok.auth_ok() is False
|
||||
monkeypatch.setenv("XAI_API_KEY", "test-key")
|
||||
assert grok.auth_ok() is True
|
||||
monkeypatch.delenv("XAI_API_KEY", raising=False)
|
||||
grok_auth = tmp_path / ".grok" / "auth.json"
|
||||
grok_auth.parent.mkdir(parents=True, exist_ok=True)
|
||||
grok_auth.write_text("{}")
|
||||
assert grok.auth_ok() is True
|
||||
|
||||
# Hermes & Cline always True
|
||||
assert get_adapter('hermes').auth_ok() is True
|
||||
assert get_adapter('cline').auth_ok() is True
|
||||
@@ -268,6 +306,15 @@ def test_adapter_discover(tmp_path):
|
||||
f.write('{"session_id": "u-cl1", "cwd": "' + ws + '"}')
|
||||
assert cline.discover(ctx_cl) == ['u-cl1']
|
||||
|
||||
# 5. Grok
|
||||
grok = get_adapter('grok')
|
||||
ctx_g = DiscoveryContext(workspace=ws, agent_name='grok', home_dir=home)
|
||||
g_sess = f"{home}/.grok/sessions/{grok._ws_dir(ctx_g)}/u-g1"
|
||||
os.makedirs(g_sess, exist_ok=True)
|
||||
with open(f"{g_sess}/chat_history.jsonl", 'w') as f:
|
||||
f.write('{"session_id": "u-g1", "cwd": "' + ws + '"}\n')
|
||||
assert grok.discover(ctx_g) == ['u-g1']
|
||||
|
||||
def test_cli_bridge_subcommands_and_quote_safety():
|
||||
import subprocess, sys
|
||||
from pathlib import Path
|
||||
@@ -286,7 +333,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'), ('cline', '/exit')]:
|
||||
for agent, expected_key in [('claude', '/exit'), ('agy', 'Exit'), ('hermes', '/exit'), ('cline', '/exit'), ('grok', '/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
|
||||
@@ -298,6 +345,7 @@ def test_delegate_agent_resolution_and_fallback():
|
||||
'agy': 'antigravity-cli',
|
||||
'hermes': 'hermes-agent',
|
||||
'cline': 'cline-agent',
|
||||
'grok': 'grok-build',
|
||||
}
|
||||
# 1. Adapter property
|
||||
for agent, expected_key in expected_map.items():
|
||||
@@ -316,6 +364,7 @@ def test_delegate_agent_resolution_and_fallback():
|
||||
hermes) delegate_agent="hermes-agent" ;;
|
||||
cline) delegate_agent="cline-agent" ;;
|
||||
agy) delegate_agent="antigravity-cli" ;;
|
||||
grok) delegate_agent="grok-build" ;;
|
||||
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -18,19 +18,19 @@ def test_bug2_headless_layout_does_not_overflow():
|
||||
|
||||
# 2. Genuine small pane (overflow)
|
||||
payload_small = {"result": {"panes": [{"pane_id": "p1", "rect": {"width": 50, "height": 30}}]}}
|
||||
d_small = compute_2xk_layout(payload_small)
|
||||
d_small = compute_2xk_layout(payload_small, min_cols=60, min_rows=20)
|
||||
assert d_small.is_overflow, f"Small pane should be overflow, got {d_small}"
|
||||
assert d_small.direction == "overflow"
|
||||
|
||||
# 3. Wide pane (split right)
|
||||
payload_wide = {"result": {"panes": [{"pane_id": "p1", "rect": {"width": 160, "height": 30}}]}}
|
||||
d_wide = compute_2xk_layout(payload_wide)
|
||||
d_wide = compute_2xk_layout(payload_wide, min_cols=60, min_rows=20)
|
||||
assert not d_wide.is_overflow
|
||||
assert d_wide.direction == "right"
|
||||
|
||||
# 4. Tall pane (split down)
|
||||
payload_tall = {"result": {"panes": [{"pane_id": "p1", "rect": {"width": 80, "height": 60}}]}}
|
||||
d_tall = compute_2xk_layout(payload_tall)
|
||||
d_tall = compute_2xk_layout(payload_tall, min_cols=60, min_rows=20)
|
||||
assert not d_tall.is_overflow
|
||||
assert d_tall.direction == "down"
|
||||
|
||||
|
||||
@@ -946,6 +946,48 @@ def test_layout_single_workspace_54x23_compact_tiling_tier1():
|
||||
assert d4.reason == "column_width_overflow"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# FEATURE: Grok Build TUI Agent Integration
|
||||
# ==============================================================================
|
||||
|
||||
def test_grok_adapter_contract_in_tier1():
|
||||
"""Verify GrokAgentAdapter registration and properties in Tier 1 suite."""
|
||||
from lib_py.agents.registry import get_adapter
|
||||
adapter = get_adapter('grok')
|
||||
assert adapter is not None
|
||||
assert adapter.name == 'grok'
|
||||
assert adapter.own_key == 'grok_session_id_own'
|
||||
assert adapter.delegate_agent_key == 'grok-build'
|
||||
assert adapter.exit_key == '/exit'
|
||||
assert adapter.ready_tokens == 'Grok|xAI|Assistant|❯|>>>'
|
||||
|
||||
|
||||
def test_grok_shell_and_scripts_integration(mam_sandbox):
|
||||
"""Verify lib.sh, create_session.sh, stop_session.sh, and workspace_uuid contain grok."""
|
||||
# 1. lib.sh kind mapping & binaries
|
||||
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', 'cline', 'grok'" 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|cline|grok)' 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|cline|grok)' in stop_content
|
||||
assert "target['grok_session_id_own'] = captured" in stop_content
|
||||
|
||||
# 4. workspace_uuid OWN_KEY
|
||||
from lib_py.workspace_uuid import OWN_KEY
|
||||
assert OWN_KEY.get('grok') == 'grok_session_id_own'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user