Improve WebUI streaming and Python tooling

This commit is contained in:
武阳
2026-05-06 20:37:17 +08:00
parent a0da3c2423
commit 9ca675be0d
13 changed files with 930 additions and 147 deletions
+51
View File
@@ -74,6 +74,35 @@ class GuiServerTests(unittest.TestCase):
self.assertEqual(payload['account_id'], 'alice')
self.assertIn('/accounts/alice/sessions', payload['session_directory'])
self.assertIn('/accounts/alice/sessions', payload['upload_directory'])
self.assertIn('/accounts/alice/python/.venv', payload['python_env_directory'])
self.assertTrue((Path(payload['python_env_directory']) / 'bin' / 'python').exists())
def test_state_update_is_scoped_to_account(self) -> None:
with tempfile.TemporaryDirectory() as d:
client, _ = _build_client(Path(d))
updated = client.post(
'/api/state',
json={'account_id': 'alice', 'allow_shell': True, 'model': 'alice-model'},
)
self.assertEqual(updated.status_code, 200)
self.assertTrue(updated.json()['allow_shell'])
self.assertEqual(updated.json()['model'], 'alice-model')
alice = client.get('/api/state', params={'account_id': 'alice'}).json()
bob = client.get('/api/state', params={'account_id': 'bob'}).json()
default = client.get('/api/state').json()
self.assertEqual(alice['model'], 'alice-model')
self.assertTrue(alice['allow_shell'])
self.assertEqual(bob['model'], 'test-model')
self.assertFalse(bob['allow_shell'])
self.assertEqual(default['model'], 'test-model')
self.assertFalse(default['allow_shell'])
def test_run_locks_are_scoped_to_account(self) -> None:
with tempfile.TemporaryDirectory() as d:
_, state = _build_client(Path(d))
self.assertIs(state.run_lock_for('alice'), state.run_lock_for('alice'))
self.assertIsNot(state.run_lock_for('alice'), state.run_lock_for('bob'))
def test_state_update_rejects_missing_cwd(self) -> None:
with tempfile.TemporaryDirectory() as d:
@@ -132,6 +161,28 @@ class GuiServerTests(unittest.TestCase):
payload = response.json()
self.assertIsNone(payload['session_id'])
def test_chat_uses_account_scoped_model_config(self) -> None:
with tempfile.TemporaryDirectory() as d:
client, _ = _build_client(Path(d))
client.post(
'/api/state',
json={'account_id': 'alice', 'model': 'alice-model'},
)
response = client.post(
'/api/chat',
json={'prompt': '/status', 'account_id': 'alice'},
)
self.assertEqual(response.status_code, 200)
self.assertIn('- Model: alice-model', response.json()['final_output'])
bob = client.post(
'/api/chat',
json={'prompt': '/status', 'account_id': 'bob'},
)
self.assertEqual(bob.status_code, 200)
self.assertIn('- Model: test-model', bob.json()['final_output'])
def test_agent_session_store_uses_session_subdirectory(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d)
+92 -1
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
from unittest import TestCase
@@ -29,6 +30,97 @@ class PythonExecToolTests(TestCase):
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_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))
@@ -67,4 +159,3 @@ class PythonExecToolTests(TestCase):
self.assertTrue(result.ok)
self.assertIn('a|b', result.content)
self.assertEqual(result.metadata.get('mode'), 'script')
+2
View File
@@ -312,6 +312,7 @@ class TestRuntimeConfigSerialization(unittest.TestCase):
),
session_directory=Path('/sessions'),
scratchpad_root=Path('/scratch'),
python_env_dir=Path('/python/.venv'),
)
payload = serialize_runtime_config(config)
restored = deserialize_runtime_config(payload)
@@ -344,6 +345,7 @@ class TestRuntimeConfigSerialization(unittest.TestCase):
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'))
def test_round_trip_none_output_schema(self) -> None:
config = AgentRuntimeConfig(