mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 15:46:43 +00:00
feat(pilot): add tcp tunnel frames + mesh sidecar package (#857)
Lays the dormant data-plane foundation for Sencho Mesh. The pilot tunnel gains TCP forwarding frames (tcp_open / tcp_open_ack / tcp_close JSON plus a 0x04 TcpData binary type) and a TcpStream surface on the bridge so a future MeshService can ride the existing WSS tunnel for cross-node container traffic. The agent rejects every tcp_open with mesh_not_enabled until a follow-up PR wires the Dockerode resolver gated by a mesh_stacks opt-in table; ships dormant. A new top-level mesh-sidecar/ package provides the per-node container that will host the L4 forwarder + control WS in production. Built as a small Node 22 alpine image and published in lockstep with the main sencho image via a parallel docker-publish workflow job. Tests cover protocol roundtrips on both packages and the sidecar forwarder end-to-end including resolve, splice, close, and stats.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Tests for the Sencho Mesh TCP frames added to the pilot tunnel protocol.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
BinaryFrameType,
|
||||
decodeBinaryFrame,
|
||||
decodeJsonFrame,
|
||||
encodeBinaryFrame,
|
||||
encodeJsonFrame,
|
||||
} from '../pilot/protocol';
|
||||
|
||||
describe('Mesh TCP JSON frames', () => {
|
||||
it('roundtrips a tcp_open frame', () => {
|
||||
const raw = encodeJsonFrame({
|
||||
t: 'tcp_open',
|
||||
s: 42,
|
||||
stack: 'api',
|
||||
service: 'db',
|
||||
port: 5432,
|
||||
});
|
||||
const decoded = decodeJsonFrame(raw);
|
||||
expect(decoded.t).toBe('tcp_open');
|
||||
if (decoded.t !== 'tcp_open') throw new Error('narrowing');
|
||||
expect(decoded.s).toBe(42);
|
||||
expect(decoded.stack).toBe('api');
|
||||
expect(decoded.service).toBe('db');
|
||||
expect(decoded.port).toBe(5432);
|
||||
});
|
||||
|
||||
it('roundtrips a tcp_open_ack success', () => {
|
||||
const raw = encodeJsonFrame({ t: 'tcp_open_ack', s: 42, ok: true });
|
||||
const decoded = decodeJsonFrame(raw);
|
||||
expect(decoded.t).toBe('tcp_open_ack');
|
||||
if (decoded.t !== 'tcp_open_ack') throw new Error('narrowing');
|
||||
expect(decoded.ok).toBe(true);
|
||||
expect(decoded.err).toBeUndefined();
|
||||
});
|
||||
|
||||
it('roundtrips a tcp_open_ack failure with error code', () => {
|
||||
const raw = encodeJsonFrame({
|
||||
t: 'tcp_open_ack',
|
||||
s: 42,
|
||||
ok: false,
|
||||
err: 'unreachable',
|
||||
});
|
||||
const decoded = decodeJsonFrame(raw);
|
||||
if (decoded.t !== 'tcp_open_ack') throw new Error('narrowing');
|
||||
expect(decoded.ok).toBe(false);
|
||||
expect(decoded.err).toBe('unreachable');
|
||||
});
|
||||
|
||||
it('roundtrips a tcp_close frame', () => {
|
||||
const raw = encodeJsonFrame({ t: 'tcp_close', s: 42 });
|
||||
const decoded = decodeJsonFrame(raw);
|
||||
expect(decoded.t).toBe('tcp_close');
|
||||
if (decoded.t !== 'tcp_close') throw new Error('narrowing');
|
||||
expect(decoded.s).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mesh TcpData binary frames', () => {
|
||||
it('encodes the 0x04 type discriminator', () => {
|
||||
const payload = Buffer.from('hello');
|
||||
const encoded = encodeBinaryFrame(BinaryFrameType.TcpData, 1, payload);
|
||||
expect(encoded[0]).toBe(0x04);
|
||||
});
|
||||
|
||||
it('roundtrips streamId + payload', () => {
|
||||
const payload = Buffer.from('SELECT 1;');
|
||||
const encoded = encodeBinaryFrame(BinaryFrameType.TcpData, 0xdeadbeef, payload);
|
||||
const decoded = decodeBinaryFrame(encoded);
|
||||
expect(decoded.type).toBe(BinaryFrameType.TcpData);
|
||||
expect(decoded.streamId).toBe(0xdeadbeef);
|
||||
expect(decoded.payload.toString()).toBe('SELECT 1;');
|
||||
});
|
||||
|
||||
it('roundtrips an empty payload', () => {
|
||||
const encoded = encodeBinaryFrame(BinaryFrameType.TcpData, 7, Buffer.alloc(0));
|
||||
const decoded = decodeBinaryFrame(encoded);
|
||||
expect(decoded.type).toBe(BinaryFrameType.TcpData);
|
||||
expect(decoded.streamId).toBe(7);
|
||||
expect(decoded.payload.length).toBe(0);
|
||||
});
|
||||
|
||||
it('preserves binary payloads byte-for-byte', () => {
|
||||
const payload = Buffer.from([0x00, 0xff, 0x01, 0x80, 0x7f, 0x10]);
|
||||
const encoded = encodeBinaryFrame(BinaryFrameType.TcpData, 1, payload);
|
||||
const decoded = decodeBinaryFrame(encoded);
|
||||
expect(decoded.payload.equals(payload)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects an unknown binary frame type', () => {
|
||||
const buf = Buffer.alloc(5);
|
||||
buf.writeUInt8(0x99, 0);
|
||||
buf.writeUInt32BE(1, 1);
|
||||
expect(() => decodeBinaryFrame(buf)).toThrow(/unknown binary frame type/);
|
||||
});
|
||||
|
||||
it('continues to accept the existing http and ws binary types', () => {
|
||||
for (const t of [BinaryFrameType.HttpReqBody, BinaryFrameType.HttpResBody, BinaryFrameType.WsMessageBinary]) {
|
||||
const buf = encodeBinaryFrame(t, 1, Buffer.from('x'));
|
||||
expect(() => decodeBinaryFrame(buf)).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user