feat: rebuild as multi-user web agent

This commit is contained in:
wuyang
2026-07-26 06:52:12 +08:00
parent 45792c8fd5
commit 37f83eaac7
634 changed files with 5060 additions and 139619 deletions
+52
View File
@@ -0,0 +1,52 @@
from __future__ import annotations
from pathlib import Path
import jwt
import pytest
from agent_platform.config import Settings
@pytest.fixture
def settings(tmp_path: Path) -> Settings:
return Settings(
model_api_base_url="https://model.example.test",
model_api_key="model-secret",
openwebui_forward_jwt_secret="identity-secret-with-at-least-32-bytes", # noqa: S106
internal_provider_key="provider-secret-with-at-least-32-bytes",
internal_gateway_key="gateway-secret-with-at-least-32-bytes",
database_url=f"sqlite+aiosqlite:///{tmp_path / 'agent.sqlite3'}",
redis_url="redis://unused",
gateway_url="http://gateway.test",
execution_provider="local-docker",
workspace_image="k1412-agent-workspace:test",
workspace_network_enabled=False,
workspace_memory_limit="512m",
workspace_cpu_limit=1.0,
workspace_pids_limit=128,
workspace_ssh_host="",
model_timeout_seconds=30,
tool_timeout_seconds=30,
max_tool_output_chars=24_000,
)
@pytest.fixture
def identity_jwt(settings: Settings) -> str:
import time
now = int(time.time())
return jwt.encode(
{
"sub": "user-123",
"email": "user@example.test",
"name": "Test User",
"role": "user",
"iss": "open-webui",
"iat": now,
"exp": now + 300,
},
settings.openwebui_forward_jwt_secret,
algorithm="HS256",
)
-88
View File
@@ -1,88 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from src.account_runtime import AccountRuntime
from src.agent_tools import build_tool_context, default_tool_registry, execute_tool
from src.agent_types import AgentRuntimeConfig
class AccountRuntimeTests(unittest.TestCase):
def test_account_runtime_discovers_profiles_and_persists_login(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-account.json').write_text(
(
'{"profiles":['
'{"name":"local","provider":"openai","identity":"dev@example.com","authMode":"api_key"},'
'{"name":"team","provider":"anthropic","identity":"team@example.com","org":"Harness"}'
']}'
),
encoding='utf-8',
)
with patch.dict('os.environ', {'OPENAI_API_KEY': 'local-token'}, clear=False):
runtime = AccountRuntime.from_workspace(workspace)
report = runtime.login('local')
restored = AccountRuntime.from_workspace(workspace)
self.assertEqual(len(runtime.profiles), 2)
self.assertTrue(report.logged_in)
self.assertEqual(report.profile_name, 'local')
self.assertIsNotNone(restored.active_session)
self.assertEqual(restored.active_session.profile_name, 'local')
self.assertIn('Credential env vars: OPENAI_API_KEY', restored.render_summary())
def test_account_runtime_logout_clears_active_session(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-account.json').write_text(
'{"profiles":[{"name":"local","provider":"openai","identity":"dev@example.com"}]}',
encoding='utf-8',
)
runtime = AccountRuntime.from_workspace(workspace)
runtime.login('local')
report = runtime.logout()
self.assertFalse(report.logged_in)
self.assertIn('Logged out dev@example.com', report.detail)
def test_account_tools_execute_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-account.json').write_text(
'{"profiles":[{"name":"local","provider":"openai","identity":"dev@example.com"}]}',
encoding='utf-8',
)
runtime = AccountRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
account_runtime=runtime,
)
list_result = execute_tool(
default_tool_registry(),
'account_list_profiles',
{},
context,
)
login_result = execute_tool(
default_tool_registry(),
'account_login',
{'target': 'local'},
context,
)
status_result = execute_tool(
default_tool_registry(),
'account_status',
{},
context,
)
self.assertTrue(list_result.ok)
self.assertIn('dev@example.com', list_result.content)
self.assertTrue(login_result.ok)
self.assertIn('profile=local', login_result.content)
self.assertTrue(status_result.ok)
self.assertIn('Configured account profiles: 1', status_result.content)
-284
View File
@@ -1,284 +0,0 @@
from __future__ import annotations
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from src.agent_context import (
build_context_snapshot,
clear_context_caches,
set_system_prompt_injection,
)
from src.ask_user_runtime import AskUserRuntime
from src.plan_runtime import PlanRuntime
from src.agent_types import AgentRuntimeConfig
from src.task_runtime import TaskRuntime
from src.team_runtime import TeamRuntime
class AgentContextTests(unittest.TestCase):
def tearDown(self) -> None:
set_system_prompt_injection(None)
clear_context_caches()
def test_user_context_loads_project_claude_md_files(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo' / 'nested'
workspace.mkdir(parents=True)
(workspace.parent / 'CLAUDE.md').write_text('root instructions\n', encoding='utf-8')
(workspace / 'CLAUDE.local.md').write_text('local instructions\n', encoding='utf-8')
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('currentDate', snapshot.user_context)
self.assertIn('claudeMd', snapshot.user_context)
self.assertIn('root instructions', snapshot.user_context['claudeMd'])
self.assertIn('local instructions', snapshot.user_context['claudeMd'])
def test_system_context_includes_cache_breaker(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
set_system_prompt_injection('debug-token')
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=Path(tmp_dir)))
self.assertEqual(snapshot.system_context['cacheBreaker'], '[CACHE_BREAKER: debug-token]')
def test_user_context_loads_plugin_cache_snapshot(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
plugin_cache = workspace / '.port_sessions' / 'plugin_cache.json'
plugin_cache.parent.mkdir(parents=True, exist_ok=True)
plugin_cache.write_text(
'{"plugins":[{"name":"demo-plugin","version":"1.2.3","enabled":true}]}',
encoding='utf-8',
)
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('pluginCache', snapshot.user_context)
self.assertIn('demo-plugin', snapshot.user_context['pluginCache'])
self.assertIn('1.2.3', snapshot.user_context['pluginCache'])
def test_user_context_loads_hook_policy_manifest(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
(workspace / '.claw-policy.json').write_text(
(
'{"trusted": false, '
'"managedSettings": {"reviewMode": "strict"}, '
'"safeEnv": ["HOOK_SAFE_TOKEN"], '
'"hooks": {"beforePrompt": ["Respect workspace policy."]}}'
),
encoding='utf-8',
)
with patch.dict('os.environ', {'HOOK_SAFE_TOKEN': 'demo-secret'}, clear=False):
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('hookPolicy', snapshot.user_context)
self.assertIn('managedSettings', snapshot.user_context)
self.assertIn('safeEnv', snapshot.user_context)
self.assertIn('trustMode', snapshot.user_context)
self.assertIn('reviewMode=strict', snapshot.user_context['managedSettings'])
self.assertIn('HOOK_SAFE_TOKEN=demo-secret', snapshot.user_context['safeEnv'])
self.assertIn('untrusted', snapshot.user_context['trustMode'])
def test_user_context_loads_mcp_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
(workspace / 'notes.txt').write_text('mcp notes\n', encoding='utf-8')
(workspace / '.claw-mcp.json').write_text(
(
'{"servers":[{"name":"workspace","resources":['
'{"uri":"mcp://workspace/notes","name":"Notes","path":"notes.txt"}'
']}]}'
),
encoding='utf-8',
)
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('mcpRuntime', snapshot.user_context)
self.assertIn('Local MCP resources: 1', snapshot.user_context['mcpRuntime'])
def test_user_context_loads_search_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
(workspace / '.claw-search.json').write_text(
'{"providers":[{"name":"local-search","provider":"searxng","baseUrl":"http://127.0.0.1:8080"}]}',
encoding='utf-8',
)
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('searchRuntime', snapshot.user_context)
self.assertIn('Configured search providers: 1', snapshot.user_context['searchRuntime'])
self.assertIn('local-search', snapshot.user_context['searchRuntime'])
def test_user_context_loads_remote_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
(workspace / '.claw-remote.json').write_text(
(
'{"profiles":[{"name":"staging","mode":"ssh","target":"dev@staging",'
'"workspaceCwd":"/srv/app","sessionUrl":"wss://remote/session"}]}'
),
encoding='utf-8',
)
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('remoteRuntime', snapshot.user_context)
self.assertIn('Configured remote profiles: 1', snapshot.user_context['remoteRuntime'])
self.assertIn('staging', snapshot.user_context['remoteRuntime'])
def test_user_context_loads_account_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
(workspace / '.claw-account.json').write_text(
'{"profiles":[{"name":"local","provider":"openai","identity":"dev@example.com"}]}',
encoding='utf-8',
)
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('accountRuntime', snapshot.user_context)
self.assertIn('Configured account profiles: 1', snapshot.user_context['accountRuntime'])
self.assertIn('dev@example.com', snapshot.user_context['accountRuntime'])
def test_user_context_loads_ask_user_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
(workspace / '.claw-ask-user.json').write_text(
'{"answers":[{"question":"Approve deploy?","answer":"yes"}]}',
encoding='utf-8',
)
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('askUserRuntime', snapshot.user_context)
self.assertIn('Queued answers: 1', snapshot.user_context['askUserRuntime'])
def test_user_context_loads_config_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
claude_dir = workspace / '.claude'
claude_dir.mkdir()
(claude_dir / 'settings.json').write_text(
'{"review":{"mode":"strict"}}',
encoding='utf-8',
)
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('configRuntime', snapshot.user_context)
self.assertIn('Config sources: 1', snapshot.user_context['configRuntime'])
self.assertIn('Effective keys: 2', snapshot.user_context['configRuntime'])
def test_user_context_loads_lsp_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
(workspace / 'sample.py').write_text('def helper(value):\n return value * 2\n', encoding='utf-8')
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('lspRuntime', snapshot.user_context)
self.assertIn('Indexed candidate files: 1', snapshot.user_context['lspRuntime'])
def test_user_context_loads_task_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
scratchpad = workspace / '.port_sessions' / 'accounts' / 'alice' / 'sessions' / 's1' / 'scratchpad'
scratchpad.mkdir(parents=True)
runtime = TaskRuntime.from_storage_path(scratchpad / 'task_runtime.json')
runtime.create_task(title='Review task runtime')
snapshot = build_context_snapshot(
AgentRuntimeConfig(cwd=workspace),
scratchpad_directory=scratchpad,
)
self.assertIn('taskRuntime', snapshot.user_context)
self.assertIn('Total tasks: 1', snapshot.user_context['taskRuntime'])
def test_user_context_loads_plan_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
scratchpad = workspace / '.port_sessions' / 'accounts' / 'alice' / 'sessions' / 's1' / 'scratchpad'
scratchpad.mkdir(parents=True)
plan_runtime = PlanRuntime.from_storage_path(scratchpad / 'plan_runtime.json')
plan_runtime.update_plan(
[{'step': 'Inspect the runtime', 'status': 'in_progress'}],
explanation='Use a stored plan.',
)
snapshot = build_context_snapshot(
AgentRuntimeConfig(cwd=workspace),
scratchpad_directory=scratchpad,
)
self.assertIn('planRuntime', snapshot.user_context)
self.assertIn('Total plan steps: 1', snapshot.user_context['planRuntime'])
def test_user_context_ignores_workspace_plan_task_when_session_scratchpad_is_set(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
TaskRuntime.from_workspace(workspace).create_task(title='Global task')
PlanRuntime.from_workspace(workspace).update_plan(
[{'step': 'Global plan', 'status': 'in_progress'}],
explanation='This must not leak into a session.',
)
scratchpad = workspace / '.port_sessions' / 'accounts' / 'alice' / 'sessions' / 's1' / 'scratchpad'
scratchpad.mkdir(parents=True)
snapshot = build_context_snapshot(
AgentRuntimeConfig(cwd=workspace),
scratchpad_directory=scratchpad,
)
self.assertNotIn('taskRuntime', snapshot.user_context)
self.assertNotIn('planRuntime', snapshot.user_context)
def test_user_context_loads_team_runtime_summary(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir) / 'repo'
workspace.mkdir(parents=True)
runtime = TeamRuntime.from_workspace(workspace)
runtime.create_team('reviewers', members=['alice', 'bob'])
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
self.assertIn('teamRuntime', snapshot.user_context)
self.assertIn('Configured teams: 1', snapshot.user_context['teamRuntime'])
@unittest.skipIf(shutil.which('git') is None, 'git is required for git context tests')
def test_git_status_snapshot_contains_branch_and_status(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
subprocess.run(['git', 'init', '-b', 'main'], cwd=workspace, check=True)
subprocess.run(['git', 'config', 'user.name', 'Tester'], cwd=workspace, check=True)
subprocess.run(['git', 'config', 'user.email', 'tester@example.com'], cwd=workspace, check=True)
(workspace / 'tracked.txt').write_text('hello\n', encoding='utf-8')
subprocess.run(['git', 'add', 'tracked.txt'], cwd=workspace, check=True)
subprocess.run(['git', 'commit', '-m', 'initial'], cwd=workspace, check=True)
(workspace / 'tracked.txt').write_text('changed\n', encoding='utf-8')
snapshot = build_context_snapshot(AgentRuntimeConfig(cwd=workspace))
git_status = snapshot.system_context.get('gitStatus', '')
self.assertIn('Current branch: main', git_status)
self.assertIn('Status:', git_status)
self.assertIn('tracked.txt', git_status)
-41
View File
@@ -1,41 +0,0 @@
from __future__ import annotations
import unittest
from src.agent_context_usage import collect_context_usage, format_context_usage
from src.agent_session import AgentSessionState
class AgentContextUsageTests(unittest.TestCase):
def test_collect_context_usage_formats_breakdown(self) -> None:
session = AgentSessionState.create(
['# Intro\nhello', '# System\nworld'],
'inspect repo',
user_context={'currentDate': "Today's date is 2026-04-01."},
system_context={'gitStatus': 'Current branch: main'},
)
session.append_assistant(
'Reading files',
(
{
'id': 'call_1',
'type': 'function',
'function': {'name': 'read_file', 'arguments': '{"path":"a.py"}'},
},
),
)
session.append_tool('read_file', 'call_1', '{"ok": true, "content": "print(1)"}')
report = collect_context_usage(
session=session,
model='test-model',
strategy='test session',
)
rendered = format_context_usage(report)
self.assertGreater(report.total_tokens, 0)
self.assertIn('## Context Usage', rendered)
self.assertIn('**Token counter:**', rendered)
self.assertIn('### System Prompt Sections', rendered)
self.assertIn('### Message Breakdown', rendered)
self.assertIn('#### Top Tools', rendered)
+142
View File
@@ -0,0 +1,142 @@
from __future__ import annotations
import asyncio
import json
from typing import Any
from agent_platform.auth import UserIdentity
from agent_platform.models import get_model_spec
from agent_platform.runtime.loop import AgentLoop
from agent_platform.runtime.tools import TOOL_METADATA
from agent_platform.store import RuntimeStore
def tool_call(call_id: str, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
return {
"id": call_id,
"type": "function",
"function": {"name": name, "arguments": json.dumps(arguments)},
}
class ScriptedProvider:
def __init__(self, responses: list[dict[str, Any]]) -> None:
self.responses = responses
self.requests = []
async def complete(self, **kwargs):
self.requests.append(kwargs)
return self.responses.pop(0)
class ParallelRegistry:
def __init__(self) -> None:
self.started = 0
self.both_started = asyncio.Event()
def specs(self, *, read_only=False, allow_delegate=True):
return [TOOL_METADATA["read_file"].openai_spec()]
async def execute(self, name, arguments, context):
self.started += 1
if self.started == 2:
self.both_started.set()
await asyncio.wait_for(self.both_started.wait(), timeout=1)
return {"ok": True, "path": arguments["path"]}
async def test_read_only_tool_calls_run_in_parallel(settings) -> None:
first = {
"choices": [
{
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
tool_call("1", "read_file", {"path": "a"}),
tool_call("2", "read_file", {"path": "b"}),
],
},
"finish_reason": "tool_calls",
}
]
}
final = {"choices": [{"message": {"role": "assistant", "content": "done"}, "finish_reason": "stop"}]}
store = RuntimeStore(settings.database_url)
await store.initialize()
registry = ParallelRegistry()
loop = AgentLoop(ScriptedProvider([first, final]), registry, store, max_tool_output_chars=10_000)
events = []
async def callback(event_type, payload):
events.append(event_type)
try:
answer = await loop.run(
spec=get_model_spec("work-light"),
messages=[{"role": "user", "content": "inspect both"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
chat_id="c1",
callback=callback,
)
assert answer == "done"
assert registry.started == 2
assert events.count("tool.completed") == 2
finally:
await store.close()
class SerialRegistry:
def __init__(self) -> None:
self.active = 0
self.max_active = 0
def specs(self, *, read_only=False, allow_delegate=True):
return [TOOL_METADATA["write_file"].openai_spec()]
async def execute(self, name, arguments, context):
self.active += 1
self.max_active = max(self.max_active, self.active)
await asyncio.sleep(0.02)
self.active -= 1
return {"ok": True}
async def test_mutating_tool_calls_are_serialized(settings) -> None:
first = {
"choices": [
{
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
tool_call("1", "write_file", {"path": "a", "content": "a"}),
tool_call("2", "write_file", {"path": "b", "content": "b"}),
],
},
"finish_reason": "tool_calls",
}
]
}
final = {"choices": [{"message": {"role": "assistant", "content": "done"}, "finish_reason": "stop"}]}
store = RuntimeStore(settings.database_url)
await store.initialize()
registry = SerialRegistry()
loop = AgentLoop(ScriptedProvider([first, final]), registry, store, max_tool_output_chars=10_000)
async def callback(event_type, payload):
return None
try:
await loop.run(
spec=get_model_spec("work-light"),
messages=[{"role": "user", "content": "write both"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
chat_id="c1",
callback=callback,
)
assert registry.max_active == 1
finally:
await store.close()
-581
View File
@@ -1,581 +0,0 @@
from __future__ import annotations
import unittest
from src.agent_manager import AgentManager, ManagedAgentGroup, ManagedAgentRecord
class TestManagedAgentRecordDefaults(unittest.TestCase):
def test_defaults(self) -> None:
rec = ManagedAgentRecord(agent_id="a1", prompt="do stuff")
self.assertEqual(rec.agent_id, "a1")
self.assertEqual(rec.prompt, "do stuff")
self.assertIsNone(rec.parent_agent_id)
self.assertIsNone(rec.group_id)
self.assertIsNone(rec.child_index)
self.assertIsNone(rec.label)
self.assertIsNone(rec.resumed_from_session_id)
self.assertIsNone(rec.session_id)
self.assertIsNone(rec.session_path)
self.assertEqual(rec.status, "running")
self.assertEqual(rec.turns, 0)
self.assertEqual(rec.tool_calls, 0)
self.assertIsNone(rec.stop_reason)
class TestManagedAgentGroupDefaults(unittest.TestCase):
def test_defaults(self) -> None:
grp = ManagedAgentGroup(group_id="g1")
self.assertEqual(grp.group_id, "g1")
self.assertIsNone(grp.label)
self.assertIsNone(grp.parent_agent_id)
self.assertEqual(grp.child_agent_ids, ())
self.assertEqual(grp.strategy, "serial")
self.assertEqual(grp.status, "running")
self.assertEqual(grp.completed_children, 0)
self.assertEqual(grp.failed_children, 0)
self.assertEqual(grp.batch_count, 0)
self.assertEqual(grp.max_batch_size, 0)
self.assertEqual(grp.dependency_skips, 0)
class TestStartAgent(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_increments_counter_and_returns_unique_ids(self) -> None:
id1 = self.mgr.start_agent(prompt="task1")
id2 = self.mgr.start_agent(prompt="task2")
id3 = self.mgr.start_agent(prompt="task3")
self.assertEqual(id1, "agent_1")
self.assertEqual(id2, "agent_2")
self.assertEqual(id3, "agent_3")
self.assertEqual(len(self.mgr.records), 3)
def test_record_stored_with_correct_fields(self) -> None:
aid = self.mgr.start_agent(prompt="hello", label="my-label")
rec = self.mgr.records[aid]
self.assertEqual(rec.agent_id, aid)
self.assertEqual(rec.prompt, "hello")
self.assertEqual(rec.label, "my-label")
self.assertEqual(rec.status, "running")
def test_with_parent_agent_id_tracks_lineage(self) -> None:
parent = self.mgr.start_agent(prompt="parent")
child = self.mgr.start_agent(prompt="child", parent_agent_id=parent)
rec = self.mgr.records[child]
self.assertEqual(rec.parent_agent_id, parent)
def test_with_group_id_registers_child(self) -> None:
gid = self.mgr.start_group(label="grp")
aid = self.mgr.start_agent(prompt="task", group_id=gid, child_index=0)
grp = self.mgr.groups[gid]
self.assertIn(aid, grp.child_agent_ids)
self.assertEqual(self.mgr.records[aid].group_id, gid)
self.assertEqual(self.mgr.records[aid].child_index, 0)
def test_resumed_agents_tracked(self) -> None:
aid = self.mgr.start_agent(
prompt="resume", resumed_from_session_id="sess-old-123"
)
rec = self.mgr.records[aid]
self.assertEqual(rec.resumed_from_session_id, "sess-old-123")
class TestStartGroup(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_creates_group_with_strategy(self) -> None:
gid = self.mgr.start_group(label="batch", strategy="parallel")
self.assertEqual(gid, "group_1")
grp = self.mgr.groups[gid]
self.assertEqual(grp.label, "batch")
self.assertEqual(grp.strategy, "parallel")
self.assertEqual(grp.status, "running")
def test_increments_group_counter(self) -> None:
g1 = self.mgr.start_group(label="a")
g2 = self.mgr.start_group(label="b")
self.assertEqual(g1, "group_1")
self.assertEqual(g2, "group_2")
def test_parent_agent_id_stored(self) -> None:
aid = self.mgr.start_agent(prompt="parent")
gid = self.mgr.start_group(label="child-group", parent_agent_id=aid)
self.assertEqual(self.mgr.groups[gid].parent_agent_id, aid)
def test_default_strategy_is_serial(self) -> None:
gid = self.mgr.start_group()
self.assertEqual(self.mgr.groups[gid].strategy, "serial")
class TestRegisterGroupChild(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_adds_agent_to_group(self) -> None:
gid = self.mgr.start_group(label="g")
aid = self.mgr.start_agent(prompt="t")
self.mgr.register_group_child(gid, aid, child_index=0)
grp = self.mgr.groups[gid]
self.assertIn(aid, grp.child_agent_ids)
self.assertEqual(self.mgr.records[aid].group_id, gid)
self.assertEqual(self.mgr.records[aid].child_index, 0)
def test_duplicate_does_not_add_twice(self) -> None:
gid = self.mgr.start_group(label="g")
aid = self.mgr.start_agent(prompt="t", group_id=gid, child_index=0)
# Already registered via start_agent; register again
self.mgr.register_group_child(gid, aid, child_index=0)
grp = self.mgr.groups[gid]
self.assertEqual(grp.child_agent_ids.count(aid), 1)
def test_unknown_group_is_noop(self) -> None:
aid = self.mgr.start_agent(prompt="t")
# Should not raise
self.mgr.register_group_child("nonexistent", aid, child_index=0)
def test_unknown_agent_does_not_crash(self) -> None:
gid = self.mgr.start_group(label="g")
# Agent does not exist; group gets the ID but record update is skipped
self.mgr.register_group_child(gid, "fake_agent", child_index=0)
self.assertIn("fake_agent", self.mgr.groups[gid].child_agent_ids)
def test_updates_child_index_on_record(self) -> None:
gid = self.mgr.start_group(label="g")
aid = self.mgr.start_agent(prompt="t")
self.mgr.register_group_child(gid, aid, child_index=5)
self.assertEqual(self.mgr.records[aid].child_index, 5)
self.assertEqual(self.mgr.records[aid].group_id, gid)
class TestFinishAgent(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_marks_completed_with_stats(self) -> None:
aid = self.mgr.start_agent(prompt="work")
self.mgr.finish_agent(
aid,
session_id="sess-1",
session_path="/path/sess",
turns=5,
tool_calls=12,
stop_reason="end_turn",
)
rec = self.mgr.records[aid]
self.assertEqual(rec.status, "completed")
self.assertEqual(rec.session_id, "sess-1")
self.assertEqual(rec.session_path, "/path/sess")
self.assertEqual(rec.turns, 5)
self.assertEqual(rec.tool_calls, 12)
self.assertEqual(rec.stop_reason, "end_turn")
def test_preserves_original_fields(self) -> None:
aid = self.mgr.start_agent(
prompt="p", parent_agent_id="parent_x", label="lbl"
)
self.mgr.finish_agent(
aid,
session_id="s",
session_path="/p",
turns=1,
tool_calls=2,
stop_reason=None,
)
rec = self.mgr.records[aid]
self.assertEqual(rec.prompt, "p")
self.assertEqual(rec.parent_agent_id, "parent_x")
self.assertEqual(rec.label, "lbl")
def test_unknown_agent_is_noop(self) -> None:
# Should not raise
self.mgr.finish_agent(
"unknown_id",
session_id=None,
session_path=None,
turns=0,
tool_calls=0,
stop_reason=None,
)
self.assertEqual(len(self.mgr.records), 0)
class TestFinishGroup(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_updates_group_status_and_stats(self) -> None:
gid = self.mgr.start_group(label="g", strategy="parallel")
self.mgr.finish_group(
gid,
status="completed",
completed_children=3,
failed_children=1,
batch_count=2,
max_batch_size=4,
dependency_skips=0,
)
grp = self.mgr.groups[gid]
self.assertEqual(grp.status, "completed")
self.assertEqual(grp.completed_children, 3)
self.assertEqual(grp.failed_children, 1)
self.assertEqual(grp.batch_count, 2)
self.assertEqual(grp.max_batch_size, 4)
self.assertEqual(grp.dependency_skips, 0)
# Preserved fields
self.assertEqual(grp.label, "g")
self.assertEqual(grp.strategy, "parallel")
def test_unknown_group_is_noop(self) -> None:
self.mgr.finish_group(
"ghost",
status="completed",
completed_children=0,
failed_children=0,
)
self.assertEqual(len(self.mgr.groups), 0)
def test_preserves_child_agent_ids(self) -> None:
gid = self.mgr.start_group(label="g")
aid = self.mgr.start_agent(prompt="t", group_id=gid, child_index=0)
self.mgr.finish_group(
gid, status="completed", completed_children=1, failed_children=0
)
self.assertIn(aid, self.mgr.groups[gid].child_agent_ids)
class TestChildrenOf(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_returns_only_children_of_specified_parent(self) -> None:
p1 = self.mgr.start_agent(prompt="parent1")
p2 = self.mgr.start_agent(prompt="parent2")
c1 = self.mgr.start_agent(prompt="c1", parent_agent_id=p1)
c2 = self.mgr.start_agent(prompt="c2", parent_agent_id=p1)
c3 = self.mgr.start_agent(prompt="c3", parent_agent_id=p2)
children_p1 = self.mgr.children_of(p1)
children_p2 = self.mgr.children_of(p2)
self.assertEqual(len(children_p1), 2)
ids_p1 = {r.agent_id for r in children_p1}
self.assertEqual(ids_p1, {c1, c2})
self.assertEqual(len(children_p2), 1)
self.assertEqual(children_p2[0].agent_id, c3)
def test_returns_empty_for_no_children(self) -> None:
aid = self.mgr.start_agent(prompt="solo")
self.assertEqual(self.mgr.children_of(aid), ())
def test_returns_empty_for_unknown_parent(self) -> None:
self.assertEqual(self.mgr.children_of("nonexistent"), ())
class TestGroupChildren(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_returns_sorted_members(self) -> None:
gid = self.mgr.start_group(label="g")
a2 = self.mgr.start_agent(prompt="b", group_id=gid, child_index=2)
a0 = self.mgr.start_agent(prompt="a", group_id=gid, child_index=0)
a1 = self.mgr.start_agent(prompt="c", group_id=gid, child_index=1)
children = self.mgr.group_children(gid)
self.assertEqual(len(children), 3)
self.assertEqual(children[0].agent_id, a0)
self.assertEqual(children[1].agent_id, a1)
self.assertEqual(children[2].agent_id, a2)
def test_none_child_index_sorted_last(self) -> None:
gid = self.mgr.start_group(label="g")
a_none = self.mgr.start_agent(prompt="x", group_id=gid)
a0 = self.mgr.start_agent(prompt="y", group_id=gid, child_index=0)
children = self.mgr.group_children(gid)
self.assertEqual(children[0].agent_id, a0)
self.assertEqual(children[1].agent_id, a_none)
def test_empty_for_unknown_group(self) -> None:
self.assertEqual(self.mgr.group_children("nope"), ())
class TestGroupSummary(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_aggregates_statistics(self) -> None:
gid = self.mgr.start_group(label="batch", strategy="parallel")
a1 = self.mgr.start_agent(
prompt="t1", group_id=gid, child_index=0,
resumed_from_session_id="old-sess",
)
a2 = self.mgr.start_agent(prompt="t2", group_id=gid, child_index=1)
self.mgr.finish_agent(
a1, session_id="s1", session_path="/p1",
turns=3, tool_calls=5, stop_reason="end_turn",
)
self.mgr.finish_agent(
a2, session_id="s2", session_path="/p2",
turns=2, tool_calls=4, stop_reason="max_turns",
)
self.mgr.finish_group(
gid, status="completed",
completed_children=2, failed_children=0,
batch_count=1, max_batch_size=2,
)
summary = self.mgr.group_summary(gid)
assert summary is not None
self.assertEqual(summary["group_id"], gid)
self.assertEqual(summary["label"], "batch")
self.assertEqual(summary["strategy"], "parallel")
self.assertEqual(summary["status"], "completed")
self.assertEqual(summary["child_count"], 2)
self.assertEqual(summary["completed_children"], 2)
self.assertEqual(summary["failed_children"], 0)
self.assertEqual(summary["resumed_children"], 1)
self.assertEqual(summary["batch_count"], 1)
self.assertEqual(summary["max_batch_size"], 2)
self.assertEqual(summary["dependency_skips"], 0)
self.assertEqual(
summary["stop_reason_counts"],
{"end_turn": 1, "max_turns": 1},
)
def test_running_agents_counted_as_na(self) -> None:
gid = self.mgr.start_group(label="g")
self.mgr.start_agent(prompt="t", group_id=gid, child_index=0)
summary = self.mgr.group_summary(gid)
assert summary is not None
self.assertEqual(summary["stop_reason_counts"], {"n/a": 1})
def test_returns_none_for_unknown_group(self) -> None:
self.assertIsNone(self.mgr.group_summary("unknown"))
class TestCompletedRecords(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_filters_only_completed(self) -> None:
a1 = self.mgr.start_agent(prompt="t1")
a2 = self.mgr.start_agent(prompt="t2")
a3 = self.mgr.start_agent(prompt="t3")
self.mgr.finish_agent(
a1, session_id="s", session_path="/p",
turns=1, tool_calls=1, stop_reason="done",
)
self.mgr.finish_agent(
a3, session_id="s2", session_path="/p2",
turns=2, tool_calls=3, stop_reason="done",
)
completed = self.mgr.completed_records()
self.assertEqual(len(completed), 2)
ids = {r.agent_id for r in completed}
self.assertEqual(ids, {a1, a3})
def test_empty_when_none_completed(self) -> None:
self.mgr.start_agent(prompt="running")
self.assertEqual(self.mgr.completed_records(), ())
class TestSummaryLines(unittest.TestCase):
def setUp(self) -> None:
self.mgr = AgentManager()
def test_empty_manager(self) -> None:
lines = self.mgr.summary_lines()
self.assertIn("- Managed agents: 0", lines)
self.assertIn("- Completed agents: 0", lines)
self.assertIn("- Child agents: 0", lines)
self.assertIn("- Resumed agents: 0", lines)
self.assertIn("- Agent groups: 0", lines)
self.assertIn("- Completed groups: 0", lines)
def test_basic_output_format(self) -> None:
a1 = self.mgr.start_agent(prompt="task", label="worker-1")
self.mgr.finish_agent(
a1, session_id="s1", session_path="/p",
turns=4, tool_calls=10, stop_reason="end_turn",
)
lines = self.mgr.summary_lines()
self.assertIn("- Managed agents: 1", lines)
self.assertIn("- Completed agents: 1", lines)
# Agent detail line
detail = [l for l in lines if "worker-1" in l]
self.assertEqual(len(detail), 1)
self.assertIn("status=completed", detail[0])
self.assertIn("turns=4", detail[0])
self.assertIn("tool_calls=10", detail[0])
self.assertIn("stop=end_turn", detail[0])
def test_group_info_in_agent_line(self) -> None:
gid = self.mgr.start_group(label="g")
self.mgr.start_agent(prompt="t", group_id=gid, child_index=0, label="child-0")
lines = self.mgr.summary_lines()
detail = [l for l in lines if "child-0" in l]
self.assertEqual(len(detail), 1)
self.assertIn(f"group={gid}", detail[0])
self.assertIn("child_index=0", detail[0])
def test_resumed_from_in_agent_line(self) -> None:
self.mgr.start_agent(
prompt="t", label="res",
resumed_from_session_id="old-sess-id",
)
lines = self.mgr.summary_lines()
detail = [l for l in lines if "res" in l]
self.assertTrue(any("resumed_from=old-sess-id" in l for l in detail))
def test_agent_without_label_uses_id(self) -> None:
aid = self.mgr.start_agent(prompt="no label")
lines = self.mgr.summary_lines()
detail = [l for l in lines if aid in l]
self.assertEqual(len(detail), 1)
def test_truncation_at_8_agents(self) -> None:
for i in range(10):
self.mgr.start_agent(prompt=f"task-{i}")
lines = self.mgr.summary_lines()
self.assertIn("- Managed agents: 10", lines)
plus_line = [l for l in lines if "plus" in l and "managed agents" in l]
self.assertEqual(len(plus_line), 1)
self.assertIn("2 more managed agents", plus_line[0])
def test_truncation_at_6_groups(self) -> None:
for i in range(8):
self.mgr.start_group(label=f"grp-{i}")
lines = self.mgr.summary_lines()
plus_line = [l for l in lines if "plus" in l and "agent groups" in l]
self.assertEqual(len(plus_line), 1)
self.assertIn("2 more agent groups", plus_line[0])
def test_group_summary_line_format(self) -> None:
gid = self.mgr.start_group(label="my-batch", strategy="parallel")
a1 = self.mgr.start_agent(prompt="t1", group_id=gid, child_index=0)
self.mgr.finish_agent(
a1, session_id="s", session_path="/p",
turns=1, tool_calls=2, stop_reason="end_turn",
)
self.mgr.finish_group(
gid, status="completed",
completed_children=1, failed_children=0,
batch_count=1, max_batch_size=1,
)
lines = self.mgr.summary_lines()
grp_line = [l for l in lines if "my-batch" in l and "group_status" in l]
self.assertEqual(len(grp_line), 1)
self.assertIn("group_status=completed", grp_line[0])
self.assertIn("children=1", grp_line[0])
self.assertIn("completed=1", grp_line[0])
self.assertIn("failed=0", grp_line[0])
self.assertIn("strategy=parallel", grp_line[0])
self.assertIn("stop_reasons=end_turn:1", grp_line[0])
def test_child_and_resumed_counts(self) -> None:
p = self.mgr.start_agent(prompt="parent")
self.mgr.start_agent(prompt="c1", parent_agent_id=p)
self.mgr.start_agent(
prompt="c2", parent_agent_id=p,
resumed_from_session_id="old",
)
lines = self.mgr.summary_lines()
self.assertIn("- Child agents: 2", lines)
self.assertIn("- Resumed agents: 1", lines)
class TestMultipleAgentsAndGroupsInteraction(unittest.TestCase):
"""End-to-end scenario with multiple groups and cross-references."""
def test_full_lifecycle(self) -> None:
mgr = AgentManager()
# Parent agent spawns two groups
parent = mgr.start_agent(prompt="orchestrate", label="orchestrator")
g1 = mgr.start_group(label="build", parent_agent_id=parent, strategy="serial")
g2 = mgr.start_group(label="test", parent_agent_id=parent, strategy="parallel")
# Group 1 children
b1 = mgr.start_agent(prompt="build-fe", group_id=g1, child_index=0, parent_agent_id=parent)
b2 = mgr.start_agent(prompt="build-be", group_id=g1, child_index=1, parent_agent_id=parent)
# Group 2 children, one resumed
t1 = mgr.start_agent(
prompt="test-unit", group_id=g2, child_index=0,
parent_agent_id=parent, resumed_from_session_id="old-session",
)
t2 = mgr.start_agent(prompt="test-e2e", group_id=g2, child_index=1, parent_agent_id=parent)
# Finish agents
for aid, turns, tc, sr in [
(b1, 3, 8, "end_turn"),
(b2, 4, 10, "end_turn"),
(t1, 2, 5, "end_turn"),
(t2, 6, 15, "max_turns"),
]:
mgr.finish_agent(aid, session_id=f"s-{aid}", session_path=f"/p/{aid}", turns=turns, tool_calls=tc, stop_reason=sr)
mgr.finish_group(g1, status="completed", completed_children=2, failed_children=0, batch_count=2, max_batch_size=1)
mgr.finish_group(g2, status="completed", completed_children=1, failed_children=1, batch_count=1, max_batch_size=2, dependency_skips=1)
# Verify children_of
children = mgr.children_of(parent)
self.assertEqual(len(children), 4)
# Verify group_children ordering
g1_children = mgr.group_children(g1)
self.assertEqual(g1_children[0].agent_id, b1)
self.assertEqual(g1_children[1].agent_id, b2)
g2_children = mgr.group_children(g2)
self.assertEqual(g2_children[0].agent_id, t1)
self.assertEqual(g2_children[1].agent_id, t2)
# Verify completed records (parent is still running)
completed = mgr.completed_records()
self.assertEqual(len(completed), 4)
# Verify group summaries
s1 = mgr.group_summary(g1)
assert s1 is not None
self.assertEqual(s1["child_count"], 2)
self.assertEqual(s1["resumed_children"], 0)
self.assertEqual(s1["dependency_skips"], 0)
s2 = mgr.group_summary(g2)
assert s2 is not None
self.assertEqual(s2["child_count"], 2)
self.assertEqual(s2["resumed_children"], 1)
self.assertEqual(s2["dependency_skips"], 1)
self.assertEqual(s2["stop_reason_counts"], {"end_turn": 1, "max_turns": 1})
# Verify summary_lines produces output
lines = mgr.summary_lines()
self.assertIn("- Managed agents: 5", lines)
self.assertIn("- Completed agents: 4", lines)
self.assertIn("- Child agents: 4", lines)
self.assertIn("- Resumed agents: 1", lines)
self.assertIn("- Agent groups: 2", lines)
self.assertIn("- Completed groups: 2", lines)
class TestFrozenDataclasses(unittest.TestCase):
def test_record_is_frozen(self) -> None:
rec = ManagedAgentRecord(agent_id="a", prompt="p")
with self.assertRaises(AttributeError):
rec.status = "completed" # type: ignore[misc]
def test_group_is_frozen(self) -> None:
grp = ManagedAgentGroup(group_id="g")
with self.assertRaises(AttributeError):
grp.status = "completed" # type: ignore[misc]
if __name__ == "__main__":
unittest.main()
-475
View File
@@ -1,475 +0,0 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from src.agent_plugin_cache import (
MAX_PLUGIN_LINES,
MAX_PLUGIN_PREVIEW_CHARS,
PluginCacheEntry,
_coerce_entry,
_extract_entries,
discover_plugin_cache,
load_plugin_cache_summary,
)
class TestDiscoverPluginCacheNone(unittest.TestCase):
"""discover_plugin_cache returns None when no cache files exist."""
def test_returns_none_for_empty_directory(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = discover_plugin_cache(Path(tmp))
self.assertIsNone(result)
def test_returns_none_when_port_sessions_dir_is_empty(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / ".port_sessions").mkdir()
result = discover_plugin_cache(Path(tmp))
self.assertIsNone(result)
class TestDiscoverPluginCacheListFormat(unittest.TestCase):
"""discover_plugin_cache finds cache in .port_sessions/plugin_cache.json (list format)."""
def test_list_of_strings(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
cache_file = cache_dir / "plugin_cache.json"
cache_file.write_text(json.dumps(["plugin-a", "plugin-b"]))
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertIn("plugin-a", result)
self.assertIn("plugin-b", result)
self.assertIn("Plugin entries discovered: 2", result)
def test_list_of_dicts(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
cache_file = cache_dir / "plugin_cache.json"
cache_file.write_text(
json.dumps([{"name": "alpha", "version": "1.0"}, {"name": "beta"}])
)
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertIn("alpha", result)
self.assertIn("version=1.0", result)
self.assertIn("beta", result)
class TestDiscoverPluginCacheDictPluginsKey(unittest.TestCase):
"""discover_plugin_cache finds cache in .port_sessions/plugins.json (dict with 'plugins' key)."""
def test_dict_with_plugins_list(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
cache_file = cache_dir / "plugins.json"
payload = {"plugins": [{"name": "foo"}, {"name": "bar"}]}
cache_file.write_text(json.dumps(payload))
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertIn("foo", result)
self.assertIn("bar", result)
self.assertIn("Plugin entries discovered: 2", result)
class TestDiscoverPluginCacheDictEntriesKey(unittest.TestCase):
"""discover_plugin_cache handles dict with 'entries' key format."""
def test_dict_with_entries_list(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
cache_file = cache_dir / "plugin_cache.json"
payload = {"entries": [{"name": "entry-a"}, {"name": "entry-b"}]}
cache_file.write_text(json.dumps(payload))
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertIn("entry-a", result)
self.assertIn("entry-b", result)
class TestDiscoverPluginCacheDictKeyAsName(unittest.TestCase):
"""discover_plugin_cache handles dict where values are dicts (key=name format)."""
def test_dict_values_are_dicts(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
cache_file = cache_dir / "plugin_cache.json"
payload = {
"my-plugin": {"version": "2.0", "source": "/path/to/it"},
"other-plugin": {"version": "3.1"},
}
cache_file.write_text(json.dumps(payload))
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertIn("my-plugin", result)
self.assertIn("version=2.0", result)
self.assertIn("source=/path/to/it", result)
self.assertIn("other-plugin", result)
class TestCoerceEntry(unittest.TestCase):
"""_coerce_entry handles various input types."""
def test_string_entry(self) -> None:
entry = _coerce_entry("simple-plugin")
self.assertIsNotNone(entry)
self.assertEqual(entry.name, "simple-plugin")
self.assertTrue(entry.enabled)
def test_string_entry_strips_whitespace(self) -> None:
entry = _coerce_entry(" padded-name ")
self.assertIsNotNone(entry)
self.assertEqual(entry.name, "padded-name")
def test_empty_string_returns_none(self) -> None:
self.assertIsNone(_coerce_entry(""))
self.assertIsNone(_coerce_entry(" "))
def test_dict_with_name_key(self) -> None:
entry = _coerce_entry({"name": "named-plugin"})
self.assertIsNotNone(entry)
self.assertEqual(entry.name, "named-plugin")
def test_dict_with_plugin_key(self) -> None:
entry = _coerce_entry({"plugin": "plugin-key"})
self.assertIsNotNone(entry)
self.assertEqual(entry.name, "plugin-key")
def test_dict_with_id_key(self) -> None:
entry = _coerce_entry({"id": "id-key"})
self.assertIsNotNone(entry)
self.assertEqual(entry.name, "id-key")
def test_name_takes_precedence_over_plugin_and_id(self) -> None:
entry = _coerce_entry({"name": "winner", "plugin": "loser", "id": "also-loser"})
self.assertIsNotNone(entry)
self.assertEqual(entry.name, "winner")
def test_dict_with_version_and_source(self) -> None:
entry = _coerce_entry(
{"name": "full", "version": "1.2.3", "source": "/src"}
)
self.assertIsNotNone(entry)
self.assertEqual(entry.version, "1.2.3")
self.assertEqual(entry.source, "/src")
def test_source_fallback_to_path(self) -> None:
entry = _coerce_entry({"name": "p", "path": "/a/b"})
self.assertIsNotNone(entry)
self.assertEqual(entry.source, "/a/b")
def test_source_fallback_to_module(self) -> None:
entry = _coerce_entry({"name": "p", "module": "my.mod"})
self.assertIsNotNone(entry)
self.assertEqual(entry.source, "my.mod")
def test_disabled_plugin(self) -> None:
entry = _coerce_entry({"name": "off", "enabled": False})
self.assertIsNotNone(entry)
self.assertFalse(entry.enabled)
def test_enabled_none_defaults_to_true(self) -> None:
entry = _coerce_entry({"name": "on"})
self.assertIsNotNone(entry)
self.assertTrue(entry.enabled)
def test_enabled_truthy_value(self) -> None:
entry = _coerce_entry({"name": "on", "enabled": 1})
self.assertIsNotNone(entry)
self.assertTrue(entry.enabled)
def test_empty_dict_returns_none(self) -> None:
self.assertIsNone(_coerce_entry({}))
def test_non_string_returns_none(self) -> None:
self.assertIsNone(_coerce_entry(42))
self.assertIsNone(_coerce_entry(None))
self.assertIsNone(_coerce_entry(True))
self.assertIsNone(_coerce_entry([]))
def test_dict_with_non_string_name_returns_none(self) -> None:
self.assertIsNone(_coerce_entry({"name": 123}))
self.assertIsNone(_coerce_entry({"name": ""}))
def test_empty_version_is_none(self) -> None:
entry = _coerce_entry({"name": "p", "version": ""})
self.assertIsNotNone(entry)
self.assertIsNone(entry.version)
def test_non_string_version_is_none(self) -> None:
entry = _coerce_entry({"name": "p", "version": 5})
self.assertIsNotNone(entry)
self.assertIsNone(entry.version)
def test_empty_source_is_none(self) -> None:
entry = _coerce_entry({"name": "p", "source": ""})
self.assertIsNotNone(entry)
self.assertIsNone(entry.source)
class TestExtractEntries(unittest.TestCase):
"""_extract_entries handles all payload shapes."""
def test_list_payload(self) -> None:
entries = _extract_entries(["a", "b"])
self.assertEqual(len(entries), 2)
def test_dict_plugins_key(self) -> None:
entries = _extract_entries({"plugins": [{"name": "x"}]})
self.assertEqual(len(entries), 1)
self.assertEqual(entries[0].name, "x")
def test_dict_entries_key(self) -> None:
entries = _extract_entries({"entries": [{"name": "y"}]})
self.assertEqual(len(entries), 1)
self.assertEqual(entries[0].name, "y")
def test_dict_key_as_name(self) -> None:
entries = _extract_entries({"k1": {"version": "1"}, "k2": {"version": "2"}})
names = {e.name for e in entries}
self.assertEqual(names, {"k1", "k2"})
def test_plugins_key_takes_precedence_over_key_as_name(self) -> None:
payload = {"plugins": [{"name": "from-plugins"}], "other": {"version": "1"}}
entries = _extract_entries(payload)
self.assertEqual(len(entries), 1)
self.assertEqual(entries[0].name, "from-plugins")
def test_non_dict_values_ignored_in_key_as_name(self) -> None:
entries = _extract_entries({"good": {"version": "1"}, "bad": "string-val"})
self.assertEqual(len(entries), 1)
self.assertEqual(entries[0].name, "good")
def test_empty_list_returns_empty(self) -> None:
self.assertEqual(_extract_entries([]), [])
def test_invalid_payload_type(self) -> None:
self.assertEqual(_extract_entries("not-valid"), [])
self.assertEqual(_extract_entries(42), [])
class TestLoadPluginCacheSummary(unittest.TestCase):
"""load_plugin_cache_summary returns rendered summary string."""
def test_returns_none_when_no_cache(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = load_plugin_cache_summary(Path(tmp))
self.assertIsNone(result)
def test_returns_summary_string(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
(cache_dir / "plugin_cache.json").write_text(
json.dumps([{"name": "my-plugin", "version": "1.0"}])
)
result = load_plugin_cache_summary(Path(tmp))
self.assertIsNotNone(result)
self.assertIn("my-plugin", result)
self.assertIn("Plugin cache loaded from:", result)
class TestRenderedSummaryCounts(unittest.TestCase):
"""Rendered summary shows correct enabled/disabled counts."""
def _make_cache(self, tmp: str, entries: list) -> str | None:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir(exist_ok=True)
(cache_dir / "plugin_cache.json").write_text(json.dumps(entries))
return discover_plugin_cache(Path(tmp))
def test_all_enabled(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = self._make_cache(tmp, [{"name": "a"}, {"name": "b"}, {"name": "c"}])
self.assertIn("Enabled plugins: 3", result)
self.assertNotIn("Disabled plugins:", result)
def test_some_disabled(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = self._make_cache(
tmp,
[
{"name": "a"},
{"name": "b", "enabled": False},
{"name": "c", "enabled": False},
],
)
self.assertIn("Enabled plugins: 1", result)
self.assertIn("Disabled plugins: 2", result)
def test_disabled_shown_in_line(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = self._make_cache(
tmp, [{"name": "off-plugin", "enabled": False}]
)
self.assertIn("disabled", result)
self.assertIn("off-plugin", result)
class TestPreviewTruncation(unittest.TestCase):
"""Preview truncation works (MAX_PLUGIN_PREVIEW_CHARS=4000)."""
def test_long_output_is_truncated(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
# Create entries with very long names so the rendered output exceeds the limit
long_name = "x" * 500
entries = [{"name": f"{long_name}-{i}"} for i in range(20)]
(cache_dir / "plugin_cache.json").write_text(json.dumps(entries))
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertLessEqual(len(result), MAX_PLUGIN_PREVIEW_CHARS)
self.assertTrue(result.endswith("..."))
def test_short_output_not_truncated(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
entries = [{"name": "small"}]
(cache_dir / "plugin_cache.json").write_text(json.dumps(entries))
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertFalse(result.endswith("..."))
class TestMaxPluginLinesTruncation(unittest.TestCase):
"""More than MAX_PLUGIN_LINES (12) shows truncation message."""
def test_more_than_max_lines_shows_truncation(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
count = MAX_PLUGIN_LINES + 5
entries = [{"name": f"plugin-{i}"} for i in range(count)]
(cache_dir / "plugin_cache.json").write_text(json.dumps(entries))
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertIn(f"... plus 5 more plugin entries", result)
self.assertIn(f"Plugin entries discovered: {count}", result)
def test_exactly_max_lines_no_truncation(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
entries = [{"name": f"plugin-{i}"} for i in range(MAX_PLUGIN_LINES)]
(cache_dir / "plugin_cache.json").write_text(json.dumps(entries))
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertNotIn("more plugin entries", result)
class TestMalformedJsonSkipped(unittest.TestCase):
"""Malformed JSON files are gracefully skipped."""
def test_invalid_json_skipped(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
(cache_dir / "plugin_cache.json").write_text("{not valid json!!!")
result = discover_plugin_cache(Path(tmp))
self.assertIsNone(result)
def test_malformed_first_valid_second(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
# First candidate: malformed
(cache_dir / "plugin_cache.json").write_text("not json")
# Second candidate: valid
(cache_dir / "plugins.json").write_text(
json.dumps({"plugins": [{"name": "fallback"}]})
)
result = discover_plugin_cache(Path(tmp))
self.assertIsNotNone(result)
self.assertIn("fallback", result)
def test_valid_json_but_empty_entries(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / ".port_sessions"
cache_dir.mkdir()
(cache_dir / "plugin_cache.json").write_text(json.dumps([]))
result = discover_plugin_cache(Path(tmp))
self.assertIsNone(result)
class TestAdditionalWorkingDirectories(unittest.TestCase):
"""additional_working_directories are searched."""
def test_finds_cache_in_additional_dir(self) -> None:
with tempfile.TemporaryDirectory() as main, tempfile.TemporaryDirectory() as extra:
cache_dir = Path(extra) / ".port_sessions"
cache_dir.mkdir()
(cache_dir / "plugin_cache.json").write_text(
json.dumps([{"name": "extra-plugin"}])
)
result = discover_plugin_cache(Path(main), (extra,))
self.assertIsNotNone(result)
self.assertIn("extra-plugin", result)
def test_main_dir_preferred_over_additional(self) -> None:
with tempfile.TemporaryDirectory() as main, tempfile.TemporaryDirectory() as extra:
for base, name in [(main, "main-plugin"), (extra, "extra-plugin")]:
cache_dir = Path(base) / ".port_sessions"
cache_dir.mkdir()
(cache_dir / "plugin_cache.json").write_text(
json.dumps([{"name": name}])
)
result = discover_plugin_cache(Path(main), (extra,))
self.assertIsNotNone(result)
self.assertIn("main-plugin", result)
def test_load_plugin_cache_summary_with_additional_dirs(self) -> None:
with tempfile.TemporaryDirectory() as main, tempfile.TemporaryDirectory() as extra:
cache_dir = Path(extra) / ".port_sessions"
cache_dir.mkdir()
(cache_dir / "plugin_cache.json").write_text(
json.dumps([{"name": "via-summary"}])
)
result = load_plugin_cache_summary(Path(main), (extra,))
self.assertIsNotNone(result)
self.assertIn("via-summary", result)
class TestPluginCacheEntry(unittest.TestCase):
"""PluginCacheEntry dataclass behavior."""
def test_defaults(self) -> None:
entry = PluginCacheEntry(name="test")
self.assertEqual(entry.name, "test")
self.assertTrue(entry.enabled)
self.assertIsNone(entry.version)
self.assertIsNone(entry.source)
def test_frozen(self) -> None:
entry = PluginCacheEntry(name="test")
with self.assertRaises(AttributeError):
entry.name = "other" # type: ignore[misc]
if __name__ == "__main__":
unittest.main()
-335
View File
@@ -1,335 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from src.agent_prompting import build_prompt_context, build_system_prompt_parts, render_system_prompt
from src.plan_runtime import PlanRuntime
from src.agent_runtime import LocalCodingAgent
from src.agent_session import AgentSessionState
from src.agent_tools import default_tool_registry
from src.agent_types import AgentPermissions, AgentRuntimeConfig, ModelConfig
from src.task_runtime import TaskRuntime
class AgentPromptingTests(unittest.TestCase):
def test_prompt_builder_contains_expected_sections(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
runtime_config = AgentRuntimeConfig(
cwd=Path(tmp_dir),
permissions=AgentPermissions(
allow_file_write=True,
allow_shell_commands=False,
),
)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 系统规则', prompt)
self.assertIn('# 处理任务', prompt)
self.assertIn('# 工作空间边界', prompt)
self.assertIn('当前 session 目录是本轮任务的默认工作区', prompt)
self.assertIn('平台服务代码目录始终只读', prompt)
self.assertIn('# 使用工具', prompt)
self.assertIn('# Skills', prompt)
self.assertIn('product-data', prompt)
self.assertIn('__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__', prompt)
self.assertIn('主工作目录:', prompt)
def test_prompt_builder_respects_enabled_skill_names(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
runtime_config = AgentRuntimeConfig(
cwd=Path(tmp_dir),
enabled_skill_names=('verify',),
)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('verify', prompt)
self.assertNotIn('product-data', prompt)
self.assertNotIn('online-mining', prompt)
def test_session_state_exports_messages_in_order(self) -> None:
state = AgentSessionState.create(['sys one', 'sys two'], 'hello')
state.append_assistant('working', ())
state.append_tool('read_file', 'call_1', '{"ok": true}')
messages = state.to_openai_messages()
self.assertEqual(messages[0]['role'], 'system')
self.assertEqual(messages[1]['role'], 'user')
self.assertEqual(messages[2]['role'], 'assistant')
self.assertEqual(messages[3]['role'], 'tool')
self.assertEqual(messages[3]['tool_call_id'], 'call_1')
def test_agent_can_render_prompt_without_contacting_model(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
prompt = agent.render_system_prompt()
self.assertIn('Zhongkong Agent', prompt)
self.assertIn('# 系统规则', prompt)
self.assertIn('# 环境信息', prompt)
def test_prompt_builder_mentions_plugins_when_cache_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
plugin_cache = workspace / '.port_sessions' / 'plugin_cache.json'
plugin_cache.parent.mkdir(parents=True, exist_ok=True)
plugin_cache.write_text(
'{"plugins":[{"name":"example-plugin","enabled":true}]}',
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 插件', prompt)
def test_prompt_builder_mentions_hook_policy_when_manifest_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-policy.json').write_text(
'{"trusted": false, "hooks": {"beforePrompt": ["Follow workspace policy."]}}',
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# Hook 策略', prompt)
def test_prompt_builder_mentions_mcp_when_manifest_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'notes.txt').write_text('mcp notes\n', encoding='utf-8')
(workspace / '.claw-mcp.json').write_text(
(
'{"servers":[{"name":"workspace","resources":['
'{"uri":"mcp://workspace/notes","name":"Notes","path":"notes.txt"}'
']}]}'
),
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# MCP', prompt)
def test_prompt_builder_mentions_search_when_runtime_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-search.json').write_text(
'{"providers":[{"name":"local-search","provider":"searxng","baseUrl":"http://127.0.0.1:8080"}]}',
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 搜索', prompt)
self.assertIn('web_search', prompt)
def test_prompt_builder_mentions_remote_when_manifest_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-remote.json').write_text(
(
'{"profiles":[{"name":"staging","mode":"ssh","target":"dev@staging",'
'"workspaceCwd":"/srv/app"}]}'
),
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 远程环境', prompt)
def test_prompt_builder_mentions_account_when_runtime_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-account.json').write_text(
'{"profiles":[{"name":"local","provider":"openai","identity":"dev@example.com"}]}',
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 账号', prompt)
def test_prompt_builder_mentions_ask_user_when_runtime_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-ask-user.json').write_text(
'{"answers":[{"question":"Approve deploy?","answer":"yes"}]}',
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 询问用户', prompt)
def test_prompt_builder_mentions_config_when_runtime_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
claude_dir = workspace / '.claude'
claude_dir.mkdir()
(claude_dir / 'settings.json').write_text(
'{"review":{"mode":"strict"}}',
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 配置', prompt)
def test_prompt_builder_mentions_lsp_when_runtime_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'sample.py').write_text('def helper(value):\n return value * 2\n', encoding='utf-8')
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# LSP', prompt)
self.assertIn('使用 LSP 工具', prompt)
def test_prompt_builder_mentions_tasks_when_runtime_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
scratchpad = workspace / '.port_sessions' / 'accounts' / 'alice' / 'sessions' / 's1' / 'scratchpad'
scratchpad.mkdir(parents=True)
runtime = TaskRuntime.from_storage_path(scratchpad / 'task_runtime.json')
runtime.create_task(title='Inspect runtime tasks')
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(
runtime_config,
model_config,
scratchpad_directory=scratchpad,
)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 任务', prompt)
def test_prompt_builder_mentions_teams_when_runtime_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-teams.json').write_text(
'{"teams":[{"name":"reviewers","members":["alice","bob"]}]}',
encoding='utf-8',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(runtime_config, model_config)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 团队', prompt)
def test_prompt_builder_mentions_planning_when_runtime_is_loaded(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
scratchpad = workspace / '.port_sessions' / 'accounts' / 'alice' / 'sessions' / 's1' / 'scratchpad'
scratchpad.mkdir(parents=True)
runtime = PlanRuntime.from_storage_path(scratchpad / 'plan_runtime.json')
runtime.update_plan(
[{'step': 'Inspect runtime planning', 'status': 'pending'}],
explanation='Track the current plan.',
)
runtime_config = AgentRuntimeConfig(cwd=workspace)
model_config = ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct')
prompt_context = build_prompt_context(
runtime_config,
model_config,
scratchpad_directory=scratchpad,
)
parts = build_system_prompt_parts(
prompt_context=prompt_context,
runtime_config=runtime_config,
tools=default_tool_registry(),
)
prompt = render_system_prompt(parts)
self.assertIn('# 计划', prompt)
-126
View File
@@ -1,126 +0,0 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from src.agent_registry import (
create_agent_definition,
delete_agent_definition,
load_agent_registry,
render_agent_detail,
render_agents_report,
update_agent_definition,
)
def _write_agent(path: Path, *, name: str, description: str, body: str, extra: str = '') -> None:
payload = (
'---\n'
f'name: {name}\n'
f'description: "{description}"\n'
f'{extra}'
'---\n\n'
f'{body}\n'
)
path.write_text(payload, encoding='utf-8')
class AgentRegistryTests(unittest.TestCase):
def test_create_update_delete_agent_definition_roundtrip(self) -> None:
with tempfile.TemporaryDirectory() as home_dir, tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
with patch.dict(os.environ, {'HOME': home_dir}):
created = create_agent_definition(
workspace,
agent_type='reviewer',
description='Review implementation changes.',
system_prompt='Inspect diffs and summarize risks.',
source='project',
tools=('read_file', 'grep_search'),
model='demo-model',
initial_prompt='Start with changed files.',
)
self.assertEqual(created.action, 'created')
self.assertTrue(Path(created.file_path).exists())
snapshot = load_agent_registry(workspace)
detail = render_agent_detail(snapshot, 'reviewer')
self.assertIn('demo-model', detail)
self.assertIn('Start with changed files.', detail)
updated = update_agent_definition(
workspace,
agent_type='reviewer',
description='Review code and tests carefully.',
system_prompt='Focus on regressions and missing coverage.',
source='project',
)
self.assertEqual(updated.action, 'updated')
snapshot = load_agent_registry(workspace)
detail = render_agent_detail(snapshot, 'reviewer')
self.assertIn('Review code and tests carefully.', detail)
self.assertIn('Focus on regressions and missing coverage.', detail)
deleted = delete_agent_definition(
workspace,
agent_type='reviewer',
source='project',
)
self.assertEqual(deleted.action, 'deleted')
self.assertFalse(Path(deleted.file_path).exists())
def test_project_agent_overrides_built_in_agent(self) -> None:
with tempfile.TemporaryDirectory() as home_dir, tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
agents_dir = workspace / '.claude' / 'agents'
agents_dir.mkdir(parents=True)
_write_agent(
agents_dir / 'Explore.md',
name='Explore',
description='Project-specific explore agent.',
body='Search this repository carefully before answering.',
extra=(
'tools: read_file, grep_search\n'
'model: child-model\n'
'initialPrompt: Begin with rg-style discovery.\n'
),
)
with patch.dict(os.environ, {'HOME': home_dir}):
snapshot = load_agent_registry(workspace)
active = {agent.agent_type: agent for agent in snapshot.active_agents}
self.assertIn('Explore', active)
self.assertEqual(active['Explore'].source, 'projectSettings')
self.assertEqual(active['Explore'].model, 'child-model')
self.assertEqual(active['Explore'].tools, ('read_file', 'grep_search'))
self.assertEqual(active['Explore'].initial_prompt, 'Begin with rg-style discovery.')
report = render_agents_report(snapshot, cwd=workspace)
self.assertIn('Explore [projectSettings]', report)
self.assertIn('Shadowed Agents', report)
detail = render_agent_detail(snapshot, 'Explore')
self.assertIn('Project-specific explore agent.', detail)
self.assertIn('Begin with rg-style discovery.', detail)
self.assertIn('Search this repository carefully before answering.', detail)
def test_invalid_agent_file_is_reported(self) -> None:
with tempfile.TemporaryDirectory() as home_dir, tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
agents_dir = workspace / '.claude' / 'agents'
agents_dir.mkdir(parents=True)
(agents_dir / 'broken.md').write_text(
'---\nname: broken\n---\n',
encoding='utf-8',
)
with patch.dict(os.environ, {'HOME': home_dir}):
snapshot = load_agent_registry(workspace)
self.assertEqual(len(snapshot.failed_files), 1)
self.assertIn('missing a description', snapshot.failed_files[0].error)
File diff suppressed because it is too large Load Diff
-534
View File
@@ -1,534 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
import json
import sys
import os
from pathlib import Path
from unittest.mock import patch
from src.agent_runtime import LocalCodingAgent
from src.agent_slash_commands import looks_like_command, parse_slash_command
from src.agent_types import AgentRuntimeConfig, ModelConfig
from src.plan_runtime import PlanRuntime
from src.task_runtime import TaskRuntime
class _FakeHTTPResponse:
def __init__(self, payload: str) -> None:
self.payload = payload
def read(self) -> bytes:
return self.payload.encode('utf-8')
def __enter__(self) -> '_FakeHTTPResponse':
return self
def __exit__(self, exc_type, exc, tb) -> None:
return None
def _write_fake_mcp_server(workspace: Path) -> Path:
server_path = workspace / 'fake_mcp_server.py'
server_path.write_text(
(
'import json, sys\n'
'TOOLS = [{"name": "echo", "description": "Echo text", "inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}}}]\n'
'for raw in sys.stdin:\n'
' raw = raw.strip()\n'
' if not raw:\n'
' continue\n'
' message = json.loads(raw)\n'
' method = message.get("method")\n'
' if method == "initialize":\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"protocolVersion": "2025-11-25", "capabilities": {"resources": {}, "tools": {}}, "serverInfo": {"name": "fake-remote", "version": "1.0.0"}}}\n'
' print(json.dumps(response), flush=True)\n'
' continue\n'
' if method == "notifications/initialized":\n'
' continue\n'
' if method == "tools/list":\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"tools": TOOLS}}\n'
' print(json.dumps(response), flush=True)\n'
' continue\n'
' if method == "tools/call":\n'
' text = message.get("params", {}).get("arguments", {}).get("text", "")\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"content": [{"type": "text", "text": "echo:" + text}], "isError": False}}\n'
' print(json.dumps(response), flush=True)\n'
' continue\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"resources": []}}\n'
' print(json.dumps(response), flush=True)\n'
),
encoding='utf-8',
)
return server_path
class AgentSlashCommandTests(unittest.TestCase):
def test_parse_slash_command(self) -> None:
parsed = parse_slash_command('/context extra args')
assert parsed is not None
self.assertEqual(parsed.command_name, 'context')
self.assertEqual(parsed.args, 'extra args')
self.assertFalse(parsed.is_mcp)
def test_looks_like_command(self) -> None:
self.assertTrue(looks_like_command('context'))
self.assertFalse(looks_like_command('foo/bar'))
def test_model_command_updates_agent_model(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
result = agent.run('/model local/test-model')
self.assertIn('Set model to local/test-model', result.final_output)
self.assertEqual(agent.model_config.model, 'local/test-model')
def test_unknown_command_returns_local_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
result = agent.run('/unknown-command')
self.assertEqual(result.final_output, 'Unknown skill: unknown-command')
def test_context_command_renders_usage_report(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'CLAUDE.md').write_text('repo instructions\n', encoding='utf-8')
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
result = agent.run('/context')
self.assertIn('## Context Usage', result.final_output)
self.assertIn('### Estimated usage by category', result.final_output)
self.assertIn('### Memory Files', result.final_output)
def test_token_budget_command_renders_local_report(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
result = agent.run('/token-budget')
self.assertIn('# Token Budget', result.final_output)
self.assertIn('Hard input limit', result.final_output)
self.assertIn('Auto-compact buffer', result.final_output)
def test_agents_command_lists_and_shows_custom_agents(self) -> None:
with tempfile.TemporaryDirectory() as home_dir, tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
agents_dir = workspace / '.claude' / 'agents'
agents_dir.mkdir(parents=True)
(agents_dir / 'reviewer.md').write_text(
(
'---\n'
'name: reviewer\n'
'description: "Review implementation changes carefully."\n'
'tools: read_file, grep_search\n'
'model: test-child-model\n'
'---\n\n'
'Inspect code changes and summarize risks.\n'
),
encoding='utf-8',
)
with patch.dict(os.environ, {'HOME': home_dir}):
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
list_result = agent.run('/agents')
detail_result = agent.run('/agents show reviewer')
self.assertIn('# Agents', list_result.final_output)
self.assertIn('reviewer [projectSettings]', list_result.final_output)
self.assertIn('# Agent: reviewer', detail_result.final_output)
self.assertIn('Inspect code changes and summarize risks.', detail_result.final_output)
def test_agents_command_can_create_update_and_delete_project_agent(self) -> None:
with tempfile.TemporaryDirectory() as home_dir, tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
with patch.dict(os.environ, {'HOME': home_dir}):
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
create_result = agent.run(
'/agents create reviewer :: Review code changes carefully. :: Inspect code changes and summarize risks.'
)
self.assertIn('action=created', create_result.final_output)
self.assertTrue((workspace / '.claude' / 'agents' / 'reviewer.md').exists())
update_result = agent.run(
'/agents update reviewer Updated review description :: Focus on regressions and missing tests.'
)
self.assertIn('action=updated', update_result.final_output)
detail_result = agent.run('/agents reviewer')
self.assertIn('Updated review description', detail_result.final_output)
self.assertIn('Focus on regressions and missing tests.', detail_result.final_output)
delete_result = agent.run('/agents delete reviewer')
self.assertIn('action=deleted', delete_result.final_output)
self.assertFalse((workspace / '.claude' / 'agents' / 'reviewer.md').exists())
def test_mcp_and_resource_commands_render_local_reports(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'notes.txt').write_text('mcp notes\n', encoding='utf-8')
(workspace / '.claw-mcp.json').write_text(
(
'{"servers":[{"name":"workspace","resources":['
'{"uri":"mcp://workspace/notes","name":"Notes","path":"notes.txt"}'
']}]}'
),
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
mcp_result = agent.run('/mcp')
resources_result = agent.run('/resources')
resource_result = agent.run('/resource mcp://workspace/notes')
legacy_mcp_result = agent.run('/mcp (MCP)')
self.assertIn('# MCP', mcp_result.final_output)
self.assertIn('Local MCP resources: 1', mcp_result.final_output)
self.assertIn('# MCP Resources', resources_result.final_output)
self.assertIn('mcp://workspace/notes', resources_result.final_output)
self.assertIn('# MCP Resource', resource_result.final_output)
self.assertIn('mcp notes', resource_result.final_output)
self.assertIn('# MCP', legacy_mcp_result.final_output)
def test_mcp_tools_command_renders_transport_backed_tools(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
server_path = _write_fake_mcp_server(workspace)
(workspace / '.claw-mcp.json').write_text(
json.dumps(
{
'mcpServers': {
'remote': {
'command': sys.executable,
'args': ['-u', str(server_path)],
}
}
}
),
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
tools_result = agent.run('/mcp tools')
tool_result = agent.run('/mcp tool echo')
self.assertIn('# MCP Tools', tools_result.final_output)
self.assertIn('echo', tools_result.final_output)
self.assertIn('# MCP Tool Result', tool_result.final_output)
self.assertIn('echo:', tool_result.final_output)
def test_search_commands_render_and_update_local_search_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-search.json').write_text(
(
'{"providers":['
'{"name":"local-search","provider":"searxng","baseUrl":"http://127.0.0.1:8080"},'
'{"name":"backup-search","provider":"searxng","baseUrl":"http://127.0.0.2:8080"}'
']}'
),
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
with patch(
'src.search_runtime.request.urlopen',
return_value=_FakeHTTPResponse(
'{"results":[{"title":"Alpha","url":"https://example.com/alpha","content":"Search snippet"}]}'
),
):
search_result = agent.run('/search alpha query')
providers_result = agent.run('/search providers')
activate_result = agent.run('/search use backup-search')
provider_result = agent.run('/search provider backup-search')
self.assertIn('# Web Search', search_result.final_output)
self.assertIn('Alpha', search_result.final_output)
self.assertIn('# Search Providers', providers_result.final_output)
self.assertIn('local-search', providers_result.final_output)
self.assertIn('backup-search', providers_result.final_output)
self.assertIn('provider=backup-search', activate_result.final_output)
self.assertIn('# Search Provider', provider_result.final_output)
self.assertIn('backup-search', provider_result.final_output)
def test_lsp_commands_render_local_reports(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'sample.py').write_text(
'def helper(value):\n'
' """Double a value."""\n'
' return value * 2\n'
'\n'
'def run(item):\n'
' return helper(item)\n',
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
summary_result = agent.run('/lsp')
symbols_result = agent.run('/lsp symbols sample.py')
definition_result = agent.run('/lsp definition sample.py 6 12')
diagnostics_result = agent.run('/lsp diagnostics sample.py')
self.assertIn('# LSP', summary_result.final_output)
self.assertIn('Indexed candidate files: 1', summary_result.final_output)
self.assertIn('# LSP Document Symbols', symbols_result.final_output)
self.assertIn('function helper', symbols_result.final_output)
self.assertIn('# LSP Definition', definition_result.final_output)
self.assertIn('function helper', definition_result.final_output)
self.assertIn('# LSP Diagnostics', diagnostics_result.final_output)
def test_remote_commands_render_and_update_local_remote_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-remote.json').write_text(
(
'{"profiles":[{"name":"staging","mode":"ssh","target":"dev@staging",'
'"workspaceCwd":"/srv/app","sessionUrl":"wss://remote/session"}]}'
),
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
remotes_result = agent.run('/remotes')
remote_result = agent.run('/remote')
ssh_result = agent.run('/ssh staging')
disconnect_result = agent.run('/disconnect')
self.assertIn('# Remote Profiles', remotes_result.final_output)
self.assertIn('staging', remotes_result.final_output)
self.assertIn('# Remote', remote_result.final_output)
self.assertIn('Configured remote profiles: 1', remote_result.final_output)
self.assertIn('mode=ssh', ssh_result.final_output)
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)
(workspace / '.claw-account.json').write_text(
'{"profiles":[{"name":"local","provider":"openai","identity":"dev@example.com"}]}',
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
account_result = agent.run('/account')
profiles_result = agent.run('/account profiles')
login_result = agent.run('/login local')
logout_result = agent.run('/logout')
self.assertIn('# Account', account_result.final_output)
self.assertIn('Configured account profiles: 1', account_result.final_output)
self.assertIn('# Account Profiles', profiles_result.final_output)
self.assertIn('dev@example.com', profiles_result.final_output)
self.assertIn('profile=local', login_result.final_output)
self.assertIn('logged_in=False', logout_result.final_output)
def test_ask_commands_render_local_ask_user_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-ask-user.json').write_text(
'{"answers":[{"question":"Approve deploy?","answer":"yes"}]}',
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
status_result = agent.run('/ask')
history_result = agent.run('/ask history')
self.assertIn('# Ask User', status_result.final_output)
self.assertIn('Queued answers: 1', status_result.final_output)
self.assertIn('# Ask User History', history_result.final_output)
def test_team_commands_render_local_team_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-teams.json').write_text(
'{"teams":[{"name":"reviewers","members":["alice","bob"]}]}',
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
teams_result = agent.run('/teams')
team_result = agent.run('/team reviewers')
messages_result = agent.run('/messages')
self.assertIn('# Teams', teams_result.final_output)
self.assertIn('reviewers', teams_result.final_output)
self.assertIn('# Team', team_result.final_output)
self.assertIn('alice', team_result.final_output)
self.assertIn('# Team Messages', messages_result.final_output)
def test_config_commands_render_local_reports(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
claude_dir = workspace / '.claude'
claude_dir.mkdir()
(claude_dir / 'settings.json').write_text(
'{"review":{"mode":"strict"}}',
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
config_result = agent.run('/config')
effective_result = agent.run('/config effective')
value_result = agent.run('/config get review.mode')
source_result = agent.run('/settings source project')
self.assertIn('# Config', config_result.final_output)
self.assertIn('Config sources: 1', config_result.final_output)
self.assertIn('# Config Effective', effective_result.final_output)
self.assertIn('"review"', effective_result.final_output)
self.assertIn('# Config Value', value_result.final_output)
self.assertIn('"strict"', value_result.final_output)
self.assertIn('# Config Source', source_result.final_output)
def test_tasks_and_task_commands_render_local_reports(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = TaskRuntime.from_workspace(workspace)
mutation = runtime.create_task(
title='Review runtime tasks',
status='in_progress',
)
task_id = mutation.task.task_id if mutation.task is not None else ''
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
tasks_result = agent.run('/tasks')
task_result = agent.run(f'/task {task_id}')
todo_result = agent.run('/todo in_progress')
next_result = agent.run('/task-next')
self.assertIn('# Tasks', tasks_result.final_output)
self.assertIn(task_id, tasks_result.final_output)
self.assertIn('# Task', task_result.final_output)
self.assertIn('in_progress', task_result.final_output)
self.assertIn('# Tasks', todo_result.final_output)
self.assertIn('# Next Tasks', next_result.final_output)
self.assertIn(task_id, next_result.final_output)
def test_plan_command_renders_local_report(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
plan_runtime = PlanRuntime.from_workspace(workspace)
plan_runtime.update_plan(
[{'step': 'Inspect the plan command', 'status': 'in_progress'}],
explanation='Use the local plan runtime.',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
plan_result = agent.run('/plan')
self.assertIn('# Plan', plan_result.final_output)
self.assertIn('Inspect the plan command', plan_result.final_output)
def test_tools_and_status_commands_render_local_reports(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
tools_result = agent.run('/tools')
status_result = agent.run('/status')
self.assertIn('# Tools', tools_result.final_output)
self.assertIn('`read_file`', tools_result.final_output)
self.assertIn('# Status', status_result.final_output)
self.assertIn('Token counter:', status_result.final_output)
self.assertIn('Last run: none', status_result.final_output)
def test_hooks_and_trust_commands_render_local_reports(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-policy.json').write_text(
(
'{"trusted": false, '
'"managedSettings": {"reviewMode": "strict"}, '
'"safeEnv": ["HOOK_SAFE_TOKEN"]}'
),
encoding='utf-8',
)
with patch.dict('os.environ', {'HOOK_SAFE_TOKEN': 'demo-secret'}, clear=False):
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
hooks_result = agent.run('/hooks')
trust_result = agent.run('/trust')
self.assertIn('# Hook Policy', hooks_result.final_output)
self.assertIn('Local hook/policy manifests', hooks_result.final_output)
self.assertIn('# Trust', trust_result.final_output)
self.assertIn('untrusted', trust_result.final_output)
self.assertIn('reviewMode=strict', trust_result.final_output)
self.assertIn('HOOK_SAFE_TOKEN=demo-secret', trust_result.final_output)
def test_clear_command_clears_saved_runtime_state(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='Qwen/Qwen3-Coder-30B-A3B-Instruct'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
agent.last_session = agent.build_session('hello')
agent.last_run_result = object() # type: ignore[assignment]
result = agent.run('/clear')
self.assertIn('Cleared ephemeral Python agent state', result.final_output)
self.assertIsNone(agent.last_session)
self.assertIsNone(agent.last_run_result)
-269
View File
@@ -1,269 +0,0 @@
"""Security tests for agent_tools.py: path traversal, destructive commands, and env var filtering."""
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from src.agent_tools import (
ToolExecutionError,
ToolPermissionError,
_build_subprocess_env,
_ensure_shell_allowed,
_is_sensitive_env_var,
_resolve_path,
build_tool_context,
default_tool_registry,
)
from src.agent_types import AgentPermissions, AgentRuntimeConfig
from src.session_env_vars import (
clear_session_env_vars,
set_session_env_var,
)
def _make_context(
tmp_dir: str,
*,
allow_shell: bool = False,
allow_destructive: bool = False,
) -> "ToolExecutionContext": # noqa: F821
config = AgentRuntimeConfig(
cwd=Path(tmp_dir),
permissions=AgentPermissions(
allow_shell_commands=allow_shell,
allow_destructive_shell_commands=allow_destructive,
),
)
return build_tool_context(config, tool_registry=default_tool_registry())
# ---------------------------------------------------------------------------
# _resolve_path – path traversal prevention
# ---------------------------------------------------------------------------
class TestResolvePath(unittest.TestCase):
def test_relative_path_within_workspace(self):
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / "hello.txt").write_text("hi")
ctx = _make_context(tmp)
result = _resolve_path("hello.txt", ctx)
self.assertEqual(result, (Path(tmp) / "hello.txt").resolve())
def test_absolute_path_within_workspace(self):
with tempfile.TemporaryDirectory() as tmp:
target = Path(tmp) / "sub" / "file.txt"
target.parent.mkdir()
target.write_text("data")
ctx = _make_context(tmp)
result = _resolve_path(str(target), ctx)
self.assertEqual(result, target.resolve())
def test_traversal_with_dotdot_blocked(self):
with tempfile.TemporaryDirectory() as tmp:
ctx = _make_context(tmp)
with self.assertRaises(ToolExecutionError):
_resolve_path("../outside", ctx)
def test_traversal_etc_passwd_blocked(self):
with tempfile.TemporaryDirectory() as tmp:
ctx = _make_context(tmp)
with self.assertRaises(ToolExecutionError):
_resolve_path("../../etc/passwd", ctx)
def test_allow_missing_true_permits_nonexistent(self):
with tempfile.TemporaryDirectory() as tmp:
ctx = _make_context(tmp)
result = _resolve_path("does_not_exist.txt", ctx, allow_missing=True)
self.assertEqual(result, (Path(tmp) / "does_not_exist.txt").resolve())
def test_allow_missing_false_raises_for_nonexistent(self):
with tempfile.TemporaryDirectory() as tmp:
ctx = _make_context(tmp)
with self.assertRaises(OSError):
_resolve_path("does_not_exist.txt", ctx, allow_missing=False)
# ---------------------------------------------------------------------------
# _ensure_shell_allowed – destructive command blocking
# ---------------------------------------------------------------------------
class TestEnsureShellAllowed(unittest.TestCase):
def _ctx(self, *, allow_shell: bool = True, allow_destructive: bool = False) -> "ToolExecutionContext": # noqa: F821
self._tmp = tempfile.TemporaryDirectory()
return _make_context(
self._tmp.name,
allow_shell=allow_shell,
allow_destructive=allow_destructive,
)
def tearDown(self):
if hasattr(self, "_tmp"):
self._tmp.cleanup()
# -- safe commands pass --------------------------------------------------
def test_safe_commands_allowed(self):
ctx = self._ctx()
for cmd in ("ls -la", "cat file.txt", "echo hello", "grep foo bar.txt"):
_ensure_shell_allowed(cmd, ctx) # should not raise
# -- destructive commands blocked -----------------------------------------
def test_rm_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("rm -rf /", ctx)
def test_mv_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("mv a b", ctx)
def test_dd_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("dd if=/dev/zero of=/dev/sda", ctx)
def test_shutdown_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("shutdown -h now", ctx)
def test_reboot_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("reboot ", ctx)
def test_mkfs_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("mkfs.ext4 /dev/sda1", ctx)
def test_chmod_recursive_777_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("chmod -R 777 /", ctx)
def test_chown_recursive_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("chown -R root:root /", ctx)
def test_git_reset_hard_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("git reset --hard", ctx)
def test_git_clean_fd_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("git clean -fd", ctx)
def test_truncation_operator_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed(": > important.log", ctx)
# -- chained commands with destructive sub-commands -----------------------
def test_chained_and_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("echo hi && rm -rf /", ctx)
def test_chained_or_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("false || rm file", ctx)
def test_chained_semicolon_blocked(self):
ctx = self._ctx()
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("echo ok; mv a b", ctx)
# -- shell commands entirely disabled ------------------------------------
def test_shell_disabled_raises(self):
ctx = self._ctx(allow_shell=False)
with self.assertRaises(ToolPermissionError):
_ensure_shell_allowed("ls", ctx)
# -- allow_destructive bypasses blocking ---------------------------------
def test_destructive_allowed_bypasses(self):
ctx = self._ctx(allow_destructive=True)
# All destructive commands should pass without raising
for cmd in (
"rm -rf /",
"mv a b",
"dd if=/dev/zero of=/dev/sda",
"shutdown -h now",
"mkfs.ext4 /dev/sda1",
"chmod -R 777 /",
"chown -R root:root /",
"git reset --hard",
"git clean -fd",
": > file",
):
_ensure_shell_allowed(cmd, ctx) # should not raise
# ---------------------------------------------------------------------------
# _is_sensitive_env_var – secret-name detection
# ---------------------------------------------------------------------------
class TestIsSensitiveEnvVar(unittest.TestCase):
def test_common_sensitive_vars_detected(self):
for name in (
"MY_SECRET",
"GITHUB_TOKEN",
"DB_PASSWORD",
"SSH_PRIVATE_KEY",
"MY_API_KEY",
"CREDENTIAL_STORE",
"AUTH_HEADER",
):
self.assertTrue(
_is_sensitive_env_var(name),
f"{name} should be detected as sensitive",
)
def test_non_sensitive_vars_allowed(self):
for name in ("HOME", "PATH", "LANG", "TERM", "USER", "SHELL"):
self.assertFalse(
_is_sensitive_env_var(name),
f"{name} should not be detected as sensitive",
)
def test_case_insensitive(self):
self.assertTrue(_is_sensitive_env_var("my_secret"))
self.assertTrue(_is_sensitive_env_var("Github_Token"))
self.assertTrue(_is_sensitive_env_var("db_password"))
# ---------------------------------------------------------------------------
# _build_subprocess_env – session env var merging
# ---------------------------------------------------------------------------
class TestBuildSubprocessEnv(unittest.TestCase):
def setUp(self):
clear_session_env_vars()
def tearDown(self):
clear_session_env_vars()
def test_session_env_var_appears_in_subprocess_env(self):
with tempfile.TemporaryDirectory() as tmp:
ctx = _make_context(tmp)
set_session_env_var("CLAW_SESSION_FOO", "from-session")
env = _build_subprocess_env(ctx)
self.assertEqual(env["CLAW_SESSION_FOO"], "from-session")
def test_extra_env_overrides_session_env(self):
with tempfile.TemporaryDirectory() as tmp:
config = AgentRuntimeConfig(cwd=Path(tmp))
ctx = build_tool_context(
config,
tool_registry=default_tool_registry(),
extra_env={"CLAW_OVERRIDE_ME": "from-extra"},
)
set_session_env_var("CLAW_OVERRIDE_ME", "from-session")
env = _build_subprocess_env(ctx)
self.assertEqual(env["CLAW_OVERRIDE_ME"], "from-extra")
if __name__ == "__main__":
unittest.main()
-80
View File
@@ -1,80 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from src.ask_user_runtime import AskUserRuntime
from src.agent_tools import build_tool_context, default_tool_registry, execute_tool
from src.agent_types import AgentRuntimeConfig
class AskUserRuntimeTests(unittest.TestCase):
def test_ask_user_runtime_consumes_queued_answers(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-ask-user.json').write_text(
'{"answers":[{"question":"Approve deploy?","answer":"yes"}]}',
encoding='utf-8',
)
runtime = AskUserRuntime.from_workspace(workspace)
response = runtime.answer(question='Approve deploy?')
restored = AskUserRuntime.from_workspace(workspace)
self.assertEqual(response.answer, 'yes')
self.assertEqual(response.source, 'queued')
self.assertEqual(len(restored.queued_answers), 0)
self.assertEqual(len(restored.history), 1)
def test_ask_user_tool_executes_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-ask-user.json').write_text(
'{"answers":[{"question":"Choose mode","answer":"safe"}]}',
encoding='utf-8',
)
runtime = AskUserRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
ask_user_runtime=runtime,
)
result = execute_tool(
default_tool_registry(),
'ask_user_question',
{
'question': 'Choose mode',
'choices': ['safe', 'fast'],
'allow_free_text': False,
},
context,
)
self.assertTrue(result.ok)
self.assertIn('# Ask User', result.content)
self.assertIn('safe', result.content)
self.assertEqual(result.metadata.get('action'), 'ask_user_question')
def test_ask_user_tool_requests_web_review_when_no_answer_is_queued(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = AskUserRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
ask_user_runtime=runtime,
)
result = execute_tool(
default_tool_registry(),
'ask_user_question',
{
'question': 'Choose mining strategy',
'choices': ['fast sample', 'strict filter'],
'allow_free_text': True,
},
context,
)
self.assertTrue(result.ok)
self.assertEqual(result.metadata.get('action'), 'ask_user_question')
self.assertTrue(result.metadata.get('requires_user_review'))
self.assertIn('Choose mining strategy', result.content)
self.assertIn('fast sample', result.content)
+25
View File
@@ -0,0 +1,25 @@
from __future__ import annotations
import pytest
from fastapi import HTTPException
from agent_platform.auth import decode_openwebui_identity, verify_service_bearer
def test_signed_identity_is_verified(identity_jwt: str, settings) -> None:
identity = decode_openwebui_identity(identity_jwt, settings.openwebui_forward_jwt_secret)
assert identity.user_id == "user-123"
assert identity.email == "user@example.test"
def test_invalid_identity_is_rejected(settings) -> None:
with pytest.raises(HTTPException) as error:
decode_openwebui_identity("not-a-jwt", settings.openwebui_forward_jwt_secret)
assert error.value.status_code == 401
def test_service_bearer_uses_exact_match() -> None:
verify_service_bearer("Bearer expected", "expected")
with pytest.raises(HTTPException) as error:
verify_service_bearer("Bearer expect", "expected")
assert error.value.status_code == 401
-109
View File
@@ -1,109 +0,0 @@
from __future__ import annotations
import os
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
from src.background_runtime import BackgroundSessionRuntime
PROJECT_ROOT = Path(__file__).resolve().parents[1]
class BackgroundRuntimeTests(unittest.TestCase):
def test_runtime_can_launch_and_kill_generic_process(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = BackgroundSessionRuntime(workspace / '.port_sessions' / 'background')
record = runtime.launch(
[sys.executable, '-c', 'import time; time.sleep(10)'],
prompt='sleep',
workspace_cwd=workspace,
model='local/test-model',
process_cwd=workspace,
)
running = runtime.load_record(record.background_id)
killed = runtime.kill(record.background_id)
for _ in range(30):
if runtime.load_record(record.background_id).status != 'running':
break
time.sleep(0.1)
self.assertEqual(running.status, 'running')
self.assertEqual(killed.status, 'killed')
self.assertEqual(killed.stop_reason, 'killed')
def test_agent_background_cli_exposes_ps_logs_and_attach(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
run_dir = Path(tmp_dir)
workspace = run_dir / 'workspace'
workspace.mkdir()
env = os.environ.copy()
existing_pythonpath = env.get('PYTHONPATH')
env['PYTHONPATH'] = (
f'{PROJECT_ROOT}:{existing_pythonpath}'
if existing_pythonpath
else str(PROJECT_ROOT)
)
launch = subprocess.run(
[
sys.executable,
'-m',
'src.main',
'agent-bg',
'/help',
'--cwd',
str(workspace),
],
cwd=run_dir,
env=env,
check=True,
capture_output=True,
text=True,
)
background_id = next(
line.split('=', 1)[1]
for line in launch.stdout.splitlines()
if line.startswith('background_id=')
)
runtime = BackgroundSessionRuntime(run_dir / '.port_sessions' / 'background')
record = runtime.load_record(background_id)
for _ in range(60):
if record.status in {'completed', 'failed', 'exited'}:
break
time.sleep(0.1)
record = runtime.load_record(background_id)
ps = subprocess.run(
[sys.executable, '-m', 'src.main', 'agent-ps'],
cwd=run_dir,
env=env,
check=True,
capture_output=True,
text=True,
)
logs = subprocess.run(
[sys.executable, '-m', 'src.main', 'agent-logs', background_id],
cwd=run_dir,
env=env,
check=True,
capture_output=True,
text=True,
)
attach = subprocess.run(
[sys.executable, '-m', 'src.main', 'agent-attach', background_id],
cwd=run_dir,
env=env,
check=True,
capture_output=True,
text=True,
)
self.assertIn(background_id, launch.stdout)
self.assertIn(background_id, ps.stdout)
self.assertIn('# Background Logs', logs.stdout)
self.assertIn('# Slash Commands', logs.stdout)
self.assertIn('# Background Attach', attach.stdout)
-324
View File
@@ -1,324 +0,0 @@
"""Tests for the bash run_in_background path (local fallback only).
Covers:
- BashBgStore: record_start / mark_completed / mark_auto_resumed race semantics.
- _run_bash_background local fallback: spawns a detached process with file
redirects, registers it via the bg_register callback, output/pid/exit_code
files materialize.
- _run_bash_status / _run_bash_kill: route through bg_status_query / bg_kill_request
callbacks and format their result.
Remote (jupyter_runtime) path is intentionally not exercised here — that
requires a live wsh account and is covered by the e2e checklist in the plan.
"""
from __future__ import annotations
import time
from pathlib import Path
import pytest
from src.agent_tool_core import ToolExecutionContext
from src.agent_tools import (
_run_bash_background,
_run_bash_kill,
_run_bash_status,
)
from src.agent_types import AgentPermissions
from src.bash_bg_store import BashBgStore, BgTaskSpec, BgTaskStatus
def _make_context(
tmp_path: Path,
*,
bg_register=None,
bg_status_query=None,
bg_kill_request=None,
) -> ToolExecutionContext:
return ToolExecutionContext(
root=tmp_path,
command_timeout_seconds=10.0,
max_output_chars=4096,
permissions=AgentPermissions(allow_shell_commands=True),
scratchpad_directory=tmp_path,
bg_register=bg_register,
bg_status_query=bg_status_query,
bg_kill_request=bg_kill_request,
account_id='acct-test',
session_id='sess-test',
run_id='run-test',
)
# ----- BashBgStore --------------------------------------------------------
def test_store_record_and_complete(tmp_path: Path) -> None:
store = BashBgStore(tmp_path / 'bg.db')
spec = BgTaskSpec(
task_id='bg_abc',
account_key='acct',
session_id='sess',
run_id='run',
pid=12345,
task_dir=str(tmp_path / 'bg_abc'),
output_path=str(tmp_path / 'bg_abc' / 'output'),
pid_path=str(tmp_path / 'bg_abc' / 'pid'),
exit_code_path=str(tmp_path / 'bg_abc' / 'exit_code'),
command='echo hi',
started_at=time.time(),
wait_for_completion=True,
)
store.record_start(spec)
row = store.get('bg_abc')
assert row is not None
assert row['status'] == 'running'
assert row['exit_code'] is None
store.mark_completed('bg_abc', exit_code=0)
row = store.get('bg_abc')
assert row['status'] == 'completed'
assert row['exit_code'] == 0
assert row['finished_at'] is not None
def test_store_mark_auto_resumed_is_race_safe(tmp_path: Path) -> None:
store = BashBgStore(tmp_path / 'bg.db')
spec = BgTaskSpec(
task_id='bg_race',
account_key='acct',
session_id='sess',
run_id='run',
pid=1,
task_dir='/tmp/x',
output_path='/tmp/x/output',
pid_path='/tmp/x/pid',
exit_code_path='/tmp/x/exit_code',
command='true',
started_at=time.time(),
wait_for_completion=True,
)
store.record_start(spec)
store.mark_completed('bg_race', exit_code=0)
# First caller wins; subsequent callers always see False.
assert store.mark_auto_resumed('bg_race') is True
assert store.mark_auto_resumed('bg_race') is False
assert store.mark_auto_resumed('bg_race') is False
def test_store_mark_killed_only_running(tmp_path: Path) -> None:
store = BashBgStore(tmp_path / 'bg.db')
spec = BgTaskSpec(
task_id='bg_kill',
account_key='acct',
session_id='sess',
run_id='run',
pid=2,
task_dir='/tmp/y',
output_path='/tmp/y/output',
pid_path='/tmp/y/pid',
exit_code_path='/tmp/y/exit_code',
command='sleep 100',
started_at=time.time(),
wait_for_completion=False,
)
store.record_start(spec)
store.mark_killed('bg_kill')
row = store.get('bg_kill')
assert row['status'] == 'cancelled'
# Re-killing a non-running task is a no-op (status stays cancelled).
store.mark_killed('bg_kill')
row = store.get('bg_kill')
assert row['status'] == 'cancelled'
# ----- _run_bash_background local fallback --------------------------------
def test_run_bash_background_local_spawns_detached(tmp_path: Path) -> None:
captured: list[BgTaskSpec] = []
ctx = _make_context(tmp_path, bg_register=captured.append)
content, metadata = _run_bash_background(
{
'command': 'echo hello-from-bg',
'wait_for_completion': True,
},
ctx,
)
assert metadata['action'] == 'bash_bg'
task_id = metadata['task_id']
assert task_id.startswith('bg_')
assert metadata['wait_for_completion'] is True
assert 'pid=' in content
# bg_register received exactly one spec, with the agent's session/account.
assert len(captured) == 1
spec = captured[0]
assert spec.task_id == task_id
assert spec.account_key == 'acct-test'
assert spec.session_id == 'sess-test'
assert spec.run_id == 'run-test'
assert spec.command == 'echo hello-from-bg'
assert spec.wait_for_completion is True
# Wait for the detached subprocess to finish — bounded by exit_code file.
deadline = time.monotonic() + 5.0
exit_code_path = Path(spec.exit_code_path)
while time.monotonic() < deadline and not exit_code_path.exists():
time.sleep(0.05)
assert exit_code_path.exists(), 'exit_code file did not appear in 5s'
assert exit_code_path.read_text(encoding='utf-8').strip() == '0'
output_text = Path(spec.output_path).read_text(encoding='utf-8')
assert 'hello-from-bg' in output_text
def test_run_bash_background_failed_command_records_nonzero_exit(
tmp_path: Path,
) -> None:
captured: list[BgTaskSpec] = []
ctx = _make_context(tmp_path, bg_register=captured.append)
_content, metadata = _run_bash_background(
{'command': 'exit 7'},
ctx,
)
spec = captured[0]
assert metadata['action'] == 'bash_bg'
deadline = time.monotonic() + 5.0
exit_code_path = Path(spec.exit_code_path)
while time.monotonic() < deadline and not exit_code_path.exists():
time.sleep(0.05)
assert exit_code_path.exists()
assert exit_code_path.read_text(encoding='utf-8').strip() == '7'
def test_run_bash_background_requires_shell_permission(tmp_path: Path) -> None:
from src.agent_tool_core import ToolPermissionError
ctx = ToolExecutionContext(
root=tmp_path,
command_timeout_seconds=10.0,
max_output_chars=4096,
permissions=AgentPermissions(allow_shell_commands=False),
scratchpad_directory=tmp_path,
)
with pytest.raises(ToolPermissionError):
_run_bash_background({'command': 'echo nope'}, ctx)
# ----- bash_status / bash_kill via callbacks ------------------------------
def test_bash_status_routes_through_callback(tmp_path: Path) -> None:
fake_status = BgTaskStatus(
task_id='bg_xyz',
status='completed',
pid=999,
started_at=time.time() - 5.0,
finished_at=time.time(),
exit_code=0,
output_path='/tmp/x/output',
output_preview='all good\n',
auto_resumed=False,
)
queries: list[str] = []
def query(task_id: str) -> BgTaskStatus | None:
queries.append(task_id)
return fake_status
ctx = _make_context(tmp_path, bg_status_query=query)
content, metadata = _run_bash_status({'task_id': 'bg_xyz'}, ctx)
assert queries == ['bg_xyz']
assert metadata['action'] == 'bash_status'
assert metadata['status'] == 'completed'
assert metadata['exit_code'] == 0
assert 'all good' in content
assert 'task_id=bg_xyz' in content
def test_bash_status_unknown_task_raises(tmp_path: Path) -> None:
from src.agent_tool_core import ToolExecutionError
def query(_task_id: str):
return None
ctx = _make_context(tmp_path, bg_status_query=query)
with pytest.raises(ToolExecutionError):
_run_bash_status({'task_id': 'bg_missing'}, ctx)
def test_bash_status_without_callback_raises(tmp_path: Path) -> None:
from src.agent_tool_core import ToolExecutionError
ctx = _make_context(tmp_path)
with pytest.raises(ToolExecutionError):
_run_bash_status({'task_id': 'bg_anything'}, ctx)
def test_bash_kill_routes_through_callback(tmp_path: Path) -> None:
killed: list[str] = []
def kill(task_id: str) -> bool:
killed.append(task_id)
return True
ctx = _make_context(tmp_path, bg_kill_request=kill)
content, metadata = _run_bash_kill({'task_id': 'bg_kk'}, ctx)
assert killed == ['bg_kk']
assert metadata['killed'] is True
assert 'bg_kk' in content
def test_bash_kill_failure_reports(tmp_path: Path) -> None:
ctx = _make_context(tmp_path, bg_kill_request=lambda _t: False)
content, metadata = _run_bash_kill({'task_id': 'bg_dead'}, ctx)
assert metadata['killed'] is False
assert '未能终止' in content
# ----- end-to-end: spawn + register + sync via real BashBgStore -----------
def test_local_bg_lifecycle_with_real_store(tmp_path: Path) -> None:
"""Plug a real BashBgStore into bg_register and check the row appears."""
store = BashBgStore(tmp_path / 'bg.db')
def register(spec: BgTaskSpec) -> None:
store.record_start(spec)
ctx = _make_context(tmp_path, bg_register=register)
_content, metadata = _run_bash_background(
{'command': 'echo lifecycle && exit 0'}, ctx,
)
task_id = metadata['task_id']
row = store.get(task_id)
assert row is not None
assert row['status'] == 'running'
# Wait for child to write exit_code (more reliable than kill -0, since
# the unreaped child becomes a zombie and kill -0 still reports it alive).
deadline = time.monotonic() + 5.0
exit_code_path = Path(row['exit_code_path'])
while time.monotonic() < deadline and not exit_code_path.exists():
time.sleep(0.05)
if not exit_code_path.exists():
pytest.fail('background process did not exit within 5 seconds')
# Simulate manager finalize step.
exit_code = int(
exit_code_path.read_text(encoding='utf-8').strip()
)
store.mark_completed(task_id, exit_code=exit_code)
row = store.get(task_id)
assert row['status'] == 'completed'
assert row['exit_code'] == 0
-878
View File
@@ -1,878 +0,0 @@
"""
Tests for bash_security module.
Tests are organized by validator function, matching the npm test structure.
"""
import pytest
from src.bash_security import (
SecurityBehavior,
SecurityResult,
ValidationContext,
bash_command_is_safe,
check_shell_security,
extract_quoted_content,
get_destructive_command_warning,
has_unescaped_char,
interpret_command_result,
is_command_read_only,
split_command,
strip_safe_redirections,
validate_backslash_escaped_operators,
validate_backslash_escaped_whitespace,
validate_brace_expansion,
validate_carriage_return,
validate_comment_quote_desync,
validate_control_characters,
validate_dangerous_patterns,
validate_dangerous_variables,
validate_empty,
validate_git_commit,
validate_ifs_injection,
validate_incomplete_commands,
validate_jq_command,
validate_mid_word_hash,
validate_newlines,
validate_obfuscated_flags,
validate_proc_environ_access,
validate_quoted_newline,
validate_redirections,
validate_shell_metacharacters,
validate_unicode_whitespace,
validate_zsh_dangerous_commands,
)
# ---- Helper to build a context ----
def _ctx(cmd: str) -> ValidationContext:
"""Build a ValidationContext for the given command."""
base = cmd.strip().split()[0] if cmd.strip() else ''
with_dq, fully_unq, keep_qc = extract_quoted_content(cmd)
return ValidationContext(
original_command=cmd,
base_command=base,
unquoted_content=with_dq,
fully_unquoted_content=strip_safe_redirections(fully_unq),
fully_unquoted_pre_strip=fully_unq,
unquoted_keep_quote_chars=keep_qc,
)
# ===========================================================================
# extract_quoted_content
# ===========================================================================
class TestExtractQuotedContent:
def test_no_quotes(self):
dq, full, kqc = extract_quoted_content('echo hello')
assert dq == 'echo hello'
assert full == 'echo hello'
def test_single_quotes_stripped(self):
dq, full, kqc = extract_quoted_content("echo 'hello world'")
assert 'hello world' not in full
assert 'echo' in full
def test_double_quotes_in_dq_output(self):
dq, full, kqc = extract_quoted_content('echo "hello world"')
assert 'hello world' in dq # double-quoted content preserved in dq
assert 'hello world' not in full # but stripped in fully_unquoted
def test_escape_handling(self):
dq, full, kqc = extract_quoted_content('echo \\$HOME')
assert '$HOME' in full
def test_keep_quote_chars(self):
_, _, kqc = extract_quoted_content("echo 'x'#")
assert "'" in kqc # quote chars preserved
# ===========================================================================
# strip_safe_redirections
# ===========================================================================
class TestStripSafeRedirections:
def test_dev_null_output(self):
assert '>/dev/null' not in strip_safe_redirections('cmd > /dev/null')
def test_stderr_redirect(self):
assert '2>&1' not in strip_safe_redirections('cmd 2>&1')
def test_dev_null_input(self):
assert '</dev/null' not in strip_safe_redirections('cmd < /dev/null')
def test_preserves_other_redirections(self):
result = strip_safe_redirections('cmd > output.txt')
assert '> output.txt' in result
# ===========================================================================
# has_unescaped_char
# ===========================================================================
class TestHasUnescapedChar:
def test_unescaped_backtick(self):
assert has_unescaped_char('echo `date`', '`') is True
def test_escaped_backtick(self):
assert has_unescaped_char('echo \\`safe\\`', '`') is False
def test_double_backslash_then_backtick(self):
# \\\` → \\ (literal backslash) + ` (unescaped)
assert has_unescaped_char('test\\\\`date`', '`') is True
def test_no_match(self):
assert has_unescaped_char('echo hello', '`') is False
# ===========================================================================
# split_command
# ===========================================================================
class TestSplitCommand:
def test_simple(self):
assert split_command('echo hello') == ['echo hello']
def test_semicolon(self):
assert split_command('echo a; echo b') == ['echo a', 'echo b']
def test_and_and(self):
assert split_command('cmd1 && cmd2') == ['cmd1', 'cmd2']
def test_pipe(self):
assert split_command('cat file | grep pattern') == ['cat file', 'grep pattern']
def test_or_or(self):
assert split_command('cmd1 || cmd2') == ['cmd1', 'cmd2']
def test_quotes_preserved(self):
result = split_command("echo 'a; b'")
assert len(result) == 1 # semicolon inside quotes not split
def test_complex(self):
result = split_command('cd /tmp && echo hi; ls | head')
assert len(result) == 4
# ===========================================================================
# validate_empty
# ===========================================================================
class TestValidateEmpty:
def test_empty(self):
assert validate_empty(_ctx('')).behavior == SecurityBehavior.ALLOW
def test_whitespace_only(self):
assert validate_empty(_ctx(' ')).behavior == SecurityBehavior.ALLOW
def test_non_empty(self):
assert validate_empty(_ctx('ls')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_control_characters
# ===========================================================================
class TestValidateControlCharacters:
def test_null_byte(self):
result = validate_control_characters(_ctx('echo\x00hello'))
assert result.behavior == SecurityBehavior.ASK
def test_bell(self):
result = validate_control_characters(_ctx('echo\x07hello'))
assert result.behavior == SecurityBehavior.ASK
def test_clean_command(self):
result = validate_control_characters(_ctx('echo hello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_tab_allowed(self):
result = validate_control_characters(_ctx('echo\thello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_newline_allowed(self):
result = validate_control_characters(_ctx('echo\nhello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_incomplete_commands
# ===========================================================================
class TestValidateIncompleteCommands:
def test_starts_with_tab(self):
result = validate_incomplete_commands(_ctx('\techo hello'))
assert result.behavior == SecurityBehavior.ASK
def test_starts_with_dash(self):
result = validate_incomplete_commands(_ctx('-rf /'))
assert result.behavior == SecurityBehavior.ASK
def test_starts_with_operator(self):
result = validate_incomplete_commands(_ctx('&& echo hello'))
assert result.behavior == SecurityBehavior.ASK
result = validate_incomplete_commands(_ctx('; echo hello'))
assert result.behavior == SecurityBehavior.ASK
def test_normal_command(self):
result = validate_incomplete_commands(_ctx('ls -la'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_git_commit
# ===========================================================================
class TestValidateGitCommit:
def test_not_git(self):
result = validate_git_commit(_ctx('echo hello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_simple_commit(self):
result = validate_git_commit(_ctx("git commit -m 'initial commit'"))
assert result.behavior == SecurityBehavior.ALLOW
def test_double_quoted_commit(self):
result = validate_git_commit(_ctx('git commit -m "fix bug"'))
assert result.behavior == SecurityBehavior.ALLOW
def test_commit_with_substitution(self):
result = validate_git_commit(_ctx('git commit -m "$(date)"'))
assert result.behavior == SecurityBehavior.ASK
def test_commit_with_backtick(self):
result = validate_git_commit(_ctx('git commit -m "`date`"'))
assert result.behavior == SecurityBehavior.ASK
def test_commit_with_chained_commands(self):
result = validate_git_commit(_ctx("git commit -m 'msg'; rm -rf /"))
# Should passthrough (not early-allow) due to ; in remainder
assert result.behavior != SecurityBehavior.ALLOW
def test_commit_with_backslash(self):
result = validate_git_commit(_ctx('git commit -m "test\\"msg"'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_jq_command
# ===========================================================================
class TestValidateJqCommand:
def test_not_jq(self):
assert validate_jq_command(_ctx('echo hi')).behavior == SecurityBehavior.PASSTHROUGH
def test_jq_system(self):
result = validate_jq_command(_ctx('jq "system(\"rm -rf /\")"'))
assert result.behavior == SecurityBehavior.ASK
def test_jq_from_file(self):
result = validate_jq_command(_ctx('jq -f evil.jq'))
assert result.behavior == SecurityBehavior.ASK
def test_jq_slurpfile(self):
result = validate_jq_command(_ctx('jq --slurpfile x data.json'))
assert result.behavior == SecurityBehavior.ASK
def test_safe_jq(self):
result = validate_jq_command(_ctx('jq ".name" data.json'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_obfuscated_flags
# ===========================================================================
class TestValidateObfuscatedFlags:
def test_ansi_c_quoting(self):
result = validate_obfuscated_flags(_ctx("find . $'-exec' evil"))
assert result.behavior == SecurityBehavior.ASK
def test_locale_quoting(self):
result = validate_obfuscated_flags(_ctx('find . $"-exec" evil'))
assert result.behavior == SecurityBehavior.ASK
def test_echo_safe(self):
result = validate_obfuscated_flags(_ctx("echo $'hello'"))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_empty_quotes_before_dash(self):
result = validate_obfuscated_flags(_ctx("find . '' -exec evil"))
assert result.behavior == SecurityBehavior.ASK
def test_quoted_flag(self):
result = validate_obfuscated_flags(_ctx('find . "-exec" rm {} ;'))
assert result.behavior == SecurityBehavior.ASK
def test_normal_command(self):
result = validate_obfuscated_flags(_ctx('ls -la'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_three_consecutive_quotes(self):
result = validate_obfuscated_flags(_ctx("find . '''exec"))
assert result.behavior == SecurityBehavior.ASK
# ===========================================================================
# validate_shell_metacharacters
# ===========================================================================
class TestValidateShellMetacharacters:
def test_semicolon_in_quotes(self):
result = validate_shell_metacharacters(_ctx('echo "a;b"'))
# unquoted_content (with_double_quotes) has the ; inside
assert result.behavior == SecurityBehavior.PASSTHROUGH or result.behavior == SecurityBehavior.ASK
def test_find_name_with_pipe(self):
# Single-quoted pipe is stripped entirely from unquoted content → safe
ctx = _ctx("find . -name '|evil'")
result = validate_shell_metacharacters(ctx)
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_double_quoted_metachar(self):
# Check that we catch metacharacters in unquoted positions
# The npm version checks the double-quote-retained string for
# quoted metacharacters, but we strip quote chars. So we test
# the actual dangerous case: unquoted semicolon
ctx = _ctx('find . -name evil; rm -rf /')
# This won't be caught by this specific validator (it looks for
# metacharacters INSIDE quoted args, not command separators)
result = validate_shell_metacharacters(ctx)
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_clean_command(self):
assert validate_shell_metacharacters(_ctx('ls -la')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_dangerous_variables
# ===========================================================================
class TestValidateDangerousVariables:
def test_variable_in_pipe(self):
result = validate_dangerous_variables(_ctx('$CMD | grep x'))
assert result.behavior == SecurityBehavior.ASK
def test_variable_in_redirect(self):
result = validate_dangerous_variables(_ctx('echo x > $FILE'))
assert result.behavior == SecurityBehavior.ASK
def test_safe_variable(self):
result = validate_dangerous_variables(_ctx('echo $HOME'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_dangerous_patterns
# ===========================================================================
class TestValidateDangerousPatterns:
def test_backtick(self):
result = validate_dangerous_patterns(_ctx('echo `date`'))
assert result.behavior == SecurityBehavior.ASK
def test_dollar_paren(self):
result = validate_dangerous_patterns(_ctx('echo $(date)'))
assert result.behavior == SecurityBehavior.ASK
def test_dollar_brace(self):
result = validate_dangerous_patterns(_ctx('echo ${PATH}'))
assert result.behavior == SecurityBehavior.ASK
def test_process_substitution(self):
result = validate_dangerous_patterns(_ctx('diff <(cmd1) <(cmd2)'))
assert result.behavior == SecurityBehavior.ASK
def test_safe_echo(self):
result = validate_dangerous_patterns(_ctx('echo hello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_escaped_backtick(self):
result = validate_dangerous_patterns(_ctx('echo \\`safe\\`'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_redirections
# ===========================================================================
class TestValidateRedirections:
def test_output_redirect(self):
result = validate_redirections(_ctx('echo x > file.txt'))
assert result.behavior == SecurityBehavior.ASK
def test_input_redirect(self):
result = validate_redirections(_ctx('cat < /etc/passwd'))
assert result.behavior == SecurityBehavior.ASK
def test_dev_null_stripped(self):
# >/dev/null is stripped by strip_safe_redirections
result = validate_redirections(_ctx('cmd > /dev/null'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_no_redirect(self):
result = validate_redirections(_ctx('echo hello'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_newlines
# ===========================================================================
class TestValidateNewlines:
def test_no_newlines(self):
assert validate_newlines(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
def test_newline_with_command(self):
result = validate_newlines(_ctx('echo hello\nrm -rf /'))
assert result.behavior == SecurityBehavior.ASK
def test_backslash_continuation(self):
result = validate_newlines(_ctx('cmd \\\n--flag'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_carriage_return
# ===========================================================================
class TestValidateCarriageReturn:
def test_no_cr(self):
assert validate_carriage_return(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
def test_cr_in_command(self):
result = validate_carriage_return(_ctx('echo hello\rworld'))
assert result.behavior == SecurityBehavior.ASK
assert result.is_misparsing is True
def test_cr_in_double_quotes_safe(self):
result = validate_carriage_return(_ctx('echo "hello\rworld"'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_ifs_injection
# ===========================================================================
class TestValidateIFSInjection:
def test_ifs_variable(self):
result = validate_ifs_injection(_ctx('echo$IFS/etc/passwd'))
assert result.behavior == SecurityBehavior.ASK
def test_ifs_expansion(self):
result = validate_ifs_injection(_ctx('echo ${IFS:0:1}'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_ifs_injection(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_proc_environ_access
# ===========================================================================
class TestValidateProcEnvironAccess:
def test_proc_environ(self):
result = validate_proc_environ_access(_ctx('cat /proc/self/environ'))
assert result.behavior == SecurityBehavior.ASK
def test_proc_pid_environ(self):
result = validate_proc_environ_access(_ctx('cat /proc/1/environ'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_proc_environ_access(_ctx('cat /etc/hosts')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_backslash_escaped_whitespace
# ===========================================================================
class TestValidateBackslashEscapedWhitespace:
def test_escaped_space(self):
result = validate_backslash_escaped_whitespace(_ctx('echo\\ hello'))
assert result.behavior == SecurityBehavior.ASK
def test_escaped_tab(self):
result = validate_backslash_escaped_whitespace(_ctx('echo\\\thello'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_backslash_escaped_whitespace(
_ctx('echo hello')
).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_backslash_escaped_operators
# ===========================================================================
class TestValidateBackslashEscapedOperators:
def test_escaped_semicolon(self):
result = validate_backslash_escaped_operators(_ctx('cat safe.txt \\; echo /etc/passwd'))
assert result.behavior == SecurityBehavior.ASK
def test_escaped_pipe(self):
result = validate_backslash_escaped_operators(_ctx('cmd \\| evil'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_backslash_escaped_operators(
_ctx('ls -la')
).behavior == SecurityBehavior.PASSTHROUGH
def test_inside_quotes_safe(self):
result = validate_backslash_escaped_operators(_ctx("echo '\\;'"))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_brace_expansion
# ===========================================================================
class TestValidateBraceExpansion:
def test_comma_expansion(self):
result = validate_brace_expansion(_ctx('echo {a,b,c}'))
assert result.behavior == SecurityBehavior.ASK
def test_sequence_expansion(self):
result = validate_brace_expansion(_ctx('echo {1..5}'))
assert result.behavior == SecurityBehavior.ASK
def test_no_expansion(self):
result = validate_brace_expansion(_ctx('echo {hello}'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_escaped_brace(self):
result = validate_brace_expansion(_ctx('echo \\{a,b\\}'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_excess_closing_braces(self):
result = validate_brace_expansion(_ctx("git diff {@'{'0},--output=/tmp/pwned}"))
assert result.behavior == SecurityBehavior.ASK
# ===========================================================================
# validate_unicode_whitespace
# ===========================================================================
class TestValidateUnicodeWhitespace:
def test_nbsp(self):
result = validate_unicode_whitespace(_ctx('echo\u00a0hello'))
assert result.behavior == SecurityBehavior.ASK
def test_em_space(self):
result = validate_unicode_whitespace(_ctx('echo\u2003hello'))
assert result.behavior == SecurityBehavior.ASK
def test_clean(self):
assert validate_unicode_whitespace(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_mid_word_hash
# ===========================================================================
class TestValidateMidWordHash:
def test_mid_word_hash(self):
result = validate_mid_word_hash(_ctx('echotest#comment'))
assert result.behavior == SecurityBehavior.ASK
def test_word_start_hash(self):
result = validate_mid_word_hash(_ctx('echo # comment'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_dollar_brace_hash_safe(self):
# ${#var} is bash string length, should be safe
result = validate_mid_word_hash(_ctx('echo ${#var}'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_comment_quote_desync
# ===========================================================================
class TestValidateCommentQuoteDesync:
def test_quote_in_comment(self):
result = validate_comment_quote_desync(_ctx("echo hello # it's a comment"))
assert result.behavior == SecurityBehavior.ASK
def test_clean_comment(self):
result = validate_comment_quote_desync(_ctx('echo hello # clean comment'))
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_no_comment(self):
assert validate_comment_quote_desync(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_quoted_newline
# ===========================================================================
class TestValidateQuotedNewline:
def test_quoted_newline_with_hash(self):
result = validate_quoted_newline(_ctx("echo 'hello\n# dangerous line'"))
assert result.behavior == SecurityBehavior.ASK
def test_no_newline(self):
assert validate_quoted_newline(_ctx('echo hello')).behavior == SecurityBehavior.PASSTHROUGH
def test_newline_without_hash(self):
assert validate_quoted_newline(_ctx("echo 'hello\nworld'")).behavior == SecurityBehavior.PASSTHROUGH
# ===========================================================================
# validate_zsh_dangerous_commands
# ===========================================================================
class TestValidateZshDangerousCommands:
def test_zmodload(self):
result = validate_zsh_dangerous_commands(_ctx('zmodload zsh/system'))
assert result.behavior == SecurityBehavior.ASK
def test_zpty(self):
result = validate_zsh_dangerous_commands(_ctx('zpty cmd echo'))
assert result.behavior == SecurityBehavior.ASK
def test_emulate(self):
result = validate_zsh_dangerous_commands(_ctx('emulate -c evil'))
assert result.behavior == SecurityBehavior.ASK
def test_fc_e(self):
result = validate_zsh_dangerous_commands(_ctx('fc -e vim'))
assert result.behavior == SecurityBehavior.ASK
def test_normal_command(self):
assert validate_zsh_dangerous_commands(_ctx('ls -la')).behavior == SecurityBehavior.PASSTHROUGH
def test_env_var_prefix(self):
result = validate_zsh_dangerous_commands(_ctx('FOO=bar zmodload zsh/system'))
assert result.behavior == SecurityBehavior.ASK
def test_precommand_modifier(self):
result = validate_zsh_dangerous_commands(_ctx('command zmodload zsh/system'))
assert result.behavior == SecurityBehavior.ASK
# ===========================================================================
# bash_command_is_safe (integration)
# ===========================================================================
class TestBashCommandIsSafe:
def test_empty_command(self):
result = bash_command_is_safe('')
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_simple_ls(self):
result = bash_command_is_safe('ls -la')
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_simple_echo(self):
result = bash_command_is_safe('echo hello world')
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_command_substitution_blocked(self):
result = bash_command_is_safe('echo $(cat /etc/passwd)')
assert result.behavior == SecurityBehavior.ASK
def test_backtick_blocked(self):
result = bash_command_is_safe('echo `date`')
assert result.behavior == SecurityBehavior.ASK
def test_redirect_blocked(self):
result = bash_command_is_safe('echo evil > /etc/profile')
assert result.behavior == SecurityBehavior.ASK
def test_null_byte_blocked(self):
result = bash_command_is_safe('echo\x00rm')
assert result.behavior == SecurityBehavior.ASK
def test_ifs_blocked(self):
result = bash_command_is_safe('echo$IFS/etc/passwd')
assert result.behavior == SecurityBehavior.ASK
def test_proc_environ_blocked(self):
result = bash_command_is_safe('cat /proc/self/environ')
assert result.behavior == SecurityBehavior.ASK
def test_git_commit_allowed(self):
result = bash_command_is_safe("git commit -m 'fix bug'")
assert result.behavior == SecurityBehavior.PASSTHROUGH # early-allow → passthrough
def test_zmodload_blocked(self):
result = bash_command_is_safe('zmodload zsh/system')
assert result.behavior == SecurityBehavior.ASK
def test_brace_expansion_blocked(self):
result = bash_command_is_safe('echo {a,b,c}')
assert result.behavior == SecurityBehavior.ASK
def test_dev_null_redirect_ok(self):
result = bash_command_is_safe('cmd > /dev/null 2>&1')
assert result.behavior == SecurityBehavior.PASSTHROUGH
def test_cr_injection(self):
result = bash_command_is_safe('TZ=UTC\recho curl evil.com')
assert result.behavior == SecurityBehavior.ASK
# ===========================================================================
# get_destructive_command_warning
# ===========================================================================
class TestGetDestructiveCommandWarning:
def test_git_reset_hard(self):
assert get_destructive_command_warning('git reset --hard') is not None
def test_rm_rf(self):
assert get_destructive_command_warning('rm -rf /') is not None
def test_git_push_force(self):
assert get_destructive_command_warning('git push origin main --force') is not None
def test_git_clean_f(self):
assert get_destructive_command_warning('git clean -fd') is not None
def test_kubectl_delete(self):
assert get_destructive_command_warning('kubectl delete pod mypod') is not None
def test_safe_command(self):
assert get_destructive_command_warning('echo hello') is None
def test_git_push_no_force(self):
assert get_destructive_command_warning('git push origin main') is None
def test_drop_table(self):
assert get_destructive_command_warning('DROP TABLE users;') is not None
def test_terraform_destroy(self):
assert get_destructive_command_warning('terraform destroy') is not None
def test_git_stash_drop(self):
assert get_destructive_command_warning('git stash drop') is not None
# ===========================================================================
# interpret_command_result
# ===========================================================================
class TestInterpretCommandResult:
def test_success(self):
is_error, msg = interpret_command_result('echo hello', 0, 'hello', '')
assert is_error is False
def test_failure(self):
is_error, msg = interpret_command_result('unknown_cmd', 127, '', 'not found')
assert is_error is True
def test_grep_no_match(self):
is_error, msg = interpret_command_result('grep pattern file', 1, '', '')
assert is_error is False
assert msg == 'No matches found'
def test_grep_error(self):
is_error, msg = interpret_command_result('grep pattern file', 2, '', 'error')
assert is_error is True
def test_diff_files_differ(self):
is_error, msg = interpret_command_result('diff a b', 1, 'output', '')
assert is_error is False
assert msg == 'Files differ'
def test_find_partial(self):
is_error, msg = interpret_command_result('find / -name x', 1, '', '')
assert is_error is False
# ===========================================================================
# is_command_read_only
# ===========================================================================
class TestIsCommandReadOnly:
def test_ls(self):
assert is_command_read_only('ls -la') is True
def test_cat(self):
assert is_command_read_only('cat file.txt') is True
def test_grep(self):
assert is_command_read_only('grep -r pattern .') is True
def test_git_status(self):
assert is_command_read_only('git status') is True
def test_git_log(self):
assert is_command_read_only('git log --oneline') is True
def test_git_push(self):
assert is_command_read_only('git push') is False
def test_rm(self):
assert is_command_read_only('rm file.txt') is False
def test_sed_read_only(self):
assert is_command_read_only("sed -n '1,5p' file") is True
def test_sed_in_place(self):
assert is_command_read_only("sed -i 's/old/new/' file") is False
def test_find_safe(self):
assert is_command_read_only('find . -name "*.py"') is True
def test_find_exec(self):
assert is_command_read_only('find . -exec rm {} ;') is False
def test_find_delete(self):
assert is_command_read_only('find . -name "*.tmp" -delete') is False
def test_echo(self):
assert is_command_read_only('echo hello') is True
def test_python_version(self):
assert is_command_read_only('python --version') is True
def test_unknown_command(self):
assert is_command_read_only('some_random_command') is False
# ===========================================================================
# check_shell_security (integration)
# ===========================================================================
class TestCheckShellSecurity:
def test_shell_disabled(self):
allowed, msg = check_shell_security('ls', allow_shell=False)
assert allowed is False
assert 'disabled' in msg.lower()
def test_safe_command_allowed(self):
allowed, msg = check_shell_security('ls -la')
assert allowed is True
def test_destructive_blocked(self):
allowed, msg = check_shell_security('rm -rf /', allow_destructive=False)
assert allowed is False
assert 'destructive' in msg.lower()
def test_destructive_allowed_when_enabled(self):
allowed, msg = check_shell_security('rm -rf /tmp/test', allow_destructive=True)
# rm -rf still triggers destructive check, but allow_destructive=True skips it
# However rm -rf may also trigger the security check for force-remove
# Let's check: the main security check should pass (no injection)
# and destructive should be allowed
assert allowed is True
def test_injection_blocked(self):
allowed, msg = check_shell_security('echo `evil`')
assert allowed is False
assert 'backtick' in msg.lower() or 'security' in msg.lower()
def test_misparsing_always_blocked(self):
allowed, msg = check_shell_security('echo\x00rm')
assert allowed is False
def test_safe_git_commit(self):
allowed, msg = check_shell_security("git commit -m 'fix'")
assert allowed is True
-136
View File
@@ -1,136 +0,0 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from benchmarks.suites.base import BenchmarkResult, BenchmarkSuite
from benchmarks.suites.gsm8k import GSM8KBenchmark
from benchmarks.suites.humaneval import HumanEvalBenchmark
class _DummyBenchmark(BenchmarkSuite):
name = "DummySuite"
description = "dummy"
category = "coding"
def __init__(self, *, pass_result: bool, **kwargs: object) -> None:
super().__init__(**kwargs)
self._pass_result = pass_result
def load_dataset(self) -> list[dict[str, object]]:
return [{"id": "dummy/0", "value": 1}]
def build_prompt(self, problem: dict[str, object]) -> str:
del problem
return "write solution.py"
def setup_workspace(self, problem: dict[str, object], workspace: str) -> None:
del problem
Path(workspace, "input.txt").write_text("fixture", encoding="utf-8")
def run_agent(self, instruction: str, workspace: str) -> tuple[int, str, float]:
del instruction
Path(workspace, "solution.py").write_text("print('hello')\n", encoding="utf-8")
return 0, "agent completed", 0.1
def evaluate(self, problem: dict[str, object], workspace: str) -> BenchmarkResult:
del problem, workspace
if self._pass_result:
return BenchmarkResult(problem_id="dummy/0", passed=True)
return BenchmarkResult(problem_id="dummy/0", passed=False, error="boom")
class BenchmarkArtifactTests(unittest.TestCase):
def test_failed_problem_saves_artifacts(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
suite = _DummyBenchmark(
pass_result=False,
artifacts_dir=tmp_dir,
)
report = suite.run_all()
result = report.results[0]
artifact_path = result.metadata.get("artifact_path")
self.assertIsInstance(artifact_path, str)
artifact_root = Path(artifact_path)
self.assertTrue((artifact_root / "problem.json").exists())
self.assertTrue((artifact_root / "prompt.txt").exists())
self.assertTrue((artifact_root / "agent_output.txt").exists())
self.assertTrue((artifact_root / "result.json").exists())
self.assertTrue((artifact_root / "workspace" / "solution.py").exists())
payload = json.loads((artifact_root / "result.json").read_text(encoding="utf-8"))
self.assertEqual(payload["agent_exit_code"], 0)
self.assertFalse(payload["passed"])
def test_passing_problem_not_saved_by_default(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
suite = _DummyBenchmark(
pass_result=True,
artifacts_dir=tmp_dir,
)
report = suite.run_all()
result = report.results[0]
self.assertNotIn("artifact_path", result.metadata)
def test_passing_problem_saved_when_enabled(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
suite = _DummyBenchmark(
pass_result=True,
artifacts_dir=tmp_dir,
save_passing_artifacts=True,
)
report = suite.run_all()
result = report.results[0]
artifact_path = result.metadata.get("artifact_path")
self.assertIsInstance(artifact_path, str)
self.assertTrue(Path(str(artifact_path)).exists())
def test_humaneval_recovers_solution_from_chat_code_block(self) -> None:
class _RecoveringHumanEval(HumanEvalBenchmark):
def run_agent(self, instruction: str, workspace: str) -> tuple[int, str, float]:
del instruction, workspace
output = """Here is the implementation:
```python
from typing import List
def has_close_elements(numbers: List[float], threshold: float) -> bool:
for i in range(len(numbers)):
for j in range(i + 1, len(numbers)):
if abs(numbers[i] - numbers[j]) < threshold:
return True
return False
```
"""
return 0, output, 0.1
with tempfile.TemporaryDirectory() as tmp_dir:
suite = _RecoveringHumanEval(
data_dir=str(Path(tmp_dir) / "missing"),
limit=1,
)
report = suite.run_all()
result = report.results[0]
self.assertTrue(result.passed)
self.assertTrue(result.metadata.get("recovered_solution_from_output"))
def test_gsm8k_recovers_answer_from_chat_output(self) -> None:
class _RecoveringGSM8K(GSM8KBenchmark):
def run_agent(self, instruction: str, workspace: str) -> tuple[int, str, float]:
del instruction, workspace
return 0, "The answer is 18.", 0.1
with tempfile.TemporaryDirectory() as tmp_dir:
suite = _RecoveringGSM8K(
data_dir=str(Path(tmp_dir) / "missing"),
limit=1,
)
report = suite.run_all()
result = report.results[0]
self.assertTrue(result.passed)
self.assertTrue(result.metadata.get("recovered_answer_from_output"))
if __name__ == "__main__":
unittest.main()
-177
View File
@@ -1,177 +0,0 @@
from __future__ import annotations
import gzip
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from benchmarks.download_datasets import (
_download_gsm8k,
_download_humaneval,
_extract_gsm8k_answer,
_extract_math_answer,
_fetch_hf_rows,
_write_manifest,
prepare_suite,
)
class BenchmarkDatasetDownloadTests(unittest.TestCase):
def test_extract_gsm8k_answer_uses_hash_marker(self) -> None:
self.assertEqual(_extract_gsm8k_answer("work #### 42"), "42")
self.assertEqual(_extract_gsm8k_answer("Total is 1,234 dollars"), "1234")
def test_extract_math_answer_prefers_boxed_content(self) -> None:
solution = "We compute everything and get \\boxed{\\frac{3}{8}} as the result."
self.assertEqual(_extract_math_answer(solution), "3/8")
def test_fetch_hf_rows_paginates(self) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_fetcher(
endpoint: str,
params: dict[str, object],
headers: dict[str, str] | None,
timeout: float,
) -> object:
del headers, timeout
calls.append((endpoint, dict(params)))
if endpoint == "splits":
return {
"splits": [
{"dataset": "demo", "config": "main", "split": "test"},
]
}
if params["offset"] == 0:
return {
"rows": [{"row": {"value": 1}}, {"row": {"value": 2}}],
"num_rows_total": 3,
}
return {
"rows": [{"row": {"value": 3}}],
"num_rows_total": 3,
}
rows = _fetch_hf_rows(
"demo",
config_preference=("main",),
split_preference=("test",),
json_fetcher=fake_fetcher,
)
self.assertEqual(rows, [{"value": 1}, {"value": 2}, {"value": 3}])
self.assertEqual([call[0] for call in calls], ["splits", "rows", "rows"])
def test_download_gsm8k_normalizes_answers(self) -> None:
def fake_fetcher(
endpoint: str,
params: dict[str, object],
headers: dict[str, str] | None,
timeout: float,
) -> object:
del headers, timeout
if endpoint == "splits":
return {
"splits": [
{"dataset": "openai/gsm8k", "config": "main", "split": "test"},
]
}
self.assertEqual(params["dataset"], "openai/gsm8k")
return {
"rows": [
{"row": {"question": "q1", "answer": "reasoning #### 12"}},
{"row": {"question": "q2", "answer": "Total = 1,004"}},
],
"num_rows_total": 2,
}
with tempfile.TemporaryDirectory() as tmp_dir:
output_path = Path(tmp_dir) / "gsm8k.jsonl"
result = _download_gsm8k(
output_path,
timeout=1.0,
json_fetcher=fake_fetcher,
)
self.assertEqual(result.rows, 2)
rows = [json.loads(line) for line in output_path.read_text().splitlines()]
self.assertEqual(rows[0]["answer"], "12")
self.assertEqual(rows[1]["answer"], "1004")
def test_download_humaneval_decompresses_gzip_payload(self) -> None:
payload = gzip.compress(
(
json.dumps(
{
"task_id": "HumanEval/0",
"prompt": "def f():\n pass\n",
"canonical_solution": " return 1\n",
"test": "def check(candidate):\n assert True\n",
"entry_point": "f",
}
)
+ "\n"
).encode("utf-8")
)
with tempfile.TemporaryDirectory() as tmp_dir:
output_path = Path(tmp_dir) / "humaneval.jsonl"
with patch(
"benchmarks.download_datasets.fetch_bytes",
return_value=payload,
):
result = _download_humaneval(output_path, timeout=1.0)
self.assertEqual(result.rows, 1)
rows = [json.loads(line) for line in output_path.read_text().splitlines()]
self.assertEqual(rows[0]["task_id"], "HumanEval/0")
def test_prepare_suite_exports_builtin_only_suite(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
result = prepare_suite(
"ifeval",
data_dir=Path(tmp_dir),
force=True,
builtin_only=False,
official_only=False,
timeout=1.0,
)
output_path = Path(tmp_dir) / "ifeval.jsonl"
self.assertEqual(result.source, "builtin")
self.assertTrue(output_path.exists())
self.assertGreater(result.rows, 0)
def test_prepare_suite_falls_back_to_builtin_when_official_download_fails(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
with patch(
"benchmarks.download_datasets._download_mbpp",
side_effect=RuntimeError("network down"),
):
result = prepare_suite(
"mbpp",
data_dir=Path(tmp_dir),
force=True,
builtin_only=False,
official_only=False,
timeout=1.0,
)
self.assertEqual(result.source, "builtin-fallback")
self.assertIn("official download failed", result.note)
self.assertTrue((Path(tmp_dir) / "mbpp.jsonl").exists())
def test_write_manifest_writes_results(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
result = prepare_suite(
"aime",
data_dir=Path(tmp_dir),
force=True,
builtin_only=False,
official_only=False,
timeout=1.0,
)
manifest_path = _write_manifest(Path(tmp_dir), [result])
payload = json.loads(manifest_path.read_text())
self.assertEqual(payload["results"][0]["suite"], "aime")
if __name__ == "__main__":
unittest.main()
-42
View File
@@ -1,42 +0,0 @@
from __future__ import annotations
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from benchmarks.suites.base import make_temp_workspace, resolve_temp_root
class BenchmarkTempWorkspaceTests(unittest.TestCase):
def test_make_temp_workspace_sanitizes_suite_and_problem_ids(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
with patch(
"benchmarks.suites.base.tempfile.gettempdir",
return_value=tmp_dir,
):
workspace = make_temp_workspace("claw", "HumanEval", "HumanEval/0")
try:
workspace_path = Path(workspace)
self.assertTrue(workspace_path.is_dir())
self.assertEqual(workspace_path.parent, Path(tmp_dir))
self.assertNotIn("/", workspace_path.name)
self.assertIn("HumanEval_0", workspace_path.name)
finally:
shutil.rmtree(workspace, ignore_errors=True)
def test_resolve_temp_root_creates_missing_directory(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
missing_root = Path(tmp_dir) / "nested" / "tmp"
with patch(
"benchmarks.suites.base.tempfile.gettempdir",
return_value=str(missing_root),
):
resolved = resolve_temp_root()
self.assertEqual(resolved, missing_root.resolve())
self.assertTrue(missing_root.is_dir())
if __name__ == "__main__":
unittest.main()
+20
View File
@@ -0,0 +1,20 @@
from agent_platform.bootstrap import _model_payload
def test_bootstrap_models_override_provider_ids_and_are_public() -> None:
models = _model_payload()
assert len(models) == 6
assert all(model["base_model_id"] is None for model in models)
assert all(
model["access_grants"] == [{"principal_type": "user", "principal_id": "*", "permission": "read"}]
for model in models
)
chat = next(model for model in models if model["id"] == "chat-medium")
assert chat["params"]["function_calling"] == "native"
assert chat["meta"]["toolIds"] == ["server:workspace"]
assert chat["meta"]["capabilities"]["builtin_tools"] is False
work = next(model for model in models if model["id"] == "work-medium")
assert work["params"] == {}
assert work["meta"]["toolIds"] == []
-155
View File
@@ -1,155 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from src.bundled_skills import (
find_bundled_skill,
format_skills_for_system_prompt,
get_bundled_skills,
load_directory_skills,
load_project_skills,
)
from src.agent_runtime import LocalCodingAgent
from src.agent_types import AgentRuntimeConfig, ModelConfig
class BundledSkillsTests(unittest.TestCase):
def test_directory_skills_are_loaded_with_metadata(self) -> None:
skills = {skill.name: skill for skill in get_bundled_skills()}
self.assertIn('verify', skills)
self.assertEqual(skills['verify'].source, 'directory')
self.assertIn('bash', skills['verify'].allowed_tools)
def test_directory_skill_aliases_are_resolved(self) -> None:
skill = find_bundled_skill('config-help')
self.assertIsNotNone(skill)
assert skill is not None
self.assertEqual(skill.name, 'update-config')
self.assertEqual(skill.source, 'directory')
def test_directory_skill_prompt_appends_invocation_args(self) -> None:
skill = find_bundled_skill('verify')
self.assertIsNotNone(skill)
assert skill is not None
prompt = skill.get_prompt(None, 'focus: tests') # type: ignore[arg-type]
self.assertIn('Verify that the recent code changes work correctly.', prompt)
self.assertIn('## Invocation Arguments', prompt)
self.assertIn('focus: tests', prompt)
def test_custom_directory_skill_loader(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
skill_dir = root / 'sample'
skill_dir.mkdir()
(skill_dir / 'SKILL.md').write_text(
(
'---\n'
'name: sample\n'
'description: Sample directory skill.\n'
'aliases: s1, s2\n'
'allowed_tools: read_file, write_file\n'
'---\n'
'Use sample instructions.\n'
),
encoding='utf-8',
)
skills = load_directory_skills(root)
self.assertEqual(len(skills), 1)
self.assertEqual(skills[0].name, 'sample')
self.assertEqual(skills[0].aliases, ('s1', 's2'))
self.assertEqual(skills[0].allowed_tools, ('read_file', 'write_file'))
def test_skills_prompt_includes_directory_skill(self) -> None:
rendered = format_skills_for_system_prompt()
self.assertIn('verify', rendered)
self.assertIn('update-config', rendered)
def test_skills_prompt_respects_enabled_skill_filter(self) -> None:
rendered = format_skills_for_system_prompt(
enabled_skill_names=('verify', 'product-data')
)
self.assertIn('verify', rendered)
self.assertIn('product-data', rendered)
self.assertNotIn('online-mining', rendered)
def test_project_skills_are_loaded_from_workspace_skills_directory(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
skill_dir = workspace / 'skills' / 'project-skill'
skill_dir.mkdir(parents=True)
(skill_dir / 'SKILL.md').write_text(
(
'---\n'
'name: project-skill\n'
'description: Project-maintained skill.\n'
'aliases: local-skill\n'
'allowed_tools: read_file\n'
'---\n'
'Project skill body.\n'
),
encoding='utf-8',
)
skills = get_bundled_skills(workspace)
resolved = find_bundled_skill('local-skill', cwd=workspace)
by_name = {skill.name: skill for skill in skills}
self.assertIn('project-skill', by_name)
self.assertEqual(by_name['project-skill'].source, 'project')
self.assertIsNotNone(resolved)
assert resolved is not None
self.assertEqual(resolved.name, 'project-skill')
def test_project_skills_override_bundled_directory_skills(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
skill_dir = workspace / 'skills' / 'verify'
skill_dir.mkdir(parents=True)
(skill_dir / 'SKILL.md').write_text(
(
'---\n'
'name: verify\n'
'description: Project override for verify.\n'
'---\n'
'Project verify body.\n'
),
encoding='utf-8',
)
skill = find_bundled_skill('verify', cwd=workspace)
self.assertIsNotNone(skill)
assert skill is not None
self.assertEqual(skill.source, 'project')
self.assertIn('Project verify body.', skill.get_prompt(None, '')) # type: ignore[arg-type]
def test_load_project_skills_returns_empty_without_skills_directory(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
self.assertEqual(load_project_skills(Path(tmp_dir)), ())
def test_agent_skill_tool_executes_project_skill_from_cwd(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
skill_dir = workspace / 'skills' / 'project-run'
skill_dir.mkdir(parents=True)
(skill_dir / 'SKILL.md').write_text(
(
'---\n'
'name: project-run\n'
'description: Project runnable skill.\n'
'---\n'
'Run project skill body.\n'
),
encoding='utf-8',
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='test'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
result = agent._execute_skill({'skill': 'project-run', 'args': 'demo'})
self.assertTrue(result.ok)
self.assertIn('Run project skill body.', result.content)
self.assertEqual(result.metadata.get('source'), 'project')
self.assertIn('SKILL.md', str(result.metadata.get('skill_path')))
if __name__ == '__main__':
unittest.main()
-198
View File
@@ -1,198 +0,0 @@
import unittest
from unittest.mock import patch
from src.command_graph import CommandGraph, build_command_graph
from src.models import PortingModule
def _module(name: str, source_hint: str) -> PortingModule:
return PortingModule(name=name, responsibility="stub", source_hint=source_hint)
class CommandGraphTests(unittest.TestCase):
# -- construction & immutability ------------------------------------------
def test_empty_graph(self) -> None:
graph = CommandGraph(builtins=(), plugin_like=(), skill_like=())
self.assertEqual(graph.builtins, ())
self.assertEqual(graph.plugin_like, ())
self.assertEqual(graph.skill_like, ())
def test_fields_are_tuples(self) -> None:
b = (_module("b1", "core/b1.ts"),)
p = (_module("p1", "plugin/p1.ts"),)
s = (_module("s1", "skills/s1.ts"),)
graph = CommandGraph(builtins=b, plugin_like=p, skill_like=s)
self.assertIsInstance(graph.builtins, tuple)
self.assertIsInstance(graph.plugin_like, tuple)
self.assertIsInstance(graph.skill_like, tuple)
def test_frozen_dataclass_rejects_mutation(self) -> None:
graph = CommandGraph(builtins=(), plugin_like=(), skill_like=())
with self.assertRaises(AttributeError):
graph.builtins = () # type: ignore[misc]
# -- flattened -------------------------------------------------------------
def test_flattened_combines_all_categories(self) -> None:
b = (_module("b1", "core/b1.ts"),)
p = (_module("p1", "plugin/p1.ts"),)
s = (_module("s1", "skills/s1.ts"),)
graph = CommandGraph(builtins=b, plugin_like=p, skill_like=s)
self.assertEqual(graph.flattened(), b + p + s)
def test_flattened_preserves_order(self) -> None:
b1 = _module("b1", "core/b1.ts")
b2 = _module("b2", "core/b2.ts")
p1 = _module("p1", "plugin/p1.ts")
s1 = _module("s1", "skills/s1.ts")
graph = CommandGraph(builtins=(b1, b2), plugin_like=(p1,), skill_like=(s1,))
self.assertEqual(graph.flattened(), (b1, b2, p1, s1))
def test_flattened_of_empty_graph_returns_empty_tuple(self) -> None:
graph = CommandGraph(builtins=(), plugin_like=(), skill_like=())
self.assertEqual(graph.flattened(), ())
def test_flattened_length_is_sum_of_categories(self) -> None:
b = (_module("b1", "core/b1.ts"), _module("b2", "core/b2.ts"))
p = (_module("p1", "plugin/p1.ts"),)
s = ()
graph = CommandGraph(builtins=b, plugin_like=p, skill_like=s)
self.assertEqual(len(graph.flattened()), 3)
# -- as_markdown -----------------------------------------------------------
def test_as_markdown_includes_header(self) -> None:
graph = CommandGraph(builtins=(), plugin_like=(), skill_like=())
md = graph.as_markdown()
self.assertIn("# Command Graph", md)
def test_as_markdown_includes_counts(self) -> None:
b = (_module("b1", "core/b1.ts"), _module("b2", "core/b2.ts"))
p = (_module("p1", "plugin/p1.ts"),)
s = (_module("s1", "skills/s1.ts"), _module("s2", "skills/s2.ts"), _module("s3", "skills/s3.ts"))
graph = CommandGraph(builtins=b, plugin_like=p, skill_like=s)
md = graph.as_markdown()
self.assertIn("Builtins: 2", md)
self.assertIn("Plugin-like commands: 1", md)
self.assertIn("Skill-like commands: 3", md)
def test_as_markdown_empty_counts(self) -> None:
graph = CommandGraph(builtins=(), plugin_like=(), skill_like=())
md = graph.as_markdown()
self.assertIn("Builtins: 0", md)
self.assertIn("Plugin-like commands: 0", md)
self.assertIn("Skill-like commands: 0", md)
def test_as_markdown_returns_string(self) -> None:
graph = CommandGraph(builtins=(), plugin_like=(), skill_like=())
self.assertIsInstance(graph.as_markdown(), str)
class BuildCommandGraphTests(unittest.TestCase):
# -- return type -----------------------------------------------------------
@patch("src.command_graph.get_commands")
def test_returns_command_graph(self, mock_get: unittest.mock.MagicMock) -> None:
mock_get.return_value = ()
result = build_command_graph()
self.assertIsInstance(result, CommandGraph)
# -- categorization --------------------------------------------------------
@patch("src.command_graph.get_commands")
def test_plugin_source_goes_to_plugin_like(self, mock_get: unittest.mock.MagicMock) -> None:
p = _module("p1", "plugin/p1.ts")
mock_get.return_value = (p,)
graph = build_command_graph()
self.assertIn(p, graph.plugin_like)
self.assertNotIn(p, graph.builtins)
self.assertNotIn(p, graph.skill_like)
@patch("src.command_graph.get_commands")
def test_skills_source_goes_to_skill_like(self, mock_get: unittest.mock.MagicMock) -> None:
s = _module("s1", "skills/s1.ts")
mock_get.return_value = (s,)
graph = build_command_graph()
self.assertIn(s, graph.skill_like)
self.assertNotIn(s, graph.builtins)
self.assertNotIn(s, graph.plugin_like)
@patch("src.command_graph.get_commands")
def test_plain_source_goes_to_builtins(self, mock_get: unittest.mock.MagicMock) -> None:
b = _module("b1", "core/b1.ts")
mock_get.return_value = (b,)
graph = build_command_graph()
self.assertIn(b, graph.builtins)
self.assertNotIn(b, graph.plugin_like)
self.assertNotIn(b, graph.skill_like)
@patch("src.command_graph.get_commands")
def test_case_insensitive_plugin_match(self, mock_get: unittest.mock.MagicMock) -> None:
p = _module("p1", "Plugin/p1.ts")
mock_get.return_value = (p,)
graph = build_command_graph()
self.assertIn(p, graph.plugin_like)
@patch("src.command_graph.get_commands")
def test_case_insensitive_skills_match(self, mock_get: unittest.mock.MagicMock) -> None:
s = _module("s1", "Skills/s1.ts")
mock_get.return_value = (s,)
graph = build_command_graph()
self.assertIn(s, graph.skill_like)
@patch("src.command_graph.get_commands")
def test_mixed_commands_are_sorted_correctly(self, mock_get: unittest.mock.MagicMock) -> None:
b = _module("b1", "core/b1.ts")
p = _module("p1", "plugin/p1.ts")
s = _module("s1", "skills/s1.ts")
mock_get.return_value = (b, p, s)
graph = build_command_graph()
self.assertEqual(graph.builtins, (b,))
self.assertEqual(graph.plugin_like, (p,))
self.assertEqual(graph.skill_like, (s,))
@patch("src.command_graph.get_commands")
def test_empty_commands_yields_empty_graph(self, mock_get: unittest.mock.MagicMock) -> None:
mock_get.return_value = ()
graph = build_command_graph()
self.assertEqual(graph.builtins, ())
self.assertEqual(graph.plugin_like, ())
self.assertEqual(graph.skill_like, ())
@patch("src.command_graph.get_commands")
def test_all_builtins(self, mock_get: unittest.mock.MagicMock) -> None:
b1 = _module("b1", "core/b1.ts")
b2 = _module("b2", "commands/b2.ts")
mock_get.return_value = (b1, b2)
graph = build_command_graph()
self.assertEqual(len(graph.builtins), 2)
self.assertEqual(graph.plugin_like, ())
self.assertEqual(graph.skill_like, ())
@patch("src.command_graph.get_commands")
def test_builtins_plugin_skill_are_tuples_of_porting_module(self, mock_get: unittest.mock.MagicMock) -> None:
b = _module("b1", "core/b1.ts")
p = _module("p1", "plugin/p1.ts")
s = _module("s1", "skills/s1.ts")
mock_get.return_value = (b, p, s)
graph = build_command_graph()
for category in (graph.builtins, graph.plugin_like, graph.skill_like):
self.assertIsInstance(category, tuple)
for item in category:
self.assertIsInstance(item, PortingModule)
@patch("src.command_graph.get_commands")
def test_flattened_matches_original_commands(self, mock_get: unittest.mock.MagicMock) -> None:
modules = (
_module("b1", "core/b1.ts"),
_module("p1", "plugin/p1.ts"),
_module("s1", "skills/s1.ts"),
)
mock_get.return_value = modules
graph = build_command_graph()
self.assertEqual(set(graph.flattened()), set(modules))
if __name__ == "__main__":
unittest.main()
-409
View File
@@ -1,409 +0,0 @@
"""Tests for src/compact.py – the conversation compaction service."""
from __future__ import annotations
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
from unittest.mock import MagicMock
from src.agent_runtime import LocalCodingAgent
from src.agent_session import AgentMessage, AgentSessionState
from src.agent_types import AgentRuntimeConfig, ModelConfig
from src.compact import (
AUTOCOMPACT_BUFFER_TOKENS,
ERROR_INCOMPLETE_RESPONSE,
ERROR_NOT_ENOUGH_MESSAGES,
CompactionResult,
compact_conversation,
format_compact_summary,
get_compact_prompt,
get_compact_user_summary_message,
)
class TestGetCompactPrompt(unittest.TestCase):
"""Tests for the compact prompt builder."""
def test_basic_prompt_contains_all_nine_sections(self) -> None:
prompt = get_compact_prompt()
for section in [
'1. Primary Request and Intent',
'2. Key Technical Concepts',
'3. Files and Code Sections',
'4. Errors and fixes',
'5. Problem Solving',
'6. All user messages',
'7. Pending Tasks',
'8. Current Work',
'9. Optional Next Step',
]:
self.assertIn(section, prompt, f'Missing section: {section}')
def test_no_tools_preamble_present(self) -> None:
prompt = get_compact_prompt()
self.assertIn('CRITICAL: Respond with TEXT ONLY', prompt)
self.assertIn('Do NOT call any tools', prompt)
def test_no_tools_trailer_present(self) -> None:
prompt = get_compact_prompt()
self.assertIn('REMINDER: Do NOT call any tools', prompt)
def test_analysis_and_summary_example_tags_present(self) -> None:
prompt = get_compact_prompt()
self.assertIn('<analysis>', prompt)
self.assertIn('</analysis>', prompt)
self.assertIn('<summary>', prompt)
self.assertIn('</summary>', prompt)
def test_custom_instructions_appended(self) -> None:
prompt = get_compact_prompt('Focus on database changes.')
self.assertIn('Additional Instructions:', prompt)
self.assertIn('Focus on database changes.', prompt)
def test_empty_custom_instructions_ignored(self) -> None:
prompt_no_custom = get_compact_prompt()
prompt_empty = get_compact_prompt('')
prompt_whitespace = get_compact_prompt(' ')
self.assertEqual(prompt_no_custom, prompt_empty)
self.assertEqual(prompt_no_custom, prompt_whitespace)
def test_none_custom_instructions_ignored(self) -> None:
prompt_none = get_compact_prompt(None)
prompt_no_arg = get_compact_prompt()
self.assertEqual(prompt_none, prompt_no_arg)
class TestFormatCompactSummary(unittest.TestCase):
"""Tests for the summary formatting / XML stripping."""
def test_strips_analysis_block(self) -> None:
raw = '<analysis>thinking here</analysis>\n\n<summary>result</summary>'
formatted = format_compact_summary(raw)
self.assertNotIn('<analysis>', formatted)
self.assertNotIn('thinking here', formatted)
self.assertIn('result', formatted)
def test_unwraps_summary_tags(self) -> None:
raw = '<summary>The main points.\n1. First</summary>'
formatted = format_compact_summary(raw)
self.assertNotIn('<summary>', formatted)
self.assertNotIn('</summary>', formatted)
self.assertIn('Summary:', formatted)
self.assertIn('The main points.', formatted)
def test_handles_no_xml_tags(self) -> None:
raw = 'Plain text summary without any tags.'
formatted = format_compact_summary(raw)
self.assertEqual(formatted, raw)
def test_collapses_excess_blank_lines(self) -> None:
raw = '<analysis>x</analysis>\n\n\n\n<summary>y</summary>'
formatted = format_compact_summary(raw)
self.assertNotIn('\n\n\n', formatted)
def test_multiline_analysis_stripped(self) -> None:
raw = (
'<analysis>\nLine 1\nLine 2\nLine 3\n</analysis>\n'
'<summary>Final summary</summary>'
)
formatted = format_compact_summary(raw)
self.assertNotIn('Line 1', formatted)
self.assertIn('Final summary', formatted)
class TestGetCompactUserSummaryMessage(unittest.TestCase):
"""Tests for the post-compact user message builder."""
def test_basic_message_structure(self) -> None:
msg = get_compact_user_summary_message('<summary>overview</summary>')
self.assertIn('continued from a previous conversation', msg)
self.assertIn('overview', msg)
def test_transcript_path_appended(self) -> None:
msg = get_compact_user_summary_message(
'<summary>ok</summary>',
transcript_path='/tmp/transcript.json',
)
self.assertIn('/tmp/transcript.json', msg)
def test_suppress_follow_up(self) -> None:
msg = get_compact_user_summary_message(
'<summary>ok</summary>',
suppress_follow_up=True,
)
self.assertIn('without asking the user any further questions', msg)
def test_no_suppress_follow_up_default(self) -> None:
msg = get_compact_user_summary_message('<summary>ok</summary>')
self.assertNotIn('without asking the user any further questions', msg)
class TestCompactConversation(unittest.TestCase):
"""Tests for the core compact_conversation() function."""
def _make_agent(self, tmp_dir: str) -> LocalCodingAgent:
"""Create a minimal agent with a session loaded."""
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
return agent
def _set_session(
self, agent: LocalCodingAgent, messages: list[AgentMessage]
) -> None:
session = AgentSessionState(
system_prompt_parts=('You are a helpful assistant.',),
messages=messages,
)
agent.last_session = session
def _make_messages(self, count: int) -> list[AgentMessage]:
msgs: list[AgentMessage] = []
for i in range(count):
role = 'user' if i % 2 == 0 else 'assistant'
msgs.append(
AgentMessage(
role=role,
content=f'Message {i} content. ' * 10,
message_id=f'msg_{i}',
)
)
return msgs
def test_no_session_returns_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = self._make_agent(tmp_dir)
agent.last_session = None
result = compact_conversation(agent)
self.assertIsNotNone(result.error)
self.assertIn('Not enough', result.error)
def test_empty_session_returns_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = self._make_agent(tmp_dir)
self._set_session(agent, [])
result = compact_conversation(agent)
self.assertIsNotNone(result.error)
def test_too_few_messages_returns_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = self._make_agent(tmp_dir)
# With only 2 messages and preserve_count=4, nothing to compact
self._set_session(agent, self._make_messages(2))
result = compact_conversation(agent)
self.assertIsNotNone(result.error)
def test_successful_compaction(self) -> None:
"""Simulate a successful model call and verify session is compacted."""
with tempfile.TemporaryDirectory() as tmp_dir:
agent = self._make_agent(tmp_dir)
msgs = self._make_messages(10)
self._set_session(agent, msgs)
# Mock the client's complete method
from src.openai_compat import AssistantTurn
from src.agent_types import UsageStats
mock_turn = AssistantTurn(
content=(
'<analysis>Thinking through the conversation...</analysis>\n'
'<summary>\n1. Primary Request and Intent:\n'
' User wanted to test compaction.\n'
'2. Key Technical Concepts:\n - Testing\n'
'3. Files and Code Sections:\n - test.py\n'
'4. Errors and fixes:\n - None\n'
'5. Problem Solving:\n Basic testing.\n'
'6. All user messages:\n - "test compaction"\n'
'7. Pending Tasks:\n - None\n'
'8. Current Work:\n Testing compaction.\n'
'9. Optional Next Step:\n Verify it works.\n'
'</summary>'
),
tool_calls=(),
finish_reason='stop',
raw_message={},
usage=UsageStats(),
)
agent.client = MagicMock()
agent.client.complete.return_value = mock_turn
result = compact_conversation(agent)
self.assertIsNone(result.error)
self.assertGreater(result.pre_compact_token_count, 0)
# Session should have fewer messages than original 10
self.assertLess(
len(agent.last_session.messages), 10,
'Session should have fewer messages after compaction',
)
# Should contain a compact_boundary message
boundary_msgs = [
m for m in agent.last_session.messages
if m.metadata.get('kind') == 'compact_boundary'
]
self.assertEqual(len(boundary_msgs), 1)
# Should contain a compact_summary message
summary_msgs = [
m for m in agent.last_session.messages
if m.metadata.get('kind') == 'compact_summary'
]
self.assertEqual(len(summary_msgs), 1)
# Summary should not contain <analysis> block
self.assertNotIn('<analysis>', result.summary_text)
# Summary should contain the actual summary content
self.assertIn('User wanted to test compaction', result.summary_text)
# UI display transcript must remain append-only and not be replaced by
# compact_summary / compact_boundary model-context messages.
display_contents = [m.content for m in agent.last_session.display_messages]
self.assertEqual(display_contents, [m.content for m in msgs])
def test_api_error_returns_compaction_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = self._make_agent(tmp_dir)
self._set_session(agent, self._make_messages(10))
agent.client = MagicMock()
agent.client.complete.side_effect = RuntimeError('API down')
result = compact_conversation(agent)
self.assertIsNotNone(result.error)
self.assertIn('API down', result.error)
def test_empty_model_response_returns_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = self._make_agent(tmp_dir)
self._set_session(agent, self._make_messages(10))
from src.openai_compat import AssistantTurn
from src.agent_types import UsageStats
agent.client = MagicMock()
agent.client.complete.return_value = AssistantTurn(
content='',
tool_calls=(),
finish_reason='stop',
raw_message={},
usage=UsageStats(),
)
result = compact_conversation(agent)
self.assertIsNotNone(result.error)
def test_preserves_tail_messages(self) -> None:
"""The most recent messages should survive compaction."""
with tempfile.TemporaryDirectory() as tmp_dir:
agent = self._make_agent(tmp_dir)
msgs = self._make_messages(12)
self._set_session(agent, msgs)
from src.openai_compat import AssistantTurn
from src.agent_types import UsageStats
agent.client = MagicMock()
agent.client.complete.return_value = AssistantTurn(
content='<summary>Summarised.</summary>',
tool_calls=(),
finish_reason='stop',
raw_message={},
usage=UsageStats(),
)
result = compact_conversation(agent)
self.assertIsNone(result.error)
# The last 4 messages (default preserve_count) should still be present
session_contents = [m.content for m in agent.last_session.messages]
for original_msg in msgs[-4:]:
self.assertIn(
original_msg.content,
session_contents,
f'Tail message "{original_msg.message_id}" should be preserved',
)
def test_custom_instructions_passed_to_prompt(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = self._make_agent(tmp_dir)
self._set_session(agent, self._make_messages(10))
from src.openai_compat import AssistantTurn
from src.agent_types import UsageStats
agent.client = MagicMock()
agent.client.complete.return_value = AssistantTurn(
content='<summary>Custom summary.</summary>',
tool_calls=(),
finish_reason='stop',
raw_message={},
usage=UsageStats(),
)
compact_conversation(agent, custom_instructions='Focus on CSS.')
# Check that the API was called with custom instructions in the prompt
call_args = agent.client.complete.call_args
messages = call_args[0][0]
last_user_msg = messages[-1]['content']
self.assertIn('Focus on CSS.', last_user_msg)
self.assertIn('Additional Instructions:', last_user_msg)
class TestCompactSlashCommand(unittest.TestCase):
"""Test the /compact slash command handler end-to-end."""
def test_slash_compact_returns_success_message(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
# Set up a session with enough messages
session = AgentSessionState(
system_prompt_parts=('You are a helper.',),
messages=[
AgentMessage(role='user', content='Hello', message_id=f'u{i}')
if i % 2 == 0
else AgentMessage(role='assistant', content='Hi', message_id=f'a{i}')
for i in range(10)
],
)
agent.last_session = session
from src.openai_compat import AssistantTurn
from src.agent_types import UsageStats
agent.client = MagicMock()
agent.client.complete.return_value = AssistantTurn(
content='<summary>Session summarised.</summary>',
tool_calls=(),
finish_reason='stop',
raw_message={},
usage=UsageStats(),
)
result = agent.run('/compact')
self.assertIn('Conversation compacted', result.final_output)
self.assertIn('Tokens before', result.final_output)
def test_slash_compact_no_session_returns_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
result = agent.run('/compact')
self.assertIn('failed', result.final_output.lower())
class TestConstants(unittest.TestCase):
"""Verify key constants match the npm reference."""
def test_autocompact_buffer_tokens(self) -> None:
self.assertEqual(AUTOCOMPACT_BUFFER_TOKENS, 13_000)
if __name__ == '__main__':
unittest.main()
+8
View File
@@ -0,0 +1,8 @@
from dataclasses import replace
import pytest
def test_internal_secrets_must_be_at_least_32_bytes(settings) -> None:
with pytest.raises(RuntimeError, match="at least 32 bytes"):
replace(settings, internal_gateway_key="too-short").validate_gateway()
-82
View File
@@ -1,82 +0,0 @@
from __future__ import annotations
import json
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.config_runtime import ConfigRuntime
class ConfigRuntimeTests(unittest.TestCase):
def test_config_runtime_loads_and_merges_sources(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
claude_dir = workspace / '.claude'
claude_dir.mkdir()
(claude_dir / 'settings.json').write_text(
'{"model":{"name":"project-model","temperature":0.1},"review":{"strict":false}}',
encoding='utf-8',
)
(claude_dir / 'settings.local.json').write_text(
'{"model":{"temperature":0.0},"review":{"strict":true}}',
encoding='utf-8',
)
runtime = ConfigRuntime.from_workspace(workspace)
self.assertTrue(runtime.has_config())
self.assertEqual(runtime.get_value('model.name'), 'project-model')
self.assertEqual(runtime.get_value('model.temperature'), 0.0)
self.assertEqual(runtime.get_value('review.strict'), True)
self.assertIn('Config sources: 2', runtime.render_summary())
def test_config_runtime_persists_set_value(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = ConfigRuntime.from_workspace(workspace)
mutation = runtime.set_value('review.mode', 'strict', source='local')
restored = ConfigRuntime.from_workspace(workspace)
self.assertEqual(mutation.source_name, 'local')
self.assertEqual(restored.get_value('review.mode'), 'strict')
self.assertIn('review.mode', restored.render_keys())
self.assertEqual(json.loads(restored.render_value('review.mode')), 'strict')
def test_config_tools_execute_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = ConfigRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
config_runtime=runtime,
)
set_result = execute_tool(
default_tool_registry(),
'config_set',
{'key_path': 'review.mode', 'value': 'strict'},
context,
)
list_result = execute_tool(
default_tool_registry(),
'config_list',
{'prefix': 'review'},
context,
)
get_result = execute_tool(
default_tool_registry(),
'config_get',
{'key_path': 'review.mode'},
context,
)
self.assertTrue(set_result.ok)
self.assertEqual(set_result.metadata.get('source_name'), 'local')
self.assertIn('# Config Keys', list_result.content)
self.assertIn('review.mode', list_result.content)
self.assertIn('# Config Value', get_result.content)
self.assertIn('"strict"', get_result.content)
+24
View File
@@ -0,0 +1,24 @@
from agent_platform.runtime.context import ContextPolicy
def test_context_removes_rendered_tool_details_and_compacts() -> None:
policy = ContextPolicy()
messages = [
{"role": "user", "content": "old-" + "x" * 10_000},
{
"role": "assistant",
"content": '<details type="tool_calls" done="true"><summary>x</summary>secret trace</details> visible',
},
{"role": "user", "content": "current task"},
]
result = policy.prepare(
messages,
system_prompt="system",
memories=[{"content": "remember this"}],
char_budget=8_500,
)
serialized = str(result.messages)
assert "secret trace" not in serialized
assert "current task" in serialized
assert "remember this" in serialized
assert result.dropped_messages >= 1
-189
View File
@@ -1,189 +0,0 @@
"""Tests for /cost, /exit, and /diff slash commands."""
from __future__ import annotations
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from src.agent_runtime import LocalCodingAgent
from src.agent_slash_commands import preprocess_slash_command
from src.agent_types import AgentRuntimeConfig, ModelConfig, UsageStats
class TestCostCommand(unittest.TestCase):
"""Tests for the /cost slash command."""
def test_cost_shows_zero_initially(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
result = agent.run('/cost')
self.assertIn('Total cost:', result.final_output)
self.assertIn('$0.0000', result.final_output)
self.assertIn('Total input tokens:', result.final_output)
self.assertIn('Total output tokens:', result.final_output)
self.assertIn('Total tokens:', result.final_output)
def test_cost_shows_accumulated_usage(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
# Simulate accumulated usage
agent.cumulative_usage = UsageStats(
input_tokens=1000,
output_tokens=500,
cache_read_input_tokens=200,
)
agent.cumulative_cost_usd = 0.05
result = agent.run('/cost')
self.assertIn('$0.05', result.final_output)
self.assertIn('1,000', result.final_output)
self.assertIn('500', result.final_output)
self.assertIn('Cache read tokens:', result.final_output)
def test_cost_hides_zero_cache_and_reasoning(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
agent.cumulative_usage = UsageStats(
input_tokens=100,
output_tokens=50,
)
result = agent.run('/cost')
# Should NOT show cache/reasoning lines when they're zero
self.assertNotIn('Cache read tokens:', result.final_output)
self.assertNotIn('Cache creation tokens:', result.final_output)
self.assertNotIn('Reasoning tokens:', result.final_output)
def test_cost_small_amounts_show_four_decimals(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
agent.cumulative_cost_usd = 0.0023
result = agent.run('/cost')
self.assertIn('$0.0023', result.final_output)
class TestExitCommand(unittest.TestCase):
"""Tests for the /exit and /quit slash commands."""
def test_exit_triggers_system_exit(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
with self.assertRaises(SystemExit) as cm:
agent.run('/exit')
self.assertEqual(cm.exception.code, 0)
def test_quit_triggers_system_exit(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
with self.assertRaises(SystemExit):
agent.run('/quit')
class TestDiffCommand(unittest.TestCase):
"""Tests for the /diff slash command."""
def test_diff_in_non_git_directory(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
result = agent.run('/diff')
# In a non-git dir, git diff returns an error
output = result.final_output.lower()
self.assertTrue(
'no uncommitted' in output
or 'not a git' in output
or 'error' in output,
f'Expected a non-git or no-changes message, got: {result.final_output}',
)
def test_diff_in_git_repo_with_no_changes(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
subprocess.run(
['git', 'init'], cwd=str(workspace),
capture_output=True, check=True,
)
subprocess.run(
['git', 'config', 'user.email', 'test@test.com'],
cwd=str(workspace), capture_output=True,
)
subprocess.run(
['git', 'config', 'user.name', 'Test'],
cwd=str(workspace), capture_output=True,
)
(workspace / 'hello.txt').write_text('hello\n')
subprocess.run(
['git', 'add', '.'], cwd=str(workspace),
capture_output=True, check=True,
)
subprocess.run(
['git', 'commit', '-m', 'init'], cwd=str(workspace),
capture_output=True, check=True,
)
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
result = agent.run('/diff')
self.assertIn('No uncommitted changes', result.final_output)
def test_diff_shows_changes(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
subprocess.run(
['git', 'init'], cwd=str(workspace),
capture_output=True, check=True,
)
subprocess.run(
['git', 'config', 'user.email', 'test@test.com'],
cwd=str(workspace), capture_output=True,
)
subprocess.run(
['git', 'config', 'user.name', 'Test'],
cwd=str(workspace), capture_output=True,
)
(workspace / 'hello.txt').write_text('hello\n')
subprocess.run(
['git', 'add', '.'], cwd=str(workspace),
capture_output=True, check=True,
)
subprocess.run(
['git', 'commit', '-m', 'init'], cwd=str(workspace),
capture_output=True, check=True,
)
# Make a change
(workspace / 'hello.txt').write_text('hello world\n')
agent = LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
result = agent.run('/diff')
self.assertIn('hello', result.final_output)
self.assertIn('diff', result.final_output.lower())
if __name__ == '__main__':
unittest.main()
-52
View File
@@ -1,52 +0,0 @@
from __future__ import annotations
import unittest
from src.cost_tracker import CostTracker
class CostTrackerTests(unittest.TestCase):
def test_fresh_tracker_starts_at_zero_with_empty_events(self) -> None:
tracker = CostTracker()
self.assertEqual(tracker.total_units, 0)
self.assertEqual(tracker.events, [])
def test_record_single_event(self) -> None:
tracker = CostTracker()
tracker.record('inference', 42)
self.assertEqual(tracker.total_units, 42)
self.assertEqual(len(tracker.events), 1)
def test_record_multiple_events_accumulates_totals(self) -> None:
tracker = CostTracker()
tracker.record('inference', 10)
tracker.record('embedding', 20)
tracker.record('search', 30)
self.assertEqual(tracker.total_units, 60)
self.assertEqual(len(tracker.events), 3)
def test_record_zero_units(self) -> None:
tracker = CostTracker()
tracker.record('noop', 0)
self.assertEqual(tracker.total_units, 0)
self.assertEqual(len(tracker.events), 1)
self.assertIn('noop:0', tracker.events)
def test_record_large_units(self) -> None:
tracker = CostTracker()
large = 10**9
tracker.record('bulk', large)
self.assertEqual(tracker.total_units, large)
self.assertEqual(tracker.events, [f'bulk:{large}'])
def test_event_format_is_label_colon_units(self) -> None:
tracker = CostTracker()
tracker.record('inference', 42)
self.assertEqual(tracker.events[0], 'inference:42')
def test_events_are_ordered_chronologically(self) -> None:
tracker = CostTracker()
labels = ['first', 'second', 'third']
for i, label in enumerate(labels):
tracker.record(label, i)
self.assertEqual(tracker.events, ['first:0', 'second:1', 'third:2'])
-153
View File
@@ -1,153 +0,0 @@
from __future__ import annotations
import json
import tempfile
import unittest
import zipfile
from pathlib import Path
import openpyxl
from src.agent_tools import build_tool_context, default_tool_registry, execute_tool
from src.agent_types import AgentRuntimeConfig
from src.data_agent_inputs import (
extract_case_evidence,
load_input_sources,
render_source_context,
)
class DataAgentInputTests(unittest.TestCase):
def test_load_input_sources_reads_xlsx_tables(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
path = Path(tmp_dir) / 'cases.xlsx'
_write_xlsx(path)
payload = load_input_sources(tmp_dir, ['cases.xlsx'])
self.assertEqual(payload['source_count'], 1)
table = payload['sources'][0]['tables'][0]
self.assertEqual(table['title'], 'Sheet')
self.assertEqual(table['rows'][0], ['query', '预期domain', '0106-prev-domain', 'type', '备注'])
def test_load_input_sources_reads_explicit_external_file(self) -> None:
with tempfile.TemporaryDirectory() as root_dir, tempfile.TemporaryDirectory() as external_dir:
path = Path(external_dir) / 'cases.xlsx'
_write_xlsx(path)
payload = load_input_sources(root_dir, [str(path)])
self.assertEqual(payload['source_count'], 1)
self.assertEqual(payload['sources'][0]['path'], path.resolve().as_posix())
self.assertEqual(payload['sources'][0]['tables'][0]['rows'][1][0], '怎么开启查找设备')
def test_extract_case_evidence_profiles_and_extracts_rows(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
path = Path(tmp_dir) / 'cases.xlsx'
_write_xlsx(path)
payload = extract_case_evidence(tmp_dir, paths=['cases.xlsx'])
self.assertEqual(payload['required_questions'], [])
self.assertEqual(payload['evidence_count'], 2)
self.assertEqual(payload['evidence'][0]['query'], '怎么开启查找设备')
self.assertEqual(payload['evidence'][0]['expected_label'], 'productAgent')
self.assertEqual(payload['evidence'][0]['predicted_label'], 'QA')
def test_extract_case_evidence_asks_when_expected_label_is_missing(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
path = Path(tmp_dir) / 'ambiguous.xlsx'
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.append(['query', '备注'])
sheet.append(['怎么开启查找设备', '需要产品问答'])
workbook.save(path)
payload = extract_case_evidence(tmp_dir, paths=['ambiguous.xlsx'])
self.assertEqual(payload['evidence_count'], 0)
self.assertTrue(any('预期标签列' in question for question in payload['required_questions']))
def test_render_source_context_reads_docx_tables(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
path = Path(tmp_dir) / 'prd.docx'
_write_minimal_docx(path)
payload = render_source_context(tmp_dir, paths=['prd.docx'])
self.assertFalse(payload['truncated'])
self.assertIn('complex_task(tag="设备控制")', payload['context_text'])
self.assertIn('query示例: 我到家了布置下客厅灯光', payload['context_text'])
def test_tools_execute_against_registry(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
path = Path(tmp_dir) / 'cases.xlsx'
_write_xlsx(path)
context = build_tool_context(AgentRuntimeConfig(cwd=Path(tmp_dir)))
registry = default_tool_registry()
load_result = execute_tool(
registry,
'data_agent_load_input_sources',
{'paths': ['cases.xlsx'], 'max_rows_per_table': 5},
context,
)
self.assertTrue(load_result.ok, load_result.content)
loaded = json.loads(load_result.content)
evidence_result = execute_tool(
registry,
'data_agent_extract_case_evidence',
{'loaded_sources': loaded},
context,
)
render_result = execute_tool(
registry,
'data_agent_render_source_context',
{'loaded_sources': loaded, 'max_chars': 2000},
context,
)
self.assertTrue(evidence_result.ok, evidence_result.content)
evidence = json.loads(evidence_result.content)
self.assertEqual(evidence['evidence_count'], 2)
self.assertTrue(render_result.ok, render_result.content)
rendered = json.loads(render_result.content)
self.assertIn('怎么开启查找设备', rendered['context_text'])
def _write_xlsx(path: Path) -> None:
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.append(['query', '预期domain', '0106-prev-domain', 'type', '备注'])
sheet.append(['怎么开启查找设备', 'productAgent', 'QA', '设置问答', '预期落产品问答'])
sheet.append(['查找设备打开了吗', 'productAgent', 'smartApp', '状态问答', ''])
workbook.save(path)
def _write_minimal_docx(path: Path) -> None:
document_xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>中控tag:complex_task(tag="设备控制")</w:t></w:r></w:p>
<w:tbl>
<w:tr>
<w:tc><w:p><w:r><w:t>意图类别</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>query示例</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>高置信品类</w:t></w:r></w:p></w:tc>
</w:tr>
<w:tr>
<w:tc><w:p><w:r><w:t>设备控制</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>我到家了布置下客厅灯光</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>light</w:t></w:r></w:p></w:tc>
</w:tr>
</w:tbl>
</w:body>
</w:document>
'''
with zipfile.ZipFile(path, 'w') as archive:
archive.writestr('word/document.xml', document_xml)
if __name__ == '__main__':
unittest.main()
-919
View File
@@ -1,919 +0,0 @@
from __future__ import annotations
import csv
import io
import json
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.data_agent_records import (
build_planning_prompt,
confirm_generation_goal,
confirm_generation_plan,
export_dataset_records,
export_planning_eval_csv,
export_training_jsonl,
get_generation_plan,
normalize_dataset_draft,
prepare_generation_goal,
prepare_generation_plan,
render_dataset_records_table_csv,
render_planning_eval_csv,
update_generation_plan,
validate_dataset_records,
)
class DataAgentRecordTests(unittest.TestCase):
def test_normalize_dataset_draft_builds_canonical_records(self) -> None:
draft = '''
# dataset_label: 地图和生活边界数据
### case: 附近餐饮查询
用户: 帮我看看附近有什么好吃的
target: Agent(tag="life_service")
notes: 附近生活服务查询
### case: 多轮承接附近餐饮
用户: 我想出门逛逛
小爱: 好的
用户: 看看附近有什么好吃的
target: Agent(tag="life_service")
notes: 多轮承接附近生活服务查询
'''.strip()
payload = normalize_dataset_draft(
draft,
batch_id='demo',
base_timestamp=1_755_567_930_500,
timestamp_step_ms=60_000,
)
self.assertEqual(payload['warnings'], [])
records = payload['records']
self.assertEqual(len(records), 2)
self.assertEqual(records[0]['record_id'], 'gen_demo_000001')
self.assertEqual(records[0]['turn']['query'], '帮我看看附近有什么好吃的')
self.assertEqual(records[0]['prev_session'], [])
self.assertEqual(records[0]['label']['target_type'], 'agent')
self.assertEqual(records[0]['dimensions'], {'complex': False})
self.assertEqual(records[1]['turn']['query'], '看看附近有什么好吃的')
self.assertEqual(
records[1]['prev_session'],
[
{
'query': '我想出门逛逛',
'tts': '好的',
'timestamp': 1_755_569_070_500,
}
],
)
def test_normalize_dataset_draft_canonicalizes_single_quote_agent_target(self) -> None:
payload = normalize_dataset_draft(
'''
# dataset_label: 地图导航边界数据
### case: 顺路停车场
用户: 帮我找个顺路的停车场
target: Agent(tag='地图导航')
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
)
record = payload['records'][0]
self.assertEqual(record['label']['target'], 'Agent(tag="地图导航")')
self.assertEqual(record['label']['target_type'], 'agent')
def test_normalize_dataset_draft_keeps_complex_dimension_independent(self) -> None:
payload = normalize_dataset_draft(
'''
# dataset_label: 地图导航复杂样例
### case: 复杂导航
用户: 帮我规划一条先去加油站再去公司的路线
complex: true
target: Agent(tag='地图导航')
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
)
record = payload['records'][0]
self.assertEqual(record['label']['target'], 'Agent(tag="地图导航")')
self.assertEqual(record['dimensions']['complex'], True)
def test_validate_dataset_records_reports_valid_payload(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 地图和生活边界数据
### case: 附近餐饮查询
用户: 附近有什么好吃的
target: Agent(tag="life_service")
'''.strip(),
base_timestamp=1_755_567_930_500,
)['records']
result = validate_dataset_records(records)
self.assertTrue(result['ok'])
self.assertEqual(result['error_count'], 0)
def test_validate_dataset_records_rejects_current_turn_tts(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 地图和生活边界数据
### case: 附近餐饮查询
用户: 附近有什么好吃的
target: Agent(tag="life_service")
'''.strip(),
base_timestamp=1_755_567_930_500,
)['records']
records[0]['turn']['tts'] = '不应该出现'
result = validate_dataset_records(records)
self.assertFalse(result['ok'])
self.assertEqual(result['errors'][0]['path'], 'records[0].turn.tts')
def test_export_dataset_records_writes_compact_jsonl(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 地图和生活边界数据
### case: 附近餐饮查询
用户: 附近有什么好吃的
target: Agent(tag="life_service")
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
)['records']
with tempfile.TemporaryDirectory() as tmp_dir:
payload = export_dataset_records(
records,
root=tmp_dir,
output_path='tasks/demo/records.jsonl',
)
output_path = Path(tmp_dir) / payload['output_path']
lines = output_path.read_text(encoding='utf-8').splitlines()
table_path = Path(tmp_dir) / payload['table_output_path']
table_rows = list(csv.DictReader(io.StringIO(table_path.read_text(encoding='utf-8-sig'))))
self.assertEqual(payload['output_format'], 'jsonl')
self.assertEqual(payload['table_output_format'], 'csv')
self.assertEqual(payload['record_count'], 1)
self.assertEqual(len(lines), 1)
self.assertNotIn(': ', lines[0])
self.assertEqual(json.loads(lines[0])['turn']['query'], '附近有什么好吃的')
self.assertEqual(len(table_rows), 1)
self.assertEqual(
list(table_rows[0].keys()),
['request_id', 'timestamp', 'query', 'prev_session', 'context', 'label', '是否迁移Function', 'function'],
)
self.assertEqual(table_rows[0]['request_id'], 'aabbccdd')
self.assertEqual(table_rows[0]['query'], '附近有什么好吃的')
self.assertEqual(table_rows[0]['prev_session'], '[]')
self.assertEqual(table_rows[0]['context'], '{}')
self.assertEqual(table_rows[0]['label'], '地图和生活边界数据')
self.assertEqual(table_rows[0]['是否迁移Function'], '')
self.assertEqual(table_rows[0]['function'], 'complex=false\nAgent(tag="life_service")')
def test_render_dataset_records_table_csv_keeps_prev_session_json(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 地图导航边界数据
### case: 多轮顺路停车场
用户: 查一下附近停车场
小爱: 找到了附近停车场
用户: 帮我找个最顺路的
target: Agent(tag='地图导航')
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
timestamp_step_ms=60_000,
)['records']
rows = list(csv.DictReader(io.StringIO(render_dataset_records_table_csv(records))))
prev_session = json.loads(rows[0]['prev_session'])
self.assertEqual(prev_session[0]['query'], '查一下附近停车场')
self.assertEqual(prev_session[0]['tts'], '找到了附近停车场')
self.assertEqual(prev_session[0]['timestamp'], '1755567870500')
self.assertEqual(rows[0]['function'], 'complex=false\nAgent(tag="地图导航")')
def test_export_training_jsonl_uses_prompt_template_and_history(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 地图导航边界数据
### case: 多轮顺路停车场
用户: 查一下附近停车场
小爱: 找到了附近停车场
用户: 帮我找个顺路的
target: Agent(tag='地图导航')
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
timestamp_step_ms=60_000,
)['records']
records[0]['context'] = {'location': '北京', 'rag': '地图服务可用', 'other': '忽略'}
with tempfile.TemporaryDirectory() as tmp_dir:
payload = export_training_jsonl(
records,
root=tmp_dir,
output_path='output/training.jsonl',
)
output_path = Path(tmp_dir) / payload['output_path']
line = json.loads(output_path.read_text(encoding='utf-8').splitlines()[0])
self.assertEqual(payload['output_format'], 'jsonl')
self.assertEqual(line['system'], '你是小爱同学,中文智能语音助手。')
self.assertEqual(line['output'], 'complex=false\nAgent(tag="地图导航")')
self.assertIn('[知识注入]\n{\n"location": "北京",\n"rag": "地图服务可用"\n}', line['instruction'])
self.assertIn('用户: 查一下附近停车场\n小爱: 找到了附近停车场', line['instruction'])
self.assertIn('[当前query]\n用户: 帮我找个顺路的', line['instruction'])
def test_render_planning_eval_csv_uses_expected_columns(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 地图导航边界数据
### case: 多轮顺路停车场
用户: 查一下附近停车场
小爱: 找到了附近停车场
用户: 帮我找个顺路的
target: Agent(tag='地图导航')
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
)['records']
rows = list(csv.DictReader(io.StringIO(render_planning_eval_csv(records))))
self.assertEqual(
list(rows[0].keys()),
['request_id', 'newPrompt', 'query', '类别真实标签', 'code标签', 'complex'],
)
self.assertEqual(rows[0]['request_id'], 'aabbccdd')
self.assertEqual(rows[0]['query'], '帮我找个顺路的')
self.assertEqual(rows[0]['类别真实标签'], '地图导航')
self.assertEqual(rows[0]['code标签'], 'Agent(tag="地图导航")')
self.assertEqual(rows[0]['complex'], 'FALSE')
self.assertTrue(rows[0]['newPrompt'].startswith('<|im_start|>system\n你是小爱同学,中文智能语音助手。<|im_end|>'))
self.assertIn('<|im_start|>user\n请参考用户的[当前query]', rows[0]['newPrompt'])
self.assertIn('[function]', rows[0]['newPrompt'])
self.assertTrue(rows[0]['newPrompt'].endswith('<|im_start|>assistant\n'))
def test_build_planning_prompt_matches_standard_chat_template(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 地图导航边界数据
### case: 多轮顺路停车场
用户: 帮我查一下最近的停车场
小爱: 最近的停车场离你27米
用户: 帮我找一个最顺路的停车场
target: Agent(tag='地图导航')
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
timestamp_step_ms=60_000,
)['records']
records[0]['context'] = {'location': '', 'rag': ''}
prompt = build_planning_prompt(records[0], system_prompt='你是小爱同学,中文智能语音助手。\n')
self.assertEqual(
prompt,
'<|im_start|>system\n'
'你是小爱同学,中文智能语音助手。<|im_end|>\n'
'<|im_start|>user\n'
'请参考用户的[当前query]、[对话历史]、[知识注入]、[系统状态]识别出[当前query]的[function]结果,[function]是python的code形式。\n'
'[知识注入]\n'
'{\n'
'"location": "",\n'
'"rag": ""\n'
'}\n'
'[系统状态]\n'
'{}\n'
'[对话历史]\n'
'用户: 帮我查一下最近的停车场\n'
'小爱: 最近的停车场离你27米\n'
'[当前query]\n'
'用户: 帮我找一个最顺路的停车场\n'
'[function]\n'
'<|im_end|>\n'
'<|im_start|>assistant\n',
)
def test_export_planning_eval_csv_writes_file(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 时间工具数据
### case: 几点
用户: 现在几点
complex: true
target: CalendarQA(type="TIME")
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
)['records']
with tempfile.TemporaryDirectory() as tmp_dir:
payload = export_planning_eval_csv(
records,
root=tmp_dir,
output_path='output/eval_planning.csv',
)
output_path = Path(tmp_dir) / payload['output_path']
rows = list(csv.DictReader(io.StringIO(output_path.read_text(encoding='utf-8-sig'))))
self.assertEqual(payload['output_format'], 'csv')
self.assertEqual(rows[0]['类别真实标签'], 'CalendarQA')
self.assertEqual(rows[0]['code标签'], 'CalendarQA(type="TIME")')
self.assertEqual(rows[0]['complex'], 'TRUE')
def test_normalize_online_draft_uses_real_request_metadata(self) -> None:
records = normalize_dataset_draft(
'''
# dataset_label: 线上误召回badcase专项
### case: 线上样例
request_id: rid-123
timestamp: 1755567930500
用户: 附近有什么好吃的
target: Agent(tag="life_service")
'''.strip(),
source_type='online',
)['records']
self.assertEqual(records[0]['record_id'], 'online_rid-123_000001')
self.assertEqual(records[0]['source']['request_id'], 'rid-123')
self.assertEqual(records[0]['source']['timestamp'], 1_755_567_930_500)
def test_normalize_requires_confirmed_plan_when_state_root_is_provided(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
with self.assertRaisesRegex(Exception, 'confirmed_plan_id is required'):
normalize_dataset_draft(
'''
# dataset_label: 地图和生活边界数据
### case: 附近餐饮查询
用户: 附近有什么好吃的
target: Agent(tag="life_service")
'''.strip(),
plan_state_root=tmp_dir,
)
def test_generation_plan_can_be_confirmed_then_used(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
plan_payload = prepare_generation_plan(
root=tmp_dir,
dataset_label='地图和生活边界数据',
target='Agent(tag="life_service")',
total_count=2,
turn_mix='1 条单轮,1 条多轮',
coverage='附近吃喝玩乐',
exclusions='不要生成导航路线类 query',
output_path='tasks/demo/artifacts',
)
plan_id = plan_payload['plan']['plan_id']
confirmed = confirm_generation_plan(root=tmp_dir, plan_id=plan_id, confirmation='确认,开始生成')
records = normalize_dataset_draft(
'''
# dataset_label: 地图和生活边界数据
### case: 附近餐饮查询
用户: 附近有什么好吃的
target: Agent(tag="life_service")
'''.strip(),
confirmed_plan_id=confirmed['confirmed_plan_id'],
plan_state_root=tmp_dir,
)['records']
self.assertEqual(records[0]['label']['dataset_label'], '地图和生活边界数据')
def test_generation_goal_can_gate_plan_creation(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
goal_payload = prepare_generation_goal(
root=tmp_dir,
dataset_label='地图和生活边界数据',
goal_summary='生成附近生活服务边界数据',
target='Agent(tag="life_service")',
plan_hint='建议先生成 2 条单轮,输出到 tasks/demo/records.jsonl',
coverage='附近吃喝玩乐',
exclusions='不要生成导航路线类 query',
source_refs=['manual:user'],
)
goal_id = goal_payload['goal']['goal_id']
with self.assertRaisesRegex(Exception, 'generation goal is not confirmed'):
prepare_generation_plan(
root=tmp_dir,
confirmed_goal_id=goal_id,
dataset_label='地图和生活边界数据',
target='Agent(tag="life_service")',
total_count=2,
turn_mix='2 条单轮',
coverage='附近吃喝玩乐',
exclusions='不要生成导航路线类 query',
output_path='tasks/demo/artifacts',
)
confirmed_goal = confirm_generation_goal(
root=tmp_dir,
goal_id=goal_id,
confirmation='确认 goal',
reviewed_revision=1,
)
plan_payload = prepare_generation_plan(
root=tmp_dir,
confirmed_goal_id=confirmed_goal['confirmed_goal_id'],
dataset_label='地图和生活边界数据',
target='Agent(tag="life_service")',
total_count=2,
turn_mix='2 条单轮',
coverage='附近吃喝玩乐',
exclusions='不要生成导航路线类 query',
output_path='tasks/demo/artifacts',
)
self.assertEqual(plan_payload['plan']['confirmed_goal_id'], goal_id)
self.assertEqual(
goal_payload['goal']['plan_hint'],
'建议先生成 2 条单轮,输出到 tasks/demo/records.jsonl',
)
def test_generation_goal_requires_declared_targets_before_review(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
with self.assertRaisesRegex(Exception, 'must include target or target_definitions'):
prepare_generation_goal(
root=tmp_dir,
dataset_label='地图和生活边界数据',
goal_summary='生成附近生活服务边界数据',
coverage='附近吃喝玩乐',
exclusions='不要生成导航路线类 query',
)
def test_generation_plan_inherits_targets_from_confirmed_goal(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
goal_payload = prepare_generation_goal(
root=tmp_dir,
dataset_label='餐饮和导航边界数据',
goal_summary='生成餐饮服务和地图导航边界数据',
target_definitions=[
{'name': '餐饮服务', 'target': 'Agent(tag="餐饮服务")', 'rule': '找附近餐饮'},
{'name': '地图导航', 'target': 'Agent(tag="地图导航")', 'rule': '明确导航'},
],
coverage='餐饮服务和地图导航边界',
exclusions='不要生成无关闲聊',
)
goal_id = goal_payload['goal']['goal_id']
confirm_generation_goal(root=tmp_dir, goal_id=goal_id, confirmation='确认目标')
plan_payload = prepare_generation_plan(
root=tmp_dir,
confirmed_goal_id=goal_id,
dataset_label='餐饮和导航边界数据',
target='',
total_count=2,
turn_mix='2 条单轮',
coverage='餐饮服务和地图导航边界',
exclusions='不要生成无关闲聊',
output_path='tasks/demo/artifacts',
)
self.assertEqual(
[item['target'] for item in plan_payload['plan']['target_definitions']],
['Agent(tag="餐饮服务")', 'Agent(tag="地图导航")'],
)
def test_generation_plan_dedupes_existing_task_output_path(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
existing = Path(tmp_dir) / 'tasks' / 'demo' / 'artifacts'
existing.mkdir(parents=True)
plan_payload = prepare_generation_plan(
root=tmp_dir,
dataset_label='地图和生活边界数据',
target='Agent(tag="life_service")',
total_count=2,
turn_mix='1 条单轮,1 条多轮',
coverage='附近吃喝玩乐',
exclusions='不要生成导航路线类 query',
output_path='tasks/demo/artifacts',
)
self.assertEqual(
plan_payload['plan']['output_path'],
'tasks/demo-data_plan_000001/artifacts',
)
self.assertEqual(plan_payload['plan']['requested_output_path'], 'tasks/demo/artifacts')
def test_generation_plan_review_update_and_revision_confirmation(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
plan_payload = prepare_generation_plan(
root=tmp_dir,
dataset_label='地图和生活边界数据',
target='Agent(tag="life_service")',
total_count=2,
turn_mix='1 条单轮,1 条多轮',
coverage='附近吃喝玩乐',
exclusions='不要生成导航路线类 query',
output_path='tasks/demo/artifacts',
)
plan_id = plan_payload['plan']['plan_id']
updated = update_generation_plan(
root=tmp_dir,
plan_id=plan_id,
review_feedback='多轮数据多一点',
updates={'turn_mix': '1 条单轮,3 条多轮', 'total_count': 4},
)
self.assertEqual(updated['plan']['revision'], 2)
self.assertEqual(updated['plan']['turn_mix'], '1 条单轮,3 条多轮')
self.assertEqual(len(updated['plan']['review_history']), 1)
shown = get_generation_plan(root=tmp_dir, plan_id=plan_id)
self.assertEqual(shown['plan']['revision'], 2)
with self.assertRaisesRegex(Exception, 'reviewed_revision must match'):
confirm_generation_plan(
root=tmp_dir,
plan_id=plan_id,
confirmation='确认,开始生成',
reviewed_revision=1,
)
confirmed = confirm_generation_plan(
root=tmp_dir,
plan_id=plan_id,
confirmation='确认,开始生成',
reviewed_revision=2,
)
self.assertEqual(confirmed['plan']['status'], 'confirmed')
self.assertEqual(confirmed['plan']['confirmed_revision'], 2)
def test_multi_target_plan_allows_only_declared_targets(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
plan_payload = prepare_generation_plan(
root=tmp_dir,
dataset_label='餐饮和导航边界数据',
target='',
target_definitions=[
{
'name': '餐饮服务',
'target': 'Agent(tag="餐饮服务")',
'rule': '找附近美食但不导航',
},
{
'name': '地图导航',
'target': 'Agent(tag="地图导航")',
'rule': '明确要求导航去某地',
},
],
total_count=2,
turn_mix='2 条单轮',
coverage='餐饮服务和地图导航边界',
exclusions='不要生成无关闲聊',
output_path='tasks/demo/artifacts',
)
plan_id = plan_payload['plan']['plan_id']
confirm_generation_plan(root=tmp_dir, plan_id=plan_id, confirmation='确认,开始生成')
records = normalize_dataset_draft(
'''
# dataset_label: 餐饮和导航边界数据
### case: 找奶茶
用户: 附近有没有奶茶店
target: Agent(tag="餐饮服务")
### case: 导航去奶茶店
用户: 导航去最近的奶茶店
target: Agent(tag="地图导航")
'''.strip(),
confirmed_plan_id=plan_id,
plan_state_root=tmp_dir,
)['records']
self.assertEqual(records[0]['label']['target'], 'Agent(tag="餐饮服务")')
self.assertEqual(records[1]['label']['target'], 'Agent(tag="地图导航")')
def test_multi_target_plan_rejects_undeclared_targets(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
plan_payload = prepare_generation_plan(
root=tmp_dir,
dataset_label='餐饮和导航边界数据',
target='',
target_definitions=[
{'target': 'Agent(tag="餐饮服务")'},
{'target': 'Agent(tag="地图导航")'},
],
total_count=1,
turn_mix='1 条单轮',
coverage='餐饮服务和地图导航边界',
exclusions='不要生成无关闲聊',
output_path='tasks/demo/artifacts',
)
plan_id = plan_payload['plan']['plan_id']
confirm_generation_plan(root=tmp_dir, plan_id=plan_id, confirmation='确认,开始生成')
with self.assertRaisesRegex(Exception, 'record targets must match'):
normalize_dataset_draft(
'''
# dataset_label: 餐饮和导航边界数据
### case: 天气
用户: 明天天气怎么样
target: Agent(tag="天气")
'''.strip(),
confirmed_plan_id=plan_id,
plan_state_root=tmp_dir,
)
def test_tools_execute_against_registry(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
context = build_tool_context(AgentRuntimeConfig(cwd=Path(tmp_dir)))
goal_result = execute_tool(
default_tool_registry(),
'data_agent_prepare_generation_goal',
{
'dataset_label': '地图和生活边界数据',
'goal_summary': '生成附近生活服务边界数据',
'target': 'Agent(tag="life_service")',
'coverage': '附近吃喝玩乐',
'exclusions': '不要生成导航路线类 query',
'source_refs': ['manual:user'],
},
context,
)
self.assertTrue(goal_result.ok, goal_result.content)
goal_id = json.loads(goal_result.content)['goal']['goal_id']
blocked_plan_result = execute_tool(
default_tool_registry(),
'data_agent_prepare_generation_plan',
{
'dataset_label': '地图和生活边界数据',
'target': 'Agent(tag="life_service")',
'total_count': 1,
'turn_mix': '1 条单轮',
'coverage': '附近吃喝玩乐',
'exclusions': '不要生成导航路线类 query',
'output_path': 'tasks/demo/artifacts',
},
context,
)
self.assertFalse(blocked_plan_result.ok)
confirmed_goal_result = execute_tool(
default_tool_registry(),
'data_agent_confirm_generation_goal',
{'goal_id': goal_id, 'confirmation': '确认 goal', 'reviewed_revision': 1},
context,
)
self.assertTrue(confirmed_goal_result.ok, confirmed_goal_result.content)
plan_result = execute_tool(
default_tool_registry(),
'data_agent_prepare_generation_plan',
{
'confirmed_goal_id': goal_id,
'dataset_label': '地图和生活边界数据',
'target': 'Agent(tag="life_service")',
'total_count': 1,
'turn_mix': '1 条单轮',
'coverage': '附近吃喝玩乐',
'exclusions': '不要生成导航路线类 query',
'output_path': 'tasks/demo/artifacts',
},
context,
)
self.assertTrue(plan_result.ok)
plan_id = json.loads(plan_result.content)['plan']['plan_id']
update_result = execute_tool(
default_tool_registry(),
'data_agent_update_generation_plan',
{
'plan_id': plan_id,
'review_feedback': '多轮不需要,先只测单轮',
'updates': {'turn_mix': '1 条单轮'},
},
context,
)
self.assertTrue(update_result.ok)
revision = json.loads(update_result.content)['plan']['revision']
confirm_result = execute_tool(
default_tool_registry(),
'data_agent_confirm_generation_plan',
{'plan_id': plan_id, 'confirmation': '确认,开始生成', 'reviewed_revision': revision},
context,
)
self.assertTrue(confirm_result.ok)
normalize_result = execute_tool(
default_tool_registry(),
'data_agent_normalize_dataset_draft',
{
'draft_text': '''
# dataset_label: 地图和生活边界数据
### case: 附近餐饮查询
用户: 附近有什么好吃的
target: Agent(tag="life_service")
'''.strip(),
'batch_id': 'demo',
'base_timestamp': 1_755_567_930_500,
'confirmed_plan_id': plan_id,
},
context,
)
self.assertTrue(normalize_result.ok)
records = json.loads(normalize_result.content)['records']
validate_result = execute_tool(
default_tool_registry(),
'data_agent_validate_dataset_records',
{'records': records},
context,
)
export_result = execute_tool(
default_tool_registry(),
'data_agent_export_dataset_records',
{
'records': records,
'output_path': 'tasks/demo/records.jsonl',
},
context,
)
export_payload = json.loads(export_result.content) if export_result.ok else {}
exported_lines = (
(Path(tmp_dir) / export_payload['output_path']).read_text(encoding='utf-8').splitlines()
if export_result.ok
else []
)
self.assertTrue(validate_result.ok)
self.assertTrue(json.loads(validate_result.content)['ok'])
self.assertTrue(export_result.ok, export_result.content)
self.assertEqual(export_payload['record_count'], 1)
self.assertEqual(len(exported_lines), 1)
def test_data_agent_tools_accept_draft_and_records_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
context = build_tool_context(AgentRuntimeConfig(cwd=root))
draft_path = root / 'draft.txt'
draft_path.write_text(
'''
# dataset_label: 地图导航边界数据
### case: 顺路停车场
用户: 帮我找个顺路的停车场
target: Agent(tag='地图导航')
'''.strip(),
encoding='utf-8',
)
normalize_result = execute_tool(
default_tool_registry(),
'data_agent_normalize_dataset_draft',
{
'draft_path': str(draft_path),
'batch_id': 'demo',
'source_type': 'manual',
'base_timestamp': 1_755_567_930_500,
},
context,
)
self.assertTrue(normalize_result.ok, normalize_result.content)
records = json.loads(normalize_result.content)['records']
self.assertEqual(records[0]['label']['target'], 'Agent(tag="地图导航")')
records_path = root / 'records.jsonl'
records_path.write_text(
'\n'.join(json.dumps(record, ensure_ascii=False) for record in records),
encoding='utf-8',
)
validate_result = execute_tool(
default_tool_registry(),
'data_agent_validate_dataset_records',
{'records_path': str(records_path)},
context,
)
export_result = execute_tool(
default_tool_registry(),
'data_agent_export_dataset_records',
{
'records_path': str(records_path),
'output_path': 'output/records.jsonl',
},
context,
)
self.assertTrue(export_result.ok, export_result.content)
export_payload = json.loads(export_result.content)
table_exists = (root / export_payload['table_output_path']).exists()
training_result = execute_tool(
default_tool_registry(),
'data_agent_export_training_jsonl',
{'records_path': str(records_path)},
context,
)
self.assertTrue(training_result.ok, training_result.content)
training_payload = json.loads(training_result.content)
training_exists = (root / training_payload['output_path']).exists()
eval_result = execute_tool(
default_tool_registry(),
'data_agent_export_planning_eval_csv',
{'records_path': str(records_path)},
context,
)
self.assertTrue(eval_result.ok, eval_result.content)
eval_payload = json.loads(eval_result.content)
eval_exists = (root / eval_payload['output_path']).exists()
self.assertTrue(validate_result.ok, validate_result.content)
self.assertTrue(json.loads(validate_result.content)['ok'])
self.assertEqual(export_payload['record_count'], 1)
self.assertTrue(table_exists)
self.assertEqual(training_payload['output_path'], '.port_sessions/data_agent_output/training.jsonl')
self.assertTrue(training_exists)
self.assertEqual(eval_payload['output_path'], '.port_sessions/data_agent_output/eval_planning.csv')
self.assertTrue(eval_exists)
def test_tool_schemas_avoid_top_level_composition_keywords(self) -> None:
# 部分 Bedrock/Anthropic 兼容后端不接受顶层 anyOf/oneOf/allOf。
blocked_keywords = {'anyOf', 'oneOf', 'allOf'}
violations = [
name
for name, tool in default_tool_registry().items()
if blocked_keywords.intersection(tool.parameters)
]
self.assertEqual(violations, [])
def test_data_agent_tools_route_relative_outputs_to_session_output(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
scratchpad = root / '.port_sessions' / 'accounts' / 'alice' / 'sessions' / 's1' / 'scratchpad'
scratchpad.mkdir(parents=True)
context = build_tool_context(AgentRuntimeConfig(cwd=root), scratchpad_directory=scratchpad)
registry = default_tool_registry()
plan_result = execute_tool(
registry,
'data_agent_prepare_generation_plan',
{
'direct_review': True,
'dataset_label': '地图和生活边界数据',
'target': 'Agent(tag="life_service")',
'total_count': 1,
'turn_mix': '1 条单轮',
'coverage': '附近吃喝玩乐',
'exclusions': '不要生成导航路线类 query',
'output_path': 'output/demo/records.jsonl',
},
context,
)
self.assertTrue(plan_result.ok, plan_result.content)
plan_payload = json.loads(plan_result.content)
records = normalize_dataset_draft(
'''
# dataset_label: 地图和生活边界数据
### case: 附近餐饮查询
用户: 附近有什么好吃的
target: Agent(tag="life_service")
'''.strip(),
batch_id='demo',
base_timestamp=1_755_567_930_500,
)['records']
export_result = execute_tool(
registry,
'data_agent_export_dataset_records',
{'records': records, 'output_path': 'output/地图和生活边界数据/自定义名字.jsonl'},
context,
)
self.assertTrue(export_result.ok, export_result.content)
export_payload = json.loads(export_result.content)
expected = '.port_sessions/accounts/alice/sessions/s1/output/records.jsonl'
expected_table = '.port_sessions/accounts/alice/sessions/s1/output/records.csv'
self.assertEqual(plan_payload['plan']['output_path'], expected)
self.assertEqual(export_payload['output_path'], expected)
self.assertEqual(export_payload['table_output_path'], expected_table)
def test_data_agent_session_records_path_stays_stable_when_file_exists(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
scratchpad = root / '.port_sessions' / 'accounts' / 'alice' / 'sessions' / 's1' / 'scratchpad'
output_root = scratchpad.parent / 'output'
output_root.mkdir(parents=True)
(output_root / 'records.jsonl').write_text('{}\n', encoding='utf-8')
context = build_tool_context(AgentRuntimeConfig(cwd=root), scratchpad_directory=scratchpad)
plan_result = execute_tool(
default_tool_registry(),
'data_agent_prepare_generation_plan',
{
'direct_review': True,
'dataset_label': '地图和生活边界数据',
'target': 'Agent(tag="life_service")',
'total_count': 1,
'turn_mix': '1 条单轮',
'coverage': '附近吃喝玩乐',
'exclusions': '不要生成导航路线类 query',
'output_path': 'output/任意子目录/任意名字.jsonl',
},
context,
)
self.assertTrue(plan_result.ok, plan_result.content)
plan_payload = json.loads(plan_result.content)
self.assertEqual(
plan_payload['plan']['output_path'],
'.port_sessions/accounts/alice/sessions/s1/output/records.jsonl',
)
if __name__ == '__main__':
unittest.main()
-256
View File
@@ -1,256 +0,0 @@
from __future__ import annotations
import importlib.util
import json
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.data_agent_router_sessions import (
convert_router_candidates_to_records,
profile_router_sessions,
sample_router_candidates,
search_router_sessions,
)
HAS_PYARROW = importlib.util.find_spec('pyarrow') is not None
class DataAgentRouterSessionTests(unittest.TestCase):
def test_sample_router_candidates_accepts_search_payload(self) -> None:
payload = {
'candidates': [
_candidate('s1', 'phone', 'QA', 'unknown', '怎么查找设备'),
_candidate('s2', 'car', 'mapCopilot', '导航', '导航去公司'),
_candidate('s3', 'phone', 'QA', 'unknown', '手机丢了怎么办'),
]
}
result = sample_router_candidates(payload, sample_size=2, strategy='stride')
self.assertEqual(result['total_candidate_count'], 3)
self.assertEqual(result['sampled_count'], 2)
self.assertEqual(result['summary']['device'][0], {'value': 'phone', 'count': 2})
self.assertEqual(result['candidates'][0]['semantic_session_id'], 's1')
self.assertEqual(result['candidates'][1]['semantic_session_id'], 's3')
def test_sample_router_candidates_accepts_json_string(self) -> None:
result = sample_router_candidates(
json.dumps([_candidate('s1', 'phone', 'QA', 'unknown', '怎么查找设备')], ensure_ascii=False),
sample_size=1,
)
self.assertEqual(result['sampled_count'], 1)
self.assertEqual(result['candidates'][0]['matched_turn']['query'], '怎么查找设备')
def test_convert_router_candidates_to_records_keeps_online_metadata(self) -> None:
candidate = _candidate('s1', 'phone', 'QA', 'unknown', '总结一下')
candidate['turn_count'] = 2
candidate['matched_turn_index'] = 1
candidate['matched_turn']['timestamp'] = 20
candidate['prev_turns'] = [{'timestamp': 10, 'query': '这篇文章讲了什么', 'tts': ''}]
result = convert_router_candidates_to_records(
[candidate],
dataset_label='总结类边界评测集',
default_target='Summarize',
default_complex=True,
batch_id='summary',
)
self.assertEqual(result['record_count'], 1)
record = result['records'][0]
self.assertEqual(record['source']['type'], 'online')
self.assertEqual(record['source']['request_id'], 's1-rid')
self.assertEqual(record['turn']['query'], '总结一下')
self.assertEqual(record['prev_session'], [{'query': '这篇文章讲了什么', 'tts': '', 'timestamp': 10}])
self.assertEqual(record['context']['domain'], 'QA')
self.assertEqual(record['label']['target'], 'Summarize')
self.assertEqual(record['dimensions'], {'complex': True})
def test_convert_router_candidates_to_records_applies_review_decisions(self) -> None:
candidates = [
_candidate('s1', 'phone', 'QA', 'unknown', '总结一下'),
_candidate('s2', 'phone', 'QA', 'unknown', '总结一下红楼梦'),
]
result = convert_router_candidates_to_records(
candidates,
dataset_label='总结类边界评测集',
review_decisions=[
{
'semantic_session_id': 's1',
'matched_turn_index': 0,
'decision': 'include',
'target': 'Summarize',
'complex': True,
'notes': '前文有可总结内容',
},
{
'semantic_session_id': 's2',
'matched_turn_index': 0,
'decision': 'exclude',
'notes': '开放知识对象',
},
],
)
self.assertEqual(result['record_count'], 1)
self.assertEqual(result['skipped_count'], 1)
self.assertEqual(result['records'][0]['meta']['notes'], '前文有可总结内容')
self.assertEqual(result['records'][0]['dimensions']['complex'], True)
@unittest.skipUnless(HAS_PYARROW, 'pyarrow is required for parquet tests')
def test_profile_and_search_router_sessions_read_parquet(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
_write_router_parquet(Path(tmp_dir))
profile = profile_router_sessions(tmp_dir, dates=['20260428'], max_files=1)
search = search_router_sessions(
tmp_dir,
dates=['20260428'],
domains=['mapCopilot'],
query_keywords=['导航'],
max_files=1,
max_candidates=5,
)
self.assertEqual(profile['sampled_row_count'], 2)
self.assertEqual(profile['missing_req_id_count'], 0)
self.assertTrue(any(item['value'] == 'phone' for item in profile['distributions']['device']))
self.assertEqual(search['candidate_count'], 1)
candidate = search['candidates'][0]
self.assertEqual(candidate['req_id'], 'rid-1')
self.assertEqual(candidate['matched_turn']['query'], '导航去公司')
self.assertEqual(candidate['prev_turns'][0]['query'], '你好小爱')
@unittest.skipUnless(HAS_PYARROW, 'pyarrow is required for parquet tests')
def test_router_session_tools_allow_external_absolute_paths(self) -> None:
with tempfile.TemporaryDirectory() as workspace_dir, tempfile.TemporaryDirectory() as data_dir:
_write_router_parquet(Path(data_dir))
external_partition = Path(data_dir) / 'router_session_parquet' / 'date=20260428'
profile = profile_router_sessions(
workspace_dir,
paths=[str(external_partition)],
max_files=1,
)
search = search_router_sessions(
workspace_dir,
paths=[str(external_partition)],
query_keywords=['导航'],
max_files=1,
max_candidates=5,
)
self.assertEqual(profile['sampled_row_count'], 2)
self.assertEqual(search['candidate_count'], 1)
self.assertEqual(search['candidates'][0]['matched_turn']['query'], '导航去公司')
@unittest.skipUnless(HAS_PYARROW, 'pyarrow is required for parquet tests')
def test_router_session_tools_execute_against_registry(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
_write_router_parquet(Path(tmp_dir))
context = build_tool_context(AgentRuntimeConfig(cwd=Path(tmp_dir)))
registry = default_tool_registry()
search_result = execute_tool(
registry,
'data_agent_search_router_sessions',
{'dates': ['20260428'], 'query_regex': '查找设备|手机丢了', 'max_files': 1},
context,
)
self.assertTrue(search_result.ok, search_result.content)
sample_result = execute_tool(
registry,
'data_agent_sample_router_candidates',
{'candidates': json.loads(search_result.content), 'sample_size': 1, 'strategy': 'first'},
context,
)
convert_result = execute_tool(
registry,
'data_agent_convert_router_candidates_to_records',
{
'candidates': json.loads(sample_result.content),
'dataset_label': '查找设备评测集',
'default_target': 'QA',
'batch_id': 'find_device',
},
context,
)
self.assertTrue(sample_result.ok, sample_result.content)
self.assertEqual(json.loads(sample_result.content)['sampled_count'], 1)
self.assertTrue(convert_result.ok, convert_result.content)
self.assertTrue(json.loads(convert_result.content)['validation']['ok'])
def _candidate(session_id: str, device: str, domain: str, intent: str, query: str) -> dict[str, object]:
return {
'semantic_session_id': session_id,
'req_id': f'{session_id}-rid',
'device': device,
'turn_count': 1,
'matched_turn_index': 0,
'matched_turn': {
'timestamp': 1,
'query': query,
'tts': '',
'action': {'domain': domain, 'intent': intent},
},
'prev_turns': [],
'source_path': 'router_session_parquet/date=20260428/part-0.parquet',
'date': '20260428',
}
def _write_router_parquet(root: Path) -> None:
import pyarrow as pa
import pyarrow.parquet as pq
partition = root / 'router_session_parquet' / 'date=20260428'
partition.mkdir(parents=True)
rows = [
{
'semantic_session_id': 'phone_20260428_1',
'device': 'phone',
'req_id': 'rid-1',
'turn_count': 2,
'turns_array': [
{
'timestamp': 1,
'query': '你好小爱',
'action_json': '{"domain":"dialogCopilot","intent":"问候"}',
'tts': '你好',
},
{
'timestamp': 2,
'query': '导航去公司',
'action_json': '{"domain":"mapCopilot","intent":"导航","func":"start_navigation"}',
'tts': '开始导航',
},
],
},
{
'semantic_session_id': 'phone_20260428_2',
'device': 'phone',
'req_id': 'rid-2',
'turn_count': 1,
'turns_array': [
{
'timestamp': 3,
'query': '手机丢了怎么查找设备',
'action_json': '{"domain":"QA","intent":"unknown"}',
'tts': '',
}
],
},
]
pq.write_table(pa.Table.from_pylist(rows), partition / 'part-0.parquet')
if __name__ == '__main__':
unittest.main()
+81
View File
@@ -0,0 +1,81 @@
from __future__ import annotations
import asyncio
import os
from dataclasses import replace
import pytest
import docker
from agent_platform.gateway.provider import DockerExecutionProvider, workspace_ref
@pytest.mark.skipif(os.getenv("RUN_DOCKER_INTEGRATION") != "1", reason="set RUN_DOCKER_INTEGRATION=1")
async def test_real_docker_workspaces_are_isolated(settings) -> None:
client = docker.from_env()
settings = replace(settings, workspace_network_enabled=True)
provider = DockerExecutionProvider(settings, client=client)
users = ("integration-user-a", "integration-user-b")
try:
assert (await provider.write_file(users[0], "secret.txt", "only-a")).ok
assert (await provider.write_file(users[1], "secret.txt", "only-b")).ok
first = await provider.read_file(users[0], "secret.txt", 1, 10)
second = await provider.read_file(users[1], "secret.txt", 1, 10)
assert "only-a" in first.output and "only-b" not in first.output
assert "only-b" in second.output and "only-a" not in second.output
escaped = await provider.exec(users[0], "test ! -e /var/run/docker.sock", ".", 10)
assert escaped.ok
git_init = "git init -q && git config user.email test@example.invalid && git config user.name Integration"
assert (await provider.exec(users[0], git_init, ".", 10)).ok
assert (await provider.write_file(users[0], "tracked.txt", "before\n")).ok
assert (await provider.exec(users[0], "git add tracked.txt && git commit -qm initial", ".", 10)).ok
patch = (
"diff --git a/tracked.txt b/tracked.txt\n"
"index 90be1a7..2a140c2 100644\n"
"--- a/tracked.txt\n"
"+++ b/tracked.txt\n"
"@@ -1 +1 @@\n"
"-before\n"
"+after\n"
)
assert (await provider.apply_patch(users[0], patch, ".")).ok
assert "after" in (await provider.read_file(users[0], "tracked.txt", 1, 10)).output
started = await provider.start_process(users[0], "sleep 0.2; echo background-done", ".")
process_id = started.metadata["process_id"]
polled = await provider.poll_process(users[0], process_id)
for _ in range(20):
if not polled.metadata.get("running"):
break
await asyncio.sleep(0.1)
polled = await provider.poll_process(users[0], process_id)
assert not polled.metadata.get("running")
assert polled.exit_code == 0
assert "background-done" in polled.output
container = client.containers.get(workspace_ref(users[0]).container_name)
container.reload()
second_container = client.containers.get(workspace_ref(users[1]).container_name)
second_container.reload()
assert container.attrs["Config"]["User"] == "1000:1000"
assert container.attrs["HostConfig"]["ReadonlyRootfs"] is True
assert container.attrs["HostConfig"]["CapDrop"] == ["ALL"]
assert container.attrs["HostConfig"]["PidsLimit"] == settings.workspace_pids_limit
assert set(container.attrs["NetworkSettings"]["Networks"]) == {workspace_ref(users[0]).network_name}
assert set(second_container.attrs["NetworkSettings"]["Networks"]) == {workspace_ref(users[1]).network_name}
finally:
for user in users:
ref = workspace_ref(user)
try:
client.containers.get(ref.container_name).remove(force=True)
except docker.errors.NotFound:
pass
try:
client.volumes.get(ref.volume_name).remove(force=True)
except docker.errors.NotFound:
pass
try:
client.networks.get(ref.network_name).remove()
except docker.errors.NotFound:
pass
-179
View File
@@ -1,179 +0,0 @@
from __future__ import annotations
import unittest
from src.execution_registry import (
ExecutionRegistry,
MirroredCommand,
MirroredTool,
build_execution_registry,
)
from src.commands import PORTED_COMMANDS
from src.tools import PORTED_TOOLS
class TestBuildExecutionRegistry(unittest.TestCase):
"""Tests for build_execution_registry and basic registry properties."""
def test_returns_execution_registry(self) -> None:
registry = build_execution_registry()
self.assertIsInstance(registry, ExecutionRegistry)
def test_has_non_empty_commands(self) -> None:
registry = build_execution_registry()
self.assertGreater(len(registry.commands), 0)
def test_has_non_empty_tools(self) -> None:
registry = build_execution_registry()
self.assertGreater(len(registry.tools), 0)
def test_command_count_matches_ported_commands(self) -> None:
registry = build_execution_registry()
self.assertEqual(len(registry.commands), len(PORTED_COMMANDS))
def test_tool_count_matches_ported_tools(self) -> None:
registry = build_execution_registry()
self.assertEqual(len(registry.tools), len(PORTED_TOOLS))
class TestCommandLookup(unittest.TestCase):
"""Tests for ExecutionRegistry.command() lookup."""
def setUp(self) -> None:
self.registry = build_execution_registry()
self.known_name = self.registry.commands[0].name
def test_lookup_exact_case(self) -> None:
result = self.registry.command(self.known_name)
self.assertIsNotNone(result)
self.assertEqual(result.name, self.known_name)
def test_lookup_case_insensitive_lower(self) -> None:
result = self.registry.command(self.known_name.lower())
self.assertIsNotNone(result)
self.assertEqual(result.name, self.known_name)
def test_lookup_case_insensitive_upper(self) -> None:
result = self.registry.command(self.known_name.upper())
self.assertIsNotNone(result)
self.assertEqual(result.name, self.known_name)
def test_lookup_case_insensitive_mixed(self) -> None:
mixed = ''.join(
c.upper() if i % 2 else c.lower()
for i, c in enumerate(self.known_name)
)
result = self.registry.command(mixed)
self.assertIsNotNone(result)
self.assertEqual(result.name, self.known_name)
def test_returns_none_for_unknown_name(self) -> None:
result = self.registry.command('__nonexistent_command_xyz__')
self.assertIsNone(result)
class TestToolLookup(unittest.TestCase):
"""Tests for ExecutionRegistry.tool() lookup."""
def setUp(self) -> None:
self.registry = build_execution_registry()
self.known_name = self.registry.tools[0].name
def test_lookup_exact_case(self) -> None:
result = self.registry.tool(self.known_name)
self.assertIsNotNone(result)
self.assertEqual(result.name, self.known_name)
def test_lookup_case_insensitive_lower(self) -> None:
result = self.registry.tool(self.known_name.lower())
self.assertIsNotNone(result)
self.assertEqual(result.name, self.known_name)
def test_lookup_case_insensitive_upper(self) -> None:
result = self.registry.tool(self.known_name.upper())
self.assertIsNotNone(result)
self.assertEqual(result.name, self.known_name)
def test_lookup_case_insensitive_mixed(self) -> None:
mixed = ''.join(
c.upper() if i % 2 else c.lower()
for i, c in enumerate(self.known_name)
)
result = self.registry.tool(mixed)
self.assertIsNotNone(result)
self.assertEqual(result.name, self.known_name)
def test_returns_none_for_unknown_name(self) -> None:
result = self.registry.tool('__nonexistent_tool_xyz__')
self.assertIsNone(result)
class TestMirroredCommand(unittest.TestCase):
"""Tests for MirroredCommand dataclass."""
def test_has_correct_name(self) -> None:
cmd = MirroredCommand(name='review', source_hint='copilot')
self.assertEqual(cmd.name, 'review')
def test_has_correct_source_hint(self) -> None:
cmd = MirroredCommand(name='review', source_hint='copilot')
self.assertEqual(cmd.source_hint, 'copilot')
def test_is_frozen(self) -> None:
cmd = MirroredCommand(name='review', source_hint='copilot')
with self.assertRaises(AttributeError):
cmd.name = 'other' # type: ignore[misc]
def test_execute_returns_string(self) -> None:
registry = build_execution_registry()
cmd = registry.commands[0]
result = cmd.execute('test prompt')
self.assertIsInstance(result, str)
self.assertIn('Mirrored command', result)
class TestMirroredTool(unittest.TestCase):
"""Tests for MirroredTool dataclass."""
def test_has_correct_name(self) -> None:
tool = MirroredTool(name='BashTool', source_hint='vscode')
self.assertEqual(tool.name, 'BashTool')
def test_has_correct_source_hint(self) -> None:
tool = MirroredTool(name='BashTool', source_hint='vscode')
self.assertEqual(tool.source_hint, 'vscode')
def test_is_frozen(self) -> None:
tool = MirroredTool(name='BashTool', source_hint='vscode')
with self.assertRaises(AttributeError):
tool.name = 'other' # type: ignore[misc]
def test_execute_returns_string(self) -> None:
registry = build_execution_registry()
tool = registry.tools[0]
result = tool.execute('test payload')
self.assertIsInstance(result, str)
self.assertIn('Mirrored tool', result)
class TestEmptyRegistry(unittest.TestCase):
"""Tests for an empty ExecutionRegistry."""
def setUp(self) -> None:
self.registry = ExecutionRegistry(commands=(), tools=())
def test_command_returns_none(self) -> None:
self.assertIsNone(self.registry.command('anything'))
def test_tool_returns_none(self) -> None:
self.assertIsNone(self.registry.tool('anything'))
def test_empty_commands_tuple(self) -> None:
self.assertEqual(len(self.registry.commands), 0)
def test_empty_tools_tuple(self) -> None:
self.assertEqual(len(self.registry.tools), 0)
if __name__ == '__main__':
unittest.main()
-365
View File
@@ -1,365 +0,0 @@
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.lsp_runtime import LSPRuntime
class ExtendedToolTests(unittest.TestCase):
def test_web_fetch_rejects_file_url(self) -> None:
"""Verify that file:// URLs are blocked to prevent SSRF attacks."""
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
target = workspace / 'page.txt'
target.write_text('hello from web fetch\n', encoding='utf-8')
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
tool_registry=default_tool_registry(),
)
result = execute_tool(
default_tool_registry(),
'web_fetch',
{'url': target.resolve().as_uri()},
context,
)
self.assertFalse(result.ok)
self.assertIn('http or https', result.content)
def test_tool_search_lists_matching_tools(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
context = build_tool_context(
AgentRuntimeConfig(cwd=Path(tmp_dir)),
tool_registry=registry,
)
result = execute_tool(
registry,
'tool_search',
{'query': 'file'},
context,
)
self.assertTrue(result.ok)
self.assertIn('# Tool Search', result.content)
self.assertIn('read_file', result.content)
self.assertIn('write_file', result.content)
def test_grep_search_skips_generated_dirs_by_default(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.next' / 'static').mkdir(parents=True)
(workspace / '.next' / 'static' / 'bundle.js').write_text(
'router_session_parquet should not be searched by default\n',
encoding='utf-8',
)
(workspace / 'README.md').write_text(
'router_session_parquet is documented here\n',
encoding='utf-8',
)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
tool_registry=registry,
)
result = execute_tool(
registry,
'grep_search',
{'pattern': 'router_session_parquet'},
context,
)
self.assertTrue(result.ok)
self.assertIn('README.md:1:', result.content)
self.assertNotIn('.next/static/bundle.js', result.content)
def test_grep_search_truncates_very_long_lines(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
long_line = 'prefix needle ' + ('x' * 5000)
(workspace / 'large.txt').write_text(long_line, encoding='utf-8')
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace, max_output_chars=1200),
tool_registry=registry,
)
result = execute_tool(
registry,
'grep_search',
{'pattern': 'needle'},
context,
)
self.assertTrue(result.ok)
self.assertIn('large.txt:1:', result.content)
self.assertIn('[line truncated,', result.content)
self.assertLess(len(result.content), 1200)
def test_read_file_can_read_explicit_external_path(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as workspace_dir, tempfile.TemporaryDirectory() as external_dir:
external = Path(external_dir) / 'reference.txt'
external.write_text('external reference\n', encoding='utf-8')
context = build_tool_context(
AgentRuntimeConfig(cwd=Path(workspace_dir)),
tool_registry=registry,
)
result = execute_tool(
registry,
'read_file',
{'path': str(external)},
context,
)
self.assertTrue(result.ok)
self.assertIn('external reference', result.content)
def test_write_file_blocks_platform_code_paths(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'src').mkdir()
(workspace / 'src' / 'agent_tools.py').write_text('', encoding='utf-8')
(workspace / 'backend' / 'api').mkdir(parents=True)
(workspace / 'backend' / 'api' / 'server.py').write_text('', encoding='utf-8')
(workspace / 'frontend' / 'app').mkdir(parents=True)
scratchpad = (
workspace
/ '.port_sessions'
/ 'accounts'
/ 'user'
/ 'sessions'
/ 'thread'
/ 'scratchpad'
)
scratchpad.mkdir(parents=True)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
scratchpad_directory=scratchpad,
tool_registry=registry,
)
result = execute_tool(
registry,
'write_file',
{'path': 'src/new_file.py', 'content': 'print(1)\n'},
context,
)
output_result = execute_tool(
registry,
'write_file',
{
'path': '.port_sessions/accounts/user/sessions/thread/output/report.txt',
'content': 'ok\n',
},
context,
)
self.assertFalse(result.ok)
self.assertEqual(result.metadata.get('error_kind'), 'permission_denied')
self.assertTrue(output_result.ok)
def test_logical_session_paths_route_to_current_session(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
scratchpad = (
workspace
/ '.port_sessions'
/ 'accounts'
/ 'user'
/ 'sessions'
/ 'thread'
/ 'scratchpad'
)
scratchpad.mkdir(parents=True)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
scratchpad_directory=scratchpad,
tool_registry=registry,
)
write_result = execute_tool(
registry,
'write_file',
{'path': 'output/report.md', 'content': 'session report\n'},
context,
)
read_result = execute_tool(
registry,
'read_file',
{'path': 'output/report.md'},
context,
)
session_file_exists = (scratchpad.parent / 'output' / 'report.md').is_file()
root_file_exists = (workspace / 'output' / 'report.md').exists()
self.assertTrue(write_result.ok, write_result.content)
self.assertTrue(read_result.ok, read_result.content)
self.assertIn('session report', read_result.content)
self.assertTrue(session_file_exists)
self.assertFalse(root_file_exists)
def test_write_file_serializes_structured_payloads(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
scratchpad = workspace / '.port_sessions' / 'accounts' / 'user' / 'sessions' / 'thread' / 'scratchpad'
scratchpad.mkdir(parents=True)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
scratchpad_directory=scratchpad,
tool_registry=registry,
)
json_result = execute_tool(
registry,
'write_file',
{
'path': 'output/data.json',
'json_content': {'query': '导航到公司', 'target': 'Agent(tag="地图导航")'},
'newline_at_end': True,
},
context,
)
jsonl_result = execute_tool(
registry,
'write_file',
{
'path': 'output/data.jsonl',
'jsonl_records': [
{'query': '附近的奶茶', 'target': 'Agent(tag="餐饮服务")'},
{'query': '导航去海底捞', 'target': 'Agent(tag="地图导航")'},
],
'newline_at_end': True,
},
context,
)
csv_result = execute_tool(
registry,
'write_file',
{
'path': 'output/data.csv',
'csv_headers': ['query', 'target'],
'csv_rows': [
['附近的奶茶', 'Agent(tag="餐饮服务")'],
['导航去海底捞', 'Agent(tag="地图导航")'],
],
'newline_at_end': True,
},
context,
)
output_dir = scratchpad.parent / 'output'
json_text = (output_dir / 'data.json').read_text(encoding='utf-8')
jsonl_text = (output_dir / 'data.jsonl').read_text(encoding='utf-8')
csv_text = (output_dir / 'data.csv').read_text(encoding='utf-8')
self.assertTrue(json_result.ok, json_result.content)
self.assertTrue(jsonl_result.ok, jsonl_result.content)
self.assertTrue(csv_result.ok, csv_result.content)
self.assertIn('"query": "导航到公司"', json_text)
self.assertEqual(
jsonl_text.splitlines()[0],
'{"query":"附近的奶茶","target":"Agent(tag=\\"餐饮服务\\")"}',
)
self.assertIn('query,target', csv_text)
def test_sleep_tool_waits_briefly_and_returns_metadata(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
context = build_tool_context(
AgentRuntimeConfig(cwd=Path(tmp_dir)),
tool_registry=registry,
)
result = execute_tool(
registry,
'sleep',
{'seconds': 0.01},
context,
)
self.assertTrue(result.ok)
self.assertIn('slept for', result.content)
self.assertEqual(result.metadata.get('action'), 'sleep')
def test_notebook_edit_updates_ipynb_cell(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
notebook = workspace / 'demo.ipynb'
notebook.write_text(
'{\n'
' "cells": [\n'
' {"cell_type": "code", "metadata": {}, "source": ["print(1)\\n"], "outputs": [], "execution_count": null}\n'
' ],\n'
' "metadata": {},\n'
' "nbformat": 4,\n'
' "nbformat_minor": 5\n'
'}\n',
encoding='utf-8',
)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
tool_registry=registry,
)
result = execute_tool(
registry,
'notebook_edit',
{'path': 'demo.ipynb', 'cell_index': 0, 'source': 'print(2)\n'},
context,
)
updated = notebook.read_text(encoding='utf-8')
self.assertTrue(result.ok)
self.assertIn('updated notebook cell 0', result.content)
self.assertIn('print(2)', updated)
self.assertEqual(result.metadata.get('action'), 'notebook_edit')
def test_lsp_tool_returns_definition_report(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'sample.py').write_text(
'def helper(value):\n'
' return value * 2\n'
'\n'
'def run(item):\n'
' return helper(item)\n',
encoding='utf-8',
)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
tool_registry=registry,
lsp_runtime=LSPRuntime.from_workspace(workspace),
)
result = execute_tool(
registry,
'LSP',
{
'operation': 'goToDefinition',
'file_path': 'sample.py',
'line': 5,
'character': 12,
},
context,
)
self.assertTrue(result.ok)
self.assertIn('# LSP Definition', result.content)
self.assertIn('function helper', result.content)
self.assertEqual(result.metadata.get('action'), 'lsp_query')
self.assertEqual(result.metadata.get('operation'), 'goToDefinition')
-145
View File
@@ -1,145 +0,0 @@
"""Tests for the informational slash commands ported from the npm source.
Covers /output-style, /release-notes, /feedback, /upgrade, /stickers, /mobile,
/desktop, /install-github-app, /install-slack-app, /privacy-settings,
/extra-usage, /passes, /rate-limit-options, /chrome, /reload-plugins.
"""
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from src.agent_runtime import LocalCodingAgent
from src.agent_types import AgentRuntimeConfig, ModelConfig
def _make_agent(tmp_dir: str) -> LocalCodingAgent:
return LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
class ExternalSlashCommandsTest(unittest.TestCase):
"""Each test runs with CLAUDE_CODE_NO_BROWSER=1 so no browser opens."""
def setUp(self) -> None:
os.environ['CLAUDE_CODE_NO_BROWSER'] = '1'
def tearDown(self) -> None:
os.environ.pop('CLAUDE_CODE_NO_BROWSER', None)
def _run(self, cmd: str) -> str:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
return agent.run(cmd).final_output
def test_output_style_is_deprecated(self) -> None:
out = self._run('/output-style')
self.assertIn('deprecated', out.lower())
self.assertIn('/config', out)
def test_release_notes_falls_back_to_link(self) -> None:
out = self._run('/release-notes')
self.assertIn('CHANGELOG.md', out)
self.assertIn('https://github.com/anthropics/claude-code', out)
def test_release_notes_reads_local_changelog(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / 'CHANGELOG.md').write_text(
'# Changelog\n\n## 1.2.3\n- did a thing\n\n## 1.2.2\n- old\n',
encoding='utf-8',
)
agent = _make_agent(tmp)
out = agent.run('/release-notes').final_output
self.assertIn('1.2.3', out)
self.assertIn('did a thing', out)
self.assertNotIn('1.2.2', out)
def test_feedback_returns_link(self) -> None:
out = self._run('/feedback')
self.assertIn('https://github.com/anthropics/claude-code/issues', out)
def test_bug_aliases_to_feedback(self) -> None:
out = self._run('/bug')
self.assertIn('https://github.com/anthropics/claude-code/issues', out)
def test_feedback_includes_user_note(self) -> None:
out = self._run('/feedback the wrap selector keeps eating my prompt')
self.assertIn('Draft note', out)
self.assertIn('wrap selector', out)
def test_upgrade_returns_link(self) -> None:
out = self._run('/upgrade')
self.assertIn('https://claude.ai/upgrade/max', out)
def test_stickers_returns_link(self) -> None:
out = self._run('/stickers')
self.assertIn('stickermule.com/claudecode', out)
def test_mobile_lists_both_stores(self) -> None:
out = self._run('/mobile')
self.assertIn('apps.apple.com', out)
self.assertIn('play.google.com', out)
def test_ios_alias(self) -> None:
out = self._run('/ios')
self.assertIn('apps.apple.com', out)
def test_android_alias(self) -> None:
out = self._run('/android')
self.assertIn('play.google.com', out)
def test_desktop_returns_link(self) -> None:
out = self._run('/desktop')
self.assertIn('claude.ai/download', out)
def test_app_aliases_to_desktop(self) -> None:
out = self._run('/app')
self.assertIn('claude.ai/download', out)
def test_install_github_app(self) -> None:
out = self._run('/install-github-app')
self.assertIn('github.com/apps/claude', out)
def test_install_slack_app(self) -> None:
out = self._run('/install-slack-app')
self.assertIn('slack.com/marketplace/A08SF47R6P4-claude', out)
def test_privacy_settings(self) -> None:
out = self._run('/privacy-settings')
self.assertIn('claude.ai/settings/data-privacy-controls', out)
def test_extra_usage_points_to_upgrade(self) -> None:
out = self._run('/extra-usage')
self.assertIn('claude.ai/upgrade/max', out)
self.assertIn('/login', out)
def test_passes_mentions_claude_ai(self) -> None:
out = self._run('/passes')
self.assertIn('claude.ai', out.lower())
self.assertIn('passes', out.lower())
def test_rate_limit_options_lists_actions(self) -> None:
out = self._run('/rate-limit-options')
self.assertIn('/upgrade', out)
self.assertIn('/extra-usage', out)
self.assertIn('/login', out)
def test_chrome_returns_link(self) -> None:
out = self._run('/chrome')
self.assertIn('claude.ai/chrome', out)
def test_reload_plugins_reports_counts(self) -> None:
out = self._run('/reload-plugins')
self.assertIn('Reloaded plugins', out)
self.assertIn('plugin(s)', out)
self.assertIn('tool(s)', out)
self.assertIn('hook(s)', out)
if __name__ == '__main__':
unittest.main()
-291
View File
@@ -1,291 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
import zipfile
import json
from pathlib import Path
from unittest.mock import patch
import openpyxl
from fastapi.testclient import TestClient
from backend.api import server as gui_server
from backend.api.server import AgentState, create_app
def _build_client(tmp: Path) -> tuple[TestClient, AgentState]:
state = AgentState(
cwd=tmp,
model='test-model',
base_url='http://127.0.0.1:8000/v1',
api_key='local-token',
timeout_seconds=120.0,
allow_shell=False,
allow_write=False,
session_directory=tmp / 'sessions',
)
return TestClient(create_app(state)), state
class FeishuIntegrationTests(unittest.TestCase):
def test_convert_xlsx_and_docx_to_markdown(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
xlsx_path = root / '样例.xlsx'
docx_path = root / '说明.docx'
_write_xlsx(xlsx_path)
_write_docx(docx_path)
xlsx_markdown = gui_server._convert_file_to_feishu_markdown(
xlsx_path,
title='表格样例',
)
docx_markdown = gui_server._convert_file_to_feishu_markdown(
docx_path,
title='文档样例',
)
self.assertIn('| query | label |', xlsx_markdown)
self.assertIn('| 怎么去公司 | Agent(tag="地图导航") |', xlsx_markdown)
self.assertIn('产品定义说明', docx_markdown)
self.assertIn('| 功能点 | 示例 |', docx_markdown)
def test_create_online_doc_requires_feishu_login(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
client, state = _build_client(Path(tmp_dir))
file_path = _write_account_file(state, 'alice', 'draft.md', '# hello')
with patch.object(
gui_server,
'_feishu_status_payload',
return_value={'logged_in': False, 'status': 'not_logged_in'},
):
response = client.post(
'/api/files/online-doc',
json={'account_id': 'alice', 'path': str(file_path)},
)
self.assertEqual(response.status_code, 409)
self.assertEqual(response.json()['detail']['code'], 'feishu_not_logged_in')
def test_create_online_doc_calls_feishu_mcp(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
client, state = _build_client(Path(tmp_dir))
file_path = _write_account_file(state, 'alice', 'draft.md', '# hello')
with patch.object(
gui_server,
'_feishu_status_payload',
return_value={'logged_in': True, 'status': 'logged_in'},
), patch.object(
gui_server.MCPRuntime,
'call_tool',
return_value=(
'{"url":"https://mi.feishu.cn/docx/example"}',
{'server_name': 'feishu-mcp-pro', 'tool_name': 'doc_create'},
),
) as call_tool:
response = client.post(
'/api/files/online-doc',
json={'account_id': 'alice', 'path': str(file_path)},
)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload['url'], 'https://mi.feishu.cn/docx/example')
call_tool.assert_called_once()
self.assertEqual(call_tool.call_args.args[0], 'doc_create')
map_path = (
state.account_paths('alice')['base']
/ 'integrations'
/ 'feishu'
/ 'online-docs.json'
)
online_docs = json.loads(map_path.read_text(encoding='utf-8'))
self.assertEqual(
online_docs['files'][str(file_path)]['url'],
'https://mi.feishu.cn/docx/example',
)
self.assertEqual(online_docs['files'][str(file_path)]['kind'], 'doc')
def test_create_online_doc_converts_csv_to_feishu_sheet(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
client, state = _build_client(Path(tmp_dir))
file_path = _write_account_file(
state,
'alice',
'records.csv',
'query,label\n导航到公司,"Agent(tag=""地图导航"")"\n',
)
with patch.object(
gui_server,
'_feishu_status_payload',
return_value={'logged_in': True, 'status': 'logged_in'},
), patch.object(
gui_server.MCPRuntime,
'call_tool',
side_effect=[
(
'{"url":"https://mi.feishu.cn/sheets/shtcn123","spreadsheet_token":"shtcn123"}',
{'server_name': 'feishu-mcp-pro', 'tool_name': 'sheet_ops'},
),
(
'{"ok":true}',
{'server_name': 'feishu-mcp-pro', 'tool_name': 'sheet_ops'},
),
],
) as call_tool:
response = client.post(
'/api/files/online-doc',
json={'account_id': 'alice', 'path': str(file_path)},
)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload['kind'], 'sheet')
self.assertEqual(payload['url'], 'https://mi.feishu.cn/sheets/shtcn123')
self.assertEqual(call_tool.call_count, 2)
self.assertEqual(call_tool.call_args_list[0].args[0], 'sheet_ops')
self.assertEqual(call_tool.call_args_list[0].kwargs['arguments']['action'], 'create')
self.assertEqual(call_tool.call_args_list[1].kwargs['arguments']['action'], 'write')
write_params = call_tool.call_args_list[1].kwargs['arguments']['params']
self.assertEqual(write_params['range'], 'Sheet1!A1:B2')
self.assertEqual(
json.loads(write_params['values']),
[['query', 'label'], ['导航到公司', 'Agent(tag="地图导航")']],
)
map_path = (
state.account_paths('alice')['base']
/ 'integrations'
/ 'feishu'
/ 'online-docs.json'
)
online_docs = json.loads(map_path.read_text(encoding='utf-8'))
self.assertEqual(online_docs['files'][str(file_path)]['kind'], 'sheet')
def test_create_online_doc_surfaces_feishu_sheet_write_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
client, state = _build_client(Path(tmp_dir))
file_path = _write_account_file(
state,
'alice',
'records.csv',
'query,label\n导航到公司,地图导航\n',
)
with patch.object(
gui_server,
'_feishu_status_payload',
return_value={'logged_in': True, 'status': 'logged_in'},
), patch.object(
gui_server.MCPRuntime,
'call_tool',
side_effect=[
(
'{"url":"https://mi.feishu.cn/sheets/shtcn123","spreadsheet_token":"shtcn123"}',
{'server_name': 'feishu-mcp-pro', 'tool_name': 'sheet_ops'},
),
(
'{"error":"Sheet not found","code":123}',
{'server_name': 'feishu-mcp-pro', 'tool_name': 'sheet_ops'},
),
],
):
response = client.post(
'/api/files/online-doc',
json={'account_id': 'alice', 'path': str(file_path)},
)
self.assertEqual(response.status_code, 400)
self.assertIn('Sheet not found', response.json()['detail'])
def test_extract_first_url_ignores_wrapping_quotes(self) -> None:
url = gui_server._extract_first_url(
'{"url":"https://mi.feishu.cn/docx/CZIldpM3QofbbXxxAq1cEininTd"}'
)
self.assertEqual(url, 'https://mi.feishu.cn/docx/CZIldpM3QofbbXxxAq1cEininTd')
def test_feishu_command_uses_deployed_node_bin_dir(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir, patch.dict(
gui_server.os.environ,
{'CLAW_NODE_BIN_DIR': tmp_dir, 'PATH': ''},
clear=False,
):
npx_path = Path(tmp_dir) / 'npx'
npx_path.write_text('#!/usr/bin/env node\n', encoding='utf-8')
npx_path.chmod(0o755)
command = gui_server._feishu_command()
env = gui_server._feishu_env(
{
'home': Path(tmp_dir) / 'home',
'config': Path(tmp_dir) / 'config',
'npm_cache': Path(tmp_dir) / 'npm-cache',
}
)
self.assertEqual(command[0], str(npx_path))
self.assertTrue(env['PATH'].startswith(tmp_dir))
def test_create_online_doc_rejects_jsonl_for_now(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
client, state = _build_client(Path(tmp_dir))
file_path = _write_account_file(state, 'alice', 'records.jsonl', '{}\n')
response = client.post(
'/api/files/online-doc',
json={'account_id': 'alice', 'path': str(file_path)},
)
self.assertEqual(response.status_code, 400)
self.assertIn('json/jsonl', response.json()['detail'])
def _write_account_file(
state: AgentState,
account_id: str,
name: str,
content: str,
) -> Path:
path = state.account_paths(account_id)['sessions'] / 's1' / 'output' / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding='utf-8')
return path
def _write_xlsx(path: Path) -> None:
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.title = 'Sheet1'
sheet.append(['query', 'label'])
sheet.append(['怎么去公司', 'Agent(tag="地图导航")'])
workbook.save(path)
def _write_docx(path: Path) -> None:
document_xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>产品定义说明</w:t></w:r></w:p>
<w:tbl>
<w:tr>
<w:tc><w:p><w:r><w:t>功能点</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>示例</w:t></w:r></w:p></w:tc>
</w:tr>
<w:tr>
<w:tc><w:p><w:r><w:t>导航</w:t></w:r></w:p></w:tc>
<w:tc><w:p><w:r><w:t>导航到公司</w:t></w:r></w:p></w:tc>
</w:tr>
</w:tbl>
</w:body>
</w:document>
'''
with zipfile.ZipFile(path, 'w') as archive:
archive.writestr('word/document.xml', document_xml)
if __name__ == '__main__':
unittest.main()
-114
View File
@@ -1,114 +0,0 @@
"""Tests for ``src/format_utils.py``."""
from __future__ import annotations
import unittest
from src.format_utils import (
format_duration,
format_file_size,
format_number,
format_seconds_short,
format_tokens,
)
class FormatFileSizeTest(unittest.TestCase):
def test_bytes(self) -> None:
self.assertEqual(format_file_size(0), '0 bytes')
self.assertEqual(format_file_size(512), '512 bytes')
def test_kb_with_decimal(self) -> None:
self.assertEqual(format_file_size(1536), '1.5KB')
def test_kb_trims_trailing_zero(self) -> None:
self.assertEqual(format_file_size(2048), '2KB')
def test_mb(self) -> None:
self.assertEqual(format_file_size(5 * 1024 * 1024), '5MB')
def test_gb(self) -> None:
self.assertEqual(format_file_size(2 * 1024 * 1024 * 1024), '2GB')
class FormatSecondsShortTest(unittest.TestCase):
def test_basic(self) -> None:
self.assertEqual(format_seconds_short(1234), '1.2s')
def test_under_one(self) -> None:
self.assertEqual(format_seconds_short(450), '0.5s')
class FormatDurationTest(unittest.TestCase):
def test_zero(self) -> None:
self.assertEqual(format_duration(0), '0s')
def test_sub_second(self) -> None:
self.assertEqual(format_duration(0.5), '0.0s')
def test_seconds_only(self) -> None:
self.assertEqual(format_duration(5_000), '5s')
def test_minutes_seconds(self) -> None:
self.assertEqual(format_duration(125_000), '2m 5s')
def test_hours(self) -> None:
self.assertEqual(format_duration(3_725_000), '1h 2m 5s')
def test_days(self) -> None:
# 1d 2h 3m
ms = 86_400_000 + 2 * 3_600_000 + 3 * 60_000 + 0
self.assertEqual(format_duration(ms), '1d 2h 3m')
def test_hide_trailing_zeros_minutes(self) -> None:
self.assertEqual(
format_duration(120_000, hide_trailing_zeros=True), '2m',
)
def test_hide_trailing_zeros_hours(self) -> None:
self.assertEqual(
format_duration(3_600_000, hide_trailing_zeros=True), '1h',
)
def test_most_significant_only_picks_largest_unit(self) -> None:
self.assertEqual(format_duration(125_000, most_significant_only=True), '2m')
self.assertEqual(
format_duration(3_725_000, most_significant_only=True), '1h',
)
def test_rounding_carry_over(self) -> None:
# 59,500 ms rounds seconds=60 → carries to 1m 0s
self.assertEqual(format_duration(59_500 + 60_000), '2m 0s')
class FormatNumberTest(unittest.TestCase):
def test_below_thousand(self) -> None:
self.assertEqual(format_number(900), '900')
self.assertEqual(format_number(0), '0')
def test_thousands_with_decimal(self) -> None:
self.assertEqual(format_number(1321), '1.3k')
def test_thousand_keeps_decimal(self) -> None:
self.assertEqual(format_number(1000), '1.0k')
def test_millions(self) -> None:
self.assertEqual(format_number(2_500_000), '2.5m')
def test_billions(self) -> None:
self.assertEqual(format_number(3_700_000_000), '3.7b')
class FormatTokensTest(unittest.TestCase):
def test_trims_decimal_zero(self) -> None:
self.assertEqual(format_tokens(1000), '1k')
def test_keeps_meaningful_decimal(self) -> None:
self.assertEqual(format_tokens(1321), '1.3k')
def test_below_thousand(self) -> None:
self.assertEqual(format_tokens(450), '450')
if __name__ == '__main__':
unittest.main()
+87
View File
@@ -0,0 +1,87 @@
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from agent_platform.gateway.app import create_app
from agent_platform.gateway.provider import normalize_workspace_path, workspace_ref
from agent_platform.gateway.schemas import ToolResult, WorkspaceStatus
def test_workspace_paths_cannot_escape() -> None:
assert str(normalize_workspace_path("src/app.py")) == "src/app.py"
assert str(normalize_workspace_path("/workspace/src/app.py")) == "src/app.py"
for value in ("../secret", "/etc/passwd", "a/../../b", "bad\x00name"):
with pytest.raises(ValueError):
normalize_workspace_path(value)
def test_workspace_identity_is_stable_and_separate() -> None:
first = workspace_ref("user-1")
assert first == workspace_ref("user-1")
assert first != workspace_ref("user-2")
assert "user-1" not in first.container_name
assert "user-1" not in first.network_name
class FakeProvider:
provider_name = "local-docker"
async def status(self, user_id: str) -> WorkspaceStatus:
return WorkspaceStatus(
workspace_id=workspace_ref(user_id).workspace_id,
provider="local-docker",
container_name=workspace_ref(user_id).container_name,
state="running",
)
async def list_files(self, user_id, path, max_depth, limit):
return ToolResult(ok=True, output=f"{user_id}:{path}")
async def read_file(self, *args, **kwargs):
return ToolResult(ok=True)
async def write_file(self, *args, **kwargs):
return ToolResult(ok=True)
async def search_files(self, *args, **kwargs):
return ToolResult(ok=True)
async def exec(self, *args, **kwargs):
return ToolResult(ok=True)
async def apply_patch(self, *args, **kwargs):
return ToolResult(ok=True)
async def start_process(self, *args, **kwargs):
return ToolResult(ok=True)
async def poll_process(self, *args, **kwargs):
return ToolResult(ok=True)
async def cancel_process(self, *args, **kwargs):
return ToolResult(ok=True)
def test_gateway_requires_service_key_and_signed_identity(settings, identity_jwt) -> None:
app = create_app(settings, provider=FakeProvider())
with TestClient(app) as client:
assert client.post("/v1/tools/list_files", json={}).status_code == 401
headers = {
"Authorization": f"Bearer {settings.internal_gateway_key}",
"X-OpenWebUI-User-Jwt": identity_jwt,
}
response = client.post("/v1/tools/list_files", headers=headers, json={"path": "src"})
assert response.status_code == 200
assert response.json()["output"] == "user-123:src"
def test_gateway_openapi_does_not_expose_auth_headers(settings) -> None:
app = create_app(settings, provider=FakeProvider())
with TestClient(app) as client:
spec = client.get("/openapi.json").json()
operation = spec["paths"]["/v1/tools/list_files"]["post"]
assert all(
parameter["name"] not in {"Authorization", "X-OpenWebUI-User-Jwt"}
for parameter in operation.get("parameters", [])
)
-164
View File
@@ -1,164 +0,0 @@
"""Tests for ``src/git_utils.py``."""
from __future__ import annotations
import hashlib
import tempfile
import unittest
from pathlib import Path
from src import git_utils
from src.git_utils import (
find_git_root,
get_repo_remote_hash,
normalize_git_remote_url,
should_include_git_instructions,
)
class FindGitRootTest(unittest.TestCase):
def setUp(self) -> None:
find_git_root.cache_clear()
def test_finds_git_directory(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.git').mkdir()
self.assertEqual(find_git_root(tmp), str(Path(tmp).resolve()))
def test_walks_up_from_subdirectory(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.git').mkdir()
sub = Path(tmp) / 'a' / 'b' / 'c'
sub.mkdir(parents=True)
self.assertEqual(find_git_root(str(sub)), str(Path(tmp).resolve()))
def test_finds_when_git_is_a_file(self) -> None:
# Worktrees and submodules use a .git file containing 'gitdir: ...'
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.git').write_text('gitdir: /elsewhere/.git/worktrees/x')
self.assertEqual(find_git_root(tmp), str(Path(tmp).resolve()))
def test_returns_none_when_not_in_repo(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
self.assertIsNone(find_git_root(tmp))
def test_memoizes(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.git').mkdir()
first = find_git_root(tmp)
# Now remove .git — second lookup should still hit cache
(Path(tmp) / '.git').rmdir()
self.assertEqual(find_git_root(tmp), first)
class NormalizeGitRemoteUrlTest(unittest.TestCase):
def test_ssh_url(self) -> None:
self.assertEqual(
normalize_git_remote_url('git@github.com:org/repo.git'),
'github.com/org/repo',
)
def test_ssh_url_no_dot_git_suffix(self) -> None:
self.assertEqual(
normalize_git_remote_url('git@github.com:org/repo'),
'github.com/org/repo',
)
def test_https_url(self) -> None:
self.assertEqual(
normalize_git_remote_url('https://github.com/org/repo.git'),
'github.com/org/repo',
)
def test_https_with_user(self) -> None:
self.assertEqual(
normalize_git_remote_url('https://user@github.com/org/repo.git'),
'github.com/org/repo',
)
def test_ssh_protocol_url(self) -> None:
self.assertEqual(
normalize_git_remote_url('ssh://git@github.com/org/repo.git'),
'github.com/org/repo',
)
def test_lowercases_result(self) -> None:
self.assertEqual(
normalize_git_remote_url('git@GitHub.com:Org/Repo.git'),
'github.com/org/repo',
)
def test_ccr_proxy_legacy_assumes_github(self) -> None:
self.assertEqual(
normalize_git_remote_url('http://x@127.0.0.1:8080/git/org/repo'),
'github.com/org/repo',
)
def test_ccr_proxy_ghe_uses_first_segment_as_host(self) -> None:
self.assertEqual(
normalize_git_remote_url(
'http://x@127.0.0.1:8080/git/ghe.example.com/org/repo',
),
'ghe.example.com/org/repo',
)
def test_returns_none_on_garbage(self) -> None:
self.assertIsNone(normalize_git_remote_url(''))
self.assertIsNone(normalize_git_remote_url(' '))
self.assertIsNone(normalize_git_remote_url('not-a-url'))
def test_localhost_alias(self) -> None:
self.assertEqual(
normalize_git_remote_url('http://localhost:8080/git/org/repo'),
'github.com/org/repo',
)
class GetRepoRemoteHashTest(unittest.TestCase):
def test_hashes_normalized_url(self) -> None:
url = 'git@github.com:Org/Repo.git'
normalized = 'github.com/org/repo'
expected = hashlib.sha256(normalized.encode('utf-8')).hexdigest()[:16]
self.assertEqual(get_repo_remote_hash(url), expected)
def test_returns_none_for_empty(self) -> None:
self.assertIsNone(get_repo_remote_hash(None))
self.assertIsNone(get_repo_remote_hash(''))
def test_returns_none_for_unparseable(self) -> None:
self.assertIsNone(get_repo_remote_hash('not-a-url'))
class ShouldIncludeGitInstructionsTest(unittest.TestCase):
def test_default_true_when_nothing_set(self) -> None:
self.assertTrue(should_include_git_instructions(env={}))
def test_env_truthy_disables(self) -> None:
for value in ('1', 'true', 'yes', 'on'):
self.assertFalse(
should_include_git_instructions(
settings_value=True,
env={'CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS': value},
),
)
def test_env_falsy_overrides_settings(self) -> None:
# Settings says off, but env explicitly says don't disable → on
self.assertTrue(
should_include_git_instructions(
settings_value=False,
env={'CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS': '0'},
),
)
def test_settings_value_used_when_env_unset(self) -> None:
self.assertFalse(
should_include_git_instructions(settings_value=False, env={}),
)
self.assertTrue(
should_include_git_instructions(settings_value=True, env={}),
)
if __name__ == '__main__':
unittest.main()
File diff suppressed because it is too large Load Diff
-133
View File
@@ -1,133 +0,0 @@
"""Tests for ``src/ide_path_conversion.py``."""
from __future__ import annotations
import subprocess
import unittest
from unittest import mock
from src.ide_path_conversion import (
WindowsToWSLConverter,
check_wsl_distro_match,
)
class CheckWslDistroMatchTest(unittest.TestCase):
def test_matches_named_distro(self) -> None:
self.assertTrue(
check_wsl_distro_match(r'\\wsl$\Ubuntu\home\me', 'Ubuntu'),
)
def test_matches_localhost_form(self) -> None:
self.assertTrue(
check_wsl_distro_match(
r'\\wsl.localhost\Ubuntu\home\me', 'Ubuntu',
),
)
def test_mismatch(self) -> None:
self.assertFalse(
check_wsl_distro_match(r'\\wsl$\Debian\home\me', 'Ubuntu'),
)
def test_non_unc_path_returns_true(self) -> None:
self.assertTrue(check_wsl_distro_match(r'C:\Users\me', 'Ubuntu'))
class WindowsToWSLConverterToLocalPathTest(unittest.TestCase):
def test_empty_path_passthrough(self) -> None:
conv = WindowsToWSLConverter('Ubuntu')
self.assertEqual(conv.to_local_path(''), '')
def test_uses_wslpath_when_available(self) -> None:
conv = WindowsToWSLConverter(None)
with mock.patch(
'src.ide_path_conversion.subprocess.run',
return_value=subprocess.CompletedProcess(
args=[], returncode=0, stdout='/mnt/c/Users/me\n', stderr='',
),
) as run:
self.assertEqual(conv.to_local_path(r'C:\Users\me'), '/mnt/c/Users/me')
run.assert_called_once()
args = run.call_args[0][0]
self.assertEqual(args, ['wslpath', '-u', r'C:\Users\me'])
def test_falls_back_to_manual_when_wslpath_missing(self) -> None:
conv = WindowsToWSLConverter(None)
with mock.patch(
'src.ide_path_conversion.subprocess.run',
side_effect=FileNotFoundError(),
):
self.assertEqual(
conv.to_local_path(r'C:\Users\me'), '/mnt/c/Users/me',
)
def test_falls_back_to_manual_on_called_process_error(self) -> None:
conv = WindowsToWSLConverter(None)
with mock.patch(
'src.ide_path_conversion.subprocess.run',
side_effect=subprocess.CalledProcessError(1, 'wslpath'),
):
self.assertEqual(
conv.to_local_path(r'D:\path\to\file.txt'),
'/mnt/d/path/to/file.txt',
)
def test_different_distro_path_returned_as_is(self) -> None:
conv = WindowsToWSLConverter('Ubuntu')
with mock.patch(
'src.ide_path_conversion.subprocess.run',
) as run:
self.assertEqual(
conv.to_local_path(r'\\wsl$\Debian\home\me'),
r'\\wsl$\Debian\home\me',
)
run.assert_not_called()
def test_same_distro_unc_uses_wslpath(self) -> None:
conv = WindowsToWSLConverter('Ubuntu')
with mock.patch(
'src.ide_path_conversion.subprocess.run',
return_value=subprocess.CompletedProcess(
args=[], returncode=0, stdout='/home/me\n', stderr='',
),
) as run:
self.assertEqual(
conv.to_local_path(r'\\wsl$\Ubuntu\home\me'),
'/home/me',
)
run.assert_called_once()
class WindowsToWSLConverterToIdePathTest(unittest.TestCase):
def test_empty_passthrough(self) -> None:
conv = WindowsToWSLConverter(None)
self.assertEqual(conv.to_ide_path(''), '')
def test_uses_wslpath(self) -> None:
conv = WindowsToWSLConverter(None)
with mock.patch(
'src.ide_path_conversion.subprocess.run',
return_value=subprocess.CompletedProcess(
args=[], returncode=0, stdout=r'\\wsl$\Ubuntu\home\me' + '\n',
stderr='',
),
) as run:
self.assertEqual(
conv.to_ide_path('/home/me'), r'\\wsl$\Ubuntu\home\me',
)
run.assert_called_once()
args = run.call_args[0][0]
self.assertEqual(args, ['wslpath', '-w', '/home/me'])
def test_returns_original_on_failure(self) -> None:
conv = WindowsToWSLConverter(None)
with mock.patch(
'src.ide_path_conversion.subprocess.run',
side_effect=FileNotFoundError(),
):
self.assertEqual(conv.to_ide_path('/home/me'), '/home/me')
if __name__ == '__main__':
unittest.main()
-71
View File
@@ -1,71 +0,0 @@
from __future__ import annotations
import unittest
from src.jupyter_runtime import (
build_terminal_script_launcher,
clean_terminal_output,
extract_terminal_command_output,
)
class TestJupyterRuntimeOutputFiltering(unittest.TestCase):
def test_extract_terminal_command_output_drops_wrapper_echo(self) -> None:
start_marker = '__ZK_AGENT_START_test__'
raw = (
'root@host:/workspace# mkdir -p /workspace && cd /workspace && '
'printf "\\n__ZK_AGENT_START_test__\\n"; '
'( setsid bash -lc \'export A=1; echo real-output\' ) &\r\n'
f'{start_marker}\r\n'
'real-output\r\n'
)
output = extract_terminal_command_output(
clean_terminal_output(raw),
start_marker=start_marker,
)
self.assertEqual(output, 'real-output')
def test_extract_terminal_command_output_uses_last_marker(self) -> None:
start_marker = '__ZK_AGENT_START_test__'
output = extract_terminal_command_output(
f'echo text containing {start_marker}\n{start_marker}\nactual',
start_marker=start_marker,
)
self.assertEqual(output, 'actual')
def test_clean_terminal_output_drops_wrapper_job_control_line(self) -> None:
cleaned = clean_terminal_output(
'real-output\r\n'
'[1] + Done (setsid bash -lc "export A=1; lscpu")\r\n'
)
self.assertEqual(cleaned, 'real-output')
def test_clean_terminal_output_drops_continuation_prompt(self) -> None:
cleaned = clean_terminal_output('real-output\r\n# >\r\n')
self.assertEqual(cleaned, 'real-output')
def test_terminal_script_launcher_hides_complex_wrapper_from_shell(self) -> None:
launcher = build_terminal_script_launcher(
script_path='/workspace/scratchpad/.run_scripts/run.sh',
marker='__ZK_AGENT_EXIT_test__',
)
self.assertNotIn('setsid bash -lc', launcher)
self.assertEqual(
launcher,
'bash /workspace/scratchpad/.run_scripts/run.sh; '
'__zk_outer_status=$?; '
'rm -f /workspace/scratchpad/.run_scripts/run.sh; '
'if [ "$__zk_outer_status" -ne 0 ]; then '
'printf "\\n__ZK_AGENT_EXIT_test__:%s\\n" "$__zk_outer_status"; '
'fi',
)
if __name__ == '__main__':
unittest.main()
-74
View File
@@ -1,74 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from src.lsp_runtime import LSPRuntime
SAMPLE_SOURCE = '''def helper(value):
"""Double a numeric value."""
return value * 2
def orchestrate(item):
return helper(item)
class Greeter:
def greet(self, name):
return helper(len(name))
'''
class LSPRuntimeTests(unittest.TestCase):
def test_runtime_renders_symbols_definitions_references_and_hover(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'sample.py').write_text(SAMPLE_SOURCE, encoding='utf-8')
runtime = LSPRuntime.from_workspace(workspace)
summary = runtime.render_summary()
symbols = runtime.render_document_symbols('sample.py')
workspace_symbols = runtime.render_workspace_symbols('helper')
definition = runtime.render_definition('sample.py', 7, 12)
references = runtime.render_references('sample.py', 7, 12)
hover = runtime.render_hover('sample.py', 1, 5)
self.assertIn('Indexed candidate files: 1', summary)
self.assertIn('# LSP Document Symbols', symbols)
self.assertIn('function helper', symbols)
self.assertIn('function orchestrate', symbols)
self.assertIn('class Greeter', symbols)
self.assertIn('# LSP Workspace Symbols', workspace_symbols)
self.assertIn('helper', workspace_symbols)
self.assertIn('# LSP Definition', definition)
self.assertIn('function helper', definition)
self.assertIn('# LSP References', references)
self.assertIn('helper(item)', references)
self.assertIn('# LSP Hover', hover)
self.assertIn('signature=helper(value)', hover)
self.assertIn('Double a numeric value.', hover)
def test_runtime_renders_call_hierarchy_and_diagnostics(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'sample.py').write_text(SAMPLE_SOURCE, encoding='utf-8')
(workspace / 'broken.py').write_text('def broken(:\n pass\n', encoding='utf-8')
runtime = LSPRuntime.from_workspace(workspace)
hierarchy = runtime.render_prepare_call_hierarchy('sample.py', 6, 12)
incoming = runtime.render_incoming_calls('sample.py', 1, 5)
outgoing = runtime.render_outgoing_calls('sample.py', 6, 12)
diagnostics = runtime.render_diagnostics('broken.py')
self.assertIn('# LSP Call Hierarchy', hierarchy)
self.assertIn('symbol=orchestrate', hierarchy)
self.assertIn('# LSP Incoming Calls', incoming)
self.assertIn('orchestrate', incoming)
self.assertIn('greet', incoming)
self.assertIn('# LSP Outgoing Calls', outgoing)
self.assertIn('helper', outgoing)
self.assertIn('# LSP Diagnostics', diagnostics)
self.assertIn('syntax-error', diagnostics)
-279
View File
@@ -1,279 +0,0 @@
from __future__ import annotations
import json
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
from unittest.mock import patch
from src.main import _build_runtime_config, _build_agent, _run_agent_chat_loop, build_parser, main
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
class MainCliTests(unittest.TestCase):
def test_build_runtime_config_parses_model_and_session_budget_flags(self) -> None:
parser = build_parser()
args = parser.parse_args(
[
'agent',
'Summarize the repo',
'--cwd',
'.',
'--max-model-calls',
'3',
'--max-session-turns',
'5',
]
)
runtime_config = _build_runtime_config(args)
self.assertEqual(runtime_config.budget_config.max_model_calls, 3)
self.assertEqual(runtime_config.budget_config.max_session_turns, 5)
def test_agent_chat_loop_runs_multiple_turns_and_reuses_session(self) -> None:
responses = [
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'First chat reply.',
},
'finish_reason': 'stop',
}
],
'usage': {'prompt_tokens': 5, 'completion_tokens': 2},
},
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'Second chat reply.',
},
'finish_reason': 'stop',
}
],
'usage': {'prompt_tokens': 6, 'completion_tokens': 2},
},
]
recorded_results: list[str] = []
recorded_lines: list[str] = []
prompts = iter(['Second prompt', '/exit'])
def _input(prompt: str) -> str:
return next(prompts)
def _output(line: str) -> None:
recorded_lines.append(line)
def _result_printer(result, *, show_transcript: bool) -> None: # noqa: ANN001
recorded_results.append(result.final_output)
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
session_dir = workspace / '.port_sessions' / 'agent'
with patch(
'src.openai_compat.request.urlopen',
side_effect=make_urlopen_side_effect(responses),
):
parser = build_parser()
args = parser.parse_args(
[
'agent-chat',
'First prompt',
'--model',
'test-model',
'--cwd',
str(workspace),
]
)
agent = _build_agent(args)
agent.runtime_config = replace(
agent.runtime_config,
session_directory=session_dir,
)
exit_code = _run_agent_chat_loop(
agent,
initial_prompt=args.prompt,
resume_session_id=None,
show_transcript=False,
input_func=_input,
output_func=_output,
result_printer=_result_printer,
)
self.assertEqual(exit_code, 0)
self.assertEqual(recorded_results, ['First chat reply.', 'Second chat reply.'])
self.assertIn('# Agent Chat', recorded_lines)
self.assertIn('chat_ended=user_exit', recorded_lines)
def test_parser_accepts_remote_runtime_commands(self) -> None:
parser = build_parser()
args = parser.parse_args(['remote-profiles', '--cwd', '.'])
self.assertEqual(args.command, 'remote-profiles')
self.assertEqual(args.cwd, '.')
def test_parser_accepts_account_runtime_commands(self) -> None:
parser = build_parser()
args = parser.parse_args(['account-profiles', '--cwd', '.'])
self.assertEqual(args.command, 'account-profiles')
self.assertEqual(args.cwd, '.')
def test_parser_accepts_ask_user_runtime_commands(self) -> None:
parser = build_parser()
args = parser.parse_args(['ask-history', '--cwd', '.'])
self.assertEqual(args.command, 'ask-history')
self.assertEqual(args.cwd, '.')
def test_parser_accepts_search_runtime_commands(self) -> None:
parser = build_parser()
args = parser.parse_args(['search', 'repo query', '--cwd', '.', '--provider', 'local-search'])
self.assertEqual(args.command, 'search')
self.assertEqual(args.query, 'repo query')
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'])
self.assertEqual(args.command, 'mcp-tools')
self.assertEqual(args.server, 'remote')
self.assertEqual(args.cwd, '.')
def test_parser_accepts_daemon_subcommands(self) -> None:
parser = build_parser()
args = parser.parse_args(['daemon', 'ps'])
self.assertEqual(args.command, 'daemon')
self.assertEqual(args.daemon_command, 'ps')
def test_parser_accepts_config_runtime_commands(self) -> None:
parser = build_parser()
args = parser.parse_args(['config-get', 'review.mode', '--cwd', '.'])
self.assertEqual(args.command, 'config-get')
self.assertEqual(args.key_path, 'review.mode')
self.assertEqual(args.cwd, '.')
def test_parser_accepts_lsp_runtime_commands(self) -> None:
parser = build_parser()
args = parser.parse_args(['lsp-definition', 'sample.py', '4', '12', '--cwd', '.'])
self.assertEqual(args.command, 'lsp-definition')
self.assertEqual(args.file_path, 'sample.py')
self.assertEqual(args.line, 4)
self.assertEqual(args.character, 12)
self.assertEqual(args.cwd, '.')
def test_parser_accepts_token_budget_command(self) -> None:
parser = build_parser()
args = parser.parse_args(['token-budget', '--cwd', '.'])
self.assertEqual(args.command, 'token-budget')
self.assertEqual(args.cwd, '.')
def test_parser_accepts_agents_command(self) -> None:
parser = build_parser()
args = parser.parse_args(['agents', 'reviewer', '--cwd', '.', '--all'])
self.assertEqual(args.command, 'agents')
self.assertEqual(args.agent_type, 'reviewer')
self.assertTrue(args.all)
self.assertEqual(args.cwd, '.')
def test_parser_accepts_agent_management_commands(self) -> None:
parser = build_parser()
create_args = parser.parse_args(
['agents-create', 'reviewer', '--cwd', '.', '--description', 'Review code', '--prompt', 'Inspect diffs']
)
self.assertEqual(create_args.command, 'agents-create')
self.assertEqual(create_args.agent_type, 'reviewer')
self.assertEqual(create_args.description, 'Review code')
update_args = parser.parse_args(['agents-update', 'reviewer', '--cwd', '.', '--source', 'auto'])
self.assertEqual(update_args.command, 'agents-update')
self.assertEqual(update_args.source, 'auto')
delete_args = parser.parse_args(['agents-delete', 'reviewer', '--cwd', '.', '--source', 'project'])
self.assertEqual(delete_args.command, 'agents-delete')
self.assertEqual(delete_args.source, 'project')
def test_main_can_create_and_delete_agent_definition(self) -> None:
with tempfile.TemporaryDirectory() as home_dir, tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
with patch.dict('os.environ', {'HOME': home_dir}):
exit_code = main(
[
'agents-create',
'reviewer',
'--cwd',
str(workspace),
'--description',
'Review code carefully',
'--prompt',
'Inspect code and summarize risks.',
]
)
self.assertEqual(exit_code, 0)
self.assertTrue((workspace / '.claude' / 'agents' / 'reviewer.md').exists())
exit_code = main(
[
'agents-delete',
'reviewer',
'--cwd',
str(workspace),
'--source',
'project',
]
)
self.assertEqual(exit_code, 0)
self.assertFalse((workspace / '.claude' / 'agents' / 'reviewer.md').exists())
def test_parser_accepts_team_runtime_commands(self) -> None:
parser = build_parser()
args = parser.parse_args(['team-create', 'reviewers', '--member', 'alice', '--cwd', '.'])
self.assertEqual(args.command, 'team-create')
self.assertEqual(args.team_name, 'reviewers')
self.assertEqual(args.member, ['alice'])
self.assertEqual(args.cwd, '.')
-350
View File
@@ -1,350 +0,0 @@
from __future__ import annotations
import json
import sys
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 AgentRuntimeConfig, ModelConfig
from src.mcp_runtime import MCPRuntime
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
class MCPRuntimeTests(unittest.TestCase):
def _write_fake_stdio_server(self, workspace: Path) -> Path:
server_path = workspace / 'fake_mcp_server.py'
server_path.write_text(
(
'import json, sys\n'
'RESOURCES = [{"uri": "mcp://remote/notes", "name": "Remote Notes", "mimeType": "text/plain"}]\n'
'TOOLS = [{"name": "echo", "description": "Echo text", "inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}}}]\n'
'for raw in sys.stdin:\n'
' raw = raw.strip()\n'
' if not raw:\n'
' continue\n'
' message = json.loads(raw)\n'
' method = message.get("method")\n'
' if method == "initialize":\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"protocolVersion": "2025-11-25", "capabilities": {"resources": {}, "tools": {}}, "serverInfo": {"name": "fake-remote", "version": "1.0.0"}}}\n'
' print(json.dumps(response), flush=True)\n'
' continue\n'
' if method == "notifications/initialized":\n'
' continue\n'
' if method == "resources/list":\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"resources": RESOURCES}}\n'
' print(json.dumps(response), flush=True)\n'
' continue\n'
' if method == "resources/read":\n'
' uri = message.get("params", {}).get("uri")\n'
' text = "remote notes via stdio" if uri == "mcp://remote/notes" else "unknown resource"\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"contents": [{"uri": uri, "mimeType": "text/plain", "text": text}]}}\n'
' print(json.dumps(response), flush=True)\n'
' continue\n'
' if method == "tools/list":\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"tools": TOOLS}}\n'
' print(json.dumps(response), flush=True)\n'
' continue\n'
' if method == "tools/call":\n'
' params = message.get("params", {})\n'
' text = params.get("arguments", {}).get("text", "")\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "result": {"content": [{"type": "text", "text": "echo:" + text}], "isError": False}}\n'
' print(json.dumps(response), flush=True)\n'
' continue\n'
' response = {"jsonrpc": "2.0", "id": message.get("id"), "error": {"code": -32601, "message": "Method not found"}}\n'
' print(json.dumps(response), flush=True)\n'
),
encoding='utf-8',
)
return server_path
def test_runtime_discovers_and_reads_local_resources(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'notes.txt').write_text('mcp notes\n', encoding='utf-8')
(workspace / '.claw-mcp.json').write_text(
(
'{"servers":[{"name":"workspace","resources":['
'{"uri":"mcp://workspace/notes","name":"Notes","path":"notes.txt"},'
'{"uri":"mcp://workspace/inline","name":"Inline","text":"inline body"}'
']}]}'
),
encoding='utf-8',
)
runtime = MCPRuntime.from_workspace(workspace)
self.assertEqual(len(runtime.resources), 2)
self.assertIn('Local MCP resources: 2', runtime.render_summary())
self.assertEqual(runtime.read_resource('mcp://workspace/inline'), 'inline body')
self.assertIn('mcp notes', runtime.read_resource('mcp://workspace/notes'))
def test_runtime_discovers_stdio_server_and_remote_resources_and_tools(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
server_path = self._write_fake_stdio_server(workspace)
(workspace / '.claw-mcp.json').write_text(
json.dumps(
{
'mcpServers': {
'remote': {
'command': sys.executable,
'args': ['-u', str(server_path)],
}
}
}
),
encoding='utf-8',
)
runtime = MCPRuntime.from_workspace(workspace)
resources = runtime.list_resources()
tools = runtime.list_tools()
self.assertEqual(len(runtime.servers), 1)
self.assertTrue(runtime.has_transport_servers())
self.assertIn('Configured MCP servers: 1', runtime.render_summary())
self.assertEqual(len(resources), 1)
self.assertEqual(resources[0].uri, 'mcp://remote/notes')
self.assertIn('remote notes via stdio', runtime.read_resource('mcp://remote/notes'))
self.assertEqual(len(tools), 1)
self.assertEqual(tools[0].name, 'echo')
rendered, metadata = runtime.call_tool('echo', arguments={'text': 'hello'})
self.assertIn('echo:hello', rendered)
self.assertEqual(metadata.get('server_name'), 'remote')
def test_mcp_tools_execute_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'notes.txt').write_text('mcp notes\n', encoding='utf-8')
(workspace / '.claw-mcp.json').write_text(
(
'{"servers":[{"name":"workspace","resources":['
'{"uri":"mcp://workspace/notes","name":"Notes","path":"notes.txt"}'
']}]}'
),
encoding='utf-8',
)
runtime = MCPRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
mcp_runtime=runtime,
)
list_result = execute_tool(
default_tool_registry(),
'mcp_list_resources',
{},
context,
)
read_result = execute_tool(
default_tool_registry(),
'mcp_read_resource',
{'uri': 'mcp://workspace/notes'},
context,
)
self.assertTrue(list_result.ok)
self.assertIn('mcp://workspace/notes', list_result.content)
self.assertTrue(read_result.ok)
self.assertIn('mcp notes', read_result.content)
def test_mcp_transport_tools_execute_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
server_path = self._write_fake_stdio_server(workspace)
(workspace / '.claw-mcp.json').write_text(
json.dumps(
{
'mcpServers': {
'remote': {
'command': sys.executable,
'args': ['-u', str(server_path)],
}
}
}
),
encoding='utf-8',
)
runtime = MCPRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
mcp_runtime=runtime,
)
list_result = execute_tool(
default_tool_registry(),
'mcp_list_tools',
{},
context,
)
call_result = execute_tool(
default_tool_registry(),
'mcp_call_tool',
{'tool_name': 'echo', 'arguments': {'text': 'tool-run'}},
context,
)
self.assertTrue(list_result.ok)
self.assertIn('echo', list_result.content)
self.assertTrue(call_result.ok)
self.assertIn('echo:tool-run', call_result.content)
self.assertEqual(call_result.metadata.get('action'), 'mcp_call_tool')
def test_agent_can_use_mcp_tools_in_model_loop(self) -> None:
responses = [
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'I will inspect the MCP resource.',
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'mcp_read_resource',
'arguments': '{"uri": "mcp://workspace/notes"}',
},
}
],
},
'finish_reason': 'tool_calls',
}
],
'usage': {'prompt_tokens': 8, 'completion_tokens': 3},
},
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'The MCP resource says mcp notes.',
},
'finish_reason': 'stop',
}
],
'usage': {'prompt_tokens': 6, 'completion_tokens': 3},
},
]
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / 'notes.txt').write_text('mcp notes\n', encoding='utf-8')
(workspace / '.claw-mcp.json').write_text(
(
'{"servers":[{"name":"workspace","resources":['
'{"uri":"mcp://workspace/notes","name":"Notes","path":"notes.txt"}'
']}]}'
),
encoding='utf-8',
)
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',
base_url='http://127.0.0.1:8000/v1',
),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
result = agent.run('Read the MCP notes resource')
self.assertEqual(result.final_output, 'The MCP resource says mcp notes.')
self.assertEqual(result.tool_calls, 1)
tool_message = next(
message
for message in result.transcript
if message.get('role') == 'tool'
)
self.assertIn('mcp notes', tool_message.get('content', ''))
def test_agent_can_use_transport_backed_mcp_call_tool_in_model_loop(self) -> None:
responses = [
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'I will call the remote MCP tool.',
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'mcp_call_tool',
'arguments': '{"tool_name": "echo", "server": "remote", "arguments": {"text": "agent-call"}}',
},
}
],
},
'finish_reason': 'tool_calls',
}
],
'usage': {'prompt_tokens': 8, 'completion_tokens': 3},
},
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'The remote MCP tool replied with echo:agent-call.',
},
'finish_reason': 'stop',
}
],
'usage': {'prompt_tokens': 6, 'completion_tokens': 3},
},
]
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
server_path = self._write_fake_stdio_server(workspace)
(workspace / '.claw-mcp.json').write_text(
json.dumps(
{
'mcpServers': {
'remote': {
'command': sys.executable,
'args': ['-u', str(server_path)],
}
}
}
),
encoding='utf-8',
)
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',
base_url='http://127.0.0.1:8000/v1',
),
runtime_config=AgentRuntimeConfig(cwd=workspace),
)
result = agent.run('Call the remote MCP echo tool')
self.assertEqual(result.final_output, 'The remote MCP tool replied with echo:agent-call.')
self.assertEqual(result.tool_calls, 1)
tool_message = next(
message
for message in result.transcript
if message.get('role') == 'tool'
)
self.assertIn('echo:agent-call', tool_message.get('content', ''))
-143
View File
@@ -1,143 +0,0 @@
"""Tests for discovery slash commands ported from the npm source.
Covers /version, /init, /ide, /plugin, /remote-env.
"""
from __future__ import annotations
import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from src.agent_runtime import LocalCodingAgent
from src.agent_slash_commands import preprocess_slash_command
from src.agent_types import AgentRuntimeConfig, ModelConfig
def _make_agent(tmp_dir: str) -> LocalCodingAgent:
return LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
def _local_settings(tmp_dir: str) -> dict:
path = Path(tmp_dir) / '.claude' / 'settings.local.json'
if not path.exists():
return {}
return json.loads(path.read_text(encoding='utf-8'))
class VersionCommandTest(unittest.TestCase):
def test_prints_python_runtime_version(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/version').final_output
self.assertIn('claw-code-agent', out)
self.assertIn('Python', out)
class InitCommandTest(unittest.TestCase):
def test_returns_prompt_with_claude_md_instructions(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = preprocess_slash_command(agent, '/init')
self.assertTrue(result.handled)
self.assertTrue(result.should_query)
self.assertIn('CLAUDE.md', result.prompt or '')
self.assertIn('analyze this codebase', (result.prompt or '').lower())
class IdeCommandTest(unittest.TestCase):
def test_no_ide_when_env_clean(self) -> None:
clean = {k: v for k, v in os.environ.items() if k not in {
'TERM_PROGRAM', 'VSCODE_INJECTION', 'VSCODE_PID',
'JETBRAINS_IDE', 'TERMINAL_EMULATOR', 'SSH_CONNECTION',
}}
with mock.patch.dict(os.environ, clean, clear=True):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/ide').final_output
self.assertIn('No IDE detected', out)
self.assertIn('IDE auto-connect', out)
def test_detects_vscode(self) -> None:
env = {'VSCODE_PID': '1234', 'TERM_PROGRAM': 'vscode'}
with mock.patch.dict(os.environ, env, clear=True):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/ide').final_output
self.assertIn('Visual Studio Code', out)
self.assertIn('VSCODE_PID=1234', out)
class PluginCommandTest(unittest.TestCase):
def test_lists_no_plugins_when_empty(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/plugin').final_output
self.assertIn('No installed plugins', out)
def test_help_describes_usage(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/plugin help').final_output
self.assertIn('Usage: /plugin', out)
self.assertIn('list', out)
def test_unknown_subcommand(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/plugin bogus').final_output
self.assertIn('Unknown plugin subcommand', out)
class RemoteEnvCommandTest(unittest.TestCase):
def test_lists_empty_profiles(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-env').final_output
self.assertIn('Available remote environments', out)
self.assertIn('no profiles found', out)
self.assertIn('Usage:', out)
def test_clear_when_no_default_set(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-env clear').final_output
self.assertIn('No default remote environment', out)
def test_unknown_profile_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-env nope').final_output
self.assertIn('Unknown remote environment', out)
self.assertIn('nope', out)
def test_set_then_clear_persists(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.remote.json').write_text(json.dumps({
'profiles': [
{'name': 'sandbox', 'mode': 'ssh', 'target': 'user@host'},
],
}), encoding='utf-8')
agent = _make_agent(tmp)
set_out = agent.run('/remote-env sandbox').final_output
self.assertIn('Default remote environment set to sandbox', set_out)
self.assertEqual(_local_settings(tmp).get('defaultRemoteEnvironment'), 'sandbox')
agent2 = _make_agent(tmp)
list_out = agent2.run('/remote-env').final_output
self.assertIn('sandbox', list_out)
self.assertIn('(default)', list_out)
clear_out = agent2.run('/remote-env clear').final_output
self.assertIn('Cleared default remote environment', clear_out)
self.assertIsNone(_local_settings(tmp).get('defaultRemoteEnvironment'))
if __name__ == '__main__':
unittest.main()
-167
View File
@@ -1,167 +0,0 @@
"""Tests for model pricing utilities ported from utils/modelCost.ts."""
from __future__ import annotations
import unittest
from src.model_cost import (
COST_HAIKU_35,
COST_HAIKU_45,
COST_TIER_3_15,
COST_TIER_5_25,
COST_TIER_15_75,
COST_TIER_30_150,
DEFAULT_UNKNOWN_MODEL_COST,
calculate_cost_from_tokens,
calculate_usd_cost,
format_model_pricing,
get_model_costs,
get_model_pricing_string,
get_opus_4_6_cost_tier,
tokens_to_usd_cost,
)
class TierConstantsTest(unittest.TestCase):
def test_sonnet_tier(self) -> None:
self.assertEqual(COST_TIER_3_15.input_tokens, 3.0)
self.assertEqual(COST_TIER_3_15.output_tokens, 15.0)
def test_opus_4_tier(self) -> None:
self.assertEqual(COST_TIER_15_75.input_tokens, 15.0)
def test_opus_4_5_tier(self) -> None:
self.assertEqual(COST_TIER_5_25.input_tokens, 5.0)
def test_fast_mode_tier(self) -> None:
self.assertEqual(COST_TIER_30_150.input_tokens, 30.0)
def test_haiku_tiers(self) -> None:
self.assertAlmostEqual(COST_HAIKU_35.input_tokens, 0.8)
self.assertEqual(COST_HAIKU_45.input_tokens, 1.0)
class GetModelCostsTest(unittest.TestCase):
def test_opus_4_6_default(self) -> None:
self.assertIs(get_model_costs('claude-opus-4-6'), COST_TIER_5_25)
def test_opus_4_6_fast_mode(self) -> None:
self.assertIs(
get_model_costs('claude-opus-4-6', fast_mode=True),
COST_TIER_30_150,
)
def test_versioned_model_name_resolves(self) -> None:
self.assertIs(
get_model_costs('claude-opus-4-6-20251015'),
COST_TIER_5_25,
)
def test_sonnet_models_use_3_15(self) -> None:
for name in ('claude-sonnet-4-6', 'claude-sonnet-4-5', 'claude-sonnet-4'):
self.assertIs(get_model_costs(name), COST_TIER_3_15)
def test_opus_4_and_4_1_use_15_75(self) -> None:
self.assertIs(get_model_costs('claude-opus-4'), COST_TIER_15_75)
self.assertIs(get_model_costs('claude-opus-4-1'), COST_TIER_15_75)
def test_haiku_4_5(self) -> None:
self.assertIs(get_model_costs('claude-haiku-4-5-20251001'), COST_HAIKU_45)
def test_haiku_3_5(self) -> None:
self.assertIs(get_model_costs('claude-3-5-haiku-20241022'), COST_HAIKU_35)
def test_unknown_falls_back_to_default(self) -> None:
self.assertIs(get_model_costs('mystery-llm-3000'), DEFAULT_UNKNOWN_MODEL_COST)
def test_get_opus_4_6_helper_matches_fast_mode(self) -> None:
self.assertIs(get_opus_4_6_cost_tier(False), COST_TIER_5_25)
self.assertIs(get_opus_4_6_cost_tier(True), COST_TIER_30_150)
class TokensToUsdCostTest(unittest.TestCase):
def test_simple_input_output(self) -> None:
cost = tokens_to_usd_cost(
COST_TIER_3_15, input_tokens=1_000_000, output_tokens=500_000,
)
self.assertAlmostEqual(cost, 3.0 + 7.5)
def test_includes_cache_tokens(self) -> None:
cost = tokens_to_usd_cost(
COST_TIER_3_15,
input_tokens=0,
output_tokens=0,
cache_read_input_tokens=1_000_000,
cache_creation_input_tokens=1_000_000,
)
self.assertAlmostEqual(
cost,
COST_TIER_3_15.prompt_cache_read_tokens
+ COST_TIER_3_15.prompt_cache_write_tokens,
)
def test_includes_web_search(self) -> None:
cost = tokens_to_usd_cost(
COST_TIER_3_15,
input_tokens=0,
output_tokens=0,
web_search_requests=10,
)
self.assertAlmostEqual(cost, 0.10)
class CalculateUsdCostTest(unittest.TestCase):
def test_resolves_model_then_costs(self) -> None:
cost = calculate_usd_cost(
'claude-sonnet-4-6',
input_tokens=1_000_000,
output_tokens=500_000,
)
self.assertAlmostEqual(cost, 10.5)
def test_fast_mode_changes_opus_46_cost(self) -> None:
normal = calculate_usd_cost(
'claude-opus-4-6', input_tokens=1_000_000, output_tokens=0,
)
fast = calculate_usd_cost(
'claude-opus-4-6', input_tokens=1_000_000, output_tokens=0,
fast_mode=True,
)
self.assertGreater(fast, normal)
self.assertAlmostEqual(normal, 5.0)
self.assertAlmostEqual(fast, 30.0)
class CalculateCostFromTokensTest(unittest.TestCase):
def test_camel_case_dict_input(self) -> None:
cost = calculate_cost_from_tokens(
'claude-opus-4-1',
{
'inputTokens': 1_000_000,
'outputTokens': 0,
'cacheReadInputTokens': 0,
'cacheCreationInputTokens': 0,
},
)
self.assertAlmostEqual(cost, 15.0)
class FormatPricingTest(unittest.TestCase):
def test_integers_no_decimals(self) -> None:
self.assertEqual(format_model_pricing(COST_TIER_3_15), '$3/$15 per Mtok')
def test_haiku_decimals(self) -> None:
self.assertEqual(format_model_pricing(COST_HAIKU_35), '$0.80/$4 per Mtok')
def test_get_pricing_string_known(self) -> None:
self.assertEqual(
get_model_pricing_string('claude-opus-4-6'),
'$5/$25 per Mtok',
)
def test_get_pricing_string_unknown(self) -> None:
self.assertIsNone(get_model_pricing_string('unknown-model'))
if __name__ == '__main__':
unittest.main()
-138
View File
@@ -1,138 +0,0 @@
from __future__ import annotations
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from fastapi.testclient import TestClient
from backend.api.server import AgentState, _normalize_model_list, create_app
class ModelListTests(unittest.TestCase):
def test_normalize_model_list_uses_provider_prefixed_llm_ids(self) -> None:
payload = {
'data': [
{
'id': 'ASR_NonStreaming',
'object': 'model',
'owned_by': 'xiaomi',
'model_type': 'speech2text',
},
{
'id': 'DeepSeek-R1-0528',
'object': 'model',
'owned_by': 'xiaomi',
'model_type': 'llm',
},
{
'id': 'gpt-5',
'object': 'model',
'owned_by': 'azure_openai',
'model_type': 'llm',
},
{
'id': 'gpt-5.5',
'object': 'model',
'owned_by': 'azure_openai',
'model_type': 'llm',
},
{
'id': 'gpt-4o-audio-preview',
'object': 'model',
'owned_by': 'azure_openai',
'model_type': 'llm',
},
{
'id': 'embedding-v1',
'object': 'model',
'owned_by': 'example',
'model_type': 'text-embedding',
},
{
'id': 'ernie-4.0-turbo-128k',
'object': 'model',
'owned_by': 'baidu_qianfan',
'model_type': 'llm',
},
{
'id': 'Pro/deepseek-ai/DeepSeek-V3',
'object': 'model',
'owned_by': 'siliconflow',
'model_type': 'llm',
},
{
'id': 'pa/claude-opus-4-7',
'object': 'model',
'owned_by': 'ppio',
'model_type': 'llm',
},
{
'id': 'gemini-2.5-flash',
'object': 'model',
'owned_by': 'ppio',
'model_type': 'llm',
},
]
}
models = _normalize_model_list(payload)
self.assertEqual(
[model['id'] for model in models],
[
'azure_openai/gpt-5',
'ppio/gemini-2.5-flash',
'ppio/pa/claude-opus-4-7',
'siliconflow/Pro/deepseek-ai/DeepSeek-V3',
'xiaomi/DeepSeek-R1-0528',
],
)
self.assertEqual(models[0]['provider'], 'azure_openai')
self.assertEqual(models[0]['model_type'], 'llm')
def test_models_endpoint_returns_provider_prefixed_llm_models(self) -> None:
class FakeResponse:
def __enter__(self) -> 'FakeResponse':
return self
def __exit__(self, *args: object) -> None:
return None
def read(self) -> bytes:
return (
b'{"object":"list","data":['
b'{"id":"mimo-v2-flash","object":"model","owned_by":"xiaomi","model_type":"llm"},'
b'{"id":"ASR_Streaming","object":"model","owned_by":"xiaomi","model_type":"speech2text"},'
b'{"id":"gpt-5","object":"model","owned_by":"azure_openai","model_type":"llm"}'
b']}'
)
with TemporaryDirectory() as tmp_dir:
state = AgentState(
cwd=Path(tmp_dir),
model='xiaomi/mimo-v2-flash',
base_url='http://model.example/v1',
api_key='token',
timeout_seconds=120.0,
allow_shell=False,
allow_write=False,
session_directory=Path(tmp_dir) / 'sessions',
)
client = TestClient(create_app(state))
with patch('backend.api.server.request.urlopen', return_value=FakeResponse()):
response = client.get('/api/models')
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload['raw_count'], 3)
self.assertEqual(payload['filtered_count'], 2)
self.assertEqual(
[model['id'] for model in payload['models']],
['azure_openai/gpt-5', 'xiaomi/mimo-v2-flash'],
)
if __name__ == '__main__':
unittest.main()
+16
View File
@@ -0,0 +1,16 @@
from agent_platform.models import MODEL_SPECS, get_model_spec, openai_model_list
def test_fixed_public_model_matrix() -> None:
assert set(MODEL_SPECS) == {
"chat-light",
"chat-medium",
"chat-high",
"work-light",
"work-medium",
"work-high",
}
assert get_model_spec("chat-light").provider_model == "ChatGPT-5.6:Luna"
assert get_model_spec("work-medium").provider_model == "ChatGPT-5.6:Terra"
assert get_model_spec("work-high").provider_model == "ChatGPT-5.6:Sol"
assert {item["id"] for item in openai_model_list()["data"]} == set(MODEL_SPECS)
-299
View File
@@ -1,299 +0,0 @@
"""Tests for /files, /copy, /export, /stats, /tag, /rename, /branch, /effort, /doctor."""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
from src.agent_runtime import LocalCodingAgent
from src.agent_session import AgentMessage, AgentSessionState
from src.agent_types import AgentRuntimeConfig, ModelConfig, UsageStats
def _make_agent(tmp_dir: str) -> LocalCodingAgent:
return LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
def _set_session(agent: LocalCodingAgent, messages: list[AgentMessage]) -> None:
session = AgentSessionState(
system_prompt_parts=('You are a helper.',),
messages=messages,
)
agent.last_session = session
class TestFilesCommand(unittest.TestCase):
def test_no_session(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/files')
self.assertIn('No active session', result.final_output)
def test_no_files_in_context(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
AgentMessage(role='assistant', content='Hi'),
])
result = agent.run('/files')
self.assertIn('No files loaded', result.final_output)
def test_files_from_tool_calls(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Read main.py'),
AgentMessage(
role='assistant', content='',
tool_calls=(
{'id': 'tc1', 'type': 'function', 'function': {
'name': 'Read',
'arguments': json.dumps({'file_path': '/home/user/project/main.py'}),
}},
),
),
AgentMessage(
role='tool', content='print("hello")',
name='Read',
tool_call_id='tc1',
metadata={'path': '/home/user/project/main.py'},
),
])
result = agent.run('/files')
self.assertIn('Files in context', result.final_output)
self.assertIn('main.py', result.final_output)
class TestCopyCommand(unittest.TestCase):
def test_no_session(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/copy')
self.assertIn('No active session', result.final_output)
def test_no_assistant_messages(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
])
result = agent.run('/copy')
self.assertIn('No assistant responses', result.final_output)
def test_copies_latest_response(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
AgentMessage(role='assistant', content='First response.'),
AgentMessage(role='user', content='More'),
AgentMessage(role='assistant', content='Second response with details.'),
])
result = agent.run('/copy')
self.assertIn('Copied', result.final_output)
self.assertIn('response.md', result.final_output)
# Verify the file was written
tmp_file = Path(tempfile.gettempdir()) / 'claw-code' / 'response.md'
self.assertTrue(tmp_file.exists())
content = tmp_file.read_text()
self.assertEqual(content, 'Second response with details.')
def test_copies_nth_response(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
AgentMessage(role='assistant', content='First.'),
AgentMessage(role='user', content='More'),
AgentMessage(role='assistant', content='Second.'),
])
result = agent.run('/copy 1')
tmp_file = Path(tempfile.gettempdir()) / 'claw-code' / 'response.md'
content = tmp_file.read_text()
self.assertEqual(content, 'First.')
class TestExportCommand(unittest.TestCase):
def test_no_session(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/export')
self.assertIn('No active session', result.final_output)
def test_exports_with_auto_filename(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
AgentMessage(role='assistant', content='Hi there'),
])
result = agent.run('/export')
self.assertIn('Exported 2 messages', result.final_output)
self.assertIn('.txt', result.final_output)
def test_exports_with_custom_filename(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
AgentMessage(role='assistant', content='Hi'),
])
result = agent.run('/export my_chat')
self.assertIn('my_chat.txt', result.final_output)
out_file = Path(tmp) / 'my_chat.txt'
self.assertTrue(out_file.exists())
content = out_file.read_text()
self.assertIn('Hello', content)
self.assertIn('Hi', content)
class TestStatsCommand(unittest.TestCase):
def test_shows_statistics(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
AgentMessage(role='assistant', content='Hi'),
AgentMessage(role='user', content='Question'),
])
agent.cumulative_usage = UsageStats(input_tokens=500, output_tokens=200)
result = agent.run('/stats')
self.assertIn('Session Statistics', result.final_output)
self.assertIn('3 total', result.final_output)
self.assertIn('2 user', result.final_output)
self.assertIn('1 assistant', result.final_output)
self.assertIn('500', result.final_output)
self.assertIn('200', result.final_output)
class TestTagCommand(unittest.TestCase):
def test_no_tags_initially(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/tag')
self.assertIn('No tags set', result.final_output)
def test_add_tag(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/tag important')
self.assertIn('Added tag: important', result.final_output)
def test_toggle_tag(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
agent.run('/tag my-tag')
result = agent.run('/tag my-tag')
self.assertIn('Removed tag: my-tag', result.final_output)
def test_list_tags(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
agent.run('/tag alpha')
agent.run('/tag beta')
result = agent.run('/tag')
self.assertIn('alpha', result.final_output)
self.assertIn('beta', result.final_output)
class TestRenameCommand(unittest.TestCase):
def test_no_name(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/rename')
self.assertIn('Usage', result.final_output)
def test_rename_session(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/rename My Cool Session')
self.assertIn('renamed to: My Cool Session', result.final_output)
class TestBranchCommand(unittest.TestCase):
def test_no_session(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/branch')
self.assertIn('No active session', result.final_output)
def test_branch_with_session(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
AgentMessage(role='assistant', content='Hi'),
])
result = agent.run('/branch my-feature')
self.assertIn('Created branch "my-feature"', result.final_output)
self.assertIn('Saved to:', result.final_output)
def test_branch_auto_name(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
_set_session(agent, [
AgentMessage(role='user', content='Hello'),
])
result = agent.run('/branch')
self.assertIn('Created branch "branch-', result.final_output)
class TestEffortCommand(unittest.TestCase):
def test_show_current_effort(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/effort')
self.assertIn('Current effort level: auto', result.final_output)
def test_set_effort_level(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/effort high')
self.assertIn('Set effort level to: high', result.final_output)
def test_invalid_effort_level(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/effort extreme')
self.assertIn('Invalid effort level', result.final_output)
def test_all_valid_levels(self) -> None:
for level in ('low', 'medium', 'high', 'max', 'auto'):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run(f'/effort {level}')
self.assertIn(f'Set effort level to: {level}', result.final_output)
class TestDoctorCommand(unittest.TestCase):
def test_shows_report(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/doctor')
output = result.final_output
self.assertIn('Doctor Report', output)
self.assertIn('Python version', output)
self.assertIn('git', output)
self.assertIn('Model', output)
self.assertIn('Working directory', output)
def test_detects_claude_md(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / 'CLAUDE.md').write_text('memory file')
agent = _make_agent(tmp)
result = agent.run('/doctor')
self.assertIn('CLAUDE.md', result.final_output)
self.assertIn('found', result.final_output)
if __name__ == '__main__':
unittest.main()
-434
View File
@@ -1,434 +0,0 @@
from __future__ import annotations
import json
import unittest
from unittest.mock import patch
from src.openai_compat import (
OpenAICompatClient,
OpenAICompatError,
_anthropic_base_url,
_build_response_format,
_join_url,
_normalize_content,
_optional_int,
_parse_tool_arguments,
_parse_usage,
_temperature_for_model,
_uses_anthropic_messages_api,
)
from src.agent_types import ModelConfig, OutputSchemaConfig, UsageStats
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
class FakeStreamingHTTPResponse:
def __init__(self, payloads: list[dict[str, object]]) -> None:
self.lines: list[bytes] = []
for payload in payloads:
self.lines.append(b'event: message\n')
self.lines.append(f'data: {json.dumps(payload)}\n'.encode('utf-8'))
self.lines.append(b'\n')
def readline(self) -> bytes:
if not self.lines:
return b''
return self.lines.pop(0)
def __enter__(self) -> 'FakeStreamingHTTPResponse':
return self
def __exit__(self, exc_type, exc, tb) -> None:
return None
class TestJoinUrl(unittest.TestCase):
def test_base_with_trailing_slash(self):
self.assertEqual(_join_url('http://localhost:8000/', 'v1/chat'), 'http://localhost:8000/v1/chat')
def test_base_without_trailing_slash(self):
self.assertEqual(_join_url('http://localhost:8000', 'v1/chat'), 'http://localhost:8000/v1/chat')
def test_suffix_with_leading_slash(self):
self.assertEqual(_join_url('http://localhost:8000', '/v1/chat'), 'http://localhost:8000/v1/chat')
class TestNormalizeContent(unittest.TestCase):
def test_string_passthrough(self):
self.assertEqual(_normalize_content('hello'), 'hello')
def test_none_returns_empty(self):
self.assertEqual(_normalize_content(None), '')
def test_list_of_strings_joined(self):
self.assertEqual(_normalize_content(['hello', ' ', 'world']), 'hello world')
def test_list_of_text_dicts(self):
items = [{'type': 'text', 'text': 'hello'}, {'type': 'text', 'text': ' world'}]
self.assertEqual(_normalize_content(items), 'hello world')
def test_list_of_mixed_items(self):
items = ['start ', {'type': 'text', 'text': 'middle'}, ' end']
self.assertEqual(_normalize_content(items), 'start middle end')
def test_non_string_non_list_returns_str(self):
self.assertEqual(_normalize_content(42), '42')
class TestParseToolArguments(unittest.TestCase):
def test_dict_passthrough(self):
d = {'key': 'value'}
self.assertIs(_parse_tool_arguments(d), d)
def test_valid_json_string(self):
self.assertEqual(_parse_tool_arguments('{"a": 1}'), {'a': 1})
def test_empty_string_returns_empty_dict(self):
self.assertEqual(_parse_tool_arguments(''), {})
def test_none_returns_empty_dict(self):
self.assertEqual(_parse_tool_arguments(None), {})
def test_invalid_json_raises(self):
with self.assertRaises(OpenAICompatError):
_parse_tool_arguments('{bad json}')
def test_json_non_dict_raises(self):
with self.assertRaises(OpenAICompatError):
_parse_tool_arguments('[1, 2, 3]')
def test_unsupported_type_raises(self):
with self.assertRaises(OpenAICompatError):
_parse_tool_arguments(12345)
class TestParseUsage(unittest.TestCase):
def test_standard_fields(self):
usage = _parse_usage({'input_tokens': 10, 'output_tokens': 20})
self.assertEqual(usage.input_tokens, 10)
self.assertEqual(usage.output_tokens, 20)
def test_prompt_completion_aliases(self):
usage = _parse_usage({'prompt_tokens': 15, 'completion_tokens': 25})
self.assertEqual(usage.input_tokens, 15)
self.assertEqual(usage.output_tokens, 25)
def test_ollama_aliases(self):
usage = _parse_usage({'prompt_eval_count': 12, 'eval_count': 18})
self.assertEqual(usage.input_tokens, 12)
self.assertEqual(usage.output_tokens, 18)
def test_cache_tokens(self):
usage = _parse_usage({
'input_tokens': 1,
'output_tokens': 1,
'cache_creation_input_tokens': 100,
'cache_read_input_tokens': 200,
})
self.assertEqual(usage.cache_creation_input_tokens, 100)
self.assertEqual(usage.cache_read_input_tokens, 200)
def test_reasoning_tokens_top_level_and_details(self):
usage_top = _parse_usage({'input_tokens': 1, 'output_tokens': 1, 'reasoning_tokens': 50})
self.assertEqual(usage_top.reasoning_tokens, 50)
usage_details = _parse_usage({
'input_tokens': 1,
'output_tokens': 1,
'completion_tokens_details': {'reasoning_tokens': 75},
})
self.assertEqual(usage_details.reasoning_tokens, 75)
def test_non_dict_returns_empty(self):
usage = _parse_usage('not a dict')
self.assertEqual(usage, UsageStats())
def test_string_number_coercion(self):
usage = _parse_usage({'input_tokens': '10', 'output_tokens': '20'})
self.assertEqual(usage.input_tokens, 10)
self.assertEqual(usage.output_tokens, 20)
class TestBuildResponseFormat(unittest.TestCase):
def test_none_returns_none(self):
self.assertIsNone(_build_response_format(None))
def test_valid_schema(self):
schema = OutputSchemaConfig(
name='test_schema',
schema={'type': 'object', 'properties': {'x': {'type': 'integer'}}},
strict=True,
)
result = _build_response_format(schema)
self.assertEqual(result, {
'type': 'json_schema',
'json_schema': {
'name': 'test_schema',
'schema': {'type': 'object', 'properties': {'x': {'type': 'integer'}}},
'strict': True,
},
})
class TestOptionalInt(unittest.TestCase):
def test_int_passthrough(self):
self.assertEqual(_optional_int(42), 42)
def test_float_truncated(self):
self.assertEqual(_optional_int(3.9), 3)
def test_string_parsed(self):
self.assertEqual(_optional_int('7'), 7)
def test_bool_returns_zero(self):
self.assertEqual(_optional_int(True), 0)
self.assertEqual(_optional_int(False), 0)
def test_none_returns_zero(self):
self.assertEqual(_optional_int(None), 0)
def test_invalid_string_returns_zero(self):
self.assertEqual(_optional_int('abc'), 0)
class TestProviderTemperatureFloor(unittest.TestCase):
def test_minimax_temperature_is_clamped_to_provider_minimum(self):
self.assertEqual(_temperature_for_model('minimax/MiniMax-M2.5', 0.0), 0.01)
def test_wenxin_temperature_is_clamped_to_provider_minimum(self):
self.assertEqual(_temperature_for_model('wenxin/ernie-4.0-turbo-128k', 0.0), 0.1)
def test_regular_models_keep_configured_temperature(self):
self.assertEqual(_temperature_for_model('xiaomi/mimo-v2-flash', 0.0), 0.0)
def test_payload_uses_temperature_floor(self):
client = OpenAICompatClient(
ModelConfig(model='minimax/MiniMax-M2.5', temperature=0.0)
)
payload = client._build_payload( # noqa: SLF001 - verify payload compatibility.
messages=[{'role': 'user', 'content': 'hi'}],
tools=[],
stream=False,
output_schema=None,
)
self.assertEqual(payload['temperature'], 0.01)
def test_payload_omits_tool_choice_when_no_tools_are_supplied(self):
client = OpenAICompatClient(ModelConfig(model='azure_openai/gpt-4o-mini'))
payload = client._build_payload( # noqa: SLF001 - verify provider compatibility.
messages=[{'role': 'user', 'content': 'hi'}],
tools=[],
stream=False,
output_schema=None,
)
self.assertNotIn('tools', payload)
self.assertNotIn('tool_choice', payload)
def test_payload_includes_tool_choice_when_tools_are_supplied(self):
client = OpenAICompatClient(ModelConfig(model='azure_openai/gpt-4o-mini'))
payload = client._build_payload( # noqa: SLF001 - verify provider compatibility.
messages=[{'role': 'user', 'content': 'hi'}],
tools=[{'type': 'function', 'function': {'name': 'noop', 'parameters': {}}}],
stream=False,
output_schema=None,
)
self.assertIn('tools', payload)
self.assertEqual(payload['tool_choice'], 'auto')
class TestAnthropicMessagesRouting(unittest.TestCase):
def test_ppio_pa_claude_uses_anthropic_messages_api(self):
self.assertTrue(
_uses_anthropic_messages_api(
'ppio/pa/claude-opus-4-7',
'http://model.mify.ai.srv/v1',
)
)
self.assertFalse(
_uses_anthropic_messages_api(
'ppio/gemini-2.5-pro',
'http://model.mify.ai.srv/v1',
)
)
def test_anthropic_base_url_is_derived_from_openai_base(self):
self.assertEqual(
_anthropic_base_url('http://model.mify.ai.srv/v1'),
'http://model.mify.ai.srv/anthropic',
)
self.assertEqual(
_anthropic_base_url('http://model.mify.ai.srv/anthropic'),
'http://model.mify.ai.srv/anthropic',
)
def test_anthropic_complete_converts_tools_and_parses_tool_use(self):
recorded: dict[str, object] = {}
def fake_urlopen(request_obj, timeout=None): # noqa: ANN001
recorded['url'] = request_obj.full_url
recorded['payload'] = json.loads(request_obj.data.decode('utf-8'))
return FakeHTTPResponse(
{
'id': 'msg_1',
'type': 'message',
'role': 'assistant',
'content': [
{'type': 'text', 'text': '我来读取文件。'},
{
'type': 'tool_use',
'id': 'toolu_1',
'name': 'read_file',
'input': {'path': 'hello.txt'},
},
],
'stop_reason': 'tool_use',
'usage': {'input_tokens': 10, 'output_tokens': 4},
}
)
client = OpenAICompatClient(
ModelConfig(
model='ppio/pa/claude-opus-4-7',
base_url='http://model.mify.ai.srv/v1',
api_key='token',
)
)
with patch('src.openai_compat.request.urlopen', side_effect=fake_urlopen):
turn = client.complete(
messages=[
{'role': 'system', 'content': '你是工具型助手。'},
{'role': 'user', 'content': '读取 hello.txt'},
],
tools=[
{
'type': 'function',
'function': {
'name': 'read_file',
'description': '读取文件',
'parameters': {
'type': 'object',
'properties': {'path': {'type': 'string'}},
'required': ['path'],
},
},
}
],
)
self.assertEqual(
recorded['url'],
'http://model.mify.ai.srv/anthropic/v1/messages',
)
payload = recorded['payload']
self.assertEqual(payload['model'], 'ppio/pa/claude-opus-4-7')
self.assertEqual(payload['system'], '你是工具型助手。')
self.assertEqual(payload['tools'][0]['name'], 'read_file')
self.assertEqual(payload['tools'][0]['input_schema']['required'], ['path'])
self.assertEqual(turn.content, '我来读取文件。')
self.assertEqual(turn.finish_reason, 'tool_use')
self.assertEqual(turn.tool_calls[0].id, 'toolu_1')
self.assertEqual(turn.tool_calls[0].arguments, {'path': 'hello.txt'})
self.assertEqual(turn.usage.input_tokens, 10)
def test_anthropic_stream_parses_text_usage_and_tool_use(self):
payloads = [
{
'type': 'message_start',
'message': {'usage': {'input_tokens': 7, 'output_tokens': 1}},
},
{
'type': 'content_block_start',
'index': 0,
'content_block': {'type': 'text', 'text': ''},
},
{
'type': 'content_block_delta',
'index': 0,
'delta': {'type': 'text_delta', 'text': '读取'},
},
{
'type': 'content_block_start',
'index': 1,
'content_block': {
'type': 'tool_use',
'id': 'toolu_1',
'name': 'read_file',
'input': {},
},
},
{
'type': 'content_block_delta',
'index': 1,
'delta': {'type': 'input_json_delta', 'partial_json': '{"path":'},
},
{
'type': 'content_block_delta',
'index': 1,
'delta': {'type': 'input_json_delta', 'partial_json': '"hello.txt"}'},
},
{
'type': 'message_delta',
'delta': {'stop_reason': 'tool_use'},
'usage': {'output_tokens': 5},
},
{'type': 'message_stop'},
]
def fake_urlopen(request_obj, timeout=None): # noqa: ANN001
return FakeStreamingHTTPResponse(payloads)
client = OpenAICompatClient(
ModelConfig(
model='ppio/pa/claude-opus-4-7',
base_url='http://model.mify.ai.srv/v1',
api_key='token',
)
)
with patch('src.openai_compat.request.urlopen', side_effect=fake_urlopen):
events = list(
client.stream(
messages=[{'role': 'user', 'content': '读取 hello.txt'}],
tools=[
{
'type': 'function',
'function': {
'name': 'read_file',
'parameters': {'type': 'object'},
},
}
],
)
)
self.assertEqual(events[0].type, 'message_start')
self.assertEqual(
''.join(event.delta for event in events if event.type == 'content_delta'),
'读取',
)
tool_events = [event for event in events if event.type == 'tool_call_delta']
self.assertEqual(tool_events[0].tool_call_index, 0)
self.assertEqual(tool_events[0].tool_call_id, 'toolu_1')
self.assertEqual(tool_events[0].tool_name, 'read_file')
self.assertEqual(
''.join(event.arguments_delta for event in tool_events),
'{"path":"hello.txt"}',
)
self.assertTrue(any(event.type == 'usage' for event in events))
self.assertEqual(events[-1].type, 'message_stop')
self.assertEqual(events[-1].finish_reason, 'tool_use')
if __name__ == '__main__':
unittest.main()
-87
View File
@@ -1,87 +0,0 @@
from __future__ import annotations
import unittest
from src.permissions import ToolPermissionContext
class TestToolPermissionContext(unittest.TestCase):
# 1. Empty context blocks nothing
def test_empty_context_blocks_nothing(self) -> None:
ctx = ToolPermissionContext()
self.assertFalse(ctx.blocks("anything"))
self.assertFalse(ctx.blocks(""))
# 2. Exact name blocking (case insensitive)
def test_exact_name_blocking(self) -> None:
ctx = ToolPermissionContext.from_iterables(deny_names=["dangerous_tool"])
self.assertTrue(ctx.blocks("dangerous_tool"))
self.assertTrue(ctx.blocks("Dangerous_Tool"))
self.assertTrue(ctx.blocks("DANGEROUS_TOOL"))
self.assertFalse(ctx.blocks("safe_tool"))
# 3. Prefix blocking (case insensitive)
def test_prefix_blocking(self) -> None:
ctx = ToolPermissionContext.from_iterables(deny_prefixes=["admin_"])
self.assertTrue(ctx.blocks("admin_delete"))
self.assertTrue(ctx.blocks("Admin_Delete"))
self.assertTrue(ctx.blocks("ADMIN_CREATE"))
self.assertFalse(ctx.blocks("user_admin"))
# 4. Combined name + prefix blocking
def test_combined_name_and_prefix_blocking(self) -> None:
ctx = ToolPermissionContext.from_iterables(
deny_names=["rm"],
deny_prefixes=["sudo_"],
)
self.assertTrue(ctx.blocks("rm"))
self.assertTrue(ctx.blocks("sudo_restart"))
self.assertFalse(ctx.blocks("ls"))
# 5. Non-matching names are allowed
def test_non_matching_names_allowed(self) -> None:
ctx = ToolPermissionContext.from_iterables(
deny_names=["blocked"],
deny_prefixes=["bad_"],
)
self.assertFalse(ctx.blocks("allowed"))
self.assertFalse(ctx.blocks("good_tool"))
self.assertFalse(ctx.blocks("not_bad"))
# 6. from_iterables with None args
def test_from_iterables_none_args(self) -> None:
ctx = ToolPermissionContext.from_iterables(deny_names=None, deny_prefixes=None)
self.assertEqual(ctx.deny_names, frozenset())
self.assertEqual(ctx.deny_prefixes, ())
self.assertFalse(ctx.blocks("anything"))
def test_from_iterables_default_args(self) -> None:
ctx = ToolPermissionContext.from_iterables()
self.assertEqual(ctx.deny_names, frozenset())
self.assertEqual(ctx.deny_prefixes, ())
# 7. from_iterables normalizes to lowercase
def test_from_iterables_normalizes_to_lowercase(self) -> None:
ctx = ToolPermissionContext.from_iterables(
deny_names=["FooBar"],
deny_prefixes=["PFX_"],
)
self.assertIn("foobar", ctx.deny_names)
self.assertNotIn("FooBar", ctx.deny_names)
self.assertEqual(ctx.deny_prefixes, ("pfx_",))
self.assertTrue(ctx.blocks("FOOBAR"))
self.assertTrue(ctx.blocks("pfx_something"))
# 8. Multiple deny_names
def test_multiple_deny_names(self) -> None:
ctx = ToolPermissionContext.from_iterables(
deny_names=["tool_a", "tool_b", "tool_c"],
)
self.assertTrue(ctx.blocks("tool_a"))
self.assertTrue(ctx.blocks("tool_b"))
self.assertTrue(ctx.blocks("tool_c"))
self.assertFalse(ctx.blocks("tool_d"))
if __name__ == "__main__":
unittest.main()
-197
View File
@@ -1,197 +0,0 @@
from __future__ import annotations
import json
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.plan_runtime import PlanRuntime
from src.task_runtime import TaskRuntime
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
class PlanRuntimeTests(unittest.TestCase):
def test_runtime_persists_and_syncs_plan_to_tasks(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
task_runtime = TaskRuntime.from_workspace(workspace)
plan_runtime = PlanRuntime.from_workspace(workspace)
mutation = plan_runtime.update_plan(
[
{
'step': 'Inspect the runtime loop',
'status': 'in_progress',
'description': 'Read the core agent files first.',
},
{
'step': 'Patch the tool registry',
'status': 'blocked',
'depends_on': ['plan_1'],
},
],
explanation='Work through the runtime in two phases.',
task_runtime=task_runtime,
)
rendered_plan = plan_runtime.render_plan()
rendered_tasks = task_runtime.render_tasks()
rendered_task = task_runtime.render_task('plan_2')
self.assertEqual(mutation.after_count, 2)
self.assertEqual(mutation.synced_tasks, 2)
self.assertIn('Inspect the runtime loop', rendered_plan)
self.assertIn('Work through the runtime in two phases.', rendered_plan)
self.assertIn('depends_on: plan_1', rendered_plan)
self.assertIn('Inspect the runtime loop', rendered_tasks)
self.assertIn('Blocked By: plan_1', rendered_task)
def test_plan_tools_execute_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
task_runtime = TaskRuntime.from_workspace(workspace)
plan_runtime = PlanRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
plan_runtime=plan_runtime,
task_runtime=task_runtime,
)
update_result = execute_tool(
default_tool_registry(),
'update_plan',
{
'explanation': 'Follow the current plan.',
'items': [
{'step': 'Inspect the workspace', 'status': 'completed'},
{'step': 'Implement the fix', 'status': 'in_progress'},
],
},
context,
)
get_result = execute_tool(
default_tool_registry(),
'plan_get',
{},
context,
)
clear_result = execute_tool(
default_tool_registry(),
'plan_clear',
{'sync_tasks': True},
context,
)
self.assertTrue(update_result.ok)
self.assertEqual(update_result.metadata.get('total_steps'), 2)
self.assertEqual(update_result.metadata.get('synced_tasks'), 2)
self.assertIn('# Plan', get_result.content)
self.assertTrue(clear_result.ok)
self.assertEqual(clear_result.metadata.get('total_steps'), 0)
def test_agent_can_use_update_plan_tool_in_model_loop(self) -> None:
responses = [
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'I will store the plan first.',
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'update_plan',
'arguments': json.dumps(
{
'explanation': 'Start with a plan.',
'items': [
{
'step': 'Inspect the current files',
'status': 'in_progress',
},
{
'step': 'Apply the code changes',
'status': 'pending',
},
],
}
),
},
}
],
},
'finish_reason': 'tool_calls',
}
],
'usage': {'prompt_tokens': 8, 'completion_tokens': 3},
},
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'The plan was stored successfully.',
},
'finish_reason': 'stop',
}
],
'usage': {'prompt_tokens': 6, 'completion_tokens': 3},
},
]
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
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',
base_url='http://127.0.0.1:8000/v1',
),
runtime_config=AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
)
result = agent.run('Store the current plan')
self.assertIsNotNone(result.scratchpad_directory)
self.assertTrue(
(Path(result.scratchpad_directory) / 'plan_runtime.json').exists()
)
self.assertFalse((workspace / '.port_sessions' / 'plan_runtime.json').exists())
self.assertEqual(result.final_output, 'The plan was stored successfully.')
self.assertEqual(result.tool_calls, 1)
tool_message = next(
message for message in result.transcript if message.get('role') == 'tool'
)
self.assertIn('update_plan', tool_message.get('content', ''))
-210
View File
@@ -1,210 +0,0 @@
"""Tests for ``src/platform_info.py`` — platform detection and system dirs."""
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from src import platform_info
from src.platform_info import (
SUPPORTED_PLATFORMS,
LinuxDistroInfo,
SystemDirectories,
detect_vcs,
get_linux_distro_info,
get_platform,
get_system_directories,
get_wsl_version,
)
class GetPlatformTest(unittest.TestCase):
def setUp(self) -> None:
platform_info._reset_cache()
def tearDown(self) -> None:
platform_info._reset_cache()
def test_macos(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'darwin'):
self.assertEqual(get_platform(), 'macos')
def test_windows(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'win32'):
self.assertEqual(get_platform(), 'windows')
def test_linux_no_wsl(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'linux'), \
mock.patch(
'src.platform_info._read_proc_version',
return_value='Linux version 5.10 (gcc)',
):
self.assertEqual(get_platform(), 'linux')
def test_linux_wsl_microsoft_marker(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'linux'), \
mock.patch(
'src.platform_info._read_proc_version',
return_value='Linux version 5.10 microsoft-standard-WSL2',
):
self.assertEqual(get_platform(), 'wsl')
def test_linux_proc_version_unreadable(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'linux'), \
mock.patch(
'src.platform_info._read_proc_version',
side_effect=FileNotFoundError(),
):
self.assertEqual(get_platform(), 'linux')
def test_unknown_platform(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'sunos5'):
self.assertEqual(get_platform(), 'unknown')
def test_memoized(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'darwin'):
self.assertEqual(get_platform(), 'macos')
# Second call should hit cache, not re-evaluate sys.platform
with mock.patch('src.platform_info.sys.platform', 'win32'):
self.assertEqual(get_platform(), 'macos')
def test_supported_platforms_contains_expected(self) -> None:
self.assertIn('macos', SUPPORTED_PLATFORMS)
self.assertIn('wsl', SUPPORTED_PLATFORMS)
class GetWslVersionTest(unittest.TestCase):
def setUp(self) -> None:
platform_info._reset_cache()
def tearDown(self) -> None:
platform_info._reset_cache()
def test_explicit_wsl2(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'linux'), \
mock.patch(
'src.platform_info._read_proc_version',
return_value='5.15.123-microsoft-standard-WSL2',
):
self.assertEqual(get_wsl_version(), '2')
def test_wsl1_fallback(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'linux'), \
mock.patch(
'src.platform_info._read_proc_version',
return_value='4.4.0-19041-Microsoft (Microsoft@Microsoft.com)',
):
self.assertEqual(get_wsl_version(), '1')
def test_non_linux(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'darwin'):
self.assertIsNone(get_wsl_version())
def test_linux_no_microsoft_marker(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'linux'), \
mock.patch(
'src.platform_info._read_proc_version',
return_value='Linux version 6.5.0 (gcc)',
):
self.assertIsNone(get_wsl_version())
class GetLinuxDistroInfoTest(unittest.TestCase):
def test_non_linux_returns_none(self) -> None:
with mock.patch('src.platform_info.sys.platform', 'darwin'):
self.assertIsNone(get_linux_distro_info())
def test_parses_id_and_version(self) -> None:
os_release = 'NAME="Ubuntu"\nID=ubuntu\nVERSION_ID="22.04"\n'
with mock.patch('src.platform_info.sys.platform', 'linux'), \
mock.patch(
'src.platform_info.Path.read_text', return_value=os_release,
):
info = get_linux_distro_info()
assert info is not None
self.assertEqual(info.linux_distro_id, 'ubuntu')
self.assertEqual(info.linux_distro_version, '22.04')
self.assertIsNotNone(info.linux_kernel)
def test_to_dict_skips_none(self) -> None:
info = LinuxDistroInfo(linux_distro_id='fedora')
self.assertEqual(info.to_dict(), {'linuxDistroId': 'fedora'})
class DetectVcsTest(unittest.TestCase):
def test_detects_git(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.git').mkdir()
self.assertEqual(detect_vcs(tmp), ['git'])
def test_detects_multiple(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.git').mkdir()
(Path(tmp) / '.hg').mkdir()
self.assertEqual(detect_vcs(tmp), ['git', 'mercurial'])
def test_perforce_via_env(self) -> None:
with tempfile.TemporaryDirectory() as tmp, \
mock.patch.dict(os.environ, {'P4PORT': '1666'}):
self.assertIn('perforce', detect_vcs(tmp))
def test_unreadable_directory_returns_empty(self) -> None:
with mock.patch.dict(os.environ, {}, clear=True):
self.assertEqual(detect_vcs('/nonexistent/path/abc/xyz'), [])
class GetSystemDirectoriesTest(unittest.TestCase):
def test_macos_defaults(self) -> None:
dirs = get_system_directories(
home_dir='/Users/x', platform='macos', env={},
)
self.assertEqual(dirs.HOME, '/Users/x')
self.assertEqual(dirs.DESKTOP, '/Users/x/Desktop')
self.assertEqual(dirs.DOCUMENTS, '/Users/x/Documents')
self.assertEqual(dirs.DOWNLOADS, '/Users/x/Downloads')
def test_windows_uses_userprofile(self) -> None:
dirs = get_system_directories(
home_dir='C:/Users/old',
platform='windows',
env={'USERPROFILE': 'C:/Users/new'},
)
# Path normalizes to forward slashes on linux test runs; just check
# USERPROFILE was used as the base, not home_dir.
self.assertIn('Users/new', dirs.DESKTOP.replace('\\', '/'))
self.assertIn('Users/new', dirs.DOWNLOADS.replace('\\', '/'))
# HOME stays as the explicit home_dir
self.assertEqual(dirs.HOME, 'C:/Users/old')
def test_linux_xdg_overrides(self) -> None:
dirs = get_system_directories(
home_dir='/home/u',
platform='linux',
env={'XDG_DOWNLOAD_DIR': '/data/dl'},
)
self.assertEqual(dirs.DOWNLOADS, '/data/dl')
self.assertEqual(dirs.DESKTOP, '/home/u/Desktop')
def test_wsl_xdg_overrides(self) -> None:
dirs = get_system_directories(
home_dir='/home/u',
platform='wsl',
env={'XDG_DESKTOP_DIR': '/mnt/c/Users/x/Desktop'},
)
self.assertEqual(dirs.DESKTOP, '/mnt/c/Users/x/Desktop')
def test_to_dict_round_trip(self) -> None:
dirs = SystemDirectories(
HOME='/h', DESKTOP='/h/D', DOCUMENTS='/h/Doc', DOWNLOADS='/h/Dn',
)
self.assertEqual(
dirs.to_dict(),
{'HOME': '/h', 'DESKTOP': '/h/D', 'DOCUMENTS': '/h/Doc', 'DOWNLOADS': '/h/Dn'},
)
if __name__ == '__main__':
unittest.main()
-240
View File
@@ -1,240 +0,0 @@
from __future__ import annotations
import subprocess
import sys
import unittest
from pathlib import Path
from src.commands import PORTED_COMMANDS
from src.parity_audit import run_parity_audit
from src.port_manifest import build_port_manifest
from src.query_engine import QueryEnginePort
from src.tools import PORTED_TOOLS
class PortingWorkspaceTests(unittest.TestCase):
def test_manifest_counts_python_files(self) -> None:
manifest = build_port_manifest()
self.assertGreaterEqual(manifest.total_python_files, 20)
self.assertTrue(manifest.top_level_modules)
def test_query_engine_summary_mentions_workspace(self) -> None:
summary = QueryEnginePort.from_workspace().render_summary()
self.assertIn('Python Porting Workspace Summary', summary)
self.assertIn('Command surface:', summary)
self.assertIn('Tool surface:', summary)
def test_cli_summary_runs(self) -> None:
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'summary'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('Python Porting Workspace Summary', result.stdout)
def test_parity_audit_runs(self) -> None:
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'parity-audit'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('Parity Audit', result.stdout)
def test_root_file_coverage_is_complete_when_local_archive_exists(self) -> None:
audit = run_parity_audit()
if audit.archive_present:
self.assertGreaterEqual(audit.root_file_coverage[0], 8)
self.assertGreaterEqual(audit.directory_coverage[0], 3)
self.assertGreaterEqual(audit.command_entry_ratio[0], 150)
self.assertGreaterEqual(audit.tool_entry_ratio[0], 100)
def test_command_and_tool_snapshots_are_nontrivial(self) -> None:
self.assertGreaterEqual(len(PORTED_COMMANDS), 150)
self.assertGreaterEqual(len(PORTED_TOOLS), 100)
def test_commands_and_tools_cli_run(self) -> None:
commands_result = subprocess.run(
[sys.executable, '-m', 'src.main', 'commands', '--limit', '5', '--query', 'review'],
check=True,
capture_output=True,
text=True,
)
tools_result = subprocess.run(
[sys.executable, '-m', 'src.main', 'tools', '--limit', '5', '--query', 'MCP'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('Command entries:', commands_result.stdout)
self.assertIn('Tool entries:', tools_result.stdout)
def test_route_and_show_entry_cli_run(self) -> None:
route_result = subprocess.run(
[sys.executable, '-m', 'src.main', 'route', 'review MCP tool', '--limit', '5'],
check=True,
capture_output=True,
text=True,
)
show_command = subprocess.run(
[sys.executable, '-m', 'src.main', 'show-command', 'review'],
check=True,
capture_output=True,
text=True,
)
show_tool = subprocess.run(
[sys.executable, '-m', 'src.main', 'show-tool', 'MCPTool'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('review', route_result.stdout.lower())
self.assertIn('review', show_command.stdout.lower())
self.assertIn('mcptool', show_tool.stdout.lower())
def test_bootstrap_cli_runs(self) -> None:
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'bootstrap', 'review MCP tool', '--limit', '5'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('Runtime Session', result.stdout)
self.assertIn('Startup Steps', result.stdout)
self.assertIn('Routed Matches', result.stdout)
def test_bootstrap_session_tracks_turn_state(self) -> None:
from src.runtime import PortRuntime
session = PortRuntime().bootstrap_session('review MCP tool', limit=5)
self.assertGreaterEqual(len(session.turn_result.matched_tools), 1)
self.assertIn('Prompt:', session.turn_result.output)
self.assertGreaterEqual(session.turn_result.usage.input_tokens, 1)
def test_exec_command_and_tool_cli_run(self) -> None:
command_result = subprocess.run(
[sys.executable, '-m', 'src.main', 'exec-command', 'review', 'inspect security review'],
check=True,
capture_output=True,
text=True,
)
tool_result = subprocess.run(
[sys.executable, '-m', 'src.main', 'exec-tool', 'MCPTool', 'fetch resource list'],
check=True,
capture_output=True,
text=True,
)
self.assertIn("Mirrored command 'review'", command_result.stdout)
self.assertIn("Mirrored tool 'MCPTool'", tool_result.stdout)
def test_setup_report_and_registry_filters_run(self) -> None:
setup_result = subprocess.run(
[sys.executable, '-m', 'src.main', 'setup-report'],
check=True,
capture_output=True,
text=True,
)
command_result = subprocess.run(
[sys.executable, '-m', 'src.main', 'commands', '--limit', '5', '--no-plugin-commands'],
check=True,
capture_output=True,
text=True,
)
tool_result = subprocess.run(
[sys.executable, '-m', 'src.main', 'tools', '--limit', '5', '--simple-mode', '--no-mcp'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('Setup Report', setup_result.stdout)
self.assertIn('Command entries:', command_result.stdout)
self.assertIn('Tool entries:', tool_result.stdout)
def test_load_session_cli_runs(self) -> None:
from src.runtime import PortRuntime
session = PortRuntime().bootstrap_session('review MCP tool', limit=5)
session_id = Path(session.persisted_session_path).stem
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'load-session', session_id],
check=True,
capture_output=True,
text=True,
)
self.assertIn(session_id, result.stdout)
self.assertIn('messages', result.stdout)
def test_tool_permission_filtering_cli_runs(self) -> None:
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'tools', '--limit', '10', '--deny-prefix', 'mcp'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('Tool entries:', result.stdout)
self.assertNotIn('MCPTool', result.stdout)
def test_turn_loop_cli_runs(self) -> None:
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'turn-loop', 'review MCP tool', '--max-turns', '2', '--structured-output'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('## Turn 1', result.stdout)
self.assertIn('stop_reason=', result.stdout)
def test_remote_mode_clis_run(self) -> None:
remote_result = subprocess.run([sys.executable, '-m', 'src.main', 'remote-mode', 'workspace'], check=True, capture_output=True, text=True)
ssh_result = subprocess.run([sys.executable, '-m', 'src.main', 'ssh-mode', 'workspace'], check=True, capture_output=True, text=True)
teleport_result = subprocess.run([sys.executable, '-m', 'src.main', 'teleport-mode', 'workspace'], check=True, capture_output=True, text=True)
self.assertIn('mode=remote', remote_result.stdout)
self.assertIn('mode=ssh', ssh_result.stdout)
self.assertIn('mode=teleport', teleport_result.stdout)
def test_flush_transcript_cli_runs(self) -> None:
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'flush-transcript', 'review MCP tool'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('flushed=True', result.stdout)
def test_command_graph_and_tool_pool_cli_run(self) -> None:
command_graph = subprocess.run([sys.executable, '-m', 'src.main', 'command-graph'], check=True, capture_output=True, text=True)
tool_pool = subprocess.run([sys.executable, '-m', 'src.main', 'tool-pool'], check=True, capture_output=True, text=True)
self.assertIn('Command Graph', command_graph.stdout)
self.assertIn('Tool Pool', tool_pool.stdout)
def test_setup_report_mentions_deferred_init(self) -> None:
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'setup-report'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('Deferred init:', result.stdout)
self.assertIn('plugin_init=True', result.stdout)
def test_execution_registry_runs(self) -> None:
from src.execution_registry import build_execution_registry
registry = build_execution_registry()
self.assertGreaterEqual(len(registry.commands), 150)
self.assertGreaterEqual(len(registry.tools), 100)
self.assertIn('Mirrored command', registry.command('review').execute('review security'))
self.assertIn('Mirrored tool', registry.tool('MCPTool').execute('fetch mcp resources'))
def test_bootstrap_graph_and_direct_modes_run(self) -> None:
graph_result = subprocess.run([sys.executable, '-m', 'src.main', 'bootstrap-graph'], check=True, capture_output=True, text=True)
direct_result = subprocess.run([sys.executable, '-m', 'src.main', 'direct-connect-mode', 'workspace'], check=True, capture_output=True, text=True)
deep_link_result = subprocess.run([sys.executable, '-m', 'src.main', 'deep-link-mode', 'workspace'], check=True, capture_output=True, text=True)
self.assertIn('Bootstrap Graph', graph_result.stdout)
self.assertIn('mode=direct-connect', direct_result.stdout)
self.assertIn('mode=deep-link', deep_link_result.stdout)
if __name__ == '__main__':
unittest.main()
-643
View File
@@ -1,643 +0,0 @@
"""Tests for prompt_constants module.
Validates that all constants ported from npm src/constants/ are present,
correctly typed, and that helper functions behave as expected.
"""
from __future__ import annotations
import os
import platform
from unittest.mock import patch
import pytest
from src.prompt_constants import (
# Product metadata
PRODUCT_URL,
CLAUDE_AI_BASE_URL,
# System prompt prefixes
DEFAULT_SYSPROMPT_PREFIX,
AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX,
AGENT_SDK_PREFIX,
CLI_SYSPROMPT_PREFIXES,
# Cyber risk
CYBER_RISK_INSTRUCTION,
# API limits
API_IMAGE_MAX_BASE64_SIZE,
IMAGE_TARGET_RAW_SIZE,
IMAGE_MAX_WIDTH,
IMAGE_MAX_HEIGHT,
PDF_TARGET_RAW_SIZE,
API_PDF_MAX_PAGES,
PDF_EXTRACT_SIZE_THRESHOLD,
PDF_MAX_EXTRACT_SIZE,
PDF_MAX_PAGES_PER_READ,
PDF_AT_MENTION_INLINE_THRESHOLD,
API_MAX_MEDIA_PER_REQUEST,
# Tool limits
DEFAULT_MAX_RESULT_SIZE_CHARS,
MAX_TOOL_RESULT_TOKENS,
BYTES_PER_TOKEN,
MAX_TOOL_RESULT_BYTES,
MAX_TOOL_RESULTS_PER_MESSAGE_CHARS,
TOOL_SUMMARY_MAX_LENGTH,
# Spinner verbs
SPINNER_VERBS,
# Turn completion verbs
TURN_COMPLETION_VERBS,
# Figures
BLACK_CIRCLE,
BULLET_OPERATOR,
TEARDROP_ASTERISK,
UP_ARROW,
DOWN_ARROW,
LIGHTNING_BOLT,
EFFORT_LOW,
EFFORT_MEDIUM,
EFFORT_HIGH,
EFFORT_MAX,
PLAY_ICON,
PAUSE_ICON,
REFRESH_ARROW,
CHANNEL_ARROW,
INJECTED_ARROW,
FORK_GLYPH,
DIAMOND_OPEN,
DIAMOND_FILLED,
REFERENCE_MARK,
FLAG_ICON,
BLOCKQUOTE_BAR,
HEAVY_HORIZONTAL,
BRIDGE_SPINNER_FRAMES,
BRIDGE_READY_INDICATOR,
BRIDGE_FAILED_INDICATOR,
# XML tags
COMMAND_NAME_TAG,
COMMAND_MESSAGE_TAG,
COMMAND_ARGS_TAG,
BASH_INPUT_TAG,
BASH_STDOUT_TAG,
BASH_STDERR_TAG,
LOCAL_COMMAND_STDOUT_TAG,
LOCAL_COMMAND_STDERR_TAG,
LOCAL_COMMAND_CAVEAT_TAG,
TERMINAL_OUTPUT_TAGS,
TICK_TAG,
TASK_NOTIFICATION_TAG,
TASK_ID_TAG,
TOOL_USE_ID_TAG,
TASK_TYPE_TAG,
OUTPUT_FILE_TAG,
STATUS_TAG,
SUMMARY_TAG,
REASON_TAG,
WORKTREE_TAG,
WORKTREE_PATH_TAG,
WORKTREE_BRANCH_TAG,
ULTRAPLAN_TAG,
REMOTE_REVIEW_TAG,
REMOTE_REVIEW_PROGRESS_TAG,
TEAMMATE_MESSAGE_TAG,
CHANNEL_MESSAGE_TAG,
CHANNEL_TAG,
CROSS_SESSION_MESSAGE_TAG,
FORK_BOILERPLATE_TAG,
FORK_DIRECTIVE_PREFIX,
COMMON_HELP_ARGS,
COMMON_INFO_ARGS,
# Messages
NO_CONTENT_MESSAGE,
# Date utilities
get_local_iso_date,
get_session_start_date,
reset_session_start_date,
get_local_month_year,
# System prompt section caching
SystemPromptSection,
system_prompt_section,
dangerous_uncached_system_prompt_section,
resolve_system_prompt_sections,
clear_system_prompt_sections,
# Output styles
DEFAULT_OUTPUT_STYLE_NAME,
OutputStyleConfig,
OUTPUT_STYLE_CONFIGS,
# Knowledge cutoff
FRONTIER_MODEL_NAME,
get_knowledge_cutoff,
CLAUDE_MODEL_IDS,
# Prompt sections
HOOKS_SECTION,
SYSTEM_REMINDERS_SECTION,
SUMMARIZE_TOOL_RESULTS_SECTION,
DEFAULT_AGENT_PROMPT,
SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
get_language_section,
get_output_style_section,
get_scratchpad_instructions,
# Error IDs
E_TOOL_USE_SUMMARY_GENERATION_FAILED,
)
# =========================================================================
# Product metadata
# =========================================================================
class TestProductMetadata:
def test_product_url(self):
assert PRODUCT_URL == "https://claude.com/claude-code"
def test_claude_ai_base_url(self):
assert CLAUDE_AI_BASE_URL == "https://claude.ai"
# =========================================================================
# System prompt prefixes
# =========================================================================
class TestSystemPromptPrefixes:
def test_default_prefix_content(self):
assert "Claude Code" in DEFAULT_SYSPROMPT_PREFIX
assert "Anthropic" in DEFAULT_SYSPROMPT_PREFIX
def test_agent_sdk_prefix_content(self):
assert "Agent SDK" in AGENT_SDK_PREFIX
def test_cli_sysprompt_prefixes_is_frozenset(self):
assert isinstance(CLI_SYSPROMPT_PREFIXES, frozenset)
assert len(CLI_SYSPROMPT_PREFIXES) == 3
def test_all_prefixes_in_set(self):
assert DEFAULT_SYSPROMPT_PREFIX in CLI_SYSPROMPT_PREFIXES
assert AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX in CLI_SYSPROMPT_PREFIXES
assert AGENT_SDK_PREFIX in CLI_SYSPROMPT_PREFIXES
# =========================================================================
# Cyber risk
# =========================================================================
class TestCyberRisk:
def test_instruction_mentions_ctf(self):
assert "CTF" in CYBER_RISK_INSTRUCTION
def test_instruction_mentions_dos(self):
assert "DoS" in CYBER_RISK_INSTRUCTION
# =========================================================================
# API limits
# =========================================================================
class TestAPILimits:
def test_image_base64_size(self):
assert API_IMAGE_MAX_BASE64_SIZE == 5 * 1024 * 1024
def test_image_target_raw_size(self):
assert IMAGE_TARGET_RAW_SIZE == (API_IMAGE_MAX_BASE64_SIZE * 3) // 4
def test_image_dimensions(self):
assert IMAGE_MAX_WIDTH == 2000
assert IMAGE_MAX_HEIGHT == 2000
def test_pdf_target_raw_size(self):
assert PDF_TARGET_RAW_SIZE == 20 * 1024 * 1024
def test_pdf_max_pages(self):
assert API_PDF_MAX_PAGES == 100
def test_pdf_extract_threshold(self):
assert PDF_EXTRACT_SIZE_THRESHOLD == 3 * 1024 * 1024
def test_pdf_max_extract_size(self):
assert PDF_MAX_EXTRACT_SIZE == 100 * 1024 * 1024
def test_pdf_pages_per_read(self):
assert PDF_MAX_PAGES_PER_READ == 20
def test_pdf_inline_threshold(self):
assert PDF_AT_MENTION_INLINE_THRESHOLD == 10
def test_media_per_request(self):
assert API_MAX_MEDIA_PER_REQUEST == 100
# =========================================================================
# Tool limits
# =========================================================================
class TestToolLimits:
def test_default_max_result_size(self):
assert DEFAULT_MAX_RESULT_SIZE_CHARS == 50_000
def test_max_tool_result_tokens(self):
assert MAX_TOOL_RESULT_TOKENS == 100_000
def test_bytes_per_token(self):
assert BYTES_PER_TOKEN == 4
def test_max_tool_result_bytes_derived(self):
assert MAX_TOOL_RESULT_BYTES == MAX_TOOL_RESULT_TOKENS * BYTES_PER_TOKEN
assert MAX_TOOL_RESULT_BYTES == 400_000
def test_max_per_message_chars(self):
assert MAX_TOOL_RESULTS_PER_MESSAGE_CHARS == 200_000
def test_tool_summary_max_length(self):
assert TOOL_SUMMARY_MAX_LENGTH == 50
# =========================================================================
# Spinner verbs
# =========================================================================
class TestSpinnerVerbs:
def test_is_tuple(self):
assert isinstance(SPINNER_VERBS, tuple)
def test_count(self):
assert len(SPINNER_VERBS) == 187
def test_first_verb(self):
assert SPINNER_VERBS[0] == "Accomplishing"
def test_last_verb(self):
assert SPINNER_VERBS[-1] == "Zigzagging"
def test_all_strings(self):
for verb in SPINNER_VERBS:
assert isinstance(verb, str)
def test_contains_clauding(self):
assert "Clauding" in SPINNER_VERBS
def test_contains_thinking(self):
assert "Thinking" in SPINNER_VERBS
def test_no_duplicates(self):
assert len(SPINNER_VERBS) == len(set(SPINNER_VERBS))
# =========================================================================
# Turn completion verbs
# =========================================================================
class TestTurnCompletionVerbs:
def test_is_tuple(self):
assert isinstance(TURN_COMPLETION_VERBS, tuple)
def test_count(self):
assert len(TURN_COMPLETION_VERBS) == 8
def test_contains_worked(self):
assert "Worked" in TURN_COMPLETION_VERBS
def test_contains_baked(self):
assert "Baked" in TURN_COMPLETION_VERBS
def test_all_past_tense(self):
# All end in 'd' (past tense)
for verb in TURN_COMPLETION_VERBS:
assert verb[-1] == "d", f"{verb} doesn't end with 'd'"
# =========================================================================
# Figures / UI symbols
# =========================================================================
class TestFigures:
def test_black_circle_is_string(self):
assert isinstance(BLACK_CIRCLE, str)
assert len(BLACK_CIRCLE) == 1
def test_effort_symbols_are_distinct(self):
symbols = {EFFORT_LOW, EFFORT_MEDIUM, EFFORT_HIGH, EFFORT_MAX}
assert len(symbols) == 4
def test_arrows(self):
assert UP_ARROW == "\u2191"
assert DOWN_ARROW == "\u2193"
def test_bridge_spinner_frames(self):
assert isinstance(BRIDGE_SPINNER_FRAMES, tuple)
assert len(BRIDGE_SPINNER_FRAMES) == 4
def test_play_pause_icons(self):
assert PLAY_ICON == "\u25b6"
assert PAUSE_ICON == "\u23f8"
def test_diamond_symbols(self):
assert DIAMOND_OPEN == "\u25c7"
assert DIAMOND_FILLED == "\u25c6"
# =========================================================================
# XML tag constants
# =========================================================================
class TestXMLTags:
def test_command_tags(self):
assert COMMAND_NAME_TAG == "command-name"
assert COMMAND_MESSAGE_TAG == "command-message"
assert COMMAND_ARGS_TAG == "command-args"
def test_bash_tags(self):
assert BASH_INPUT_TAG == "bash-input"
assert BASH_STDOUT_TAG == "bash-stdout"
assert BASH_STDERR_TAG == "bash-stderr"
def test_terminal_output_tags_tuple(self):
assert isinstance(TERMINAL_OUTPUT_TAGS, tuple)
assert len(TERMINAL_OUTPUT_TAGS) == 6
assert BASH_INPUT_TAG in TERMINAL_OUTPUT_TAGS
assert LOCAL_COMMAND_STDOUT_TAG in TERMINAL_OUTPUT_TAGS
def test_tick_tag(self):
assert TICK_TAG == "tick"
def test_task_tags(self):
assert TASK_NOTIFICATION_TAG == "task-notification"
assert TASK_ID_TAG == "task-id"
assert TOOL_USE_ID_TAG == "tool-use-id"
def test_worktree_tags(self):
assert WORKTREE_TAG == "worktree"
assert WORKTREE_PATH_TAG == "worktreePath"
def test_fork_tags(self):
assert FORK_BOILERPLATE_TAG == "fork-boilerplate"
assert FORK_DIRECTIVE_PREFIX == "Your directive: "
def test_common_help_args(self):
assert isinstance(COMMON_HELP_ARGS, tuple)
assert "help" in COMMON_HELP_ARGS
assert "-h" in COMMON_HELP_ARGS
assert "--help" in COMMON_HELP_ARGS
def test_common_info_args(self):
assert isinstance(COMMON_INFO_ARGS, tuple)
assert "list" in COMMON_INFO_ARGS
assert "status" in COMMON_INFO_ARGS
assert "?" in COMMON_INFO_ARGS
# =========================================================================
# Message constants
# =========================================================================
class TestMessages:
def test_no_content_message(self):
assert NO_CONTENT_MESSAGE == "(no content)"
# =========================================================================
# Date utilities
# =========================================================================
class TestDateUtilities:
def test_get_local_iso_date_format(self):
d = get_local_iso_date()
parts = d.split("-")
assert len(parts) == 3
assert len(parts[0]) == 4 # year
assert len(parts[1]) == 2 # month
assert len(parts[2]) == 2 # day
def test_get_local_iso_date_override(self):
with patch.dict(os.environ, {"CLAUDE_CODE_OVERRIDE_DATE": "2025-01-15"}):
assert get_local_iso_date() == "2025-01-15"
def test_get_session_start_date_memoised(self):
reset_session_start_date()
d1 = get_session_start_date()
d2 = get_session_start_date()
assert d1 == d2
def test_reset_session_start_date(self):
reset_session_start_date()
d = get_session_start_date()
assert isinstance(d, str)
reset_session_start_date()
# After reset, should still return valid date
d2 = get_session_start_date()
assert isinstance(d2, str)
def test_get_local_month_year_format(self):
result = get_local_month_year()
parts = result.split()
assert len(parts) == 2
assert parts[1].isdigit()
assert len(parts[1]) == 4
def test_get_local_month_year_override(self):
with patch.dict(os.environ, {"CLAUDE_CODE_OVERRIDE_DATE": "2026-02-15"}):
assert get_local_month_year() == "February 2026"
# =========================================================================
# System prompt section caching
# =========================================================================
class TestSystemPromptSections:
def setup_method(self):
clear_system_prompt_sections()
def test_system_prompt_section_creates_cached(self):
s = system_prompt_section("test", lambda: "hello")
assert s.name == "test"
assert s.cache_break is False
def test_dangerous_uncached_creates_volatile(self):
s = dangerous_uncached_system_prompt_section("test", lambda: "hello", "reason")
assert s.name == "test"
assert s.cache_break is True
def test_resolve_caches_sections(self):
call_count = 0
def compute():
nonlocal call_count
call_count += 1
return f"value-{call_count}"
sections = [system_prompt_section("s1", compute)]
r1 = resolve_system_prompt_sections(sections)
r2 = resolve_system_prompt_sections(sections)
assert r1 == ["value-1"]
assert r2 == ["value-1"] # cached
assert call_count == 1
def test_uncached_recomputes(self):
call_count = 0
def compute():
nonlocal call_count
call_count += 1
return f"value-{call_count}"
sections = [dangerous_uncached_system_prompt_section("s2", compute, "test")]
r1 = resolve_system_prompt_sections(sections)
r2 = resolve_system_prompt_sections(sections)
assert r1 == ["value-1"]
assert r2 == ["value-2"] # recomputed
assert call_count == 2
def test_clear_resets_cache(self):
call_count = 0
def compute():
nonlocal call_count
call_count += 1
return f"value-{call_count}"
sections = [system_prompt_section("s3", compute)]
resolve_system_prompt_sections(sections)
clear_system_prompt_sections()
r = resolve_system_prompt_sections(sections)
assert r == ["value-2"]
assert call_count == 2
def test_resolve_handles_none(self):
sections = [system_prompt_section("nil", lambda: None)]
r = resolve_system_prompt_sections(sections)
assert r == [None]
def test_multiple_sections(self):
sections = [
system_prompt_section("a", lambda: "alpha"),
system_prompt_section("b", lambda: "beta"),
system_prompt_section("c", lambda: None),
]
r = resolve_system_prompt_sections(sections)
assert r == ["alpha", "beta", None]
# =========================================================================
# Output styles
# =========================================================================
class TestOutputStyles:
def test_default_style_name(self):
assert DEFAULT_OUTPUT_STYLE_NAME == "default"
def test_default_style_is_none(self):
assert OUTPUT_STYLE_CONFIGS[DEFAULT_OUTPUT_STYLE_NAME] is None
def test_explanatory_exists(self):
style = OUTPUT_STYLE_CONFIGS["Explanatory"]
assert style is not None
assert style.name == "Explanatory"
assert "explains" in style.description
def test_learning_exists(self):
style = OUTPUT_STYLE_CONFIGS["Learning"]
assert style is not None
assert style.name == "Learning"
assert "hands-on" in style.description
def test_output_style_config_frozen(self):
style = OutputStyleConfig(
name="Test", description="desc", prompt="prompt"
)
with pytest.raises(Exception):
style.name = "other" # type: ignore[misc]
# =========================================================================
# Knowledge cutoff
# =========================================================================
class TestKnowledgeCutoff:
def test_frontier_model_name(self):
assert FRONTIER_MODEL_NAME == "Claude Opus 4.6"
def test_opus_46_cutoff(self):
assert get_knowledge_cutoff("claude-opus-4-6-20250601") == "May 2025"
def test_sonnet_46_cutoff(self):
assert get_knowledge_cutoff("claude-sonnet-4-6-20250801") == "August 2025"
def test_opus_45_cutoff(self):
assert get_knowledge_cutoff("claude-opus-4-5-20250601") == "May 2025"
def test_haiku_cutoff(self):
assert get_knowledge_cutoff("claude-haiku-4-20250201") == "February 2025"
def test_sonnet_4_cutoff(self):
assert get_knowledge_cutoff("claude-sonnet-4-20250114") == "January 2025"
def test_unknown_model_returns_none(self):
assert get_knowledge_cutoff("gpt-4-turbo") is None
def test_claude_model_ids(self):
assert "opus" in CLAUDE_MODEL_IDS
assert "sonnet" in CLAUDE_MODEL_IDS
assert "haiku" in CLAUDE_MODEL_IDS
# =========================================================================
# Prompt section helpers
# =========================================================================
class TestPromptSectionHelpers:
def test_hooks_section_content(self):
assert "hooks" in HOOKS_SECTION
assert "user-prompt-submit-hook" in HOOKS_SECTION
def test_system_reminders_section(self):
assert "system-reminder" in SYSTEM_REMINDERS_SECTION
def test_summarize_tool_results(self):
assert "tool results" in SUMMARIZE_TOOL_RESULTS_SECTION
def test_default_agent_prompt(self):
assert "agent for Claude Code" in DEFAULT_AGENT_PROMPT
def test_dynamic_boundary(self):
assert SYSTEM_PROMPT_DYNAMIC_BOUNDARY == "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__"
def test_language_section_none_when_no_preference(self):
assert get_language_section(None) is None
assert get_language_section("") is None
def test_language_section_with_preference(self):
result = get_language_section("Spanish")
assert result is not None
assert "Spanish" in result
assert "# Language" in result
def test_output_style_section_none_when_no_config(self):
assert get_output_style_section(None) is None
def test_output_style_section_with_config(self):
config = OutputStyleConfig(
name="TestStyle",
description="A test style",
prompt="Be concise.",
)
result = get_output_style_section(config)
assert result is not None
assert "# Output Style: TestStyle" in result
assert "Be concise." in result
def test_scratchpad_none_when_no_dir(self):
assert get_scratchpad_instructions(None) is None
assert get_scratchpad_instructions("") is None
def test_scratchpad_with_dir(self):
result = get_scratchpad_instructions("/tmp/session-123")
assert result is not None
assert "/tmp/session-123" in result
assert "# Scratchpad Directory" in result
assert "temporary files" in result
# =========================================================================
# Error IDs
# =========================================================================
class TestErrorIDs:
def test_tool_use_summary_error(self):
assert E_TOOL_USE_SUMMARY_GENERATION_FAILED == 344
-237
View File
@@ -1,237 +0,0 @@
from __future__ import annotations
import os
import re
import signal
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from unittest import TestCase
from src.agent_tools import build_tool_context, default_tool_registry, execute_tool
from src.agent_types import AgentPermissions, AgentRuntimeConfig
class PythonExecToolTests(TestCase):
def test_python_exec_runs_inline_code_with_shell_permission(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
config = AgentRuntimeConfig(
cwd=Path(tmp_dir),
permissions=AgentPermissions(allow_shell_commands=True),
)
context = build_tool_context(config)
result = execute_tool(
default_tool_registry(),
'python_exec',
{'code': 'print("hello from python")'},
context,
)
self.assertTrue(result.ok)
self.assertIn('exit_code=0', result.content)
self.assertIn('hello from python', result.content)
self.assertEqual(result.metadata.get('action'), 'python_exec')
self.assertEqual(result.metadata.get('mode'), 'code')
def test_python_exec_exposes_session_scratchpad(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
scratchpad = (root / 'session' / 'scratchpad').resolve()
scratchpad.mkdir(parents=True)
config = AgentRuntimeConfig(
cwd=root,
permissions=AgentPermissions(allow_shell_commands=True),
)
context = build_tool_context(config, scratchpad_directory=scratchpad)
result = execute_tool(
default_tool_registry(),
'python_exec',
{'code': 'import os\nprint(os.environ["PYTHON_EXEC_SCRATCHPAD"])'},
context,
)
self.assertTrue(result.ok)
self.assertIn(str(scratchpad), result.content)
self.assertEqual(result.metadata.get('scratchpad_directory'), str(scratchpad))
def test_python_exec_runs_from_session_scratchpad(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
scratchpad = (root / 'session' / 'scratchpad').resolve()
scratchpad.mkdir(parents=True)
config = AgentRuntimeConfig(
cwd=root,
permissions=AgentPermissions(allow_shell_commands=True),
)
context = build_tool_context(config, scratchpad_directory=scratchpad)
result = execute_tool(
default_tool_registry(),
'python_exec',
{'code': 'import os\nprint(os.getcwd())'},
context,
)
self.assertTrue(result.ok)
self.assertIn(str(scratchpad), result.content)
def test_python_exec_prefers_user_python_env(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
user_env = (root / 'account' / 'python' / '.venv').resolve()
bin_dir = user_env / 'bin'
bin_dir.mkdir(parents=True)
python_bin = bin_dir / 'python'
try:
python_bin.symlink_to(sys.executable)
except OSError:
python_bin.write_text(
'#!/bin/sh\nexec "$PYTHON_EXEC_TEST_REAL_PYTHON" "$@"\n',
encoding='utf-8',
)
python_bin.chmod(0o755)
config = AgentRuntimeConfig(
cwd=root,
permissions=AgentPermissions(allow_shell_commands=True),
python_env_dir=user_env,
)
context = build_tool_context(
config,
extra_env={'PYTHON_EXEC_TEST_REAL_PYTHON': sys.executable},
)
result = execute_tool(
default_tool_registry(),
'python_exec',
{'code': 'import os, sys\nprint(sys.executable)\nprint(os.environ["PYTHON_EXEC_VENV"])'},
context,
)
self.assertTrue(result.ok)
self.assertIn(str(user_env), result.content)
self.assertEqual(result.metadata.get('python_env_dir'), str(user_env))
def test_python_package_show_uses_user_python_env(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
user_env = (root / 'account' / 'python' / '.venv').resolve()
bin_dir = user_env / 'bin'
bin_dir.mkdir(parents=True)
python_bin = bin_dir / 'python'
try:
python_bin.symlink_to(sys.executable)
except OSError:
python_bin.write_text(
'#!/bin/sh\nexec "$PYTHON_EXEC_TEST_REAL_PYTHON" "$@"\n',
encoding='utf-8',
)
python_bin.chmod(0o755)
config = AgentRuntimeConfig(
cwd=root,
permissions=AgentPermissions(allow_shell_commands=True),
python_env_dir=user_env,
)
context = build_tool_context(
config,
extra_env={'PYTHON_EXEC_TEST_REAL_PYTHON': sys.executable},
)
result = execute_tool(
default_tool_registry(),
'python_package',
{'action': 'show'},
context,
)
self.assertTrue(result.ok)
self.assertIn(str(user_env), result.content)
self.assertEqual(result.metadata.get('python_env_dir'), str(user_env))
def test_python_exec_is_blocked_without_shell_permission(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
config = AgentRuntimeConfig(cwd=Path(tmp_dir))
context = build_tool_context(config)
result = execute_tool(
default_tool_registry(),
'python_exec',
{'code': 'print("blocked")'},
context,
)
self.assertFalse(result.ok)
self.assertIn('--allow-shell', result.content)
self.assertEqual(result.metadata.get('error_kind'), 'permission_denied')
def test_python_exec_runs_workspace_script_with_args(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
script = root / 'tool_script.py'
script.write_text(
'import sys\nprint("|".join(sys.argv[1:]))\n',
encoding='utf-8',
)
config = AgentRuntimeConfig(
cwd=root,
permissions=AgentPermissions(allow_shell_commands=True),
)
context = build_tool_context(config)
result = execute_tool(
default_tool_registry(),
'python_exec',
{'script_path': 'tool_script.py', 'args': ['a', 'b']},
context,
)
self.assertTrue(result.ok)
self.assertIn('a|b', result.content)
self.assertEqual(result.metadata.get('mode'), 'script')
def test_python_exec_timeout_kills_spawned_children(self) -> None:
if os.name != 'posix':
self.skipTest('process group cleanup test is POSIX-only')
with tempfile.TemporaryDirectory() as tmp_dir:
config = AgentRuntimeConfig(
cwd=Path(tmp_dir),
permissions=AgentPermissions(allow_shell_commands=True),
)
context = build_tool_context(config)
child_code = (
'import subprocess, sys, time\n'
'child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"])\n'
'print(child.pid, flush=True)\n'
'time.sleep(60)\n'
)
result = execute_tool(
default_tool_registry(),
'python_exec',
{
'code': child_code,
'timeout_seconds': 1,
'max_output_chars': 2000,
},
context,
)
self.assertTrue(result.ok)
self.assertTrue(result.metadata.get('timed_out'))
match = re.search(r'\[stdout\]\s*(\d+)', result.content)
self.assertIsNotNone(match, result.content)
child_pid = int(match.group(1))
time.sleep(0.3)
if self._pid_is_running(child_pid):
try:
os.kill(child_pid, signal.SIGKILL)
except ProcessLookupError:
pass
self.fail(f'spawned child process is still running: {child_pid}')
@staticmethod
def _pid_is_running(pid: int) -> bool:
status = subprocess.run(
['ps', '-p', str(pid), '-o', 'stat='],
capture_output=True,
text=True,
check=False,
)
state = status.stdout.strip()
return bool(state) and not state.startswith('Z')
File diff suppressed because it is too large Load Diff
-101
View File
@@ -1,101 +0,0 @@
"""Tests for the local release-notes parser ported from utils/releaseNotes.ts."""
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from src.release_notes import (
MAX_RELEASE_NOTES_SHOWN,
check_for_release_notes,
get_all_release_notes,
get_recent_release_notes,
parse_changelog,
read_local_changelog,
)
SAMPLE = (
'# Changelog\n\n'
'## 1.3.0 - 2026-04-15\n'
'- new shiny\n'
'- another bullet\n\n'
'## 1.2.0\n'
'- mid bullet\n\n'
'## 1.1.0\n'
'- oldest bullet\n'
)
class ParseChangelogTest(unittest.TestCase):
def test_returns_empty_for_blank(self) -> None:
self.assertEqual(parse_changelog(''), {})
def test_extracts_versions_and_bullets(self) -> None:
parsed = parse_changelog(SAMPLE)
self.assertEqual(set(parsed.keys()), {'1.3.0', '1.2.0', '1.1.0'})
self.assertEqual(parsed['1.3.0'], ['new shiny', 'another bullet'])
self.assertEqual(parsed['1.2.0'], ['mid bullet'])
def test_skips_versions_without_bullets(self) -> None:
parsed = parse_changelog('# X\n\n## 1.0.0\nplain text\n')
self.assertEqual(parsed, {})
class RecentNotesTest(unittest.TestCase):
def test_returns_only_newer_versions(self) -> None:
notes = get_recent_release_notes('1.3.0', '1.2.0', SAMPLE)
self.assertEqual(notes, ['new shiny', 'another bullet'])
def test_first_run_returns_all(self) -> None:
notes = get_recent_release_notes('1.3.0', None, SAMPLE)
self.assertEqual(notes[0], 'new shiny')
self.assertIn('oldest bullet', notes)
def test_no_new_when_at_or_below_previous(self) -> None:
self.assertEqual(get_recent_release_notes('1.1.0', '1.3.0', SAMPLE), [])
def test_caps_at_max_shown(self) -> None:
big_changelog = '# Changelog\n\n' + ''.join(
f'## 9.9.{i}\n- bullet {i}\n\n' for i in range(20)
)
notes = get_recent_release_notes('9.9.19', '0.0.1', big_changelog)
self.assertEqual(len(notes), MAX_RELEASE_NOTES_SHOWN)
class AllNotesTest(unittest.TestCase):
def test_sorted_oldest_first(self) -> None:
all_notes = get_all_release_notes(SAMPLE)
versions = [version for version, _ in all_notes]
self.assertEqual(versions, ['1.1.0', '1.2.0', '1.3.0'])
class ReadLocalChangelogTest(unittest.TestCase):
def test_reads_when_present(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / 'CHANGELOG.md').write_text(SAMPLE, encoding='utf-8')
self.assertIn('## 1.3.0', read_local_changelog(Path(tmp)))
def test_returns_empty_when_missing(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
self.assertEqual(read_local_changelog(Path(tmp)), '')
class CheckForReleaseNotesTest(unittest.TestCase):
def test_signals_when_changelog_present_and_newer(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / 'CHANGELOG.md').write_text(SAMPLE, encoding='utf-8')
payload = check_for_release_notes('1.3.0', '1.2.0', cwd=Path(tmp))
self.assertTrue(payload['hasReleaseNotes'])
self.assertEqual(payload['releaseNotes'][0], 'new shiny')
def test_silent_when_missing_changelog(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
payload = check_for_release_notes('1.3.0', None, cwd=Path(tmp))
self.assertFalse(payload['hasReleaseNotes'])
self.assertEqual(payload['releaseNotes'], [])
if __name__ == '__main__':
unittest.main()
-123
View File
@@ -1,123 +0,0 @@
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.remote_runtime import (
RemoteRuntime,
run_deep_link_mode,
run_direct_connect_mode,
run_remote_mode,
run_ssh_mode,
run_teleport_mode,
)
class RemoteRuntimeTests(unittest.TestCase):
def test_remote_runtime_discovers_profiles_and_persists_connection(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-remote.json').write_text(
(
'{"profiles":['
'{"name":"staging","mode":"ssh","target":"dev@staging","workspaceCwd":"/srv/app"},'
'{"name":"preview","mode":"deep-link","target":"preview://session"}'
']}'
),
encoding='utf-8',
)
runtime = RemoteRuntime.from_workspace(workspace)
report = runtime.connect('staging')
restored = RemoteRuntime.from_workspace(workspace)
self.assertEqual(len(runtime.profiles), 2)
self.assertTrue(report.connected)
self.assertEqual(report.profile_name, 'staging')
self.assertIsNotNone(restored.active_connection)
self.assertEqual(restored.active_connection.profile_name, 'staging')
self.assertIn('Configured remote profiles: 2', restored.render_summary())
def test_remote_runtime_disconnect_clears_active_connection(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-remote.json').write_text(
'{"profiles":[{"name":"staging","mode":"ssh","target":"dev@staging"}]}',
encoding='utf-8',
)
runtime = RemoteRuntime.from_workspace(workspace)
runtime.connect('staging')
report = runtime.disconnect()
self.assertFalse(report.connected)
self.assertIn('Disconnected ssh target dev@staging', report.detail)
def test_remote_mode_helpers_use_manifest_backed_profiles(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-remote.json').write_text(
(
'{"profiles":['
'{"name":"workspace","mode":"remote","target":"remote://workspace"},'
'{"name":"sshbox","mode":"ssh","target":"dev@sshbox"},'
'{"name":"tele","mode":"teleport","target":"teleport://workspace"},'
'{"name":"direct","mode":"direct-connect","target":"direct://workspace"},'
'{"name":"link","mode":"deep-link","target":"deep://workspace"}'
']}'
),
encoding='utf-8',
)
remote_report = run_remote_mode('workspace', cwd=workspace)
ssh_report = run_ssh_mode('sshbox', cwd=workspace)
teleport_report = run_teleport_mode('tele', cwd=workspace)
direct_report = run_direct_connect_mode('direct', cwd=workspace)
deep_link_report = run_deep_link_mode('link', cwd=workspace)
self.assertEqual(remote_report.profile_name, 'workspace')
self.assertEqual(ssh_report.mode, 'ssh')
self.assertEqual(teleport_report.mode, 'teleport')
self.assertEqual(direct_report.mode, 'direct-connect')
self.assertEqual(deep_link_report.mode, 'deep-link')
def test_remote_tools_execute_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-remote.json').write_text(
(
'{"profiles":[{"name":"staging","mode":"ssh","target":"dev@staging",'
'"workspaceCwd":"/srv/app","sessionUrl":"wss://remote/session"}]}'
),
encoding='utf-8',
)
runtime = RemoteRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
remote_runtime=runtime,
)
list_result = execute_tool(
default_tool_registry(),
'remote_list_profiles',
{},
context,
)
connect_result = execute_tool(
default_tool_registry(),
'remote_connect',
{'target': 'staging'},
context,
)
status_result = execute_tool(
default_tool_registry(),
'remote_status',
{},
context,
)
self.assertTrue(list_result.ok)
self.assertIn('staging', list_result.content)
self.assertTrue(connect_result.ok)
self.assertIn('profile=staging', connect_result.content)
self.assertTrue(status_result.ok)
self.assertIn('Configured remote profiles: 1', status_result.content)
-109
View File
@@ -1,109 +0,0 @@
"""Tests for remote/bridge slash commands ported from the npm source.
Covers /bridge (aliases /remote-control, /rc) and /remote-setup
(alias /web-setup).
"""
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from src.agent_runtime import LocalCodingAgent
from src.agent_types import AgentRuntimeConfig, ModelConfig
def _make_agent(tmp_dir: str) -> LocalCodingAgent:
return LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
class BridgeCommandTest(unittest.TestCase):
def test_reports_unsupported_status(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/bridge').final_output
self.assertIn('not implemented', out.lower())
self.assertIn('No active local remote connection', out)
def test_remote_control_alias(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-control').final_output
self.assertIn('Remote-control bridge', out)
def test_rc_alias(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/rc').final_output
self.assertIn('Remote-control bridge', out)
def test_named_lookup_misses_unknown_profile(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/bridge nope').final_output
self.assertIn('No matching remote profile for "nope"', out)
def test_named_lookup_matches_known_profile(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.remote.json').write_text(json.dumps({
'profiles': [
{'name': 'edge', 'mode': 'ssh', 'target': 'user@edge.example'},
],
}), encoding='utf-8')
agent = _make_agent(tmp)
out = agent.run('/bridge edge').final_output
self.assertIn('Matched remote profile "edge"', out)
self.assertIn('user@edge.example', out)
class RemoteSetupCommandTest(unittest.TestCase):
def test_includes_web_url(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-setup').final_output
self.assertIn('https://claude.ai/code', out)
self.assertIn('GitHub CLI', out)
def test_web_setup_alias(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/web-setup').final_output
self.assertIn('https://claude.ai/code', out)
def test_handles_missing_gh(self) -> None:
with mock.patch('shutil.which', return_value=None):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-setup').final_output
self.assertIn('not_installed', out)
self.assertIn('cli.github.com', out)
def test_handles_authenticated_gh(self) -> None:
fake = mock.Mock(returncode=0, stdout='Logged in to github.com as octo', stderr='')
with mock.patch('shutil.which', return_value='/usr/bin/gh'), \
mock.patch('subprocess.run', return_value=fake):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-setup').final_output
self.assertIn('authenticated', out)
self.assertIn('gh auth token', out)
def test_handles_unauthenticated_gh(self) -> None:
fake = mock.Mock(returncode=1, stdout='', stderr='You are not logged into any GitHub hosts')
with mock.patch('shutil.which', return_value='/usr/bin/gh'), \
mock.patch('subprocess.run', return_value=fake):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-setup').final_output
self.assertIn('not_authenticated', out)
self.assertIn('gh auth login', out)
if __name__ == '__main__':
unittest.main()
-84
View File
@@ -1,84 +0,0 @@
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)
+163
View File
@@ -0,0 +1,163 @@
from __future__ import annotations
import json
import httpx
from fastapi.testclient import TestClient
from agent_platform.runtime.app import create_app
from agent_platform.store import RuntimeStore
class FakeModelProvider:
def __init__(self) -> None:
self.completed = []
self.forwarded = []
async def complete(self, **kwargs):
self.completed.append(kwargs)
return {
"choices": [{"message": {"role": "assistant", "content": "work complete"}, "finish_reason": "stop"}],
"usage": {"total_tokens": 10},
}
async def forward(self, payload):
self.forwarded.append(payload)
request = httpx.Request("POST", "https://model.example.test/v1/chat/completions")
if payload.get("stream"):
return httpx.Response(
200,
request=request,
headers={"content-type": "text/event-stream"},
content=(
'data: {"model":"ChatGPT-5.6:Luna","choices":[{"index":0,"delta":'
'{"tool_calls":[{"index":0,"id":"call-1","type":"function","function":'
'{"name":"read_file","arguments":"{}"}}]},"finish_reason":null}]}\n\n'
"data: [DONE]\n\n"
),
)
return httpx.Response(
200,
request=request,
headers={"content-type": "application/json"},
json={
"model": payload["model"],
"choices": [{"message": {"role": "assistant", "content": "chat reply"}, "finish_reason": "stop"}],
},
)
async def close(self):
return None
class FakeTools:
async def close(self):
return None
def specs(self, *, read_only=False, allow_delegate=True):
return []
async def execute(self, name, arguments, context):
raise AssertionError("No tool should be called")
def headers(settings, identity_jwt, chat_id="chat-1"):
return {
"Authorization": f"Bearer {settings.internal_provider_key}",
"X-OpenWebUI-User-Jwt": identity_jwt,
"X-OpenWebUI-Chat-Id": chat_id,
}
def test_model_list_requires_internal_key(settings) -> None:
app = create_app(
settings,
store=RuntimeStore(settings.database_url),
provider=FakeModelProvider(),
tools=FakeTools(),
)
with TestClient(app) as client:
assert client.get("/v1/models").status_code == 401
response = client.get(
"/v1/models",
headers={"Authorization": f"Bearer {settings.internal_provider_key}"},
)
assert response.status_code == 200
assert len(response.json()["data"]) == 6
def test_chat_maps_provider_model_and_work_upgrade_is_one_way(settings, identity_jwt) -> None:
provider = FakeModelProvider()
app = create_app(settings, store=RuntimeStore(settings.database_url), provider=provider, tools=FakeTools())
with TestClient(app) as client:
chat = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt),
json={"model": "chat-light", "messages": [{"role": "user", "content": "hello"}]},
)
assert chat.status_code == 200
assert provider.forwarded[0]["model"] == "ChatGPT-5.6:Luna"
assert chat.json()["model"] == "chat-light"
work = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt),
json={"model": "work-high", "messages": [{"role": "user", "content": "do it"}]},
)
assert work.status_code == 200
assert provider.completed[-1]["model"] == "ChatGPT-5.6:Sol"
downgrade = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt),
json={"model": "chat-medium", "messages": [{"role": "user", "content": "back"}]},
)
assert downgrade.status_code == 409
def test_chat_stream_hides_provider_model_and_preserves_native_tool_calls(settings, identity_jwt) -> None:
provider = FakeModelProvider()
app = create_app(settings, store=RuntimeStore(settings.database_url), provider=provider, tools=FakeTools())
with TestClient(app) as client:
response = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "chat-stream"),
json={
"model": "chat-light",
"stream": True,
"messages": [{"role": "user", "content": "inspect"}],
},
)
assert response.status_code == 200
assert '"model": "chat-light"' in response.text
assert "ChatGPT-5.6:Luna" not in response.text
assert '"tool_calls"' in response.text
assert '"name": "read_file"' in response.text
assert "data: [DONE]" in response.text
def test_work_stream_never_emits_delta_tool_calls(settings, identity_jwt) -> None:
app = create_app(
settings,
store=RuntimeStore(settings.database_url),
provider=FakeModelProvider(),
tools=FakeTools(),
)
with TestClient(app) as client:
response = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "stream-chat"),
json={
"model": "work-medium",
"stream": True,
"messages": [{"role": "user", "content": "do it"}],
},
)
assert response.status_code == 200
assert "work complete" in response.text
assert "tool_calls" in response.text # rendered Work status details
for line in response.text.splitlines():
if line.startswith("data: {"):
chunk = json.loads(line.removeprefix("data: "))
assert "tool_calls" not in chunk["choices"][0]["delta"]
assert '"finish_reason": "stop"' in response.text
-110
View File
@@ -1,110 +0,0 @@
"""Tests for sandbox configuration types ported from sandboxTypes.ts."""
from __future__ import annotations
import unittest
from src.sandbox_types import (
SandboxFilesystemConfig,
SandboxNetworkConfig,
SandboxRipgrepConfig,
SandboxSettings,
)
class SandboxNetworkConfigTest(unittest.TestCase):
def test_round_trip(self) -> None:
raw = {
'allowedDomains': ['example.com'],
'allowManagedDomainsOnly': True,
'allowUnixSockets': ['/tmp/sock'],
'allowAllUnixSockets': False,
'allowLocalBinding': True,
'httpProxyPort': 8080,
'socksProxyPort': 1080,
}
parsed = SandboxNetworkConfig.from_dict(raw)
self.assertEqual(parsed.allowed_domains, ['example.com'])
self.assertEqual(parsed.http_proxy_port, 8080)
self.assertEqual(parsed.to_dict(), raw)
def test_strips_none(self) -> None:
parsed = SandboxNetworkConfig.from_dict({'allowedDomains': ['a']})
self.assertEqual(parsed.to_dict(), {'allowedDomains': ['a']})
def test_rejects_wrong_type(self) -> None:
with self.assertRaises(ValueError):
SandboxNetworkConfig.from_dict({'allowedDomains': 'nope'})
class SandboxFilesystemConfigTest(unittest.TestCase):
def test_round_trip(self) -> None:
raw = {
'allowWrite': ['/tmp'],
'denyWrite': ['/etc'],
'denyRead': ['/etc/secrets'],
'allowRead': ['/etc/secrets/public'],
'allowManagedReadPathsOnly': False,
}
parsed = SandboxFilesystemConfig.from_dict(raw)
self.assertEqual(parsed.allow_write, ['/tmp'])
self.assertEqual(parsed.to_dict(), raw)
class SandboxRipgrepConfigTest(unittest.TestCase):
def test_requires_command(self) -> None:
with self.assertRaises(ValueError):
SandboxRipgrepConfig.from_dict({'args': ['-i']})
def test_round_trip(self) -> None:
parsed = SandboxRipgrepConfig.from_dict({'command': 'rg', 'args': ['--no-ignore']})
self.assertEqual(parsed.command, 'rg')
self.assertEqual(parsed.to_dict(), {'command': 'rg', 'args': ['--no-ignore']})
class SandboxSettingsTest(unittest.TestCase):
def test_full_round_trip_preserves_passthrough(self) -> None:
raw = {
'enabled': True,
'failIfUnavailable': False,
'autoAllowBashIfSandboxed': True,
'allowUnsandboxedCommands': True,
'network': {'allowedDomains': ['x.com']},
'filesystem': {'allowWrite': ['/tmp']},
'ignoreViolations': {'NetworkViolation': ['y.com']},
'enableWeakerNestedSandbox': False,
'enableWeakerNetworkIsolation': False,
'excludedCommands': ['rm -rf /'],
'ripgrep': {'command': 'rg'},
'enabledPlatforms': ['macos'],
'somethingFuture': 42,
}
parsed = SandboxSettings.from_dict(raw)
self.assertTrue(parsed.enabled)
self.assertEqual(parsed.network.allowed_domains, ['x.com'])
self.assertEqual(parsed.ignore_violations, {'NetworkViolation': ['y.com']})
self.assertEqual(parsed.extra['enabledPlatforms'], ['macos'])
self.assertEqual(parsed.extra['somethingFuture'], 42)
back = parsed.to_dict()
self.assertEqual(back['enabled'], True)
self.assertEqual(back['enabledPlatforms'], ['macos'])
self.assertEqual(back['somethingFuture'], 42)
def test_empty_returns_defaults(self) -> None:
parsed = SandboxSettings.from_dict({})
self.assertIsNone(parsed.enabled)
self.assertIsNone(parsed.network)
self.assertEqual(parsed.to_dict(), {})
def test_rejects_non_mapping(self) -> None:
with self.assertRaises(ValueError):
SandboxSettings.from_dict('nope') # type: ignore[arg-type]
def test_ignore_violations_must_be_mapping(self) -> None:
with self.assertRaises(ValueError):
SandboxSettings.from_dict({'ignoreViolations': ['nope']})
if __name__ == '__main__':
unittest.main()
-175
View File
@@ -1,175 +0,0 @@
"""Tests for SDK core types ported from entrypoints/sdk/coreTypes.ts."""
from __future__ import annotations
import unittest
from src.sdk_core_types import (
API_KEY_SOURCES,
CONFIG_SCOPES,
EXIT_REASONS,
HOOK_EVENTS,
SDK_BETAS,
JsonSchemaOutputFormat,
McpClaudeAIProxyServerConfig,
McpHttpServerConfig,
McpSdkServerConfig,
McpSSEServerConfig,
McpStdioServerConfig,
ModelUsage,
ThinkingAdaptive,
ThinkingDisabled,
ThinkingEnabled,
mcp_server_config_from_dict,
thinking_config_from_dict,
)
class HookEventsTest(unittest.TestCase):
def test_includes_known_events(self) -> None:
for required in (
'PreToolUse',
'PostToolUse',
'UserPromptSubmit',
'SessionStart',
'PreCompact',
'WorktreeCreate',
'CwdChanged',
'FileChanged',
):
self.assertIn(required, HOOK_EVENTS)
def test_no_duplicates(self) -> None:
self.assertEqual(len(HOOK_EVENTS), len(set(HOOK_EVENTS)))
class ExitReasonsTest(unittest.TestCase):
def test_known_exit_reasons(self) -> None:
for required in (
'clear', 'resume', 'logout', 'prompt_input_exit',
'other', 'bypass_permissions_disabled',
):
self.assertIn(required, EXIT_REASONS)
class EnumLiteralsTest(unittest.TestCase):
def test_api_key_sources(self) -> None:
self.assertEqual(set(API_KEY_SOURCES), {'user', 'project', 'org', 'temporary', 'oauth'})
def test_config_scopes(self) -> None:
self.assertEqual(set(CONFIG_SCOPES), {'local', 'user', 'project'})
def test_sdk_betas_known(self) -> None:
self.assertIn('context-1m-2025-08-07', SDK_BETAS)
class ModelUsageTest(unittest.TestCase):
def test_round_trip(self) -> None:
raw = {
'inputTokens': 100, 'outputTokens': 50,
'cacheReadInputTokens': 10, 'cacheCreationInputTokens': 5,
'webSearchRequests': 0, 'costUSD': 0.001,
'contextWindow': 200000, 'maxOutputTokens': 8192,
}
usage = ModelUsage.from_dict(raw)
self.assertEqual(usage.input_tokens, 100)
self.assertEqual(usage.cost_usd, 0.001)
self.assertEqual(usage.to_dict(), raw)
class ThinkingConfigTest(unittest.TestCase):
def test_adaptive(self) -> None:
cfg = thinking_config_from_dict({'type': 'adaptive'})
self.assertIsInstance(cfg, ThinkingAdaptive)
self.assertEqual(cfg.to_dict(), {'type': 'adaptive'})
def test_enabled_with_budget(self) -> None:
cfg = thinking_config_from_dict({'type': 'enabled', 'budgetTokens': 1024})
self.assertIsInstance(cfg, ThinkingEnabled)
self.assertEqual(cfg.to_dict(), {'type': 'enabled', 'budgetTokens': 1024})
def test_enabled_without_budget(self) -> None:
cfg = thinking_config_from_dict({'type': 'enabled'})
self.assertEqual(cfg.to_dict(), {'type': 'enabled'})
def test_disabled(self) -> None:
cfg = thinking_config_from_dict({'type': 'disabled'})
self.assertIsInstance(cfg, ThinkingDisabled)
def test_unknown_type_raises(self) -> None:
with self.assertRaises(ValueError):
thinking_config_from_dict({'type': 'something-else'})
class McpServerConfigTest(unittest.TestCase):
def test_stdio_default_type(self) -> None:
cfg = mcp_server_config_from_dict({'command': 'npx'})
self.assertIsInstance(cfg, McpStdioServerConfig)
self.assertEqual(cfg.command, 'npx')
def test_stdio_with_args_env(self) -> None:
cfg = mcp_server_config_from_dict({
'type': 'stdio',
'command': 'node',
'args': ['./mcp.js'],
'env': {'TOKEN': 'xyz'},
})
out = cfg.to_dict()
self.assertEqual(out['command'], 'node')
self.assertEqual(out['args'], ['./mcp.js'])
self.assertEqual(out['env'], {'TOKEN': 'xyz'})
def test_sse(self) -> None:
cfg = mcp_server_config_from_dict({'type': 'sse', 'url': 'https://x.example'})
self.assertIsInstance(cfg, McpSSEServerConfig)
def test_http_with_headers(self) -> None:
cfg = mcp_server_config_from_dict({
'type': 'http',
'url': 'https://x.example',
'headers': {'Auth': 'Bearer 1'},
})
self.assertIsInstance(cfg, McpHttpServerConfig)
self.assertEqual(cfg.headers, {'Auth': 'Bearer 1'})
def test_sdk(self) -> None:
cfg = mcp_server_config_from_dict({'type': 'sdk', 'name': 'my-sdk'})
self.assertIsInstance(cfg, McpSdkServerConfig)
def test_claudeai_proxy(self) -> None:
cfg = mcp_server_config_from_dict({
'type': 'claudeai-proxy',
'url': 'https://claude.ai/p',
'id': 'abc',
})
self.assertIsInstance(cfg, McpClaudeAIProxyServerConfig)
def test_unknown_type_raises(self) -> None:
with self.assertRaises(ValueError):
mcp_server_config_from_dict({'type': 'mystery'})
def test_stdio_requires_command(self) -> None:
with self.assertRaises(ValueError):
mcp_server_config_from_dict({'type': 'stdio'})
class JsonSchemaOutputFormatTest(unittest.TestCase):
def test_round_trip(self) -> None:
fmt = JsonSchemaOutputFormat.from_dict({
'type': 'json_schema',
'schema': {'type': 'object', 'properties': {}},
})
self.assertEqual(fmt.schema['type'], 'object')
self.assertEqual(fmt.to_dict()['type'], 'json_schema')
def test_rejects_wrong_type(self) -> None:
with self.assertRaises(ValueError):
JsonSchemaOutputFormat.from_dict({'type': 'text', 'schema': {}})
def test_requires_schema_mapping(self) -> None:
with self.assertRaises(ValueError):
JsonSchemaOutputFormat.from_dict({'type': 'json_schema'})
if __name__ == '__main__':
unittest.main()
-132
View File
@@ -1,132 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from src.agent_tools import build_tool_context, default_tool_registry, execute_tool
from src.agent_types import AgentRuntimeConfig
from src.search_runtime import SearchRuntime
class FakeHTTPResponse:
def __init__(self, payload: str) -> None:
self.payload = payload
def read(self) -> bytes:
return self.payload.encode('utf-8')
def __enter__(self) -> 'FakeHTTPResponse':
return self
def __exit__(self, exc_type, exc, tb) -> None:
return None
class SearchRuntimeTests(unittest.TestCase):
def test_provider_activation_persists_across_reload(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-search.json').write_text(
(
'{"providers":['
'{"name":"primary","provider":"searxng","baseUrl":"http://127.0.0.1:8080"},'
'{"name":"backup","provider":"searxng","baseUrl":"http://127.0.0.2:8080"}'
']}'
),
encoding='utf-8',
)
runtime = SearchRuntime.from_workspace(workspace)
report = runtime.activate_provider('backup')
reloaded = SearchRuntime.from_workspace(workspace)
self.assertEqual(report.provider_name, 'backup')
self.assertIsNotNone(reloaded.current_provider())
self.assertEqual(reloaded.current_provider().name, 'backup')
def test_search_runtime_loads_searxng_provider_from_env(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
with patch.dict('os.environ', {'SEARXNG_BASE_URL': 'http://127.0.0.1:8888'}, clear=False):
runtime = SearchRuntime.from_workspace(workspace)
provider = runtime.current_provider()
self.assertIsNotNone(provider)
self.assertEqual(provider.name, 'searxng')
self.assertEqual(provider.base_url, 'http://127.0.0.1:8888')
def test_search_runtime_parses_searxng_results(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-search.json').write_text(
'{"providers":[{"name":"local-search","provider":"searxng","baseUrl":"http://127.0.0.1:8080"}]}',
encoding='utf-8',
)
runtime = SearchRuntime.from_workspace(workspace)
with patch(
'src.search_runtime.request.urlopen',
return_value=FakeHTTPResponse(
'{"results":[{"title":"Alpha","url":"https://example.com/alpha","content":"Snippet"}]}'
),
):
provider, results = runtime.search('alpha')
self.assertEqual(provider.name, 'local-search')
self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, 'Alpha')
self.assertEqual(results[0].url, 'https://example.com/alpha')
def test_search_runtime_parses_brave_results(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-search.json').write_text(
'{"providers":[{"name":"brave-local","provider":"brave","baseUrl":"https://api.search.brave.com/res/v1/web/search","apiKeyEnv":"BRAVE_SEARCH_API_KEY"}]}',
encoding='utf-8',
)
with patch.dict('os.environ', {'BRAVE_SEARCH_API_KEY': 'demo-key'}, clear=False):
runtime = SearchRuntime.from_workspace(workspace)
with patch(
'src.search_runtime.request.urlopen',
return_value=FakeHTTPResponse(
'{"web":{"results":[{"title":"Alpha","url":"https://example.com/alpha","description":"Snippet"}]}}'
),
):
provider, results = runtime.search('alpha', provider_name='brave-local')
self.assertEqual(provider.provider, 'brave')
self.assertEqual(len(results), 1)
self.assertEqual(results[0].snippet, 'Snippet')
def test_web_search_tool_uses_search_runtime(self) -> None:
registry = default_tool_registry()
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
(workspace / '.claw-search.json').write_text(
'{"providers":[{"name":"local-search","provider":"searxng","baseUrl":"http://127.0.0.1:8080"}]}',
encoding='utf-8',
)
runtime = SearchRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(cwd=workspace),
tool_registry=registry,
search_runtime=runtime,
)
with patch(
'src.search_runtime.request.urlopen',
return_value=FakeHTTPResponse(
'{"results":[{"title":"Alpha","url":"https://example.com/alpha","content":"Snippet"}]}'
),
):
result = execute_tool(
registry,
'web_search',
{'query': 'alpha'},
context,
)
self.assertTrue(result.ok)
self.assertIn('# Web Search', result.content)
self.assertEqual(result.metadata.get('action'), 'web_search')
self.assertEqual(result.metadata.get('provider'), 'local-search')
self.assertEqual(result.metadata.get('result_count'), 1)
-63
View File
@@ -1,63 +0,0 @@
"""Tests for ``src/session_env_vars.py``."""
from __future__ import annotations
import unittest
from src.session_env_vars import (
clear_session_env_vars,
delete_session_env_var,
get_session_env_vars,
set_session_env_var,
)
class SessionEnvVarsTest(unittest.TestCase):
def setUp(self) -> None:
clear_session_env_vars()
def tearDown(self) -> None:
clear_session_env_vars()
def test_starts_empty(self) -> None:
self.assertEqual(dict(get_session_env_vars()), {})
def test_set_and_get(self) -> None:
set_session_env_var('FOO', 'bar')
self.assertEqual(get_session_env_vars()['FOO'], 'bar')
def test_set_overwrites_existing(self) -> None:
set_session_env_var('FOO', 'one')
set_session_env_var('FOO', 'two')
self.assertEqual(get_session_env_vars()['FOO'], 'two')
def test_delete_removes(self) -> None:
set_session_env_var('FOO', 'bar')
delete_session_env_var('FOO')
self.assertNotIn('FOO', get_session_env_vars())
def test_delete_missing_is_noop(self) -> None:
delete_session_env_var('NEVER_SET')
self.assertEqual(dict(get_session_env_vars()), {})
def test_clear_drops_everything(self) -> None:
set_session_env_var('A', '1')
set_session_env_var('B', '2')
clear_session_env_vars()
self.assertEqual(dict(get_session_env_vars()), {})
def test_returned_mapping_is_read_only(self) -> None:
set_session_env_var('FOO', 'bar')
view = get_session_env_vars()
with self.assertRaises(TypeError):
view['FOO'] = 'mutated' # type: ignore[index]
def test_view_reflects_subsequent_mutations(self) -> None:
view = get_session_env_vars()
self.assertNotIn('FOO', view)
set_session_env_var('FOO', 'bar')
self.assertEqual(view['FOO'], 'bar')
if __name__ == '__main__':
unittest.main()
-639
View File
@@ -1,639 +0,0 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from src.agent_types import (
AgentPermissions,
AgentRuntimeConfig,
BudgetConfig,
ModelConfig,
ModelPricing,
OutputSchemaConfig,
UsageStats,
)
from src.session_store import (
StoredAgentSession,
StoredSession,
_deserialize_output_schema,
_optional_float,
_optional_int,
consume_next_session_input,
deserialize_model_config,
deserialize_runtime_config,
enqueue_session_input,
list_session_input_queue,
load_agent_session,
load_session,
save_agent_session,
save_session,
serialize_model_config,
serialize_runtime_config,
usage_from_payload,
)
class TestStoredSessionRoundTrip(unittest.TestCase):
"""save_session then load_session preserves all fields."""
def test_round_trip(self) -> None:
session = StoredSession(
session_id='abc-123',
messages=('hello', 'world', 'foo'),
input_tokens=100,
output_tokens=200,
)
with tempfile.TemporaryDirectory() as td:
directory = Path(td)
save_session(session, directory=directory)
loaded = load_session('abc-123', directory=directory)
self.assertEqual(loaded.session_id, session.session_id)
self.assertEqual(loaded.messages, session.messages)
self.assertEqual(loaded.input_tokens, session.input_tokens)
self.assertEqual(loaded.output_tokens, session.output_tokens)
def test_round_trip_empty_messages(self) -> None:
session = StoredSession(
session_id='empty',
messages=(),
input_tokens=0,
output_tokens=0,
)
with tempfile.TemporaryDirectory() as td:
directory = Path(td)
save_session(session, directory=directory)
loaded = load_session('empty', directory=directory)
self.assertEqual(loaded.messages, ())
self.assertEqual(loaded.input_tokens, 0)
class TestStoredAgentSessionRoundTrip(unittest.TestCase):
"""save_agent_session then load_agent_session preserves all fields."""
def _make_session(self, **overrides: object) -> StoredAgentSession:
defaults: dict = {
'session_id': 'agent-001',
'model_config': {'model': 'gpt-4', 'temperature': 0.5},
'runtime_config': {'cwd': '/home/user', 'max_turns': 20},
'system_prompt_parts': ('You are helpful.',),
'user_context': {'lang': 'en'},
'system_context': {'os': 'linux'},
'messages': ({'role': 'user', 'content': 'hi'},),
'display_messages': ({'role': 'user', 'content': 'visible hi'},),
'turns': 3,
'tool_calls': 7,
'usage': {'input_tokens': 500, 'output_tokens': 300},
'total_cost_usd': 0.05,
'file_history': ({'file': 'a.py', 'action': 'edit'},),
'budget_state': {'remaining': 100},
'plugin_state': {'key': 'value'},
'scratchpad_directory': '/scratch/pad',
}
defaults.update(overrides)
return StoredAgentSession(**defaults)
def test_round_trip_all_fields(self) -> None:
session = self._make_session()
with tempfile.TemporaryDirectory() as td:
directory = Path(td)
save_agent_session(session, directory=directory)
loaded = load_agent_session('agent-001', directory=directory)
self.assertEqual(loaded.session_id, session.session_id)
self.assertEqual(loaded.model_config, session.model_config)
self.assertEqual(loaded.runtime_config, session.runtime_config)
self.assertEqual(loaded.system_prompt_parts, session.system_prompt_parts)
self.assertEqual(loaded.user_context, session.user_context)
self.assertEqual(loaded.system_context, session.system_context)
self.assertEqual(loaded.messages, session.messages)
self.assertEqual(loaded.display_messages, session.display_messages)
self.assertEqual(loaded.turns, session.turns)
self.assertEqual(loaded.tool_calls, session.tool_calls)
self.assertEqual(loaded.usage, session.usage)
self.assertAlmostEqual(loaded.total_cost_usd, session.total_cost_usd)
self.assertEqual(loaded.file_history, session.file_history)
self.assertEqual(loaded.budget_state, session.budget_state)
self.assertEqual(loaded.plugin_state, session.plugin_state)
self.assertEqual(loaded.scratchpad_directory, session.scratchpad_directory)
def test_round_trip_no_scratchpad(self) -> None:
session = self._make_session(scratchpad_directory=None)
with tempfile.TemporaryDirectory() as td:
directory = Path(td)
save_agent_session(session, directory=directory)
loaded = load_agent_session('agent-001', directory=directory)
self.assertIsNone(loaded.scratchpad_directory)
def test_load_filters_non_dict_messages(self) -> None:
"""Non-dict entries in messages list are filtered out on load."""
with tempfile.TemporaryDirectory() as td:
directory = Path(td)
path = directory / 'mixed.json'
data = {
'session_id': 'mixed',
'model_config': {},
'runtime_config': {'cwd': '/'},
'system_prompt_parts': [],
'user_context': {},
'system_context': {},
'messages': [
{'role': 'user', 'content': 'hi'},
'not a dict',
42,
None,
{'role': 'assistant', 'content': 'hey'},
],
'turns': 0,
'tool_calls': 0,
'usage': {},
'total_cost_usd': 0.0,
'file_history': [],
'budget_state': {},
'plugin_state': {},
}
path.write_text(json.dumps(data))
loaded = load_agent_session('mixed', directory=directory)
self.assertEqual(len(loaded.messages), 2)
self.assertEqual(loaded.messages[0]['role'], 'user')
self.assertEqual(loaded.messages[1]['role'], 'assistant')
self.assertEqual(loaded.display_messages, loaded.messages)
def test_load_agent_session_falls_back_to_messages_for_display(self) -> None:
with tempfile.TemporaryDirectory() as td:
directory = Path(td)
path = directory / 'legacy.json'
data = {
'session_id': 'legacy',
'model_config': {},
'runtime_config': {'cwd': '/'},
'system_prompt_parts': [],
'user_context': {},
'system_context': {},
'messages': [{'role': 'user', 'content': 'legacy hi'}],
'turns': 0,
'tool_calls': 0,
'usage': {},
'total_cost_usd': 0.0,
'file_history': [],
}
path.write_text(json.dumps(data))
loaded = load_agent_session('legacy', directory=directory)
self.assertEqual(loaded.display_messages, loaded.messages)
def test_load_defaults_for_missing_optional_fields(self) -> None:
"""Missing optional fields get sensible defaults."""
with tempfile.TemporaryDirectory() as td:
directory = Path(td)
path = directory / 'minimal.json'
data = {
'session_id': 'minimal',
'model_config': {},
'runtime_config': {'cwd': '/'},
'system_prompt_parts': [],
'user_context': {},
'system_context': {},
'messages': [],
'turns': 1,
'tool_calls': 2,
}
path.write_text(json.dumps(data))
loaded = load_agent_session('minimal', directory=directory)
self.assertEqual(loaded.usage, {})
self.assertAlmostEqual(loaded.total_cost_usd, 0.0)
self.assertEqual(loaded.file_history, ())
self.assertEqual(loaded.budget_state, {})
self.assertEqual(loaded.plugin_state, {})
self.assertIsNone(loaded.scratchpad_directory)
def test_load_non_dict_budget_state_defaults_to_empty(self) -> None:
with tempfile.TemporaryDirectory() as td:
directory = Path(td)
path = directory / 'bad-budget.json'
data = {
'session_id': 'bad-budget',
'model_config': {},
'runtime_config': {'cwd': '/'},
'system_prompt_parts': [],
'user_context': {},
'system_context': {},
'messages': [],
'turns': 0,
'tool_calls': 0,
'budget_state': 'not-a-dict',
'plugin_state': 123,
}
path.write_text(json.dumps(data))
loaded = load_agent_session('bad-budget', directory=directory)
self.assertEqual(loaded.budget_state, {})
self.assertEqual(loaded.plugin_state, {})
class TestSessionInputQueue(unittest.TestCase):
def test_consume_next_turn_uses_fifo_and_preserves_guidance(self) -> None:
with tempfile.TemporaryDirectory() as td:
directory = Path(td) / 'sessions'
directory.mkdir()
first = enqueue_session_input(
'session-1',
directory=directory,
content='first queued turn',
kind='next_turn',
)
guidance = enqueue_session_input(
'session-1',
directory=directory,
content='guide current run',
kind='guidance',
)
second = enqueue_session_input(
'session-1',
directory=directory,
content='second queued turn',
kind='next_turn',
)
consumed_first = consume_next_session_input(
'session-1',
directory=directory,
)
consumed_second = consume_next_session_input(
'session-1',
directory=directory,
)
consumed_none = consume_next_session_input(
'session-1',
directory=directory,
)
pending = list_session_input_queue('session-1', directory=directory)
self.assertIsNotNone(consumed_first)
self.assertIsNotNone(consumed_second)
assert consumed_first is not None
assert consumed_second is not None
self.assertEqual(consumed_first['id'], first['id'])
self.assertEqual(consumed_first['content'], 'first queued turn')
self.assertEqual(consumed_first['status'], 'consumed')
self.assertEqual(consumed_second['id'], second['id'])
self.assertEqual(consumed_second['content'], 'second queued turn')
self.assertIsNone(consumed_none)
self.assertEqual([item['id'] for item in pending], [guidance['id']])
self.assertEqual(pending[0]['kind'], 'guidance')
class TestModelConfigSerialization(unittest.TestCase):
"""serialize_model_config + deserialize_model_config round-trip."""
def test_round_trip_preserves_pricing(self) -> None:
pricing = ModelPricing(
input_cost_per_million_tokens_usd=3.0,
output_cost_per_million_tokens_usd=15.0,
cache_creation_input_cost_per_million_tokens_usd=1.5,
cache_read_input_cost_per_million_tokens_usd=0.5,
)
config = ModelConfig(
model='claude-3-sonnet',
base_url='https://api.example.com/v1',
api_key='sk-test-key',
temperature=0.7,
timeout_seconds=60.0,
pricing=pricing,
)
payload = serialize_model_config(config)
restored = deserialize_model_config(payload)
self.assertEqual(restored.model, config.model)
self.assertEqual(restored.base_url, config.base_url)
self.assertEqual(restored.api_key, config.api_key)
self.assertAlmostEqual(restored.temperature, config.temperature)
self.assertAlmostEqual(restored.timeout_seconds, config.timeout_seconds)
self.assertAlmostEqual(
restored.pricing.input_cost_per_million_tokens_usd,
pricing.input_cost_per_million_tokens_usd,
)
self.assertAlmostEqual(
restored.pricing.output_cost_per_million_tokens_usd,
pricing.output_cost_per_million_tokens_usd,
)
self.assertAlmostEqual(
restored.pricing.cache_creation_input_cost_per_million_tokens_usd,
pricing.cache_creation_input_cost_per_million_tokens_usd,
)
self.assertAlmostEqual(
restored.pricing.cache_read_input_cost_per_million_tokens_usd,
pricing.cache_read_input_cost_per_million_tokens_usd,
)
def test_deserialize_defaults_for_missing_fields(self) -> None:
payload = {'model': 'gpt-4'}
config = deserialize_model_config(payload)
self.assertEqual(config.model, 'gpt-4')
self.assertEqual(config.base_url, 'http://127.0.0.1:8000/v1')
self.assertEqual(config.api_key, 'local-token')
self.assertAlmostEqual(config.temperature, 0.0)
self.assertAlmostEqual(config.timeout_seconds, 120.0)
self.assertAlmostEqual(config.pricing.input_cost_per_million_tokens_usd, 0.0)
self.assertAlmostEqual(config.pricing.output_cost_per_million_tokens_usd, 0.0)
def test_deserialize_with_non_dict_pricing(self) -> None:
payload = {'model': 'test', 'pricing': 'invalid'}
config = deserialize_model_config(payload)
self.assertAlmostEqual(config.pricing.input_cost_per_million_tokens_usd, 0.0)
def test_deserialize_with_none_pricing(self) -> None:
payload = {'model': 'test', 'pricing': None}
config = deserialize_model_config(payload)
self.assertEqual(config.pricing, ModelPricing())
class TestRuntimeConfigSerialization(unittest.TestCase):
"""serialize_runtime_config + deserialize_runtime_config round-trip."""
def test_round_trip_preserves_all(self) -> None:
config = AgentRuntimeConfig(
cwd=Path('/home/user/project'),
max_turns=25,
command_timeout_seconds=45.0,
max_output_chars=8000,
stream_model_responses=True,
auto_snip_threshold_tokens=5000,
auto_compact_threshold_tokens=10000,
compact_preserve_messages=6,
permissions=AgentPermissions(
allow_file_write=True,
allow_shell_commands=True,
allow_destructive_shell_commands=False,
),
additional_working_directories=(Path('/extra/dir'),),
disable_claude_md_discovery=True,
budget_config=BudgetConfig(
max_total_tokens=100000,
max_input_tokens=50000,
max_output_tokens=30000,
max_reasoning_tokens=20000,
max_total_cost_usd=5.0,
max_tool_calls=100,
max_delegated_tasks=10,
max_model_calls=200,
max_session_turns=50,
),
output_schema=OutputSchemaConfig(
name='test_schema',
schema={'type': 'object', 'properties': {'answer': {'type': 'string'}}},
strict=True,
),
session_directory=Path('/sessions'),
scratchpad_root=Path('/scratch'),
python_env_dir=Path('/python/.venv'),
enabled_skill_names=('verify', 'product-data'),
)
payload = serialize_runtime_config(config)
restored = deserialize_runtime_config(payload)
self.assertEqual(restored.cwd, config.cwd.resolve())
self.assertEqual(restored.max_turns, 25)
self.assertAlmostEqual(restored.command_timeout_seconds, 45.0)
self.assertEqual(restored.max_output_chars, 8000)
self.assertTrue(restored.stream_model_responses)
self.assertEqual(restored.auto_snip_threshold_tokens, 5000)
self.assertEqual(restored.auto_compact_threshold_tokens, 10000)
self.assertEqual(restored.compact_preserve_messages, 6)
self.assertTrue(restored.permissions.allow_file_write)
self.assertTrue(restored.permissions.allow_shell_commands)
self.assertFalse(restored.permissions.allow_destructive_shell_commands)
self.assertTrue(restored.disable_claude_md_discovery)
self.assertEqual(restored.budget_config.max_total_tokens, 100000)
self.assertEqual(restored.budget_config.max_input_tokens, 50000)
self.assertEqual(restored.budget_config.max_output_tokens, 30000)
self.assertEqual(restored.budget_config.max_reasoning_tokens, 20000)
self.assertAlmostEqual(restored.budget_config.max_total_cost_usd, 5.0)
self.assertEqual(restored.budget_config.max_tool_calls, 100)
self.assertEqual(restored.budget_config.max_delegated_tasks, 10)
self.assertEqual(restored.budget_config.max_model_calls, 200)
self.assertEqual(restored.budget_config.max_session_turns, 50)
self.assertIsNotNone(restored.output_schema)
assert restored.output_schema is not None
self.assertEqual(restored.output_schema.name, 'test_schema')
self.assertEqual(restored.output_schema.schema, config.output_schema.schema)
self.assertTrue(restored.output_schema.strict)
self.assertEqual(restored.python_env_dir, Path('/python/.venv'))
self.assertEqual(restored.enabled_skill_names, ('verify', 'product-data'))
def test_round_trip_none_output_schema(self) -> None:
config = AgentRuntimeConfig(
cwd=Path('/home/user'),
output_schema=None,
)
payload = serialize_runtime_config(config)
restored = deserialize_runtime_config(payload)
self.assertIsNone(restored.output_schema)
def test_deserialize_defaults_for_missing_fields(self) -> None:
payload = {'cwd': '/home/user'}
config = deserialize_runtime_config(payload)
self.assertEqual(config.max_turns, 50)
self.assertAlmostEqual(config.command_timeout_seconds, 300.0)
self.assertEqual(config.max_output_chars, 50000)
self.assertFalse(config.stream_model_responses)
self.assertIsNone(config.auto_snip_threshold_tokens)
self.assertIsNone(config.auto_compact_threshold_tokens)
self.assertEqual(config.compact_preserve_messages, 4)
self.assertFalse(config.permissions.allow_file_write)
self.assertFalse(config.permissions.allow_shell_commands)
self.assertFalse(config.permissions.allow_destructive_shell_commands)
self.assertEqual(config.additional_working_directories, ())
self.assertFalse(config.disable_claude_md_discovery)
self.assertIsNone(config.budget_config.max_total_tokens)
self.assertIsNone(config.output_schema)
self.assertIsNone(config.enabled_skill_names)
def test_deserialize_non_dict_permissions(self) -> None:
payload = {'cwd': '/home', 'permissions': 'invalid'}
config = deserialize_runtime_config(payload)
self.assertFalse(config.permissions.allow_file_write)
def test_deserialize_non_dict_budget_config(self) -> None:
payload = {'cwd': '/home', 'budget_config': 42}
config = deserialize_runtime_config(payload)
self.assertIsNone(config.budget_config.max_total_tokens)
class TestUsageFromPayload(unittest.TestCase):
"""usage_from_payload correctly maps fields including defaults."""
def test_full_payload(self) -> None:
payload = {
'input_tokens': 1000,
'output_tokens': 500,
'cache_creation_input_tokens': 200,
'cache_read_input_tokens': 100,
'reasoning_tokens': 50,
}
usage = usage_from_payload(payload)
self.assertEqual(usage.input_tokens, 1000)
self.assertEqual(usage.output_tokens, 500)
self.assertEqual(usage.cache_creation_input_tokens, 200)
self.assertEqual(usage.cache_read_input_tokens, 100)
self.assertEqual(usage.reasoning_tokens, 50)
def test_partial_payload_uses_defaults(self) -> None:
payload = {'input_tokens': 42}
usage = usage_from_payload(payload)
self.assertEqual(usage.input_tokens, 42)
self.assertEqual(usage.output_tokens, 0)
self.assertEqual(usage.cache_creation_input_tokens, 0)
self.assertEqual(usage.cache_read_input_tokens, 0)
self.assertEqual(usage.reasoning_tokens, 0)
def test_none_returns_empty(self) -> None:
usage = usage_from_payload(None)
self.assertEqual(usage, UsageStats())
def test_empty_dict_returns_defaults(self) -> None:
usage = usage_from_payload({})
self.assertEqual(usage, UsageStats())
def test_non_dict_returns_empty(self) -> None:
usage = usage_from_payload('not a dict') # type: ignore[arg-type]
self.assertEqual(usage, UsageStats())
def test_string_token_values_parsed(self) -> None:
payload = {'input_tokens': '99', 'output_tokens': '77'}
usage = usage_from_payload(payload)
self.assertEqual(usage.input_tokens, 99)
self.assertEqual(usage.output_tokens, 77)
class TestOptionalInt(unittest.TestCase):
"""_optional_int handles int, str, float, None, bool correctly."""
def test_int_value(self) -> None:
self.assertEqual(_optional_int(42), 42)
def test_zero(self) -> None:
self.assertEqual(_optional_int(0), 0)
def test_negative(self) -> None:
self.assertEqual(_optional_int(-5), -5)
def test_str_numeric(self) -> None:
self.assertEqual(_optional_int('123'), 123)
def test_float_value(self) -> None:
self.assertEqual(_optional_int(3.9), 3)
def test_none_returns_none(self) -> None:
self.assertIsNone(_optional_int(None))
def test_bool_true_returns_none(self) -> None:
self.assertIsNone(_optional_int(True))
def test_bool_false_returns_none(self) -> None:
self.assertIsNone(_optional_int(False))
def test_non_numeric_string_returns_none(self) -> None:
self.assertIsNone(_optional_int('hello'))
def test_empty_string_returns_none(self) -> None:
self.assertIsNone(_optional_int(''))
class TestOptionalFloat(unittest.TestCase):
"""_optional_float handles int, str, float, None, bool correctly."""
def test_float_value(self) -> None:
self.assertAlmostEqual(_optional_float(3.14), 3.14)
def test_int_value(self) -> None:
self.assertAlmostEqual(_optional_float(42), 42.0)
def test_zero(self) -> None:
self.assertAlmostEqual(_optional_float(0), 0.0)
def test_str_numeric(self) -> None:
self.assertAlmostEqual(_optional_float('2.5'), 2.5)
def test_none_returns_none(self) -> None:
self.assertIsNone(_optional_float(None))
def test_bool_true_returns_none(self) -> None:
self.assertIsNone(_optional_float(True))
def test_bool_false_returns_none(self) -> None:
self.assertIsNone(_optional_float(False))
def test_non_numeric_string_returns_none(self) -> None:
self.assertIsNone(_optional_float('abc'))
def test_empty_string_returns_none(self) -> None:
self.assertIsNone(_optional_float(''))
class TestDeserializeOutputSchema(unittest.TestCase):
"""_deserialize_output_schema with valid, None, invalid data."""
def test_valid_payload(self) -> None:
payload = {
'name': 'my_schema',
'schema': {'type': 'object'},
'strict': True,
}
result = _deserialize_output_schema(payload)
self.assertIsNotNone(result)
assert result is not None
self.assertEqual(result.name, 'my_schema')
self.assertEqual(result.schema, {'type': 'object'})
self.assertTrue(result.strict)
def test_strict_defaults_false(self) -> None:
payload = {
'name': 'basic',
'schema': {'type': 'string'},
}
result = _deserialize_output_schema(payload)
self.assertIsNotNone(result)
assert result is not None
self.assertFalse(result.strict)
def test_none_payload(self) -> None:
self.assertIsNone(_deserialize_output_schema(None))
def test_non_dict_payload(self) -> None:
self.assertIsNone(_deserialize_output_schema('not a dict'))
self.assertIsNone(_deserialize_output_schema(42))
self.assertIsNone(_deserialize_output_schema([]))
def test_missing_schema_key(self) -> None:
self.assertIsNone(_deserialize_output_schema({'name': 'test'}))
def test_non_dict_schema(self) -> None:
self.assertIsNone(_deserialize_output_schema({'name': 'test', 'schema': 'bad'}))
def test_missing_name(self) -> None:
self.assertIsNone(_deserialize_output_schema({'schema': {'type': 'object'}}))
def test_empty_name(self) -> None:
self.assertIsNone(
_deserialize_output_schema({'name': '', 'schema': {'type': 'object'}})
)
def test_non_string_name(self) -> None:
self.assertIsNone(
_deserialize_output_schema({'name': 123, 'schema': {'type': 'object'}})
)
if __name__ == '__main__':
unittest.main()
-179
View File
@@ -1,179 +0,0 @@
"""Tests for settings-touching slash commands ported from the npm source.
Covers /theme, /voice, /sandbox-toggle (alias /sandbox), /keybindings, /btw.
"""
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from src.agent_runtime import LocalCodingAgent
from src.agent_types import AgentRuntimeConfig, ModelConfig
def _make_agent(tmp_dir: str) -> LocalCodingAgent:
return LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
def _local_settings(tmp_dir: str) -> dict:
path = Path(tmp_dir) / '.claude' / 'settings.local.json'
if not path.exists():
return {}
return json.loads(path.read_text(encoding='utf-8'))
class ThemeCommandTest(unittest.TestCase):
def test_lists_themes_when_no_arg(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/theme').final_output
self.assertIn('Available themes', out)
self.assertIn('light', out)
self.assertIn('dark', out)
self.assertIn('Usage: /theme <name>', out)
def test_rejects_unknown_theme(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/theme neon').final_output
self.assertIn('Unknown theme', out)
self.assertIn('neon', out)
def test_sets_theme_and_persists(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/theme dark').final_output
self.assertIn('Theme set to dark', out)
settings = _local_settings(tmp)
self.assertEqual(settings.get('theme'), 'dark')
def test_marks_current_theme(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
agent.run('/theme dark')
out = agent.run('/theme').final_output
self.assertIn('dark (current)', out)
class VoiceCommandTest(unittest.TestCase):
def test_toggle_enables_when_unset(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/voice').final_output
self.assertIn('Voice mode enabled', out)
self.assertEqual(_local_settings(tmp).get('voiceEnabled'), True)
def test_toggle_disables_when_enabled(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
agent.run('/voice on')
out = agent.run('/voice').final_output
self.assertIn('Voice mode disabled', out)
self.assertEqual(_local_settings(tmp).get('voiceEnabled'), False)
def test_explicit_on_off(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
self.assertIn('enabled', agent.run('/voice on').final_output)
self.assertIn('disabled', agent.run('/voice off').final_output)
def test_rejects_unknown_arg(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/voice maybe').final_output
self.assertIn('Usage', out)
class SandboxToggleCommandTest(unittest.TestCase):
def test_status_with_no_args(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/sandbox-toggle').final_output
self.assertIn('Sandbox:', out)
self.assertIn('Excluded commands', out)
self.assertIn('Usage:', out)
def test_alias_sandbox(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/sandbox').final_output
self.assertIn('Sandbox:', out)
def test_exclude_appends_pattern(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/sandbox-toggle exclude "npm run test:*"').final_output
self.assertIn('Added "npm run test:*"', out)
settings = _local_settings(tmp)
excluded = settings.get('sandbox', {}).get('excludedCommands', [])
self.assertIn('npm run test:*', excluded)
def test_exclude_dedupes(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
agent.run('/sandbox-toggle exclude "rm -rf /"')
out = agent.run('/sandbox-toggle exclude "rm -rf /"').final_output
self.assertIn('already in', out)
def test_exclude_requires_pattern(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/sandbox-toggle exclude').final_output
self.assertIn('Usage', out)
def test_unknown_subcommand(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/sandbox-toggle wat').final_output
self.assertIn('Unknown subcommand', out)
class KeybindingsCommandTest(unittest.TestCase):
def test_creates_template_when_missing(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/keybindings').final_output
path = Path(tmp) / '.claude' / 'keybindings.json'
self.assertTrue(path.exists())
self.assertIn('Created', out)
self.assertIn(str(path), out)
# Template is valid JSON-ish (has braces); strict json.loads would
# choke on the "//" comment, so just sanity-check structure.
text = path.read_text(encoding='utf-8')
self.assertIn('"bindings"', text)
def test_reports_existing_file(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
agent.run('/keybindings')
out = agent.run('/keybindings').final_output
self.assertIn('Found', out)
class BtwCommandTest(unittest.TestCase):
def test_no_question_shows_usage(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = agent.run('/btw').final_output
self.assertIn('Usage: /btw', result)
def test_question_returns_prompt_result(self) -> None:
from src.agent_slash_commands import preprocess_slash_command
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
result = preprocess_slash_command(agent, '/btw what does this codebase do?')
self.assertTrue(result.handled)
self.assertTrue(result.should_query)
self.assertIn('side question', (result.prompt or '').lower())
self.assertIn('what does this codebase do?', result.prompt or '')
if __name__ == '__main__':
unittest.main()
-64
View File
@@ -1,64 +0,0 @@
"""Tests for setup-time runtime checks ported from setup.ts."""
from __future__ import annotations
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from src.setup import (
MIN_PYTHON_VERSION,
SetupReport,
check_runtime_requirements,
run_setup,
)
class RuntimeRequirementCheckTest(unittest.TestCase):
def test_python_check_passes_on_current_runtime(self) -> None:
checks = {check.name: check for check in check_runtime_requirements()}
self.assertTrue(checks['python_version'].ok)
self.assertGreaterEqual(sys.version_info[:2], MIN_PYTHON_VERSION)
def test_python_check_fails_when_below_minimum(self) -> None:
# Force a lower version_info via patching to verify the failure branch.
fake_version = mock.Mock()
fake_version.__getitem__ = lambda self, idx: (3, 8)[idx] if isinstance(idx, int) else (3, 8)[idx]
with mock.patch('src.setup.sys') as fake_sys:
fake_sys.version_info = (3, 8, 0)
checks = {check.name: check for check in check_runtime_requirements()}
self.assertFalse(checks['python_version'].ok)
self.assertIn('below required', checks['python_version'].detail)
def test_includes_platform_and_implementation(self) -> None:
names = {check.name for check in check_runtime_requirements()}
self.assertIn('python_implementation', names)
self.assertIn('platform', names)
class SetupReportTest(unittest.TestCase):
def test_run_setup_reports_runtime_and_release_notes(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / 'CHANGELOG.md').write_text(
'# Changelog\n\n## 9.9.9\n- big release\n', encoding='utf-8',
)
report = run_setup(cwd=Path(tmp), trusted=True, last_seen_version='0.0.1')
self.assertIsInstance(report, SetupReport)
self.assertGreaterEqual(len(report.runtime_checks), 3)
self.assertIn('big release', report.release_notes)
markdown = report.as_markdown()
self.assertIn('Runtime checks', markdown)
self.assertIn('Release notes', markdown)
self.assertFalse(report.has_blocking_issues())
def test_no_release_notes_when_changelog_missing(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
report = run_setup(cwd=Path(tmp), trusted=True)
self.assertEqual(report.release_notes, ())
self.assertNotIn('Release notes', report.as_markdown())
if __name__ == '__main__':
unittest.main()
-138
View File
@@ -1,138 +0,0 @@
"""Tests for the bundled small utilities ported in ``src/small_utils.py``."""
from __future__ import annotations
import unittest
from src.small_utils import (
count,
create_agent_id,
difference,
escape_xml,
escape_xml_attr,
every_in,
intersects,
intersperse,
object_group_by,
union,
uniq,
validate_uuid,
)
class IntersperseTest(unittest.TestCase):
def test_empty(self) -> None:
self.assertEqual(intersperse([], lambda i: ','), [])
def test_single_item_no_separator(self) -> None:
self.assertEqual(intersperse(['a'], lambda i: ','), ['a'])
def test_separator_receives_index_starting_at_one(self) -> None:
seen: list[int] = []
def sep(i: int) -> str:
seen.append(i)
return f'-{i}-'
out = intersperse(['a', 'b', 'c'], sep)
self.assertEqual(out, ['a', '-1-', 'b', '-2-', 'c'])
self.assertEqual(seen, [1, 2])
class CountTest(unittest.TestCase):
def test_counts_truthy(self) -> None:
self.assertEqual(count([1, 2, 3, 4], lambda x: x % 2 == 0), 2)
def test_predicate_returning_objects_treated_as_truthy(self) -> None:
self.assertEqual(count(['', 'x', 'y'], lambda s: s), 2)
class UniqTest(unittest.TestCase):
def test_preserves_first_seen_order(self) -> None:
self.assertEqual(uniq([3, 1, 2, 1, 3, 4]), [3, 1, 2, 4])
class ObjectGroupByTest(unittest.TestCase):
def test_groups_by_key(self) -> None:
out = object_group_by(['apple', 'banana', 'avocado'], lambda s, _i: s[0])
self.assertEqual(out, {'a': ['apple', 'avocado'], 'b': ['banana']})
def test_passes_index_to_selector(self) -> None:
out = object_group_by(
['x', 'y', 'z'], lambda _s, i: 'even' if i % 2 == 0 else 'odd',
)
self.assertEqual(out, {'even': ['x', 'z'], 'odd': ['y']})
class SetOpsTest(unittest.TestCase):
def test_difference(self) -> None:
self.assertEqual(difference({1, 2, 3}, {2}), {1, 3})
def test_intersects_true(self) -> None:
self.assertTrue(intersects({1, 2}, {2, 3}))
def test_intersects_false(self) -> None:
self.assertFalse(intersects({1, 2}, {3, 4}))
def test_intersects_empty_short_circuits(self) -> None:
self.assertFalse(intersects(set(), {1}))
self.assertFalse(intersects({1}, set()))
def test_every_in(self) -> None:
self.assertTrue(every_in({1, 2}, {1, 2, 3}))
self.assertFalse(every_in({1, 4}, {1, 2, 3}))
self.assertTrue(every_in(set(), {1, 2}))
def test_union(self) -> None:
self.assertEqual(union({1, 2}, {2, 3}), {1, 2, 3})
class XmlEscapeTest(unittest.TestCase):
def test_escape_xml(self) -> None:
self.assertEqual(
escape_xml('a & b < c > d'), 'a &amp; b &lt; c &gt; d',
)
def test_escape_xml_amp_first_no_double_escape(self) -> None:
self.assertEqual(escape_xml('<&>'), '&lt;&amp;&gt;')
def test_escape_xml_attr_includes_quotes(self) -> None:
self.assertEqual(
escape_xml_attr('he said "hi" & \'bye\''),
'he said &quot;hi&quot; &amp; &apos;bye&apos;',
)
class ValidateUuidTest(unittest.TestCase):
def test_valid_lowercase(self) -> None:
u = '12345678-1234-1234-1234-123456789012'
self.assertEqual(validate_uuid(u), u)
def test_valid_uppercase(self) -> None:
u = 'ABCDEF12-1234-5678-90AB-CDEF12345678'
self.assertEqual(validate_uuid(u), u)
def test_invalid_format(self) -> None:
self.assertIsNone(validate_uuid('not-a-uuid'))
def test_non_string_returns_none(self) -> None:
self.assertIsNone(validate_uuid(123))
self.assertIsNone(validate_uuid(None))
class CreateAgentIdTest(unittest.TestCase):
def test_no_label(self) -> None:
agent_id = create_agent_id()
self.assertRegex(agent_id, r'^a[0-9a-f]{16}$')
def test_with_label(self) -> None:
agent_id = create_agent_id('compact')
self.assertRegex(agent_id, r'^acompact-[0-9a-f]{16}$')
def test_unique_across_calls(self) -> None:
ids = {create_agent_id() for _ in range(50)}
self.assertEqual(len(ids), 50)
if __name__ == '__main__':
unittest.main()
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from agent_platform.store import RuntimeStore
async def test_conversation_mode_upgrade_is_one_way(settings) -> None:
store = RuntimeStore(settings.database_url)
await store.initialize()
try:
assert await store.select_mode("u1", "c1", "chat") == "chat"
assert await store.select_mode("u1", "c1", "work") == "work"
assert await store.select_mode("u1", "c1", "chat") == "work"
assert await store.select_mode("u2", "c1", "chat") == "chat"
finally:
await store.close()
async def test_memory_and_events_are_user_scoped(settings) -> None:
store = RuntimeStore(settings.database_url)
await store.initialize()
try:
memory_id = await store.remember("u1", "prefers compact answers")
assert [item["id"] for item in await store.recall("u1", "compact")] == [memory_id]
assert await store.recall("u2", "compact") == []
await store.append_event("r1", "u1", "c1", 1, "run.created", {"x": 1})
await store.append_event("r2", "u2", "c1", 1, "run.created", {"x": 2})
assert [event["payload"]["x"] for event in await store.events_for_chat("u1", "c1")] == [1]
finally:
await store.close()
-249
View File
@@ -1,249 +0,0 @@
from __future__ import annotations
import json
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.task_runtime import TaskRuntime
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
class TaskRuntimeTests(unittest.TestCase):
def test_runtime_persists_and_renders_tasks(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = TaskRuntime.from_workspace(workspace)
created = runtime.create_task(
title='Implement task runtime',
description='Add persistent tasks.',
status='in_progress',
)
assert created.task is not None
runtime.update_task(created.task.task_id, status='completed')
rendered_tasks = runtime.render_tasks()
rendered_task = runtime.render_task(created.task.task_id)
self.assertIn('Implement task runtime', rendered_tasks)
self.assertIn('completed', rendered_task)
def test_task_tools_execute_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = TaskRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
task_runtime=runtime,
)
create_result = execute_tool(
default_tool_registry(),
'task_create',
{'title': 'Review task tools', 'status': 'pending'},
context,
)
self.assertTrue(create_result.ok)
task_id = str(create_result.metadata.get('task_id'))
next_result = execute_tool(
default_tool_registry(),
'task_next',
{},
context,
)
list_result = execute_tool(
default_tool_registry(),
'task_list',
{},
context,
)
get_result = execute_tool(
default_tool_registry(),
'task_get',
{'task_id': task_id},
context,
)
update_result = execute_tool(
default_tool_registry(),
'task_update',
{'task_id': task_id, 'status': 'completed'},
context,
)
todo_result = execute_tool(
default_tool_registry(),
'todo_write',
{'items': [{'title': 'Replace with todo snapshot', 'status': 'in_progress'}]},
context,
)
self.assertIn('Review task tools', next_result.content)
self.assertIn(task_id, list_result.content)
self.assertIn('Review task tools', get_result.content)
self.assertTrue(update_result.ok)
self.assertEqual(update_result.metadata.get('task_status'), 'completed')
self.assertTrue(todo_result.ok)
self.assertEqual(todo_result.metadata.get('total_tasks'), 1)
def test_next_tasks_respects_dependencies_and_completion(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = TaskRuntime.from_workspace(workspace)
runtime.replace_tasks(
[
{'task_id': 'scan', 'title': 'Scan workspace', 'status': 'pending'},
{
'task_id': 'patch',
'title': 'Patch files',
'status': 'blocked',
'blocked_by': ['scan'],
},
]
)
first_next = runtime.render_next_tasks()
runtime.complete_task('scan')
second_next = runtime.render_next_tasks()
self.assertIn('Scan workspace', first_next)
self.assertNotIn('Patch files', first_next)
self.assertIn('Patch files', second_next)
def test_task_execution_tools_handle_block_start_and_complete(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = TaskRuntime.from_workspace(workspace)
runtime.replace_tasks(
[
{'task_id': 'scan', 'title': 'Scan workspace', 'status': 'pending'},
{
'task_id': 'patch',
'title': 'Patch files',
'status': 'blocked',
'blocked_by': ['scan'],
},
]
)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
task_runtime=runtime,
)
blocked_start = execute_tool(
default_tool_registry(),
'task_start',
{'task_id': 'patch'},
context,
)
complete_scan = execute_tool(
default_tool_registry(),
'task_complete',
{'task_id': 'scan'},
context,
)
start_patch = execute_tool(
default_tool_registry(),
'task_start',
{'task_id': 'patch', 'owner': 'agent_1', 'active_form': 'Patching files'},
context,
)
self.assertTrue(blocked_start.ok)
self.assertIn('[blocked]', blocked_start.content)
self.assertTrue(complete_scan.ok)
self.assertTrue(start_patch.ok)
self.assertIn('[in_progress]', start_patch.content)
self.assertEqual(runtime.get_task('patch').owner, 'agent_1')
def test_agent_can_use_task_tools_in_model_loop(self) -> None:
responses = [
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'I will create a task first.',
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'task_create',
'arguments': '{"title": "Review runtime tasks", "status": "pending"}',
},
}
],
},
'finish_reason': 'tool_calls',
}
],
'usage': {'prompt_tokens': 8, 'completion_tokens': 3},
},
{
'choices': [
{
'message': {
'role': 'assistant',
'content': 'The task was created successfully.',
},
'finish_reason': 'stop',
}
],
'usage': {'prompt_tokens': 6, 'completion_tokens': 3},
},
]
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
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',
base_url='http://127.0.0.1:8000/v1',
),
runtime_config=AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
)
result = agent.run('Create a task for the current work')
self.assertIsNotNone(result.scratchpad_directory)
self.assertTrue(
(Path(result.scratchpad_directory) / 'task_runtime.json').exists()
)
self.assertFalse((workspace / '.port_sessions' / 'task_runtime.json').exists())
self.assertEqual(result.final_output, 'The task was created successfully.')
self.assertEqual(result.tool_calls, 1)
tool_message = next(
message
for message in result.transcript
if message.get('role') == 'tool'
)
self.assertIn('task_create', tool_message.get('content', ''))
-76
View File
@@ -1,76 +0,0 @@
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.team_runtime import TeamRuntime
class TeamRuntimeTests(unittest.TestCase):
def test_team_runtime_persists_team_and_messages(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = TeamRuntime.from_workspace(workspace)
runtime.create_team('reviewers', members=['alice', 'bob'])
runtime.send_message(
team_name='reviewers',
text='Please review the patch.',
sender='agent',
recipient='alice',
)
restored = TeamRuntime.from_workspace(workspace)
self.assertEqual(len(restored.teams), 1)
self.assertEqual(restored.teams[0].name, 'reviewers')
self.assertEqual(len(restored.messages), 1)
self.assertIn('Please review the patch.', restored.render_messages())
def test_team_tools_execute_against_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
workspace = Path(tmp_dir)
runtime = TeamRuntime.from_workspace(workspace)
context = build_tool_context(
AgentRuntimeConfig(
cwd=workspace,
permissions=AgentPermissions(allow_file_write=True),
),
team_runtime=runtime,
)
create_result = execute_tool(
default_tool_registry(),
'team_create',
{'team_name': 'reviewers', 'members': ['alice', 'bob']},
context,
)
send_result = execute_tool(
default_tool_registry(),
'send_message',
{
'team_name': 'reviewers',
'message': 'Check notebook changes',
'sender': 'agent',
},
context,
)
list_result = execute_tool(
default_tool_registry(),
'team_list',
{},
context,
)
message_result = execute_tool(
default_tool_registry(),
'team_messages',
{'team_name': 'reviewers'},
context,
)
self.assertTrue(create_result.ok)
self.assertEqual(create_result.metadata.get('action'), 'team_create')
self.assertTrue(send_result.ok)
self.assertEqual(send_result.metadata.get('action'), 'send_message')
self.assertIn('reviewers', list_result.content)
self.assertIn('Check notebook changes', message_result.content)
-186
View File
@@ -1,186 +0,0 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from benchmarks.run_terminal_bench_local import (
TerminalBenchTask,
build_host_agent_command,
build_verifier_exec_command,
discover_tasks,
filter_tasks,
parse_dockerfile_workdir,
strip_canary,
)
class TerminalBenchLocalTests(unittest.TestCase):
def test_strip_canary_removes_leading_markers(self) -> None:
raw = "<!-- canary -->\n# canary line\n\nactual instruction\n"
self.assertEqual(strip_canary(raw), "actual instruction")
def test_parse_dockerfile_workdir_uses_last_workdir(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
dockerfile = Path(tmp_dir) / "Dockerfile"
dockerfile.write_text(
"FROM python:3.11\nWORKDIR /repo\nRUN echo hi\nWORKDIR /repo/app\n",
encoding="utf-8",
)
self.assertEqual(parse_dockerfile_workdir(dockerfile), "/repo/app")
def test_parse_dockerfile_workdir_defaults_when_missing(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
dockerfile = Path(tmp_dir) / "Dockerfile"
dockerfile.write_text("FROM ubuntu:22.04\n", encoding="utf-8")
self.assertEqual(parse_dockerfile_workdir(dockerfile), "/workspace")
def test_discover_tasks_and_filter(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
task_a = root / "task-a"
task_a.mkdir()
(task_a / "instruction.md").write_text("solve a\n", encoding="utf-8")
(task_a / "task.toml").write_text(
"""
schema_version = "1.1"
[task]
name = "terminal-bench/headless-terminal"
description = "demo"
[environment]
docker_image = "example/demo:latest"
""".strip()
+ "\n",
encoding="utf-8",
)
env_a = task_a / "environment"
env_a.mkdir()
(env_a / "Dockerfile").write_text("FROM ubuntu\nWORKDIR /work\n", encoding="utf-8")
task_b = root / "task-b"
task_b.mkdir()
(task_b / "instruction.md").write_text("solve b\n", encoding="utf-8")
(task_b / "task.toml").write_text(
"""
schema_version = "1.1"
[task]
name = "terminal-bench/other-task"
description = "demo"
[environment]
docker_image = "example/other:latest"
""".strip()
+ "\n",
encoding="utf-8",
)
env_b = task_b / "environment"
env_b.mkdir()
(env_b / "docker-compose.yaml").write_text("services: {}\n", encoding="utf-8")
tasks = discover_tasks(root)
self.assertEqual(len(tasks), 2)
selected = filter_tasks(
tasks,
include_patterns=["headless-*"],
exclude_patterns=[],
limit=None,
)
self.assertEqual(len(selected), 1)
self.assertEqual(selected[0].short_name, "headless-terminal")
self.assertFalse(selected[0].has_docker_compose)
selected = filter_tasks(
tasks,
include_patterns=[],
exclude_patterns=["other-*"],
limit=None,
)
self.assertEqual(len(selected), 1)
self.assertEqual(selected[0].short_name, "headless-terminal")
def test_build_host_agent_command_uses_current_interpreter(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
workspace_dir = root / "workspace"
repo_dir = root / "repo"
agent_logs_dir = root / "agent"
workspace_dir.mkdir()
repo_dir.mkdir()
agent_logs_dir.mkdir()
task = TerminalBenchTask(
task_dir=root,
name="terminal-bench/demo",
short_name="demo",
instruction="solve it",
docker_image="example/demo:latest",
agent_timeout_sec=30.0,
verifier_timeout_sec=30.0,
workdir="/workspace",
has_docker_compose=False,
)
cmd = build_host_agent_command(
task=task,
workspace_dir=workspace_dir,
repo_dir=repo_dir,
agent_logs_dir=agent_logs_dir,
)
self.assertIn(" -m src.main agent ", cmd)
self.assertIn("instruction=$(cat", cmd)
self.assertNotIn("claw-code-agent agent", cmd)
def test_build_verifier_command_fakeroot_adds_flags(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
workspace_dir = root / "workspace"
verifier_logs_dir = root / "verifier"
task_dir = root / "task"
tests_dir = task_dir / "tests"
workspace_dir.mkdir()
verifier_logs_dir.mkdir()
tests_dir.mkdir(parents=True)
image_path = root / "image.sif"
image_path.write_text("fake", encoding="utf-8")
task = TerminalBenchTask(
task_dir=task_dir,
name="terminal-bench/demo",
short_name="demo",
instruction="solve it",
docker_image="example/demo:latest",
agent_timeout_sec=30.0,
verifier_timeout_sec=30.0,
workdir="/workspace",
has_docker_compose=False,
)
cmd_no_fakeroot = build_verifier_exec_command(
task=task,
image_path=image_path,
workspace_dir=workspace_dir,
task_dir=task_dir,
verifier_logs_dir=verifier_logs_dir,
env={},
fakeroot=False,
)
self.assertNotIn("--fakeroot", cmd_no_fakeroot)
self.assertNotIn("--writable-tmpfs", cmd_no_fakeroot)
cmd_fakeroot = build_verifier_exec_command(
task=task,
image_path=image_path,
workspace_dir=workspace_dir,
task_dir=task_dir,
verifier_logs_dir=verifier_logs_dir,
env={},
fakeroot=True,
)
self.assertIn("--fakeroot", cmd_fakeroot)
self.assertIn("--writable-tmpfs", cmd_fakeroot)
self.assertIn("--contain", cmd_fakeroot)
self.assertIn("TMPDIR", cmd_fakeroot)
self.assertIn("CURL_CA_BUNDLE", cmd_fakeroot)
if __name__ == "__main__":
unittest.main()
-100
View File
@@ -1,100 +0,0 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from src.agent_session import AgentSessionState
from src.agent_context_usage import ContextUsageReport, MessageBreakdown
from src.agent_types import BudgetConfig
from src.token_budget import calculate_token_budget, format_token_budget
class TokenBudgetTests(unittest.TestCase):
def test_calculate_token_budget_reports_soft_and_hard_limits(self) -> None:
session = AgentSessionState.create(
['# System\nYou are helpful.'],
'Inspect the repository and summarize the current implementation status.',
user_context={'currentDate': "Today's date is 2026-04-11."},
system_context={'gitStatus': 'Current branch: main'},
)
session.append_assistant('Reading files and checking runtime state.')
fake_usage = ContextUsageReport(
model='test-model',
total_tokens=200,
raw_max_tokens=128_000,
percentage=0.15,
strategy='token_budget',
message_count=len(session.messages),
categories=(),
system_prompt_sections=(),
user_context_entries=(),
system_context_entries=(),
memory_files=(),
message_breakdown=MessageBreakdown(
user_message_tokens=50,
assistant_message_tokens=50,
tool_call_tokens=0,
tool_result_tokens=0,
user_context_tokens=10,
tool_calls_by_type=(),
),
token_counter_backend='heuristic',
token_counter_source='test',
token_counter_accurate=False,
)
with patch('src.token_budget.collect_context_usage', return_value=fake_usage):
snapshot = calculate_token_budget(
session=session,
model='test-model',
budget_config=BudgetConfig(),
)
rendered = format_token_budget(snapshot)
self.assertGreater(snapshot.projected_input_tokens, 0)
self.assertGreater(snapshot.hard_input_limit_tokens, snapshot.soft_input_limit_tokens)
self.assertGreater(snapshot.chat_overhead_tokens, 0)
self.assertIn('# Token Budget', rendered)
self.assertIn('Hard input limit', rendered)
self.assertIn('Auto-compact buffer', rendered)
def test_calculate_token_budget_honors_explicit_max_input_tokens(self) -> None:
session = AgentSessionState.create(
['# System\nYou are helpful.'],
'This prompt is deliberately longer than the tiny configured input budget. ' * 4,
)
fake_usage = ContextUsageReport(
model='test-model',
total_tokens=120,
raw_max_tokens=128_000,
percentage=0.09,
strategy='token_budget',
message_count=len(session.messages),
categories=(),
system_prompt_sections=(),
user_context_entries=(),
system_context_entries=(),
memory_files=(),
message_breakdown=MessageBreakdown(
user_message_tokens=80,
assistant_message_tokens=0,
tool_call_tokens=0,
tool_result_tokens=0,
user_context_tokens=0,
tool_calls_by_type=(),
),
token_counter_backend='heuristic',
token_counter_source='test',
token_counter_accurate=False,
)
with patch('src.token_budget.collect_context_usage', return_value=fake_usage):
snapshot = calculate_token_budget(
session=session,
model='test-model',
budget_config=BudgetConfig(max_input_tokens=20),
)
self.assertTrue(snapshot.exceeds_hard_limit)
self.assertGreater(snapshot.overflow_tokens, 0)
self.assertLessEqual(snapshot.hard_input_limit_tokens, 20)
-67
View File
@@ -1,67 +0,0 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from src.tokenizer_runtime import (
ResolvedTokenCounter,
TokenCounterInfo,
clear_token_counter_cache,
count_tokens,
describe_token_counter,
resolve_token_counter,
)
class TokenizerRuntimeTests(unittest.TestCase):
def tearDown(self) -> None:
clear_token_counter_cache()
def test_gpt_models_prefer_tiktoken_backend_when_available(self) -> None:
fake_counter = ResolvedTokenCounter(
info=TokenCounterInfo(
backend='tiktoken',
source='o200k_base',
accurate=True,
),
count_text=lambda text: len(text.split()),
)
with patch('src.tokenizer_runtime._try_build_tiktoken_counter', return_value=fake_counter):
with patch('src.tokenizer_runtime._try_build_transformers_counter', return_value=None):
info = describe_token_counter('gpt-4o-mini')
token_count = count_tokens('hello world from claw code', 'gpt-4o-mini')
self.assertEqual(info.backend, 'tiktoken')
self.assertTrue(info.accurate)
self.assertEqual(token_count, 5)
def test_transformers_backend_can_be_selected_with_env_override(self) -> None:
fake_counter = ResolvedTokenCounter(
info=TokenCounterInfo(
backend='transformers',
source='/tmp/fake-tokenizer (local_files_only)',
accurate=True,
),
count_text=lambda text: len(text.split()),
)
with patch.dict(
'os.environ',
{'CLAW_CODE_TOKENIZER_PATH': '/tmp/fake-tokenizer'},
clear=False,
):
with patch('src.tokenizer_runtime._try_build_transformers_counter', return_value=fake_counter):
info = describe_token_counter('Qwen/Qwen3-Coder-30B-A3B-Instruct')
token_count = count_tokens('one two three', 'Qwen/Qwen3-Coder-30B-A3B-Instruct')
self.assertEqual(info.backend, 'transformers')
self.assertTrue(info.accurate)
self.assertEqual(token_count, 3)
def test_fallback_backend_is_used_when_all_tokenizers_fail(self) -> None:
with patch('src.tokenizer_runtime._try_build_tiktoken_counter', return_value=None):
with patch('src.tokenizer_runtime._try_build_transformers_counter', return_value=None):
counter = resolve_token_counter('unknown-model')
token_count = count_tokens('abcd' * 5, 'unknown-model')
self.assertEqual(counter.info.backend, 'heuristic')
self.assertFalse(counter.info.accurate)
self.assertGreater(token_count, 0)
-106
View File
@@ -1,106 +0,0 @@
from __future__ import annotations
import unittest
from src.models import PortingModule
from src.permissions import ToolPermissionContext
from src.tool_pool import ToolPool, assemble_tool_pool
class TestAssembleToolPool(unittest.TestCase):
def test_returns_tool_pool_with_tools(self) -> None:
pool = assemble_tool_pool()
self.assertIsInstance(pool, ToolPool)
self.assertIsInstance(pool.tools, tuple)
self.assertTrue(all(isinstance(t, PortingModule) for t in pool.tools))
def test_default_mode_includes_tools(self) -> None:
pool = assemble_tool_pool()
self.assertGreater(len(pool.tools), 0)
def test_simple_mode_flag_stored(self) -> None:
pool_default = assemble_tool_pool()
pool_simple = assemble_tool_pool(simple_mode=True)
self.assertFalse(pool_default.simple_mode)
self.assertTrue(pool_simple.simple_mode)
def test_include_mcp_flag_stored(self) -> None:
pool_default = assemble_tool_pool()
pool_no_mcp = assemble_tool_pool(include_mcp=False)
self.assertTrue(pool_default.include_mcp)
self.assertFalse(pool_no_mcp.include_mcp)
def test_simple_mode_reduces_tools(self) -> None:
pool_full = assemble_tool_pool(simple_mode=False)
pool_simple = assemble_tool_pool(simple_mode=True)
self.assertGreater(len(pool_full.tools), len(pool_simple.tools))
simple_names = {t.name for t in pool_simple.tools}
self.assertTrue(simple_names.issubset({'BashTool', 'FileReadTool', 'FileEditTool'}))
def test_include_mcp_false_excludes_mcp_tools(self) -> None:
pool = assemble_tool_pool(include_mcp=False)
for tool in pool.tools:
self.assertNotIn('mcp', tool.name.lower())
self.assertNotIn('mcp', tool.source_hint.lower())
def test_permission_context_filters_blocked_tools(self) -> None:
ctx = ToolPermissionContext.from_iterables(deny_names=['BashTool'])
pool = assemble_tool_pool(permission_context=ctx)
tool_names = {t.name for t in pool.tools}
self.assertNotIn('BashTool', tool_names)
pool_unfiltered = assemble_tool_pool()
unfiltered_names = {t.name for t in pool_unfiltered.tools}
self.assertIn('BashTool', unfiltered_names)
class TestToolPoolAsMarkdown(unittest.TestCase):
def test_includes_header_and_tool_count(self) -> None:
pool = assemble_tool_pool()
md = pool.as_markdown()
self.assertIn('# Tool Pool', md)
self.assertIn(f'Tool count: {len(pool.tools)}', md)
def test_includes_mode_flags(self) -> None:
pool = assemble_tool_pool(simple_mode=True, include_mcp=False)
md = pool.as_markdown()
self.assertIn('Simple mode: True', md)
self.assertIn('Include MCP: False', md)
def test_shows_at_most_15_tools(self) -> None:
pool = assemble_tool_pool()
self.assertGreater(len(pool.tools), 15, 'Need >15 tools for this test')
md = pool.as_markdown()
tool_lines = [line for line in md.splitlines() if line.startswith('- ')]
self.assertEqual(len(tool_lines), 15)
def test_empty_tools_renders_correctly(self) -> None:
pool = ToolPool(tools=(), simple_mode=False, include_mcp=True)
md = pool.as_markdown()
self.assertIn('# Tool Pool', md)
self.assertIn('Tool count: 0', md)
self.assertIn('Simple mode: False', md)
self.assertIn('Include MCP: True', md)
tool_lines = [line for line in md.splitlines() if line.startswith('- ')]
self.assertEqual(len(tool_lines), 0)
def test_tool_lines_contain_name_and_source_hint(self) -> None:
tool = PortingModule(
name='TestTool',
responsibility='testing',
source_hint='test/path.ts',
)
pool = ToolPool(tools=(tool,), simple_mode=False, include_mcp=False)
md = pool.as_markdown()
self.assertIn('- TestTool — test/path.ts', md)
class TestToolPoolFrozen(unittest.TestCase):
def test_cannot_mutate_fields(self) -> None:
pool = ToolPool(tools=(), simple_mode=False, include_mcp=True)
with self.assertRaises(AttributeError):
pool.simple_mode = True # type: ignore[misc]
if __name__ == '__main__':
unittest.main()
-77
View File
@@ -1,77 +0,0 @@
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)
-187
View File
@@ -1,187 +0,0 @@
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())