Implement Phase 1 tasks 1.1-1.4 (scaffold + core shell + Teltonika framing)

- Project scaffold (Node 22 + TS 5 + pnpm + vitest + ESLint flat config)
- Core shell: TCP server, session loop, adapter registry, types
- Configuration (zod-validated env) and pino logger
- Teltonika adapter: IMEI handshake, frame envelope, CRC-16/IBM,
  codec dispatch registry, DeviceAuthority seam (AllowAllAuthority default)

Codec data parsers (1.5-1.7), Redis publisher (1.8), and downstream
tasks remain. 36 tests covering CRC, framing, handshake, device
authority, config, and core server. typecheck/lint/test/build all clean.
This commit is contained in:
2026-04-30 15:47:26 +02:00
parent c8a5f4cd68
commit 1e9219d14a
35 changed files with 6217 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import * as net from 'node:net';
import type { Logger } from 'pino';
import type { Adapter, AdapterContext, Metrics, Position } from '../../src/core/types.js';
import { startServer } from '../../src/core/server.js';
function makeMockContext(): AdapterContext {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
child: vi.fn().mockReturnThis(),
} as unknown as Logger;
const metrics: Metrics = {
inc: vi.fn(),
observe: vi.fn(),
};
return {
publish: vi.fn(async (_p: Position) => {}),
logger,
metrics,
};
}
describe('startServer', () => {
const servers: net.Server[] = [];
afterEach(() => {
for (const server of servers) {
server.close();
}
servers.length = 0;
});
it('invokes handleSession with a real socket when a client connects', async () => {
const handleSession = vi.fn().mockResolvedValue(undefined);
const adapter: Adapter = {
name: 'test-adapter',
ports: [0], // port 0 = OS-assigned ephemeral port
handleSession,
};
const ctx = makeMockContext();
const server = startServer(0, adapter, ctx);
servers.push(server);
// Wait for the server to start listening
const port = await new Promise<number>((resolve) => {
server.on('listening', () => {
const addr = server.address();
resolve(typeof addr === 'object' && addr !== null ? addr.port : 0);
});
});
// Connect a client
const client = new net.Socket();
client.connect(port, '127.0.0.1');
// Wait for handleSession to be called
await vi.waitFor(() => expect(handleSession).toHaveBeenCalledOnce(), { timeout: 2000 });
const [socketArg, ctxArg] = handleSession.mock.calls[0] as [net.Socket, AdapterContext];
expect(socketArg).toBeInstanceOf(net.Socket);
expect(ctxArg).toBeDefined();
expect(typeof ctxArg.publish).toBe('function');
expect(ctxArg.logger).toBeDefined();
client.destroy();
});
});