import { describe, it, expect, vi } from 'vitest'; import type { Position } from '../src/core/types.js'; import type { CodecHandlerContext } from '../src/adapters/teltonika/codec/registry.js'; import { codec16Handler } from '../src/adapters/teltonika/codec/data/codec16.js'; import { loadFixturesFromDir, compareToExpected } from './fixtures/_loader.js'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // --------------------------------------------------------------------------- // Test context factory // --------------------------------------------------------------------------- function makeTestCtx(positions: Position[]): CodecHandlerContext { return { imei: 'FIXTURE', publish: async (pos: Position) => { positions.push(pos); }, logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), child: vi.fn().mockReturnThis(), } as unknown as CodecHandlerContext['logger'], }; } // --------------------------------------------------------------------------- // Auto-discovered fixture tests // --------------------------------------------------------------------------- const fixtureDir = path.join(__dirname, 'fixtures/teltonika/codec16'); const fixtures = loadFixturesFromDir(fixtureDir); describe('Codec 16 parser — fixture tests', () => { for (const fixture of fixtures) { it(`parses ${fixture.name}`, async () => { const positions: Position[] = []; const ctx = makeTestCtx(positions); const result = await codec16Handler.handle(fixture.body, ctx); expect(result.recordCount).toBe(fixture.expected.ack_record_count); const mismatch = compareToExpected(positions, fixture.expected.positions); expect(mismatch, mismatch ?? '').toBeNull(); }); } }); // --------------------------------------------------------------------------- // Unit tests — Codec 16-specific behaviours // --------------------------------------------------------------------------- describe('Codec 16 parser — unit tests', () => { it('codec_id is 0x10', () => { expect(codec16Handler.codec_id).toBe(0x10); }); it('sets __generation_type on every record', async () => { const fixtureBody = Buffer.from( '10010000016B40D8EA300100000000000000000000000000000004000501010400FF00000001', 'hex', ); const positions: Position[] = []; await codec16Handler.handle(fixtureBody, makeTestCtx(positions)); expect(positions[0]?.attributes['__generation_type']).toBe(5); }); it('does NOT set __generation_type to undefined — key must be present', async () => { // Both records in the canonical fixture have __generation_type=5 const fixtureBody = Buffer.from( '10020000016BDBC7833000000000000000000000000000000000000B05040200010000030002000B00270042563A00000000016BDBC7871800000000000000000000000000000000000B05040200010000030002000B00260042563A000002', 'hex', ); const positions: Position[] = []; await codec16Handler.handle(fixtureBody, makeTestCtx(positions)); for (const pos of positions) { expect('__generation_type' in pos.attributes).toBe(true); } }); it('reads IO IDs greater than 255 correctly', async () => { const fixtureBody = Buffer.from( '10010000016B40D8EA300100000000000000000000000000000004000501010400FF00000001', 'hex', ); const positions: Position[] = []; await codec16Handler.handle(fixtureBody, makeTestCtx(positions)); // IO ID 0x0400 = 1024 must appear in attributes expect(positions[0]?.attributes['1024']).toBe(255); // Event IO ID 0x0400 = 1024 must appear under __event expect(positions[0]?.attributes['__event']).toBe(1024); }); it('logs a debug message for reserved Generation Type 3', async () => { // Build a body with Generation Type = 3 const body = Buffer.from( '10010000016B40D8EA300100000000000000000000000000000000000301010100FF00000001', 'hex', ); const positions: Position[] = []; const ctx = makeTestCtx(positions); const debugFn = ctx.logger.debug as ReturnType; await codec16Handler.handle(body, ctx); expect(positions[0]?.attributes['__generation_type']).toBe(3); // Debug should have been called at least once expect(debugFn).toHaveBeenCalled(); }); });