Punkt 2 implementieren GitHub CoPilot

This commit is contained in:
chk
2026-06-08 17:28:43 +02:00
parent 172606c7a3
commit 0109066946
14 changed files with 1239 additions and 518 deletions

View File

@@ -79,8 +79,73 @@ describe('InfoServer', () => {
expect(status.lastCommands).toEqual(['G1 X10 Y10']);
expect(status.lastPings).toEqual(['Ping']);
expect(status.senders).toEqual([
{ name: 'Base', status: 'connected' },
{ name: 'Hand', status: 'disconnected' }
{
name: 'Base',
state: 'connected',
url: null,
isTestMode: false,
error: null,
reconnectAttempt: 0,
reconnectTimer: false,
health: 'ok',
reason: undefined
},
{
name: 'Hand',
state: 'disconnected',
url: null,
isTestMode: false,
error: null,
reconnectAttempt: 0,
reconnectTimer: false,
health: 'disconnected',
reason: 'no active socket connection'
}
]);
});
test('returns sender health details from instance.getStatus()', async () => {
const key = fs.readFileSync('https/localhost.key');
const cert = fs.readFileSync('https/localhost.pem');
const httpsOptions = { key, cert, passphrase: 'abcd' };
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0 };
const senders = [
{
name: 'Reconnect',
instance: {
getStatus: () => ({
state: 'reconnecting',
url: 'reconnect.test',
error: 'timeout',
isTestMode: false,
reconnectAttempt: 2,
reconnectTimer: true
})
}
}
];
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
port = await listen(server);
const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/status`);
expect(statusCode).toBe(200);
const status = JSON.parse(body);
expect(status.senders).toEqual([
{
name: 'Reconnect',
state: 'reconnecting',
url: 'reconnect.test',
isTestMode: false,
error: 'timeout',
reconnectAttempt: 2,
reconnectTimer: true,
health: 'warning',
reason: undefined
}
]);
});

View File

@@ -93,4 +93,24 @@ describe('InputWS', () => {
client.close();
});
test('receives GCode text and broadcasts updated position', async () => {
server = http.createServer();
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
const robot = createDummyRobot();
wss = initInputWS(server, robot, GCode, sharedState);
port = await listen(server);
const client = await connectWebSocket(port);
const messagePromise = waitForMessage(client);
client.send('G1 X1 Y2 Z3');
const message = await messagePromise;
const parsed = JSON.parse(message);
expect(parsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0 });
expect(robot.sendCommand).toHaveBeenCalled();
client.close();
});
});

View File

@@ -1,76 +1,72 @@
var Sender = require('../robot/WSSenderGrbl.js')
describe("WS-SenderGRBL.execCommand", () => {
test("writes correct G-code to mocked WS tSocket", () => {
// Create instance (will try real connection, but we override tSocket immediately)
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z");
// Mock tSocket.write
sender.tSocket = {
written: "",
write: function(txt) {
this.written = txt; // store what was written
}
};
// Provide some sample motion data
const mOld = { x: 0, y: 0, z: 0, a:0, b:0, c:0, e:0 }; // not used in your code
const mNew = { x: 12.34, y: 1.0, z: 2.0, a:0, b:0, c:0, e:0 };
sender.execCommand("G1", mOld, mNew);
// ✅ verify output
expect(sender.tSocket.written).toBe("G90 G1 x12.34 y57.30 z57.30 f2300.00\r\n");
});
test("writes correct G-code to mocked tSocket Ellbow", () => {
// Create instance (will try real connection, but we override tSocket immediately)
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "a", yAxisGrbl = null, zAxisGrbl = null );
// Mock tSocket.write
sender.tSocket = {
written: "",
write: function(txt) {
this.written = txt; // store what was written
}
};
// Provide some sample motion data
const mOld = { x: 0, y: 0, z: 0, a:Math.PI, b:0, c:0, e:0 }; // not used in your code
const mNew = { x: 12.34, y: Math.PI/2, z: 0, a:Math.PI/8, b:0, c:0, e:0 };
sender.execCommand("G1", mOld, mNew);
// ✅ verify output
expect(sender.tSocket.written).toBe("G90 G1 x22.50 f2300.00\r\n");
});
test("writes correct G-code G92 to mocked WS tSocket", () => {
// Create instance (will try real connection, but we override tSocket immediately)
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z");
// Mock tSocket.write
sender.tSocket = {
written: "",
write: function(txt) {
this.written = txt; // store what was written
}
};
// Provide some sample motion data
const mOld = { x: 0, y: 0, z: 0, a:0, b:0, c:0, e:0 }; // not used in your code
const mNew = { x: 12.34, y: 1.0, z: 2.0, a:0, b:0, c:0, e:0 };
sender.execCommand("G92", mOld, mNew);
// ✅ verify output
expect(sender.tSocket.written.replace("G90 ","")).toBe("G92 x12.34 y57.30 z57.30 f2300.00\r\n");
});
});
const WSSenderGrbl = require('../robot/WSSenderGrbl.js');
describe("WSSenderGrbl implements SenderInterface", () => {
test("is an instance of SenderInterface and exposes required methods", () => {
const SenderInterface = require('../robot/SenderInterface');
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
expect(sender).toBeInstanceOf(SenderInterface);
expect(typeof sender.connect).toBe('function');
expect(typeof sender.send).toBe('function');
expect(typeof sender.getStatus).toBe('function');
expect(typeof sender.disconnect).toBe('function');
});
test("test mode has connected status", () => {
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
expect(sender.getStatus()).toMatchObject({ state: 'connected', isTestMode: true });
});
test("send() returns false when not connected, true in test mode", () => {
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
expect(sender.send("G1 x1")).toBe(true);
sender.ws = null;
expect(sender.send("G1 x1")).toBe(false);
});
test("disconnect() transitions to disconnected state", () => {
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
sender.disconnect();
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
});
});
describe("WSSenderGrbl.execCommand writes correct G-code via send()", () => {
test("writes correct G-code for xyz axes", () => {
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
const mNew = { x: 12.34, y: 1.0, z: 2.0, a: 0, b: 0, c: 0, e: 0 };
sender.execCommand("G1", mOld, mNew);
expect(sender.ws.written).toBe("G90 G1 x12.34 y57.30 z57.30 f2300.00\n");
});
test("writes correct G-code for elbow (a axis)", () => {
const sender = new WSSenderGrbl("test.test", 2300, "a", null, null);
const mOld = { x: 0, y: 0, z: 0, a: Math.PI, b: 0, c: 0, e: 0 };
const mNew = { x: 12.34, y: Math.PI / 2, z: 0, a: Math.PI / 8, b: 0, c: 0, e: 0 };
sender.execCommand("G1", mOld, mNew);
expect(sender.ws.written).toBe("G90 G1 x22.50 f2300.00\n");
});
test("G92 command is sent without extra G90 prefix", () => {
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
const mNew = { x: 12.34, y: 1.0, z: 2.0, a: 0, b: 0, c: 0, e: 0 };
sender.execCommand("G92", mOld, mNew);
expect(sender.ws.written.replace("G90 ", "")).toBe("G92 x12.34 y57.30 z57.30 f2300.00\n");
});
});

View File

@@ -0,0 +1,103 @@
const EventEmitter = require('events');
const SenderInterface = require('../robot/SenderInterface');
const TelnetSender = require('../robot/TelnetSenderGRBL');
describe('Sender Interface and TelnetSenderGRBL implementation', () => {
test('TelnetSenderGRBL implements the required sender interface methods', () => {
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
expect(sender).toBeInstanceOf(SenderInterface);
expect(typeof sender.connect).toBe('function');
expect(typeof sender.send).toBe('function');
expect(typeof sender.getStatus).toBe('function');
expect(typeof sender.disconnect).toBe('function');
});
test('test mode sender rejects invalid send payloads and supports idempotent disconnect', () => {
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
expect(sender.getStatus()).toMatchObject({ state: 'connected' });
sender.tSocket = null;
expect(sender.send('HELLO')).toBe(false);
expect(sender.send('')).toBe(false);
sender.disconnect();
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
sender.disconnect();
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
});
test('sender reconnects on connection failure and eventually connects', async () => {
let connectAttempts = 0;
const netMock = {
createConnection: () => {
const socket = new EventEmitter();
socket.end = jest.fn();
process.nextTick(() => {
connectAttempts += 1;
if (connectAttempts === 1) {
socket.emit('error', new Error('connection failed'));
} else {
socket.emit('connect');
}
});
return socket;
}
};
class DummyTelnetSocket {
constructor(connection) {
this.connection = connection;
}
on(event, listener) {
this.connection.on(event, listener);
return this;
}
write(txt) {
this.connection.written = txt;
}
}
const sender = new TelnetSender('localhost', 2300, 'x', 'y', 'z', null, null, null, null, {
netModule: netMock,
TelnetSocketClass: DummyTelnetSocket,
setTimeoutFn: (fn) => fn(),
autoConnect: false,
reconnectDelay: 1,
maxReconnectDelay: 2
});
const result = await sender.connect();
expect(connectAttempts).toBe(2);
expect(result.getStatus()).toMatchObject({
state: 'connected',
url: 'localhost'
});
expect(result.getStatus().reconnectTimer).toBe(false);
});
test('test mode sender has connected status and can send/disconnect', async () => {
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
expect(sender.getStatus()).toMatchObject({
state: 'connected',
url: 'test.test',
isTestMode: true
});
expect(sender.send('HELLO')).toBe(true);
expect(sender.tSocket.written).toBe('HELLO\r\n');
sender.disconnect();
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
await expect(sender.connect()).resolves.toBe(sender);
expect(sender.getStatus()).toMatchObject({ state: 'connected' });
});
});

114
test/StartRobot.test.js Normal file
View File

@@ -0,0 +1,114 @@
const { createApp } = require('../startRobot');
describe('startRobot orchestrator', () => {
test('creates HTTPS and info servers and binds modules', () => {
const readFileSync = jest.fn()
.mockReturnValueOnce('fake-key')
.mockReturnValueOnce('fake-cert');
const httpsServerMock = {
listen: jest.fn()
};
const httpsModuleMock = {
createServer: jest.fn(() => httpsServerMock)
};
const initInputWS = jest.fn();
const infoServerMock = {
listen: jest.fn()
};
const createInfoServer = jest.fn(() => infoServerMock);
const TelnetSenderClass = jest.fn(() => ({ tSocket: null }));
const robotInstances = [];
class RobotClass {
constructor() {
this.cmdReceivers = [];
robotInstances.push(this);
}
}
const result = createApp({
fsModule: { readFileSync },
httpsModule: httpsModuleMock,
processEnv: {},
RobotClass,
GCodeModule: { dummy: true },
TelnetSenderClass,
initInputWSFn: initInputWS,
createInfoServerFn: createInfoServer,
setTimeoutFn: (fn) => fn(),
consoleObj: { log: jest.fn(), warn: jest.fn(), error: jest.fn() }
});
expect(readFileSync).toHaveBeenCalledTimes(2);
expect(httpsModuleMock.createServer).toHaveBeenCalledWith({
enable: true,
key: 'fake-key',
cert: 'fake-cert',
passphrase: 'abcd'
});
expect(initInputWS).toHaveBeenCalledWith(httpsServerMock, expect.any(RobotClass), { dummy: true }, expect.any(Object));
expect(createInfoServer).toHaveBeenCalledWith(
expect.objectContaining({ key: 'fake-key', cert: 'fake-cert', passphrase: 'abcd' }),
expect.any(Object),
expect.any(RobotClass),
{ dummy: true },
expect.arrayContaining([
expect.objectContaining({ name: 'Base', instance: expect.any(Object) }),
expect.objectContaining({ name: 'Elbow', instance: expect.any(Object) }),
expect.objectContaining({ name: 'Hand', instance: expect.any(Object) })
])
);
expect(httpsServerMock.listen).toHaveBeenCalledWith(2095);
expect(infoServerMock.listen).toHaveBeenCalledWith(2098);
expect(result).toHaveProperty('httpsServer', httpsServerMock);
expect(result).toHaveProperty('infoServer', infoServerMock);
expect(result).toHaveProperty('senders');
expect(result.senders).toHaveLength(3);
expect(result.startupStatus).toEqual({
https: { ok: true },
senders: [
{ name: 'Base', status: 'disconnected', reason: 'no active socket connection' },
{ name: 'Elbow', status: 'disconnected', reason: 'no active socket connection' },
{ name: 'Hand', status: 'disconnected', reason: 'no active socket connection' }
]
});
expect(result.sharedState.connectedClients).toEqual([]);
});
test('reports missing HTTPS certificates on startup', () => {
const readFileSync = jest.fn().mockImplementation(() => {
throw new Error('ENOENT: no such file or directory');
});
const httpsModuleMock = {
createServer: jest.fn()
};
const result = createApp({
fsModule: { readFileSync },
httpsModule: httpsModuleMock,
processEnv: {},
RobotClass: class {},
GCodeModule: { dummy: true },
TelnetSenderClass: jest.fn(),
initInputWSFn: jest.fn(),
createInfoServerFn: jest.fn(),
setTimeoutFn: jest.fn(),
consoleObj: { log: jest.fn(), error: jest.fn(), warn: jest.fn() }
});
expect(result.startupStatus.https.ok).toBe(false);
expect(result.startupStatus.https.error).toMatch(/Failed to load HTTPS certificate\/key/);
expect(httpsModuleMock.createServer).not.toHaveBeenCalled();
expect(result.httpsServer).toBeUndefined();
expect(result.infoServer).toBeUndefined();
expect(result.senders).toBeUndefined();
});
});