Implemented the next parity slice.
New runtime/code: - src/ask_user_runtime.py - src/team_runtime.py New real tools in src/agent_tools.py: - ask_user_question - team_create - team_delete - team_list - team_get - send_message - team_messages - notebook_edit
This commit is contained in:
@@ -224,6 +224,41 @@ class AgentSlashCommandTests(unittest.TestCase):
|
||||
self.assertIn('profile=staging', ssh_result.final_output)
|
||||
self.assertIn('connected=False', disconnect_result.final_output)
|
||||
|
||||
def test_workflow_and_trigger_commands_render_local_reports(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
(workspace / '.claw-workflows.json').write_text(
|
||||
(
|
||||
'{"workflows":['
|
||||
'{"name":"review","description":"Review changes.","steps":["Inspect diff","Summarize findings"]}'
|
||||
']}'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
(workspace / '.claw-triggers.json').write_text(
|
||||
(
|
||||
'{"triggers":['
|
||||
'{"trigger_id":"nightly","name":"Nightly","workflow":"review","schedule":"0 0 * * *"}'
|
||||
']}'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
agent = LocalCodingAgent(
|
||||
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
|
||||
runtime_config=AgentRuntimeConfig(cwd=workspace),
|
||||
)
|
||||
workflows_result = agent.run('/workflows')
|
||||
workflow_result = agent.run('/workflow review')
|
||||
trigger_result = agent.run('/trigger nightly')
|
||||
trigger_run_result = agent.run('/trigger run nightly')
|
||||
self.assertIn('# Workflows', workflows_result.final_output)
|
||||
self.assertIn('review', workflows_result.final_output)
|
||||
self.assertIn('# Workflow', workflow_result.final_output)
|
||||
self.assertIn('Review changes', workflow_result.final_output)
|
||||
self.assertIn('# Remote Trigger', trigger_result.final_output)
|
||||
self.assertIn('trigger_id=nightly', trigger_result.final_output)
|
||||
self.assertIn('# Remote Trigger Run', trigger_run_result.final_output)
|
||||
|
||||
def test_account_commands_render_and_update_local_account_runtime(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
|
||||
@@ -154,6 +154,28 @@ class MainCliTests(unittest.TestCase):
|
||||
self.assertEqual(args.provider, 'local-search')
|
||||
self.assertEqual(args.cwd, '.')
|
||||
|
||||
def test_parser_accepts_worktree_runtime_commands(self) -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(['worktree-exit', '--action', 'remove', '--discard-changes', '--cwd', '.'])
|
||||
self.assertEqual(args.command, 'worktree-exit')
|
||||
self.assertEqual(args.action, 'remove')
|
||||
self.assertTrue(args.discard_changes)
|
||||
self.assertEqual(args.cwd, '.')
|
||||
|
||||
def test_parser_accepts_workflow_runtime_commands(self) -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(['workflow-run', 'review', '--arguments-json', '{"path":"src"}', '--cwd', '.'])
|
||||
self.assertEqual(args.command, 'workflow-run')
|
||||
self.assertEqual(args.workflow_name, 'review')
|
||||
self.assertEqual(args.cwd, '.')
|
||||
|
||||
def test_parser_accepts_remote_trigger_runtime_commands(self) -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(['trigger-run', 'nightly', '--body-json', '{"depth":"quick"}', '--cwd', '.'])
|
||||
self.assertEqual(args.command, 'trigger-run')
|
||||
self.assertEqual(args.trigger_id, 'nightly')
|
||||
self.assertEqual(args.cwd, '.')
|
||||
|
||||
def test_parser_accepts_mcp_runtime_commands(self) -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(['mcp-tools', '--cwd', '.', '--server', 'remote'])
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from src.agent_tools import build_tool_context, default_tool_registry, execute_tool
|
||||
from src.agent_types import AgentPermissions, AgentRuntimeConfig
|
||||
from src.remote_trigger_runtime import RemoteTriggerRuntime
|
||||
|
||||
|
||||
class RemoteTriggerRuntimeTests(unittest.TestCase):
|
||||
def test_remote_trigger_runtime_discovers_and_runs_trigger(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
(workspace / '.claw-triggers.json').write_text(
|
||||
(
|
||||
'{"triggers":['
|
||||
'{"trigger_id":"nightly","name":"Nightly","workflow":"review",'
|
||||
'"schedule":"0 0 * * *","body":{"depth":"full"}}'
|
||||
']}'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
runtime = RemoteTriggerRuntime.from_workspace(workspace)
|
||||
trigger_report = runtime.render_trigger('nightly')
|
||||
run_report = runtime.render_run_report('nightly', body={'depth': 'quick'})
|
||||
|
||||
self.assertIn('trigger_id=nightly', trigger_report)
|
||||
self.assertIn('workflow=review', run_report)
|
||||
self.assertIn('"depth": "quick"', run_report)
|
||||
|
||||
def test_remote_trigger_tool_supports_create_update_run(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
runtime = RemoteTriggerRuntime.from_workspace(workspace)
|
||||
context = build_tool_context(
|
||||
AgentRuntimeConfig(
|
||||
cwd=workspace,
|
||||
permissions=AgentPermissions(allow_file_write=True),
|
||||
),
|
||||
remote_trigger_runtime=runtime,
|
||||
)
|
||||
create_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'remote_trigger',
|
||||
{
|
||||
'action': 'create',
|
||||
'body': {
|
||||
'trigger_id': 'nightly',
|
||||
'name': 'Nightly',
|
||||
'workflow': 'review',
|
||||
'body': {'depth': 'full'},
|
||||
},
|
||||
},
|
||||
context,
|
||||
)
|
||||
update_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'remote_trigger',
|
||||
{
|
||||
'action': 'update',
|
||||
'trigger_id': 'nightly',
|
||||
'body': {'schedule': '0 0 * * *'},
|
||||
},
|
||||
context,
|
||||
)
|
||||
run_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'remote_trigger',
|
||||
{
|
||||
'action': 'run',
|
||||
'trigger_id': 'nightly',
|
||||
'body': {'depth': 'quick'},
|
||||
},
|
||||
context,
|
||||
)
|
||||
|
||||
self.assertTrue(create_result.ok)
|
||||
self.assertEqual(create_result.metadata.get('trigger_id'), 'nightly')
|
||||
self.assertTrue(update_result.ok)
|
||||
self.assertEqual(update_result.metadata.get('remote_trigger_action'), 'update')
|
||||
self.assertTrue(run_result.ok)
|
||||
self.assertIn('# Remote Trigger Run', run_result.content)
|
||||
@@ -0,0 +1,77 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from src.agent_tools import build_tool_context, default_tool_registry, execute_tool
|
||||
from src.agent_types import AgentRuntimeConfig
|
||||
from src.workflow_runtime import WorkflowRuntime
|
||||
|
||||
|
||||
class WorkflowRuntimeTests(unittest.TestCase):
|
||||
def test_workflow_runtime_discovers_and_runs_workflow(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
(workspace / '.claw-workflows.json').write_text(
|
||||
(
|
||||
'{"workflows":['
|
||||
'{"name":"review","description":"Review the current patch.",'
|
||||
'"steps":[{"title":"Inspect diff","detail":"Read {path}"},{"title":"Summarize"}],'
|
||||
'"prompt":"Review changes under {path}"}'
|
||||
']}'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
runtime = WorkflowRuntime.from_workspace(workspace)
|
||||
rendered = runtime.render_workflow('review')
|
||||
run_report = runtime.render_run_report('review', arguments={'path': 'src/'})
|
||||
|
||||
self.assertIn('Review the current patch', rendered)
|
||||
self.assertIn('Read src/', run_report)
|
||||
self.assertIn('Review changes under src/', run_report)
|
||||
|
||||
def test_workflow_tools_execute_against_runtime(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
(workspace / '.claw-workflows.json').write_text(
|
||||
(
|
||||
'{"workflows":['
|
||||
'{"name":"build","description":"Build the project.",'
|
||||
'"steps":["Inspect package","Run build"]}'
|
||||
']}'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
runtime = WorkflowRuntime.from_workspace(workspace)
|
||||
context = build_tool_context(
|
||||
AgentRuntimeConfig(cwd=workspace),
|
||||
workflow_runtime=runtime,
|
||||
)
|
||||
list_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'workflow_list',
|
||||
{},
|
||||
context,
|
||||
)
|
||||
get_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'workflow_get',
|
||||
{'workflow_name': 'build'},
|
||||
context,
|
||||
)
|
||||
run_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'workflow_run',
|
||||
{'workflow_name': 'build', 'arguments': {'target': 'dist'}},
|
||||
context,
|
||||
)
|
||||
|
||||
self.assertTrue(list_result.ok)
|
||||
self.assertIn('build', list_result.content)
|
||||
self.assertTrue(get_result.ok)
|
||||
self.assertIn('Build the project', get_result.content)
|
||||
self.assertTrue(run_result.ok)
|
||||
self.assertEqual(run_result.metadata.get('action'), 'workflow_run')
|
||||
self.assertIn('# Workflow Run', run_result.content)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.agent_runtime import LocalCodingAgent
|
||||
from src.agent_tools import build_tool_context, default_tool_registry, execute_tool
|
||||
from src.agent_types import AgentPermissions, AgentRuntimeConfig, ModelConfig
|
||||
from src.worktree_runtime import WorktreeRuntime
|
||||
|
||||
|
||||
class _FakeHTTPResponse:
|
||||
def __init__(self, payload: dict[str, object]) -> None:
|
||||
self.payload = payload
|
||||
|
||||
def read(self) -> bytes:
|
||||
return json.dumps(self.payload).encode('utf-8')
|
||||
|
||||
def __enter__(self) -> '_FakeHTTPResponse':
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def _make_urlopen_side_effect(responses: list[dict[str, object]]):
|
||||
queued = [_FakeHTTPResponse(payload) for payload in responses]
|
||||
|
||||
def _fake_urlopen(request_obj, timeout=None): # noqa: ANN001
|
||||
return queued.pop(0)
|
||||
|
||||
return _fake_urlopen
|
||||
|
||||
|
||||
def _init_git_repo(workspace: Path) -> None:
|
||||
subprocess.run(['git', 'init', '-q'], cwd=workspace, check=True)
|
||||
subprocess.run(['git', 'config', 'user.email', 'test@example.com'], cwd=workspace, check=True)
|
||||
subprocess.run(['git', 'config', 'user.name', 'Test User'], cwd=workspace, check=True)
|
||||
(workspace / 'README.md').write_text('hello\n', encoding='utf-8')
|
||||
subprocess.run(['git', 'add', 'README.md'], cwd=workspace, check=True)
|
||||
subprocess.run(['git', 'commit', '-qm', 'init'], cwd=workspace, check=True)
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which('git'), 'git is required for worktree tests')
|
||||
class WorktreeRuntimeTests(unittest.TestCase):
|
||||
def test_worktree_runtime_enters_and_exits_managed_session(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
_init_git_repo(workspace)
|
||||
runtime = WorktreeRuntime.from_workspace(workspace)
|
||||
enter_report = runtime.enter('feature-preview')
|
||||
worktree_path = Path(enter_report.worktree_path or '')
|
||||
exit_report = runtime.exit(action='keep')
|
||||
|
||||
self.assertTrue(enter_report.active)
|
||||
self.assertTrue(worktree_path.exists())
|
||||
self.assertIn('feature-preview', enter_report.worktree_branch or '')
|
||||
self.assertFalse(exit_report.active)
|
||||
self.assertEqual(exit_report.original_cwd, str(workspace))
|
||||
|
||||
def test_worktree_tools_execute_against_runtime(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
_init_git_repo(workspace)
|
||||
runtime = WorktreeRuntime.from_workspace(workspace)
|
||||
context = build_tool_context(
|
||||
AgentRuntimeConfig(
|
||||
cwd=workspace,
|
||||
permissions=AgentPermissions(allow_file_write=True),
|
||||
),
|
||||
worktree_runtime=runtime,
|
||||
)
|
||||
enter_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'worktree_enter',
|
||||
{'name': 'preview'},
|
||||
context,
|
||||
)
|
||||
status_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'worktree_status',
|
||||
{},
|
||||
context,
|
||||
)
|
||||
exit_result = execute_tool(
|
||||
default_tool_registry(),
|
||||
'worktree_exit',
|
||||
{'action': 'remove', 'discard_changes': True},
|
||||
context,
|
||||
)
|
||||
|
||||
self.assertTrue(enter_result.ok)
|
||||
self.assertIn('preview', enter_result.content)
|
||||
self.assertEqual(enter_result.metadata.get('action'), 'worktree_enter')
|
||||
self.assertTrue(status_result.ok)
|
||||
self.assertIn('Active managed worktree: True', status_result.content)
|
||||
self.assertTrue(exit_result.ok)
|
||||
self.assertEqual(exit_result.metadata.get('action'), 'worktree_exit')
|
||||
|
||||
def test_agent_switches_cwd_after_worktree_enter(self) -> None:
|
||||
responses = [
|
||||
{
|
||||
'choices': [
|
||||
{
|
||||
'message': {
|
||||
'role': 'assistant',
|
||||
'content': 'Entering worktree.',
|
||||
'tool_calls': [
|
||||
{
|
||||
'id': 'call_enter',
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'worktree_enter',
|
||||
'arguments': '{"name":"preview"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
'finish_reason': 'tool_calls',
|
||||
}
|
||||
],
|
||||
'usage': {'prompt_tokens': 8, 'completion_tokens': 2},
|
||||
},
|
||||
{
|
||||
'choices': [
|
||||
{
|
||||
'message': {
|
||||
'role': 'assistant',
|
||||
'content': 'Writing inside the worktree.',
|
||||
'tool_calls': [
|
||||
{
|
||||
'id': 'call_write',
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'write_file',
|
||||
'arguments': '{"path":"note.txt","content":"from worktree\\n"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
'finish_reason': 'tool_calls',
|
||||
}
|
||||
],
|
||||
'usage': {'prompt_tokens': 8, 'completion_tokens': 2},
|
||||
},
|
||||
{
|
||||
'choices': [
|
||||
{
|
||||
'message': {
|
||||
'role': 'assistant',
|
||||
'content': 'done',
|
||||
},
|
||||
'finish_reason': 'stop',
|
||||
}
|
||||
],
|
||||
'usage': {'prompt_tokens': 6, 'completion_tokens': 1},
|
||||
},
|
||||
]
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
_init_git_repo(workspace)
|
||||
with patch(
|
||||
'src.openai_compat.request.urlopen',
|
||||
side_effect=_make_urlopen_side_effect(responses),
|
||||
):
|
||||
agent = LocalCodingAgent(
|
||||
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
|
||||
runtime_config=AgentRuntimeConfig(
|
||||
cwd=workspace,
|
||||
permissions=AgentPermissions(allow_file_write=True),
|
||||
),
|
||||
)
|
||||
result = agent.run('Use a worktree and write a file there')
|
||||
runtime = WorktreeRuntime.from_workspace(workspace)
|
||||
assert runtime.active_session is not None
|
||||
worktree_path = Path(runtime.active_session.worktree_path)
|
||||
|
||||
self.assertEqual(result.final_output, 'done')
|
||||
self.assertFalse((workspace / 'note.txt').exists())
|
||||
self.assertTrue((worktree_path / 'note.txt').exists())
|
||||
self.assertEqual(agent.runtime_config.cwd, worktree_path.resolve())
|
||||
|
||||
Reference in New Issue
Block a user