📄 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