📄 src/models/test/sidecar.test.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 * 
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 * 
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import { describe, it, expect } from 'vitest';
16import {
17    writeSidecar, parseSidecar, colsFromSidecar, extractRecordCols,
18    ColumnType, columnTypeForBits, instanceSpecFromFields,
19    type InstanceFieldSpec, type SidecarInput,
20} from '../enriched-index.ts';
21import { SidecarType } from '../enums.ts';
22import type { InstanceData } from '../interfaces.ts';
23
24// Book's instanceFields as they appear in generated-src/book.json
25const bookInstanceFields = [
26    { name: 'downloadTxId', type: 'string' },
27    { name: 'method', type: 'enum' },
28    { name: 'isAiTranslation', type: 'boolean' },
29    { name: 'aiEngine', type: 'string' },
30    { name: 'addedAt', type: 'number', bits: 32 },
31    { name: 'nbPages', type: 'number', bits: 16 },
32    { name: 'sizeBytes', type: 'number', bits: 32 },
33    { name: 'isbn', type: 'string' },
34    { name: 'publisher', type: 'string' },
35    { name: 'publicationDate', type: 'string' },
36    { name: 'publishedAt', type: 'string' },
37    { name: 'qualityScore', type: 'enum' },
38];
39
40// Bits for the enum fields come from versions.json layout in real usage
41const bookFieldLayouts = { 1: { bits: 3 }, 11: { bits: 3 } };
42
43const bookSpec = instanceSpecFromFields(bookInstanceFields, bookFieldLayouts);
44
45const instance = (txId: string, isDefault = false): InstanceData => ({
46    downloadTxId: txId,
47    method: 1,                       // Scan
48    isAiTranslation: false,
49    aiEngine: null,
50    addedAt: 1717000000,
51    nbPages: 320,
52    sizeBytes: 1048576,
53    isbn: '978-3-16-148410-0',
54    publisher: 'Test Press',
55    publicationDate: '2001',
56    publishedAt: null,
57    qualityScore: 4,                 // VeryGood
58    isDefault,
59});
60
61const extendedFixture = (): SidecarInput => ({
62    sidecarType: SidecarType.Extended,
63    recordCount: 3,
64    columns: [
65        { name: 'ids', type: ColumnType.Uint16, data: new Uint16Array([0, 1, 2]) },
66        { name: 'descriptions', type: ColumnType.NullableString, data: ['first', null, 'third'] },
67        { name: 'textQuality', type: ColumnType.Uint8, data: new Uint8Array([5, 0, 3]) },
68        { name: 'addedYears', type: ColumnType.Uint32, data: new Uint32Array([2024, 2025, 2026]) },
69    ],
70    authorIds: [[7, 9], [], [12]],
71    instanceFields: bookSpec,
72    instances: [
73        [instance('tx-a', true), instance('tx-b')],
74        [],
75        [instance('tx-c', true)],
76    ],
77});
78
79describe('Sidecar wire format (SPEC §4)', () => {
80    it('round-trips header, columns, and structured tails', () => {
81        const buf = writeSidecar(extendedFixture());
82        const parsed = parseSidecar(buf);
83
84        expect(parsed.header.recordCount).toBe(3);
85        expect(parsed.header.sidecarType).toBe(SidecarType.Extended);
86        expect(parsed.header.columnCount).toBe(4);
87
88        expect(Array.from(parsed.columns.get('ids') as Uint16Array)).toEqual([0, 1, 2]);
89        expect(parsed.columns.get('descriptions')).toEqual(['first', null, 'third']);
90        expect(Array.from(parsed.columns.get('textQuality') as Uint8Array)).toEqual([5, 0, 3]);
91        expect(Array.from(parsed.columns.get('addedYears') as Uint32Array)).toEqual([2024, 2025, 2026]);
92
93        expect(parsed.authorIds).toEqual([[7, 9], [], [12]]);
94        expect(parsed.instances![0].length).toBe(2);
95        expect(parsed.instances![0][0]).toEqual(instance('tx-a', true));
96        expect(parsed.instances![1]).toEqual([]);
97        expect(parsed.instances![2][0]).toEqual(instance('tx-c', true));
98    });
99
100    it('embeds the instance field descriptor in the file (self-describing)', () => {
101        const parsed = parseSidecar(writeSidecar(extendedFixture()));
102        const names = parsed.instanceFields!.map(f => f.name);
103        expect(names).toEqual([...bookInstanceFields.map(f => f.name), 'isDefault']);
104        expect(parsed.instanceFields![1]).toEqual({ name: 'method', type: ColumnType.Uint8 });
105        expect(parsed.instanceFields![5]).toEqual({ name: 'nbPages', type: ColumnType.Uint16 });
106        expect(parsed.instanceFields![12]).toEqual({ name: 'isDefault', type: ColumnType.Bool });
107    });
108
109    it('old files survive schema evolution: a file written with FEWER fields still parses', () => {
110        // Simulates a sidecar built before new instanceFields were added.
111        const oldSpec: InstanceFieldSpec[] = [
112            { name: 'downloadTxId', type: ColumnType.NullableString },
113            { name: 'nbPages', type: ColumnType.Uint16 },
114        ];
115        const buf = writeSidecar({
116            sidecarType: SidecarType.Extended,
117            recordCount: 1,
118            columns: [{ name: 'ids', type: ColumnType.Uint16, data: new Uint16Array([0]) }],
119            instanceFields: oldSpec,
120            instances: [[{ downloadTxId: 'old-tx', nbPages: 100, isDefault: true }]],
121        });
122
123        // Parsed with TODAY's code, no schema knowledge needed:
124        const parsed = parseSidecar(buf);
125        expect(parsed.instances![0][0]).toEqual({ downloadTxId: 'old-tx', nbPages: 100, isDefault: true });
126        // Newer fields are simply absent — merge falls back to defaults.
127        expect(parsed.instances![0][0]['qualityScore']).toBeUndefined();
128    });
129
130    it('a hypothetical second multipleInstances model needs zero parser changes', () => {
131        // e.g. a future Recording model with completely different fields
132        const recordingSpec = instanceSpecFromFields([
133            { name: 'streamTxId', type: 'string' },
134            { name: 'durationSec', type: 'number', bits: 32 },
135            { name: 'codec', type: 'enum' },
136            { name: 'lossless', type: 'boolean' },
137        ], { 2: { bits: 2 } });
138
139        const buf = writeSidecar({
140            sidecarType: SidecarType.Extended,
141            recordCount: 1,
142            columns: [{ name: 'ids', type: ColumnType.Uint16, data: new Uint16Array([0]) }],
143            instanceFields: recordingSpec,
144            instances: [[{ streamTxId: 'ar://abc', durationSec: 3600, codec: 2, lossless: true, isDefault: true }]],
145        });
146
147        const parsed = parseSidecar(buf);
148        expect(parsed.instances![0][0]).toEqual({
149            streamTxId: 'ar://abc', durationSec: 3600, codec: 2, lossless: true, isDefault: true,
150        });
151    });
152
153    it('rejects instances without a field spec', () => {
154        expect(() => writeSidecar({
155            sidecarType: SidecarType.Extended,
156            recordCount: 1,
157            columns: [],
158            instances: [[{ x: 1 }]],
159        })).toThrow(/instanceFields spec/);
160    });
161
162    it('slim sidecars carry no structured tails', () => {
163        const buf = writeSidecar({
164            sidecarType: SidecarType.Slim,
165            recordCount: 2,
166            columns: [
167                { name: 'ids', type: ColumnType.Uint16, data: new Uint16Array([0, 1]) },
168                { name: 'titles', type: ColumnType.NullableString, data: ['A', 'B'] },
169            ],
170        });
171        const parsed = parseSidecar(buf);
172        expect(parsed.authorIds).toBeUndefined();
173        expect(parsed.instances).toBeUndefined();
174        expect(parsed.columns.get('titles')).toEqual(['A', 'B']);
175    });
176
177    it('rejects an invalid magic number', () => {
178        const bad = new Uint8Array(16);
179        expect(() => parseSidecar(bad.buffer)).toThrow(/magic/);
180    });
181
182    it('columnTypeForBits derives the narrowest wire type from versions.json bits', () => {
183        expect(columnTypeForBits(1)).toBe(ColumnType.Uint8);
184        expect(columnTypeForBits(3)).toBe(ColumnType.Uint8);
185        expect(columnTypeForBits(8)).toBe(ColumnType.Uint8);
186        expect(columnTypeForBits(16)).toBe(ColumnType.Uint16);
187        expect(columnTypeForBits(32)).toBe(ColumnType.Uint32);
188        expect(columnTypeForBits(0)).toBe(ColumnType.NullableString);
189    });
190
191    it('instanceSpecFromFields maps schema types to wire types', () => {
192        expect(bookSpec[0]).toEqual({ name: 'downloadTxId', type: ColumnType.NullableString });
193        expect(bookSpec[1]).toEqual({ name: 'method', type: ColumnType.Uint8 });        // 3 bits from layout
194        expect(bookSpec[2]).toEqual({ name: 'isAiTranslation', type: ColumnType.Bool });
195        expect(bookSpec[4]).toEqual({ name: 'addedAt', type: ColumnType.Uint32 });      // bits 32
196        expect(bookSpec[5]).toEqual({ name: 'nbPages', type: ColumnType.Uint16 });      // bits 16
197    });
198});
199
200describe('Cols helpers', () => {
201    it('colsFromSidecar flattens columns and tails', () => {
202        const parsed = parseSidecar(writeSidecar(extendedFixture()));
203        const cols = colsFromSidecar(parsed);
204        expect(Array.from(cols['ids'])).toEqual([0, 1, 2]);
205        expect(cols['descriptions']).toEqual(['first', null, 'third']);
206        expect(cols['authorIds']).toEqual([[7, 9], [], [12]]);
207        expect(cols['instances'].length).toBe(3);
208    });
209
210    it('extractRecordCols slices a single record at index 0', () => {
211        const parsed = parseSidecar(writeSidecar(extendedFixture()));
212        const cols = extractRecordCols(parsed, 2);
213        expect(Array.from(cols['ids'])).toEqual([2]);
214        expect(cols['descriptions']).toEqual(['third']);
215        expect(cols['authorIds']).toEqual([[12]]);
216        expect(cols['instances'][0][0]).toEqual(instance('tx-c', true));
217    });
218
219    it('provenance sidecars nest columns under fields', () => {
220        const buf = writeSidecar({
221            sidecarType: SidecarType.Provenance,
222            recordCount: 2,
223            columns: [
224                { name: 'ids', type: ColumnType.Uint16, data: new Uint16Array([0, 1]) },
225                { name: 'textQuality', type: ColumnType.Uint8, data: new Uint8Array([2, 1]) },
226                { name: 'description', type: ColumnType.Uint8, data: new Uint8Array([3, 0]) },
227            ],
228        });
229        const parsed = parseSidecar(buf);
230        const cols = colsFromSidecar(parsed);
231        expect(Array.from(cols['ids'])).toEqual([0, 1]);
232        expect(Array.from(cols['fields']['textQuality'])).toEqual([2, 1]);
233
234        const single = extractRecordCols(parsed, 1);
235        expect(Array.from(single['fields']['description'])).toEqual([0]);
236    });
237});
238