feat(agent): add Grok Build TUI adapter, runtime dispatch, and skill support

This commit is contained in:
2026-08-26 10:49:34 +09:00
parent 926ca5452b
commit ad8201d706
27 changed files with 414 additions and 40 deletions
+53 -4
View File
@@ -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