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 { codec8eHandler } from '../src/adapters/teltonika/codec/data/codec8e.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/codec8e'); const fixtures = loadFixturesFromDir(fixtureDir); describe('Codec 8E parser — fixture tests', () => { for (const fixture of fixtures) { it(`parses ${fixture.name}`, async () => { const positions: Position[] = []; const ctx = makeTestCtx(positions); const result = await codec8eHandler.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 — 8E-specific behaviours // --------------------------------------------------------------------------- describe('Codec 8E parser — unit tests', () => { it('codec_id is 0x8E', () => { expect(codec8eHandler.codec_id).toBe(0x8e); }); it('NX values are Buffer instances (not numbers or strings)', async () => { // Use fixture 02 (nx-mixed) directly — just re-load and check types const fixtureBody = Buffer.from( '8E010000016B40D8EA3001000000000000000000000000000000000000030000000000000000000300010001AB00020008DEADBEEF01020304000300406162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA001', 'hex', ); const positions: Position[] = []; await codec8eHandler.handle(fixtureBody, makeTestCtx(positions)); expect(positions).toHaveLength(1); const attrs = positions[0]?.attributes; expect(Buffer.isBuffer(attrs?.['1'])).toBe(true); expect(Buffer.isBuffer(attrs?.['2'])).toBe(true); expect(Buffer.isBuffer(attrs?.['3'])).toBe(true); }); it('NX zero-length value is an empty Buffer', async () => { const fixtureBody = Buffer.from( '8E010000016B40D8EA300000000000000000000000000000000000000001000000000000000000010005000001', 'hex', ); const positions: Position[] = []; await codec8eHandler.handle(fixtureBody, makeTestCtx(positions)); const attrVal = positions[0]?.attributes['5']; expect(Buffer.isBuffer(attrVal)).toBe(true); expect((attrVal as Buffer).length).toBe(0); }); it('does not set __generation_type attribute', async () => { // Codec 8E positions must NOT have __generation_type const fixtureBody = Buffer.from( '8E010000016B412CEE000100000000000000000000000000000000010005000100010100010011001D00010010015E2C880002000B000000003544C87A000E000000001DD7E06A000001', 'hex', ); const positions: Position[] = []; await codec8eHandler.handle(fixtureBody, makeTestCtx(positions)); expect(positions[0]?.attributes['__generation_type']).toBeUndefined(); }); it('throws on cursor invariant violation', async () => { // A body with N1=1 but deliberately short const body = Buffer.from( '8E010000016B412CEE000100000000000000000000000000000000010005000100010100010011001D00010010015E2C880002000B000000003544C87A000E000000001DD7E06A0000', 'hex', ); // The above is missing the last byte (trailing N2), so body.length - 1 won't be reached const positions: Position[] = []; await expect(codec8eHandler.handle(body, makeTestCtx(positions))).rejects.toThrow(); }); });