# 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: `.hex` (raw frame, hex-encoded with whitespace stripped) and `.expected.json` (the expected `Position[]` after parsing). - `test/fixtures/_loader.ts` โ€” helpers: - `loadFixture(path): { hex: Buffer; expected: Position[] }` - `compareToExpected(actual: Position[], expected: Position[]): void` (deep-equals with `bigint`/`Buffer` aware 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.md` documenting 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`: ```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 Type `0x05`. ### 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`: ```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 return `0x0000`). ### Frame tests `test/frame.test.ts`: - IMEI handshake happy path. - IMEI handshake malformed length. - Frame envelope: bytes split across multiple `data` events. - 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 test` runs all fixture tests and they pass. - [ ] `pnpm test --coverage` reports โ‰ฅ 90% line coverage for `src/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 `.bin` files: 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](https://github.com/traccar/traccar) project's Teltonika decoder) before declaring done. ## Done (Fill in once complete.)