c17c2768eb
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
188 lines
7.3 KiB
Python
188 lines
7.3 KiB
Python
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())
|
|
|