Tasks 1.1-1.9 marked done with their landing commit SHAs. Tasks 1.10 (observability), 1.12 (production hardening), and 1.13 (device authority) marked paused with explicit resume triggers — pilot deployment on real Teltonika hardware takes priority. Task 1.11 remains as next, in slimmed form for the pilot (no /readyz healthcheck since the metrics endpoint is part of paused 1.10).
7.0 KiB
Task 1.9 — Fixture suite & testing strategy
Phase: 1 — Inbound telemetry
Status: 🟩 Done — landed in commit 381287b
Depends on: 1.1
Wiki refs: docs/wiki/sources/teltonika-ingestion-architecture.md § 5.6, docs/wiki/sources/teltonika-data-sending-protocols.md
Goal
Establish the fixture-based testing infrastructure and seed it with the canonical hex captures from the Teltonika documentation. This is the only place where the parser's correctness is actually verified. Bugs in binary protocol parsers are silent; tests are the defense.
Deliverables
test/fixtures/teltonika/codec8/,test/fixtures/teltonika/codec8e/,test/fixtures/teltonika/codec16/populated with at least:- 3 captures from the canonical Teltonika doc (one per codec, with full parsed expectations).
- 1 synthetic edge case per codec (empty IO bag, max-size IO values, multi-record).
- Each fixture is a pair:
<name>.hex(raw frame, hex-encoded with whitespace stripped) and<name>.expected.json(the expectedPosition[]after parsing). test/fixtures/_loader.ts— helpers:loadFixture(path): { hex: Buffer; expected: Position[] }compareToExpected(actual: Position[], expected: Position[]): void(deep-equals withbigint/Bufferaware comparator).
- A vitest test pattern that automatically picks up every fixture pair in a directory and generates a test per pair. So adding a new fixture file = a new test, no boilerplate.
test/fixtures/teltonika/README.mddocumenting the format and how to add new captures.
Specification
Fixture format
fixture-name.hex:
000000000000003608010000016B40D8EA30
01000000000000000000000000000000010
5021503010101425E0F01F10000601A014E
0000000000000000010000C7CF
Whitespace and newlines are ignored. Implementer strips [^0-9a-fA-F] and parses with Buffer.from(hex, 'hex').
fixture-name.expected.json:
{
"positions": [
{
"device_id": "FIXTURE",
"timestamp": "2019-06-10T10:04:46.000Z",
"latitude": 0,
"longitude": 0,
"altitude": 0,
"angle": 0,
"speed": 0,
"satellites": 0,
"priority": 1,
"attributes": {
"21": 3,
"1": 1,
"66": 24079,
"241": 24602,
"78": "__bigint:0",
"__event": 1
}
}
],
"ack_record_count": 1
}
The __bigint: and __buffer_b64: prefixes are how the JSON file represents the special types. The loader decodes them into real bigint / Buffer instances before comparison.
device_id in fixtures is a placeholder ("FIXTURE") because the captures don't include the IMEI — the codec parsers receive the IMEI from the framing layer's session context, not from the body itself.
Bootstrap fixtures (must be present at end of this task)
From the canonical Teltonika doc (docs/raw/Teltonika Data Sending Protocols - Teltonika Telematics Wiki.md):
Codec 8
01-single-record-all-widths.hex: 1st example — one record with N1=2, N2=1, N4=1, N8=1.02-single-record-reduced.hex: 2nd example — one record with N1=2, N2=1, N4=0, N8=0.03-two-records.hex: 3rd example — two records with minimal IO.
Codec 8 Extended
01-canonical.hex: doc example — one record, N1=1, N2=1, N4=1, N8=2, NX=0.
Codec 16
01-canonical.hex: doc example — two records with Generation Type0x05.
Synthetic fixtures (must be present)
Codec 8
04-empty-io-bag.hex: one record, N=0 (no IO elements). Smallest valid record.05-multi-record-large.hex: 10 records to exercise the loop and N1==N2 invariant.
Codec 8 Extended
02-nx-mixed.hex: one record with NX=3, lengths 1, 8, 64.03-nx-zero-length.hex: one record with one NX entry of length 0.04-nx-large-length.hex: one record with one NX entry of length 300+ (verifies 16-bit length read).
Codec 16
02-each-generation-type.hex: 8 records, one per Generation Type 0–7.03-large-io-id.hex: one record with an IO ID > 255 (e.g.0x0400).
Test runner pattern
In test/codec8.test.ts, codec8e.test.ts, codec16.test.ts:
import { describe, it, expect } from 'vitest';
import { loadFixturesFromDir } from './fixtures/_loader';
import { codec8Handler } from '../src/adapters/teltonika/codec/data/codec8';
describe('Codec 8 parser', () => {
for (const fixture of loadFixturesFromDir('test/fixtures/teltonika/codec8')) {
it(`parses ${fixture.name}`, async () => {
const positions: Position[] = [];
const ctx = makeTestCtx(positions);
const result = await codec8Handler.handle(fixture.body, ctx);
expect(positions).toEqual(fixture.expected.positions);
expect(result.recordCount).toBe(fixture.expected.ack_record_count);
});
}
});
This pattern means adding a new fixture file = a new test, automatically. No editing the test file.
CRC tests
A separate test/crc.test.ts covers crc16Ibm against:
- The canonical doc CRCs (each fixture's CRC computed over the body should match the trailing CRC bytes).
- A few hand-computed reference values (from online CRC-16/IBM calculators, recorded in the test).
- An empty buffer (
crc16Ibm(Buffer.alloc(0))should return0x0000).
Frame tests
test/frame.test.ts:
- IMEI handshake happy path.
- IMEI handshake malformed length.
- Frame envelope: bytes split across multiple
dataevents. - Frame envelope: CRC mismatch path returns the right outcome (no ACK, connection stays open).
- Frame envelope: unknown codec ID drops the connection.
Acceptance criteria
- All bootstrap and synthetic fixtures listed above are present.
pnpm testruns all fixture tests and they pass.pnpm test --coveragereports ≥ 90% line coverage forsrc/adapters/teltonika/codec/.- Adding a new fixture pair to a codec's fixtures directory automatically produces a new test (verified manually by adding a temp fixture).
- The fixture README documents the format clearly enough that a new contributor can add a capture without reading the test code.
Risks / open questions
- Where do real production captures come from? Until devices are streaming to a staging environment, we only have doc captures. Plan: record the first day of staging traffic into
tcpdump-style captures, extract a few representative frames per device model, contribute them as fixtures with the model name in the filename. This step is a follow-up after staging deployment, not a Phase 1 blocker. - Hex format vs binary
.binfiles: hex is reviewable in PRs and documented in the doc. Stick with hex. - Confirming expected outputs: bootstrap fixtures' expected outputs come directly from the canonical doc's parsed tables. Synthetic fixture expectations are computed by hand and double-checked against the parser output once the parser is believed correct — circular if the parser is buggy. Mitigation: cross-check at least one synthetic fixture against an external Teltonika parser (e.g. the open-source Traccar project's Teltonika decoder) before declaring done.
Done
(Fill in once complete.)