Files
tcp-ingestion/.planning/phase-1-telemetry/09-fixture-suite.md
T
julian 90d6a73a60 Sync ROADMAP statuses with landed work; mark 1.10/1.12/1.13 as paused
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).
2026-04-30 16:49:07 +02:00

157 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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 07.
- `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.)