flybrain/packages/feed/tests/codec.test.ts
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

152 lines
6.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { FeedCodecError, decodeSnapshot, encodeSnapshot } from '../src/codec';
import type { AttachmentKind, FeedHeader } from '../src/types';
function sampleHeader(attachments: AttachmentKind[]): FeedHeader {
return {
protocol: 1,
seq: 42,
wallMs: 1_700_000_000_000,
status: 'running',
realtimeFactor: 1.0,
uptimeSeconds: 120.5,
runSeconds: 120.5,
brainMs: 120_500,
frame: 3600,
buttons: 0b0001_0001,
rates: { command_0: 12.5, command_1: 6.2, reward_pam: 2.1 },
populationRate: 13.2,
spikeCount: 3,
learning: { enabled: true, updates: 10, changed: 4, synapses: 4_200_000, signal: 0.12 },
game: {
mode: 'OVERWORLD',
semanticRewards: true,
map: 1,
badges: 0,
uniqueLocations: 5,
rewardTotal: 0.42,
rewardCounts: { story: 0, explore: 3, area: 1, pokedex: 0, trainer: 0, wildwin: 1, badge: 0 },
},
milestone: { rank: 2, label: 'Left the house', next: 'Reached Route 1', sinceSeconds: 30, attempts: 0 },
sugar: { active: false, remainingMs: 0, cooldownMs: 0, lastBy: null, todayCount: 1 },
events: [{ id: 1, wallMs: 1_700_000_000_000, brainMs: 120_000, kind: 'reward', label: 'Explored a new room', value: 0.05, rewardKind: 'explore' }],
attachments,
};
}
test('encodeSnapshot/decodeSnapshot round trip with all three attachments', () => {
const header = sampleHeader(['frame', 'audio', 'spikes']);
const frame = new Uint8Array(92_160).fill(7);
const audio = new Uint8Array(11_760).fill(9);
const spikes = new Uint8Array(17_407).fill(0);
spikes[0] = 0b0000_0011;
const encoded = encodeSnapshot(header, { frame, audio, spikes });
const decoded = decodeSnapshot(encoded);
assert.deepEqual(decoded.header, header);
assert.equal(decoded.attachments.size, 3);
assert.deepEqual(decoded.attachments.get('frame'), frame);
assert.deepEqual(decoded.attachments.get('audio'), audio);
assert.deepEqual(decoded.attachments.get('spikes'), spikes);
});
test('encodeSnapshot/decodeSnapshot round trip with no attachments', () => {
const header = sampleHeader([]);
const encoded = encodeSnapshot(header, {});
const decoded = decodeSnapshot(encoded);
assert.deepEqual(decoded.header, header);
assert.equal(decoded.attachments.size, 0);
});
test('encodeSnapshot honours header.attachments order regardless of object key order', () => {
const header = sampleHeader(['spikes', 'frame']);
const spikes = new Uint8Array([1, 2, 3]);
const frame = new Uint8Array([4, 5, 6]);
// Provide attachments in the opposite key order; encoding must still follow header.attachments.
const encoded = encodeSnapshot(header, { frame, spikes });
const decoded = decodeSnapshot(encoded);
assert.deepEqual([...decoded.attachments.keys()], ['spikes', 'frame']);
assert.deepEqual(decoded.attachments.get('spikes'), spikes);
assert.deepEqual(decoded.attachments.get('frame'), frame);
});
test('encodeSnapshot rejects a header.protocol other than 1', () => {
const header = { ...sampleHeader([]), protocol: 2 as unknown as 1 };
assert.throws(() => encodeSnapshot(header, {}), FeedCodecError);
});
test('encodeSnapshot rejects a mismatch between header.attachments and provided attachments', () => {
const header = sampleHeader(['frame']);
assert.throws(() => encodeSnapshot(header, { frame: new Uint8Array(1), audio: new Uint8Array(1) }), FeedCodecError);
assert.throws(() => encodeSnapshot(header, {}), FeedCodecError);
});
test('decodeSnapshot rejects a bad protocol', () => {
const header = sampleHeader([]);
const badHeaderBytes = new TextEncoder().encode(JSON.stringify({ ...header, protocol: 7 }));
const buffer = new Uint8Array(4 + badHeaderBytes.length);
new DataView(buffer.buffer).setUint32(0, badHeaderBytes.length, true);
buffer.set(badHeaderBytes, 4);
assert.throws(() => decodeSnapshot(buffer), /protocol/);
});
test('decodeSnapshot rejects truncation in the header length prefix', () => {
const tooShort = new Uint8Array([1, 2, 3]); // fewer than 4 bytes for the u32 length prefix
assert.throws(() => decodeSnapshot(tooShort), FeedCodecError);
});
test('decodeSnapshot rejects truncation in the header body', () => {
const buffer = new Uint8Array(4 + 2);
new DataView(buffer.buffer).setUint32(0, 100, true); // claims 100 bytes of header, only 2 present
assert.throws(() => decodeSnapshot(buffer), /truncated/);
});
test('decodeSnapshot rejects truncation in an attachment body', () => {
const header = sampleHeader(['frame']);
const encoded = encodeSnapshot(header, { frame: new Uint8Array(10) });
const truncated = encoded.subarray(0, encoded.length - 5); // cut off part of the attachment bytes
assert.throws(() => decodeSnapshot(truncated), /truncated/);
});
test('decodeSnapshot rejects trailing bytes after the last attachment', () => {
const header = sampleHeader(['frame']);
const encoded = encodeSnapshot(header, { frame: new Uint8Array(10) });
const withTrailingJunk = new Uint8Array(encoded.length + 3);
withTrailingJunk.set(encoded);
assert.throws(() => decodeSnapshot(withTrailingJunk), /trailing/);
});
test('decodeSnapshot rejects an attachment list with duplicate kinds', () => {
const header = { ...sampleHeader(['frame']), attachments: ['frame', 'frame'] as AttachmentKind[] };
const headerBytes = new TextEncoder().encode(JSON.stringify(header));
const frameBytes = new Uint8Array(10);
const buffer = new Uint8Array(4 + headerBytes.length + 4 + frameBytes.length);
const view = new DataView(buffer.buffer);
let offset = 0;
view.setUint32(offset, headerBytes.length, true);
offset += 4;
buffer.set(headerBytes, offset);
offset += headerBytes.length;
view.setUint32(offset, frameBytes.length, true);
offset += 4;
buffer.set(frameBytes, offset);
assert.throws(() => decodeSnapshot(buffer), /more than once/);
});
test('decodeSnapshot rejects malformed JSON in the header', () => {
const badJson = new TextEncoder().encode('{not json');
const buffer = new Uint8Array(4 + badJson.length);
new DataView(buffer.buffer).setUint32(0, badJson.length, true);
buffer.set(badJson, 4);
assert.throws(() => decodeSnapshot(buffer), /JSON/);
});