Add bounded MCP text input for Sprinter programs
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Живой MCP-прогон: строка с Shift и Enter проходит через DSS gets()."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from mcp import Client, StdioServerParameters
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
async def probe() -> None:
|
||||
home = Path(os.environ['MAME_HOME']).resolve()
|
||||
binary = Path(os.environ.get('MAME_BIN', home / 'sprinter')).resolve()
|
||||
package = ROOT / 'tests/gets/.sprinter-cc-gets'
|
||||
with tempfile.TemporaryDirectory(prefix='sprinter-type-mcp-', dir='/tmp') as temp:
|
||||
socket = str(Path(temp) / 'session.sock')
|
||||
parameters = StdioServerParameters(
|
||||
command=sys.executable,
|
||||
args=[str(ROOT / 'toolchain/sdbg_mcp.py'), '--build', str(package),
|
||||
'--socket', socket, '--mame-home', str(home),
|
||||
'--mame-bin', str(binary)])
|
||||
async with Client(parameters) as client:
|
||||
async def call(name: str, arguments: dict | None = None) -> dict:
|
||||
result = await client.call_tool(name, arguments or {})
|
||||
if result.is_error or result.structured_content is None:
|
||||
raise RuntimeError(name + ': ' + repr(result.content))
|
||||
return result.structured_content
|
||||
|
||||
names = {tool.name for tool in (await client.list_tools()).tools}
|
||||
if 'type_string' not in names:
|
||||
raise RuntimeError('MCP не публикует type_string')
|
||||
await call('start_session')
|
||||
try:
|
||||
deadline = time.monotonic() + 90
|
||||
while time.monotonic() < deadline:
|
||||
status = await call('session_status')
|
||||
if status['phase'] == 'ready':
|
||||
break
|
||||
if status['phase'] in ('failed', 'stopped'):
|
||||
raise RuntimeError('Launcher: ' + repr(status))
|
||||
await asyncio.sleep(.2)
|
||||
else:
|
||||
raise TimeoutError('gets не дошёл до main')
|
||||
|
||||
source = str(ROOT / 'tests/gets/gets.c')
|
||||
point = await call('set_line_breakpoint', {'file': source, 'line': 16})
|
||||
await call('continue_execution')
|
||||
await asyncio.sleep(.5)
|
||||
if not (await call('session_status'))['running']:
|
||||
raise RuntimeError('gets не ожидает строку')
|
||||
typed = await call('type_string', {'value': 'Ab9\n'})
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
status = await call('session_status')
|
||||
if not status['running']:
|
||||
break
|
||||
await asyncio.sleep(.1)
|
||||
else:
|
||||
shot = await call('screenshot')
|
||||
destination = Path('/private/tmp/sprinter-c-mcp-gets-timeout.png')
|
||||
shutil.copyfile(shot['path'], destination)
|
||||
raise TimeoutError('Enter не завершил gets: ' +
|
||||
repr({'typed': typed, 'status': status,
|
||||
'screenshot': str(destination)}))
|
||||
location = await call('where')
|
||||
if typed['typed'] != 4 or not typed['complete'] or \
|
||||
not any(item['line'] == 16 for item in location['sources']):
|
||||
raise RuntimeError('Неверный результат набора: ' +
|
||||
repr((typed, location)))
|
||||
shot = await call('screenshot')
|
||||
destination = Path('/private/tmp/sprinter-c-mcp-gets.png')
|
||||
shutil.copyfile(shot['path'], destination)
|
||||
await call('clear_breakpoint', {'identifier': point['id']})
|
||||
print(json.dumps({'event': 'mcp_type_string_verified',
|
||||
'tools': len(names), 'typed': typed,
|
||||
'line': 16, 'screenshot': str(destination)},
|
||||
ensure_ascii=False), flush=True)
|
||||
finally:
|
||||
await call('stop_session')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(probe())
|
||||
@@ -77,6 +77,32 @@ class McpAdapterTests(unittest.TestCase):
|
||||
self.assertEqual(keys[1]['tag'], keys[-2]['tag'])
|
||||
self.assertTrue(all(item['owner'] == client.owner for item in keys))
|
||||
|
||||
def test_type_string_checks_layout_before_input_and_releases_each_key(self):
|
||||
calls = []
|
||||
frame = 0
|
||||
|
||||
def rpc(path, method, arguments, timeout=10, **identity):
|
||||
nonlocal frame
|
||||
calls.append((method, arguments))
|
||||
if method == 'status':
|
||||
return {'session_id': 'test-session', 'build_id': 'test-build',
|
||||
'generation': 7, 'running': True}
|
||||
if method == 'snapshot':
|
||||
frame += 1
|
||||
return {'state': 'running', 'frame': frame}
|
||||
return {}
|
||||
|
||||
client = McpSession('/tmp/sprinter-test.sock', rpc=rpc)
|
||||
with self.assertRaisesRegex(SessionError, 'нет раскладки'):
|
||||
client.type_string('a€')
|
||||
self.assertEqual(calls, [])
|
||||
result = client.type_string('ab')
|
||||
self.assertEqual(result, {'requested': 2, 'typed': 2,
|
||||
'complete': True, 'stopped': False})
|
||||
keys = [args for method, args in calls if method == 'input_key']
|
||||
self.assertEqual([item['down'] for item in keys],
|
||||
[True, False, True, False])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user