Add integration test suites for protocol and MCP tool layers
- test/integration.ts: Protocol-level tests (Connect, RegistersGet, MemoryGet, MemorySet, DisplayGet) - test/integration.test.ts: MCP tool-level tests (16 tools including semantic layer) Both suites validate the VICE binary monitor protocol implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c722e18496
commit
69e05806e1
2 changed files with 737 additions and 0 deletions
457
test/integration.test.ts
Normal file
457
test/integration.test.ts
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
/**
|
||||
* VICE MCP Integration Test Suite
|
||||
*
|
||||
* Tests all MCP tools against a running VICE instance.
|
||||
*
|
||||
* Prerequisites:
|
||||
* x64sc -binarymonitor -binarymonitoraddress ip4://127.0.0.1:6502
|
||||
*
|
||||
* Run:
|
||||
* npx tsx test/integration.test.ts
|
||||
*/
|
||||
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
interface MCPRequest {
|
||||
jsonrpc: '2.0';
|
||||
id: number;
|
||||
method: string;
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface MCPResponse {
|
||||
jsonrpc: '2.0';
|
||||
id: number;
|
||||
result?: unknown;
|
||||
error?: { code: number; message: string };
|
||||
}
|
||||
|
||||
class MCPTestClient {
|
||||
private process: ChildProcess;
|
||||
private requestId = 0;
|
||||
private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
||||
private buffer = '';
|
||||
|
||||
constructor(serverPath: string) {
|
||||
this.process = spawn('node', [serverPath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
this.process.stdout!.on('data', (data: Buffer) => {
|
||||
this.buffer += data.toString();
|
||||
this.processBuffer();
|
||||
});
|
||||
|
||||
this.process.stderr!.on('data', (data: Buffer) => {
|
||||
// Log errors but don't fail - some debug output goes to stderr
|
||||
const msg = data.toString().trim();
|
||||
if (msg && !msg.includes('too large to log')) {
|
||||
console.error('[MCP stderr]', msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private processBuffer() {
|
||||
const lines = this.buffer.split('\n');
|
||||
this.buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const response: MCPResponse = JSON.parse(line);
|
||||
const pending = this.pending.get(response.id);
|
||||
if (pending) {
|
||||
this.pending.delete(response.id);
|
||||
if (response.error) {
|
||||
pending.reject(new Error(response.error.message));
|
||||
} else {
|
||||
pending.resolve(response.result);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip non-JSON lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async call(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
||||
const id = ++this.requestId;
|
||||
const request: MCPRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject });
|
||||
this.process.stdin!.write(JSON.stringify(request) + '\n');
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (this.pending.has(id)) {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`Timeout waiting for response to ${method}`));
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async callTool(name: string, args: Record<string, unknown> = {}): Promise<unknown> {
|
||||
const response = await this.call('tools/call', { name, arguments: args }) as {
|
||||
content?: Array<{ type: string; text: string }>;
|
||||
};
|
||||
|
||||
// MCP wraps tool results in content[].text as JSON string
|
||||
if (response?.content?.[0]?.text) {
|
||||
try {
|
||||
return JSON.parse(response.content[0].text);
|
||||
} catch {
|
||||
return response.content[0].text;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.process.kill();
|
||||
}
|
||||
}
|
||||
|
||||
// Test utilities
|
||||
function assert(condition: boolean, message: string) {
|
||||
if (!condition) throw new Error(`Assertion failed: ${message}`);
|
||||
}
|
||||
|
||||
function assertExists(value: unknown, message: string) {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error(`Assertion failed: ${message} - value is ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNoError(result: unknown, toolName: string) {
|
||||
const r = result as Record<string, unknown>;
|
||||
if (r.error) {
|
||||
throw new Error(`${toolName} returned error: ${JSON.stringify(r)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test definitions
|
||||
interface TestResult {
|
||||
name: string;
|
||||
passed: boolean;
|
||||
error?: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const tests: Array<{ name: string; fn: (client: MCPTestClient) => Promise<void> }> = [];
|
||||
|
||||
function test(name: string, fn: (client: MCPTestClient) => Promise<void>) {
|
||||
tests.push({ name, fn });
|
||||
}
|
||||
|
||||
// === CONNECTION TESTS ===
|
||||
|
||||
test('connect - connects to VICE', async (client) => {
|
||||
const result = await client.callTool('connect') as Record<string, unknown>;
|
||||
assertNoError(result, 'connect');
|
||||
assert(result.connected === true, 'should be connected');
|
||||
assert(result.host === '127.0.0.1', 'should use default host');
|
||||
assert(result.port === 6502, 'should use default port');
|
||||
});
|
||||
|
||||
test('status - shows connected state', async (client) => {
|
||||
const result = await client.callTool('status') as Record<string, unknown>;
|
||||
assertNoError(result, 'status');
|
||||
assert(result.connected === true, 'should be connected');
|
||||
assertExists(result.host, 'should have host');
|
||||
assertExists(result.port, 'should have port');
|
||||
});
|
||||
|
||||
// === MEMORY TESTS ===
|
||||
|
||||
test('readMemory - reads screen RAM', async (client) => {
|
||||
const result = await client.callTool('readMemory', {
|
||||
address: 0x0400,
|
||||
length: 40,
|
||||
}) as Record<string, unknown>;
|
||||
assertNoError(result, 'readMemory');
|
||||
assertExists(result.hex, 'should have hex output');
|
||||
assertExists(result.ascii, 'should have ascii output');
|
||||
assert((result.bytes as number[]).length === 40, 'should read 40 bytes');
|
||||
});
|
||||
|
||||
test('writeMemory - writes to zero page', async (client) => {
|
||||
// Use zero page instead of I/O registers for reliable test
|
||||
const testAddr = 0x00FB; // Unused zero page location
|
||||
|
||||
// Save original
|
||||
const original = await client.callTool('readMemory', {
|
||||
address: testAddr,
|
||||
length: 1,
|
||||
}) as Record<string, unknown>;
|
||||
const originalValue = (original.bytes as number[])[0];
|
||||
|
||||
// Write new value
|
||||
const testValue = (originalValue + 1) & 0xFF; // Different value
|
||||
const result = await client.callTool('writeMemory', {
|
||||
address: testAddr,
|
||||
bytes: [testValue],
|
||||
}) as Record<string, unknown>;
|
||||
assertNoError(result, 'writeMemory');
|
||||
|
||||
// Verify
|
||||
const verify = await client.callTool('readMemory', {
|
||||
address: testAddr,
|
||||
length: 1,
|
||||
}) as Record<string, unknown>;
|
||||
assert((verify.bytes as number[])[0] === testValue, `should read back ${testValue}`);
|
||||
|
||||
// Restore
|
||||
await client.callTool('writeMemory', {
|
||||
address: testAddr,
|
||||
bytes: [originalValue],
|
||||
});
|
||||
});
|
||||
|
||||
// === REGISTER TESTS ===
|
||||
|
||||
test('getRegisters - returns CPU state', async (client) => {
|
||||
const result = await client.callTool('getRegisters') as Record<string, unknown>;
|
||||
assertNoError(result, 'getRegisters');
|
||||
assertExists(result.a, 'should have accumulator');
|
||||
assertExists(result.x, 'should have X register');
|
||||
assertExists(result.y, 'should have Y register');
|
||||
assertExists(result.sp, 'should have stack pointer');
|
||||
assertExists(result.pc, 'should have program counter');
|
||||
assertExists(result.flags, 'should have flags');
|
||||
});
|
||||
|
||||
// === SEMANTIC LAYER TESTS ===
|
||||
|
||||
test('readScreen - decodes PETSCII', async (client) => {
|
||||
const result = await client.callTool('readScreen') as Record<string, unknown>;
|
||||
assertNoError(result, 'readScreen');
|
||||
assertExists(result.lines, 'should have lines array');
|
||||
assert(Array.isArray(result.lines), 'lines should be array');
|
||||
assert((result.lines as string[]).length === 25, 'should have 25 lines');
|
||||
assertExists(result.screenAddress, 'should have screen address');
|
||||
});
|
||||
|
||||
test('readVicState - interprets VIC-II', async (client) => {
|
||||
const result = await client.callTool('readVicState') as Record<string, unknown>;
|
||||
assertNoError(result, 'readVicState');
|
||||
assertExists(result.borderColor, 'should have border color');
|
||||
assertExists(result.backgroundColor, 'should have background color');
|
||||
assertExists(result.graphicsMode, 'should have graphics mode');
|
||||
assertExists(result.spriteEnable, 'should have sprite enable');
|
||||
|
||||
// Check border color has name
|
||||
const border = result.borderColor as Record<string, unknown>;
|
||||
assertExists(border.name, 'border color should have name');
|
||||
});
|
||||
|
||||
test('readSprites - returns all 8 sprites with diagnostics', async (client) => {
|
||||
const result = await client.callTool('readSprites') as Record<string, unknown>;
|
||||
assertNoError(result, 'readSprites');
|
||||
assert(result.count === 8, 'should have 8 sprites');
|
||||
assertExists(result.sprites, 'should have sprites array');
|
||||
|
||||
const sprites = result.sprites as Array<Record<string, unknown>>;
|
||||
assert(sprites.length === 8, 'sprites array should have 8 entries');
|
||||
|
||||
// Check first sprite has required fields
|
||||
const sprite0 = sprites[0];
|
||||
assertExists(sprite0.enabled, 'sprite should have enabled');
|
||||
assertExists(sprite0.position, 'sprite should have position');
|
||||
assertExists(sprite0.color, 'sprite should have color');
|
||||
assertExists(sprite0.dataAddress, 'sprite should have dataAddress');
|
||||
|
||||
// Check dataAddress has diagnostics (AX design)
|
||||
const dataAddr = sprite0.dataAddress as Record<string, unknown>;
|
||||
assertExists(dataAddr.region, 'dataAddress should have region');
|
||||
assertExists(dataAddr.severity, 'dataAddress should have severity');
|
||||
});
|
||||
|
||||
test('readColorRam - returns color info', async (client) => {
|
||||
const result = await client.callTool('readColorRam', { summary: true }) as Record<string, unknown>;
|
||||
assertNoError(result, 'readColorRam');
|
||||
assertExists(result.summary, 'should have summary');
|
||||
const summary = result.summary as Record<string, unknown>;
|
||||
assertExists(summary.usage, 'should have color usage');
|
||||
});
|
||||
|
||||
// === EXECUTION CONTROL TESTS ===
|
||||
|
||||
test('step - executes single instruction', async (client) => {
|
||||
const before = await client.callTool('getRegisters') as Record<string, unknown>;
|
||||
const pcBefore = (before.pc as Record<string, unknown>).value;
|
||||
|
||||
const result = await client.callTool('step') as Record<string, unknown>;
|
||||
assertNoError(result, 'step');
|
||||
|
||||
const after = await client.callTool('getRegisters') as Record<string, unknown>;
|
||||
const pcAfter = (after.pc as Record<string, unknown>).value;
|
||||
|
||||
// PC should have changed (unless we hit a JAM)
|
||||
// Just verify we got a response
|
||||
assertExists(pcAfter, 'should have PC after step');
|
||||
});
|
||||
|
||||
test('continue - resumes execution', async (client) => {
|
||||
const result = await client.callTool('continue') as Record<string, unknown>;
|
||||
assertNoError(result, 'continue');
|
||||
|
||||
// Give it a moment to run
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
const status = await client.callTool('status') as Record<string, unknown>;
|
||||
// Should be running now
|
||||
assertExists(status.running, 'should have running state');
|
||||
});
|
||||
|
||||
// === BREAKPOINT TESTS ===
|
||||
|
||||
test('setBreakpoint - creates breakpoint', async (client) => {
|
||||
const result = await client.callTool('setBreakpoint', {
|
||||
address: 0xE000,
|
||||
}) as Record<string, unknown>;
|
||||
assertNoError(result, 'setBreakpoint');
|
||||
assertExists(result.breakpointId, 'should return breakpoint ID');
|
||||
|
||||
// Clean up
|
||||
const bpId = result.breakpointId as number;
|
||||
await client.callTool('deleteBreakpoint', { breakpointId: bpId });
|
||||
});
|
||||
|
||||
test('listBreakpoints - shows breakpoints', async (client) => {
|
||||
// Create a breakpoint
|
||||
const bp = await client.callTool('setBreakpoint', {
|
||||
address: 0xE544,
|
||||
}) as Record<string, unknown>;
|
||||
const bpId = bp.breakpointId as number;
|
||||
|
||||
const result = await client.callTool('listBreakpoints') as Record<string, unknown>;
|
||||
assertNoError(result, 'listBreakpoints');
|
||||
assertExists(result.breakpoints, 'should have breakpoints array');
|
||||
|
||||
// Clean up
|
||||
await client.callTool('deleteBreakpoint', { breakpointId: bpId });
|
||||
});
|
||||
|
||||
test('setWatchpoint - creates memory watch', async (client) => {
|
||||
const result = await client.callTool('setWatchpoint', {
|
||||
startAddress: 0xD020,
|
||||
type: 'store',
|
||||
}) as Record<string, unknown>;
|
||||
assertNoError(result, 'setWatchpoint');
|
||||
assertExists(result.watchpointId, 'should return watchpoint ID');
|
||||
|
||||
// Clean up
|
||||
const wpId = result.watchpointId as number;
|
||||
await client.callTool('deleteBreakpoint', { breakpointId: wpId });
|
||||
});
|
||||
|
||||
// === DISASSEMBLY TEST ===
|
||||
|
||||
test('disassemble - shows assembly', async (client) => {
|
||||
const result = await client.callTool('disassemble', {
|
||||
address: 0xE000,
|
||||
count: 5,
|
||||
}) as Record<string, unknown>;
|
||||
assertNoError(result, 'disassemble');
|
||||
assertExists(result.instructions, 'should have instructions');
|
||||
|
||||
const instrs = result.instructions as Array<Record<string, unknown>>;
|
||||
assert(instrs.length === 5, 'should have 5 instructions');
|
||||
|
||||
// Check instruction format
|
||||
const instr0 = instrs[0];
|
||||
assertExists(instr0.address, 'instruction should have address');
|
||||
assertExists(instr0.mnemonic, 'instruction should have mnemonic');
|
||||
});
|
||||
|
||||
// === RESET TEST ===
|
||||
|
||||
test('reset - soft resets machine', async (client) => {
|
||||
const result = await client.callTool('reset', { hard: false }) as Record<string, unknown>;
|
||||
assertNoError(result, 'reset');
|
||||
|
||||
// Give it time to reset
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// Check we're still connected
|
||||
const status = await client.callTool('status') as Record<string, unknown>;
|
||||
assert(status.connected === true, 'should still be connected after reset');
|
||||
});
|
||||
|
||||
// === RUN TESTS ===
|
||||
|
||||
async function runTests() {
|
||||
const serverPath = path.join(__dirname, '..', 'dist', 'index.js');
|
||||
console.log(`\nVICE MCP Integration Tests`);
|
||||
console.log(`==========================`);
|
||||
console.log(`Server: ${serverPath}\n`);
|
||||
|
||||
const client = new MCPTestClient(serverPath);
|
||||
const results: TestResult[] = [];
|
||||
|
||||
// Wait for server to start
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// Initialize MCP session
|
||||
try {
|
||||
await client.call('initialize', {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'integration-test', version: '1.0.0' },
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize MCP session:', e);
|
||||
client.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const { name, fn } of tests) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
await fn(client);
|
||||
results.push({ name, passed: true, duration: Date.now() - start });
|
||||
console.log(` ✓ ${name} (${Date.now() - start}ms)`);
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e.message : String(e);
|
||||
results.push({ name, passed: false, error, duration: Date.now() - start });
|
||||
console.log(` ✗ ${name}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
client.close();
|
||||
|
||||
// Summary
|
||||
const passed = results.filter(r => r.passed).length;
|
||||
const failed = results.filter(r => !r.passed).length;
|
||||
const totalTime = results.reduce((sum, r) => sum + r.duration, 0);
|
||||
|
||||
console.log(`\n---------------------------`);
|
||||
console.log(`Passed: ${passed}/${results.length}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
console.log(`Total time: ${totalTime}ms`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log(`\nFailed tests:`);
|
||||
for (const r of results.filter(r => !r.passed)) {
|
||||
console.log(` - ${r.name}: ${r.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests().catch(e => {
|
||||
console.error('Test runner error:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
280
test/integration.ts
Normal file
280
test/integration.ts
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
#!/usr/bin/env npx ts-node
|
||||
/**
|
||||
* VICE MCP Integration Tests
|
||||
*
|
||||
* Run against a VICE instance with binary monitor enabled:
|
||||
* x64sc -binarymonitor -binarymonitoraddress ip4://127.0.0.1:6502
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node test/integration.ts
|
||||
*/
|
||||
|
||||
import { Socket } from "net";
|
||||
|
||||
const HOST = "127.0.0.1";
|
||||
const PORT = 6502;
|
||||
|
||||
// Protocol constants
|
||||
const STX = 0x02;
|
||||
const API_VERSION = 0x02;
|
||||
|
||||
// Commands
|
||||
const CMD_MEMORY_GET = 0x01;
|
||||
const CMD_MEMORY_SET = 0x02;
|
||||
const CMD_REGISTERS_GET = 0x31;
|
||||
const CMD_DISPLAY_GET = 0x84;
|
||||
|
||||
// Response types
|
||||
const RESP_MEMORY_GET = 0x01;
|
||||
const RESP_REGISTER_INFO = 0x31;
|
||||
const RESP_STOPPED = 0x62;
|
||||
const RESP_DISPLAY_GET = 0x84;
|
||||
|
||||
let requestId = 0;
|
||||
let socket: Socket;
|
||||
let responseBuffer = Buffer.alloc(0);
|
||||
|
||||
interface TestResult {
|
||||
name: string;
|
||||
passed: boolean;
|
||||
message: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const results: TestResult[] = [];
|
||||
|
||||
function nextRequestId(): number {
|
||||
requestId = (requestId + 1) & 0xffffffff;
|
||||
return requestId;
|
||||
}
|
||||
|
||||
function buildPacket(command: number, body: Buffer): Buffer {
|
||||
const header = Buffer.alloc(11);
|
||||
header[0] = STX;
|
||||
header[1] = API_VERSION;
|
||||
header.writeUInt32LE(body.length, 2);
|
||||
header.writeUInt32LE(nextRequestId(), 6);
|
||||
header[10] = command;
|
||||
return Buffer.concat([header, body]);
|
||||
}
|
||||
|
||||
async function sendCommand(command: number, body: Buffer): Promise<{ responseType: number; errorCode: number; body: Buffer }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const packet = buildPacket(command, body);
|
||||
const reqId = requestId;
|
||||
|
||||
console.log(` Sending cmd 0x${command.toString(16)}, body ${body.length} bytes, reqId ${reqId}`);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Timeout waiting for response"));
|
||||
}, 10000);
|
||||
|
||||
const handleData = () => {
|
||||
// Response header: STX(1) + API(1) + bodyLength(4) + responseType(1) + errorCode(1) + requestId(4) = 12 bytes
|
||||
while (responseBuffer.length >= 12) {
|
||||
if (responseBuffer[0] !== STX) {
|
||||
responseBuffer = responseBuffer.subarray(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bodyLength = responseBuffer.readUInt32LE(2);
|
||||
const totalLength = 12 + bodyLength;
|
||||
|
||||
if (responseBuffer.length < totalLength) {
|
||||
break; // Wait for more data
|
||||
}
|
||||
|
||||
const responseType = responseBuffer[6];
|
||||
const errorCode = responseBuffer[7];
|
||||
const respReqId = responseBuffer.readUInt32LE(8);
|
||||
const respBody = responseBuffer.subarray(12, totalLength);
|
||||
|
||||
responseBuffer = responseBuffer.subarray(totalLength);
|
||||
|
||||
// Skip async events (Stopped, Resumed) unless they match our request
|
||||
if (respReqId === 0xffffffff) {
|
||||
console.log(` Got async event type 0x${responseType.toString(16)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (respReqId === reqId || responseType === RESP_REGISTER_INFO) {
|
||||
clearTimeout(timeout);
|
||||
socket.off("data", onData);
|
||||
resolve({ responseType, errorCode, body: respBody });
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
console.log(` Received ${data.length} bytes: ${data.subarray(0, Math.min(20, data.length)).toString("hex")}...`);
|
||||
responseBuffer = Buffer.concat([responseBuffer, data]);
|
||||
handleData();
|
||||
};
|
||||
|
||||
socket.on("data", onData);
|
||||
socket.write(packet);
|
||||
});
|
||||
}
|
||||
|
||||
async function connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
socket = new Socket();
|
||||
socket.on("connect", resolve);
|
||||
socket.on("error", reject);
|
||||
socket.connect(PORT, HOST);
|
||||
});
|
||||
}
|
||||
|
||||
async function runTest(name: string, fn: () => Promise<void>): Promise<void> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
await fn();
|
||||
const duration = Date.now() - start;
|
||||
results.push({ name, passed: true, message: "OK", duration });
|
||||
console.log(`✅ ${name} (${duration}ms)`);
|
||||
} catch (error) {
|
||||
const duration = Date.now() - start;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
results.push({ name, passed: false, message, duration });
|
||||
console.log(`❌ ${name}: ${message} (${duration}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
async function testConnect(): Promise<void> {
|
||||
await connect();
|
||||
if (!socket.writable) {
|
||||
throw new Error("Socket not writable after connect");
|
||||
}
|
||||
}
|
||||
|
||||
async function testRegistersGet(): Promise<void> {
|
||||
const body = Buffer.alloc(1);
|
||||
body[0] = 0; // MainCPU memspace
|
||||
|
||||
const resp = await sendCommand(CMD_REGISTERS_GET, body);
|
||||
|
||||
if (resp.errorCode !== 0) {
|
||||
throw new Error(`Error code 0x${resp.errorCode.toString(16)}`);
|
||||
}
|
||||
if (resp.body.length < 10) {
|
||||
throw new Error(`Response too short: ${resp.body.length} bytes`);
|
||||
}
|
||||
console.log(` Got ${resp.body.length} bytes of register data`);
|
||||
}
|
||||
|
||||
async function testMemoryGet(): Promise<void> {
|
||||
// Read $0400-$04FF (screen RAM)
|
||||
const body = Buffer.alloc(8);
|
||||
body[0] = 0; // No side effects
|
||||
body.writeUInt16LE(0x0400, 1); // Start
|
||||
body.writeUInt16LE(0x04ff, 3); // End
|
||||
body[5] = 0; // MainCPU memspace
|
||||
body.writeUInt16LE(0, 6); // Bank ID
|
||||
|
||||
const resp = await sendCommand(CMD_MEMORY_GET, body);
|
||||
|
||||
if (resp.errorCode !== 0) {
|
||||
throw new Error(`Error code 0x${resp.errorCode.toString(16)}`);
|
||||
}
|
||||
|
||||
// Response body: length(2) + data
|
||||
const dataLength = resp.body.readUInt16LE(0);
|
||||
if (dataLength !== 256) {
|
||||
throw new Error(`Expected 256 bytes, got ${dataLength}`);
|
||||
}
|
||||
console.log(` Got ${dataLength} bytes of memory data`);
|
||||
}
|
||||
|
||||
async function testMemorySet(): Promise<void> {
|
||||
// Write a single byte to $D020 (border color)
|
||||
const body = Buffer.alloc(9);
|
||||
body[0] = 0; // No side effects
|
||||
body.writeUInt16LE(0xd020, 1); // Start
|
||||
body.writeUInt16LE(0xd020, 3); // End
|
||||
body[5] = 0; // MainCPU memspace
|
||||
body.writeUInt16LE(0, 6); // Bank ID
|
||||
body[8] = 0x00; // Black
|
||||
|
||||
const resp = await sendCommand(CMD_MEMORY_SET, body);
|
||||
|
||||
if (resp.errorCode !== 0) {
|
||||
throw new Error(`Error code 0x${resp.errorCode.toString(16)}`);
|
||||
}
|
||||
console.log(` Successfully wrote to $D020`);
|
||||
}
|
||||
|
||||
async function testDisplayGet(): Promise<void> {
|
||||
// DisplayGet: VC(1) + FM(1)
|
||||
const body = Buffer.alloc(2);
|
||||
body[0] = 1; // Use VIC-II
|
||||
body[1] = 0; // Indexed 8-bit format
|
||||
|
||||
const resp = await sendCommand(CMD_DISPLAY_GET, body);
|
||||
|
||||
if (resp.errorCode !== 0) {
|
||||
throw new Error(`Error code 0x${resp.errorCode.toString(16)} (${getErrorName(resp.errorCode)})`);
|
||||
}
|
||||
|
||||
// Parse response: FL(4) + DW(2) + DH(2) + ...
|
||||
if (resp.body.length < 21) {
|
||||
throw new Error(`Response too short: ${resp.body.length} bytes`);
|
||||
}
|
||||
|
||||
const fieldsLength = resp.body.readUInt32LE(0);
|
||||
const width = resp.body.readUInt16LE(4);
|
||||
const height = resp.body.readUInt16LE(6);
|
||||
const bufferLength = resp.body.readUInt32LE(17);
|
||||
|
||||
console.log(` Display: ${width}x${height}, buffer ${bufferLength} bytes, total response ${resp.body.length} bytes`);
|
||||
|
||||
if (bufferLength < 100000) {
|
||||
throw new Error(`Buffer too small: ${bufferLength} bytes (expected ~157KB)`);
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorName(code: number): string {
|
||||
const names: Record<number, string> = {
|
||||
0x00: "OK",
|
||||
0x01: "ObjectMissing",
|
||||
0x02: "InvalidMemspace",
|
||||
0x80: "InvalidCmdLength",
|
||||
0x81: "InvalidParameterLength",
|
||||
0x82: "InvalidAPI",
|
||||
0x83: "InvalidCmdType",
|
||||
0x84: "InvalidTarget",
|
||||
0x85: "InvalidParameter",
|
||||
};
|
||||
return names[code] || `Unknown(0x${code.toString(16)})`;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("VICE MCP Integration Tests");
|
||||
console.log("==========================\n");
|
||||
console.log(`Connecting to VICE at ${HOST}:${PORT}...\n`);
|
||||
|
||||
await runTest("Connect", testConnect);
|
||||
await runTest("RegistersGet", testRegistersGet);
|
||||
await runTest("MemoryGet", testMemoryGet);
|
||||
await runTest("MemorySet", testMemorySet);
|
||||
await runTest("DisplayGet", testDisplayGet);
|
||||
|
||||
// Summary
|
||||
console.log("\n==========================");
|
||||
const passed = results.filter(r => r.passed).length;
|
||||
const failed = results.filter(r => !r.passed).length;
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||||
|
||||
if (socket) {
|
||||
socket.end();
|
||||
}
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in a new issue