Add bounded logical Z80 disassembly to shared MCP

This commit is contained in:
Александр Петров
2026-09-17 23:11:08 +03:00
parent 113d468c48
commit 958ad0ae0f
13 changed files with 103 additions and 16 deletions
+14 -1
View File
@@ -38,7 +38,7 @@ async def probe() -> None:
names = {tool.name for tool in (await client.list_tools()).tools}
if not {'start_session', 'stop_session', 'where', 'read_variable',
'press_key', 'list_breakpoints'} <= names:
'press_key', 'list_breakpoints', 'disassemble_logical'} <= names:
raise RuntimeError('Нет инструментов автономного запуска')
initial = await call('session_status')
if initial['phase'] != 'idle':
@@ -67,6 +67,18 @@ async def probe() -> None:
location = await call('where')
if location.get('function', {}).get('name') != 'main':
raise RuntimeError('MCP не остановился в main: ' + repr(location))
bytes_before = await call('read_memory',
{'address': hex(location['pc']), 'length': 32})
disassembly = await call('disassemble_logical',
{'address': hex(location['pc']), 'length': 32})
bytes_after = await call('read_memory',
{'address': hex(location['pc']), 'length': 32})
if disassembly['space'] != 'logical_z80' or \
disassembly['address'] != location['pc'] or \
not disassembly['text'].upper().startswith(
f"{location['pc']:04X}:") or \
bytes_before['hex'] != bytes_after['hex']:
raise RuntimeError('Нет дизассемблирования main: ' + repr(disassembly))
await call('read_variable', {'name': 'errno'})
shot = await call('screenshot')
if Path(shot['path']).read_bytes()[:8] != b'\x89PNG\r\n\x1a\n':
@@ -145,6 +157,7 @@ async def probe() -> None:
'tools': len(names), 'start_ms': start_ms,
'session_id': status['session_id'],
'socket': socket, 'pc': location['pc'],
'disassembly_head': disassembly['text'][:90],
'key': key['key'], 'after_getchar_line': 63},
ensure_ascii=False), flush=True)
finally:
+20
View File
@@ -29,6 +29,10 @@ class DummyBridge:
if command == 'snapshot': return {'state': self.state}
if command == 'console_print': return {'printed': True}
if command == 'memory': return {'hex': '00' * arguments['length']}
if command == 'disassemble_logical':
return {'space': 'logical_z80', 'address': arguments['address'],
'length': arguments['length'], 'text': '8100: NOP',
'generation': self.generation}
if command == 'key': return {'accepted': True}
raise AssertionError(command)
@@ -243,6 +247,22 @@ class ServerTests(unittest.TestCase):
finally:
controller.close()
def test_disassemble_logical_is_bounded_and_labels_bank_mapping(self):
controller = SessionController(DummySession())
try:
result = controller.call('disassemble_logical',
{'address': 0x8100, 'length': 32})
self.assertEqual(result['space'], 'logical_z80')
self.assertEqual(result['address'], 0x8100)
self.assertEqual(result['bank_pages'], {})
with self.assertRaisesRegex(SessionError, 'Дизассемблирование'):
controller.call('disassemble_logical',
{'address': 0xffff, 'length': 2})
self.assertEqual(len([call for call in controller.session.bridge.calls
if call[0] == 'disassemble_logical']), 1)
finally:
controller.close()
def test_control_lease_blocks_other_client_and_can_be_released_or_expire(self):
controller = SessionController(DummySession())
try: