Implement Phase 1 tasks 1.5-1.7 + 1.9 (Codec 8/8E/16 parsers + fixture suite)
- Codec 8 parser (1-byte IO IDs, no NX/Generation Type) - Codec 8 Extended parser (2-byte IO IDs + variable-length NX section) - Codec 16 parser (mixed widths + Generation Type, supports IO IDs > 255) - Shared GPS element / timestamp helpers in gps-element.ts - Fixture loader with bigint/Buffer sentinel encoding and auto-discovery - 12 fixture pairs across codec8/8E/16 (canonical doc + synthetic edge cases) - Cross-checked Codec 8 against Traccar's TeltonikaProtocolDecoder (no discrepancies) 26 new tests. Total 62 passing across 10 test files. typecheck/lint/test/build all clean.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user