@freqhole/api-client 0.1.31 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,631 +0,0 @@
1
- // unit tests for the playlistz domain package.
2
- // pure - no server, no automerge, no browser APIs required.
3
- import {
4
- parsePlaylistDoc,
5
- emptyPlaylistDoc,
6
- type SongEntry,
7
- type ImageRef,
8
- } from "../playlistz/schema.js";
9
- import {
10
- upsertSong,
11
- removeSong,
12
- reorderSongs,
13
- setMetadata,
14
- addImage,
15
- setPrimaryImage,
16
- addPeer,
17
- stampLastSeen,
18
- setAclRole,
19
- tombstone,
20
- } from "../playlistz/mutations.js";
21
- import {
22
- freqholePlaylistToDoc,
23
- spumePlaylistToDoc,
24
- docToFreqholePlaylist,
25
- } from "../playlistz/convert.js";
26
- import {
27
- encodeShareToken,
28
- decodeShareToken,
29
- shareFragment,
30
- } from "../playlistz/shareLink.js";
31
- import {
32
- encodeMessage,
33
- decodeMessage,
34
- ProtocolError,
35
- PLAYLISTZ_ALPN,
36
- AUTOMERGE_ALPN,
37
- type Message,
38
- } from "../playlistz/protocol.js";
39
-
40
- // ---- minimal assertion helpers ----
41
-
42
- function assert(condition: boolean, message: string): void {
43
- if (!condition) throw new Error(message);
44
- }
45
-
46
- function assertEqual<T>(a: T, b: T, label: string): void {
47
- if (a !== b)
48
- throw new Error(`${label}: expected ${JSON.stringify(a)} got ${JSON.stringify(b)}`);
49
- }
50
-
51
- function assertDeepEqual(a: unknown, b: unknown, label: string): void {
52
- const as = JSON.stringify(a);
53
- const bs = JSON.stringify(b);
54
- if (as !== bs)
55
- throw new Error(`${label}: expected ${bs} got ${as}`);
56
- }
57
-
58
- // ---- test runner ----
59
-
60
- export async function runPlaylistzTests(): Promise<{
61
- passed: number;
62
- failed: number;
63
- }> {
64
- let passed = 0;
65
- let failed = 0;
66
-
67
- async function test(name: string, fn: () => void | Promise<void>) {
68
- try {
69
- await fn();
70
- console.log(`✓ ${name}`);
71
- passed++;
72
- } catch (err) {
73
- console.log(
74
- `✗ ${name}: ${err instanceof Error ? err.message : String(err)}`,
75
- );
76
- failed++;
77
- }
78
- }
79
-
80
- console.log("playlistz domain tests\n");
81
-
82
- // ---- schema defaults ----
83
-
84
- await test("emptyPlaylistDoc returns valid doc with defaults", () => {
85
- const doc = emptyPlaylistDoc();
86
- assertEqual(doc.version, 1, "version");
87
- assertEqual(doc.title, "", "title");
88
- assertEqual(doc.description, "", "description");
89
- assert(doc.createdAt.length > 0, "createdAt non-empty");
90
- assertDeepEqual(doc.songs, {}, "songs");
91
- assertDeepEqual(doc.order, [], "order");
92
- assertDeepEqual(doc.peers, {}, "peers");
93
- });
94
-
95
- await test("emptyPlaylistDoc respects init overrides", () => {
96
- const doc = emptyPlaylistDoc({ title: "my list", description: "desc" });
97
- assertEqual(doc.title, "my list", "title");
98
- assertEqual(doc.description, "desc", "description");
99
- });
100
-
101
- await test("parsePlaylistDoc passes through valid data", () => {
102
- const now = new Date().toISOString();
103
- const raw = {
104
- version: 1 as const,
105
- title: "test",
106
- description: "",
107
- createdAt: now,
108
- lastModified: now,
109
- lastModifiedBy: "",
110
- images: [],
111
- urls: [],
112
- songs: {},
113
- order: [],
114
- peers: {},
115
- };
116
- const doc = parsePlaylistDoc(raw);
117
- assertEqual(doc.title, "test", "title");
118
- });
119
-
120
- await test("parsePlaylistDoc degrades gracefully on invalid data", () => {
121
- // invalid data: version wrong type, should fall back to defaults
122
- const doc = parsePlaylistDoc({ version: "bad", title: 999 });
123
- assertEqual(doc.version, 1, "version defaults to 1");
124
- assertEqual(doc.title, "", "title defaults to empty string");
125
- });
126
-
127
- await test("parsePlaylistDoc degrades on null", () => {
128
- const doc = parsePlaylistDoc(null);
129
- assertEqual(doc.version, 1, "version");
130
- });
131
-
132
- // ---- mutations ----
133
-
134
- function makeSong(id: string): SongEntry {
135
- return {
136
- id,
137
- title: `song ${id}`,
138
- artist: "artist",
139
- album: "album",
140
- duration: 180,
141
- mimeType: "audio/mpeg",
142
- fileSize: 1024,
143
- sha256: `sha256-${id}`,
144
- images: [],
145
- urls: [],
146
- };
147
- }
148
-
149
- await test("upsertSong adds song and appends to order", () => {
150
- const doc = emptyPlaylistDoc();
151
- upsertSong(doc, makeSong("a"));
152
- assert("a" in doc.songs, "song a in songs");
153
- assertEqual(doc.order.length, 1, "order length");
154
- assertEqual(doc.order[0], "a", "order[0]");
155
- });
156
-
157
- await test("upsertSong does not duplicate order entry", () => {
158
- const doc = emptyPlaylistDoc();
159
- upsertSong(doc, makeSong("a"));
160
- upsertSong(doc, makeSong("a"));
161
- assertEqual(doc.order.length, 1, "order length stays 1");
162
- });
163
-
164
- await test("upsertSong bumps lastModified", () => {
165
- const doc = emptyPlaylistDoc();
166
- // set a known old timestamp so any call to nowIso() will be strictly after it
167
- doc.lastModified = "2000-01-01T00:00:00.000Z";
168
- upsertSong(doc, makeSong("a"), "node1");
169
- assert(doc.lastModified > "2000-01-01T00:00:00.000Z", "lastModified bumped past sentinel");
170
- assertEqual(doc.lastModifiedBy, "node1", "lastModifiedBy set");
171
- });
172
-
173
- await test("removeSong deletes from songs and order", () => {
174
- const doc = emptyPlaylistDoc();
175
- upsertSong(doc, makeSong("a"));
176
- upsertSong(doc, makeSong("b"));
177
- removeSong(doc, "a");
178
- assert(!("a" in doc.songs), "a removed from songs");
179
- assert(!doc.order.includes("a"), "a removed from order");
180
- assertEqual(doc.order.length, 1, "order length");
181
- assertEqual(doc.order[0], "b", "b remains");
182
- });
183
-
184
- await test("removeSong on missing id is a no-op", () => {
185
- const doc = emptyPlaylistDoc();
186
- removeSong(doc, "nonexistent"); // should not throw
187
- });
188
-
189
- await test("reorderSongs moves song to target index", () => {
190
- const doc = emptyPlaylistDoc();
191
- upsertSong(doc, makeSong("a"));
192
- upsertSong(doc, makeSong("b"));
193
- upsertSong(doc, makeSong("c"));
194
- // move "c" (index 2) to index 0
195
- reorderSongs(doc, "c", 0);
196
- assertEqual(doc.order[0], "c", "c is first");
197
- assertEqual(doc.order[1], "a", "a is second");
198
- assertEqual(doc.order[2], "b", "b is third");
199
- });
200
-
201
- await test("reorderSongs clamps to valid range", () => {
202
- const doc = emptyPlaylistDoc();
203
- upsertSong(doc, makeSong("a"));
204
- upsertSong(doc, makeSong("b"));
205
- // move "a" to index 99 (should clamp to end)
206
- reorderSongs(doc, "a", 99);
207
- assertEqual(doc.order[doc.order.length - 1], "a", "a is last");
208
- });
209
-
210
- await test("reorderSongs ignores missing songId", () => {
211
- const doc = emptyPlaylistDoc();
212
- reorderSongs(doc, "ghost", 0); // should not throw
213
- });
214
-
215
- await test("setMetadata updates title and description", () => {
216
- const doc = emptyPlaylistDoc();
217
- setMetadata(doc, { title: "new title", description: "new desc" });
218
- assertEqual(doc.title, "new title", "title");
219
- assertEqual(doc.description, "new desc", "description");
220
- });
221
-
222
- await test("setMetadata partial update only changes supplied fields", () => {
223
- const doc = emptyPlaylistDoc({ title: "original" });
224
- setMetadata(doc, { description: "only desc" });
225
- assertEqual(doc.title, "original", "title unchanged");
226
- assertEqual(doc.description, "only desc", "description updated");
227
- });
228
-
229
- await test("addImage appends to playlist images", () => {
230
- const doc = emptyPlaylistDoc();
231
- const ref: ImageRef = { blobId: "blob1", isPrimary: false, blobType: "thumbnail" };
232
- addImage(doc, ref);
233
- assertEqual(doc.images.length, 1, "images length");
234
- assertEqual(doc.images[0].blobId, "blob1", "blobId");
235
- });
236
-
237
- await test("addImage with isPrimary clears siblings", () => {
238
- const doc = emptyPlaylistDoc();
239
- addImage(doc, { blobId: "blob1", isPrimary: true, blobType: "original" });
240
- addImage(doc, { blobId: "blob2", isPrimary: true, blobType: "thumbnail" });
241
- // blob1 should no longer be primary
242
- assert(!doc.images[0].isPrimary, "blob1 isPrimary cleared");
243
- assert(doc.images[1].isPrimary, "blob2 is primary");
244
- });
245
-
246
- await test("addImage to song images", () => {
247
- const doc = emptyPlaylistDoc();
248
- upsertSong(doc, makeSong("s1"));
249
- const ref: ImageRef = { blobId: "songblob", isPrimary: false, blobType: "thumbnail" };
250
- addImage(doc, ref, { songId: "s1" });
251
- assertEqual(doc.songs["s1"].images.length, 1, "song images length");
252
- });
253
-
254
- await test("addImage to missing song is a no-op", () => {
255
- const doc = emptyPlaylistDoc();
256
- addImage(doc, { blobId: "x", isPrimary: false, blobType: "original" }, { songId: "ghost" });
257
- // no throw
258
- });
259
-
260
- await test("setPrimaryImage sets exactly one primary", () => {
261
- const doc = emptyPlaylistDoc();
262
- addImage(doc, { blobId: "a", isPrimary: true, blobType: "original" });
263
- addImage(doc, { blobId: "b", isPrimary: false, blobType: "thumbnail" });
264
- setPrimaryImage(doc, "b");
265
- assert(!doc.images[0].isPrimary, "a no longer primary");
266
- assert(doc.images[1].isPrimary, "b is primary");
267
- });
268
-
269
- await test("addPeer is idempotent", () => {
270
- const doc = emptyPlaylistDoc();
271
- addPeer(doc, "node1");
272
- const firstJoinedAt = doc.peers["node1"].joinedAt;
273
- addPeer(doc, "node1"); // second call should not overwrite
274
- assertEqual(doc.peers["node1"].joinedAt, firstJoinedAt, "joinedAt unchanged");
275
- });
276
-
277
- await test("addPeer does not bump lastModified", () => {
278
- const doc = emptyPlaylistDoc();
279
- const before = doc.lastModified;
280
- addPeer(doc, "node1");
281
- assertEqual(doc.lastModified, before, "lastModified not bumped");
282
- });
283
-
284
- await test("stampLastSeen updates peer lastSeenAt", () => {
285
- const doc = emptyPlaylistDoc();
286
- addPeer(doc, "node1");
287
- stampLastSeen(doc, "node1");
288
- assert(doc.peers["node1"].lastSeenAt != null, "lastSeenAt set");
289
- });
290
-
291
- await test("stampLastSeen on unknown peer is a no-op", () => {
292
- const doc = emptyPlaylistDoc();
293
- stampLastSeen(doc, "ghost"); // should not throw
294
- });
295
-
296
- await test("setAclRole creates acl and sets role", () => {
297
- const doc = emptyPlaylistDoc();
298
- setAclRole(doc, "node1", "editor");
299
- assert(doc.acl != null, "acl created");
300
- assertEqual(doc.acl!["node1"].role, "editor", "role");
301
- });
302
-
303
- await test("tombstone sets deleted flag", () => {
304
- const doc = emptyPlaylistDoc();
305
- tombstone(doc);
306
- assert(doc.deleted === true, "deleted is true");
307
- });
308
-
309
- // ---- converters ----
310
-
311
- await test("freqholePlaylistToDoc maps snake_case + is_primary number", () => {
312
- const doc = freqholePlaylistToDoc(
313
- {
314
- id: "fpl1",
315
- title: "FH Playlist",
316
- description: "desc",
317
- is_public: 1,
318
- images: [
319
- { blob_id: "img1", is_primary: 1, blob_type: "original" },
320
- { blob_id: "img2", is_primary: 0, blob_type: "thumbnail" },
321
- ],
322
- urls: [{ id: "u1", name: "site", url: "https://example.com" }],
323
- created_by_id: null,
324
- created_at: 1000,
325
- updated_at: 2000,
326
- deleted_at: null,
327
- deleted_by: null,
328
- created_by: null,
329
- updated_by: null,
330
- song_count: 1,
331
- },
332
- [
333
- {
334
- id: "fs1",
335
- media_blob_id: "mediablob1",
336
- title: "FH Song",
337
- track_number: 1,
338
- disc_number: 1,
339
- duration: 120,
340
- images: [],
341
- urls: [],
342
- bpm: null,
343
- track_artist: "The Artist",
344
- metadata: null,
345
- lyrics: "la la la",
346
- created_at: 1000,
347
- updated_at: 2000,
348
- deleted_at: null,
349
- deleted_by: null,
350
- created_by: null,
351
- updated_by: null,
352
- created_by_username: null,
353
- updated_by_username: null,
354
- play_count: null,
355
- },
356
- ],
357
- );
358
- assertEqual(doc.title, "FH Playlist", "title");
359
- assert(doc.images[0].isPrimary, "first image is primary");
360
- assert(!doc.images[1].isPrimary, "second image not primary");
361
- assertEqual(doc.songs["fs1"].sha256, "mediablob1", "sha256 from media_blob_id");
362
- assertEqual(doc.songs["fs1"].artist, "The Artist", "artist from track_artist");
363
- assertEqual(doc.songs["fs1"].lyrics, "la la la", "lyrics");
364
- });
365
-
366
- await test("spumePlaylistToDoc maps spume field names", () => {
367
- const doc = spumePlaylistToDoc(
368
- {
369
- playlist_id: "spl1",
370
- title: "Spume Playlist",
371
- description: "spume desc",
372
- created_at: 1000,
373
- updated_at: 2000,
374
- images: [
375
- { local_blob_id: "lb1", is_primary: true, blob_type: "original" },
376
- { remote_blob_id: "rb1", is_primary: false, blob_type: "thumbnail" },
377
- ],
378
- },
379
- [
380
- {
381
- id: "ss1",
382
- sha256: "spumehash1",
383
- title: "Spume Song",
384
- artist_name: "Spume Artist",
385
- album_title: "Spume Album",
386
- duration_seconds: 240,
387
- mime_type: "audio/ogg",
388
- file_size: 4096,
389
- blake3: "blake3hash",
390
- lyrics: "words",
391
- },
392
- ],
393
- );
394
- assertEqual(doc.title, "Spume Playlist", "title");
395
- assertEqual(doc.images[0].blobId, "lb1", "local_blob_id mapped");
396
- assertEqual(doc.images[1].blobId, "rb1", "remote_blob_id mapped");
397
- assertEqual(doc.songs["ss1"].sha256, "spumehash1", "sha256");
398
- assertEqual(doc.songs["ss1"].artist, "Spume Artist", "artist_name mapped");
399
- assertEqual(doc.songs["ss1"].album, "Spume Album", "album_title mapped");
400
- assertEqual(doc.songs["ss1"].duration, 240, "duration_seconds mapped");
401
- assertEqual(doc.songs["ss1"].blake3, "blake3hash", "blake3");
402
- });
403
-
404
- await test("spumePlaylistToDoc skips images with no blob id", () => {
405
- const doc = spumePlaylistToDoc(
406
- {
407
- playlist_id: "pl",
408
- title: "T",
409
- images: [{ is_primary: false, blob_type: "original" }],
410
- },
411
- [],
412
- );
413
- assertEqual(doc.images.length, 0, "no images (no blob id)");
414
- });
415
-
416
- await test("docToFreqholePlaylist round-trip preserves core fields", () => {
417
- const original = freqholePlaylistToDoc(
418
- {
419
- id: "x",
420
- title: "RT Playlist",
421
- description: "rt desc",
422
- is_public: 0,
423
- images: [],
424
- urls: [],
425
- created_by_id: null,
426
- created_at: 1000,
427
- updated_at: 2000,
428
- deleted_at: null,
429
- deleted_by: null,
430
- created_by: null,
431
- updated_by: null,
432
- song_count: 0,
433
- },
434
- [],
435
- );
436
- const { playlist } = docToFreqholePlaylist(original);
437
- assertEqual(playlist.title, "RT Playlist", "title round-trips");
438
- assertEqual(playlist.description, "rt desc", "description round-trips");
439
- });
440
-
441
- await test("docToFreqholePlaylist songs preserve title and artist", () => {
442
- const doc = emptyPlaylistDoc({ title: "pl" });
443
- upsertSong(doc, {
444
- id: "s1",
445
- title: "Song Title",
446
- artist: "Artist",
447
- album: "Album",
448
- duration: 100,
449
- mimeType: "audio/mpeg",
450
- fileSize: 512,
451
- sha256: "hash1",
452
- images: [],
453
- urls: [],
454
- });
455
- const { songs } = docToFreqholePlaylist(doc);
456
- assertEqual(songs.length, 1, "one song");
457
- assertEqual(songs[0].title, "Song Title", "title");
458
- assertEqual(songs[0].track_artist, "Artist", "track_artist from artist");
459
- });
460
-
461
- // ---- share token ----
462
-
463
- await test("encodeShareToken + decodeShareToken round-trip", () => {
464
- const payload = { v: 1 as const, n: "a".repeat(64), d: "docid123" };
465
- const token = encodeShareToken(payload);
466
- const decoded = decodeShareToken(token);
467
- assert(decoded !== null, "decoded is not null");
468
- assertEqual(decoded!.n, payload.n, "nodeId");
469
- assertEqual(decoded!.d, payload.d, "docId");
470
- assertEqual(decoded!.v, 1, "version");
471
- });
472
-
473
- await test("shareFragment builds correct fragment", () => {
474
- const payload = { v: 1 as const, n: "n", d: "d", t: "title" };
475
- const frag = shareFragment(payload);
476
- assert(frag.startsWith("#share/"), "starts with #share/");
477
- const decoded = decodeShareToken(frag);
478
- assert(decoded !== null, "fragment decodes");
479
- assertEqual(decoded!.t, "title", "title hint preserved");
480
- });
481
-
482
- await test("decodeShareToken strips #share/ prefix", () => {
483
- const payload = { v: 1 as const, n: "nn", d: "dd" };
484
- const frag = shareFragment(payload);
485
- const decoded = decodeShareToken(frag);
486
- assert(decoded !== null, "decoded");
487
- assertEqual(decoded!.d, "dd", "docId");
488
- });
489
-
490
- await test("decodeShareToken strips share/ prefix", () => {
491
- const payload = { v: 1 as const, n: "nn", d: "dd" };
492
- const token = encodeShareToken(payload);
493
- const decoded = decodeShareToken("share/" + token);
494
- assert(decoded !== null, "decoded");
495
- });
496
-
497
- await test("decodeShareToken strips full url with #share/", () => {
498
- const payload = { v: 1 as const, n: "nn", d: "dd" };
499
- const token = encodeShareToken(payload);
500
- const url = "https://example.com/app#share/" + token;
501
- const decoded = decodeShareToken(url);
502
- assert(decoded !== null, "decoded from full url");
503
- assertEqual(decoded!.n, "nn", "nodeId");
504
- });
505
-
506
- await test("decodeShareToken accepts base64url encoding", () => {
507
- // base64url uses - and _ instead of + and /; also no padding
508
- const payload = { v: 1 as const, n: "nn", d: "dd" };
509
- const token = encodeShareToken(payload);
510
- // tokens from encodeShareToken are already base64url - decoding should work
511
- assert(!token.includes("+"), "no + in token");
512
- assert(!token.includes("/"), "no / in token");
513
- assert(!token.includes("="), "no padding");
514
- const decoded = decodeShareToken(token);
515
- assert(decoded !== null, "decoded");
516
- });
517
-
518
- await test("decodeShareToken trims whitespace", () => {
519
- const payload = { v: 1 as const, n: "n1", d: "d1" };
520
- const token = encodeShareToken(payload);
521
- const decoded = decodeShareToken(" " + token + "\n");
522
- assert(decoded !== null, "decoded with whitespace");
523
- });
524
-
525
- await test("decodeShareToken returns null on garbage", () => {
526
- const result = decodeShareToken("not-a-valid-token!!!");
527
- assert(result === null, "null on garbage");
528
- });
529
-
530
- await test("decodeShareToken returns null on empty string", () => {
531
- assert(decodeShareToken("") === null, "null on empty");
532
- });
533
-
534
- await test("decodeShareToken returns null on missing required fields", () => {
535
- // payload missing 'd'
536
- const partial = btoa(JSON.stringify({ v: 1, n: "nodeonly" }));
537
- assert(decodeShareToken(partial) === null, "null on missing d");
538
- });
539
-
540
- // ---- protocol ----
541
-
542
- await test("ALPN constants are correct", () => {
543
- assertEqual(PLAYLISTZ_ALPN, "freqhole-playlistz/1", "playlistz ALPN");
544
- assertEqual(AUTOMERGE_ALPN, "iroh/automerge-repo/1", "automerge ALPN");
545
- });
546
-
547
- await test("encodeMessage + decodeMessage round-trip for hello", () => {
548
- const msg: Message = { v: 1, type: "hello", nodeId: "abc123" };
549
- const bytes = encodeMessage(msg);
550
- const decoded = decodeMessage(bytes);
551
- assertDeepEqual(decoded, msg, "hello round-trip");
552
- });
553
-
554
- await test("encodeMessage + decodeMessage for playlists response", () => {
555
- const msg: Message = {
556
- v: 1,
557
- type: "playlists",
558
- items: [{ docId: "doc1", title: "My List", songCount: 5 }],
559
- };
560
- const decoded = decodeMessage(encodeMessage(msg));
561
- assertDeepEqual(decoded, msg, "playlists round-trip");
562
- });
563
-
564
- await test("decodeMessage throws ProtocolError on non-JSON bytes", () => {
565
- const bytes = new TextEncoder().encode("not json {{{{");
566
- try {
567
- decodeMessage(bytes);
568
- throw new Error("should have thrown");
569
- } catch (err) {
570
- assert(err instanceof ProtocolError, "ProtocolError thrown");
571
- assertEqual((err as ProtocolError).code, "parse_error", "code");
572
- }
573
- });
574
-
575
- await test("decodeMessage throws ProtocolError on unknown type", () => {
576
- const bytes = new TextEncoder().encode(
577
- JSON.stringify({ v: 1, type: "unknown_type_xyz" }),
578
- );
579
- try {
580
- decodeMessage(bytes);
581
- throw new Error("should have thrown");
582
- } catch (err) {
583
- assert(err instanceof ProtocolError, "ProtocolError thrown");
584
- assertEqual((err as ProtocolError).code, "schema_error", "code");
585
- }
586
- });
587
-
588
- await test("decodeMessage throws ProtocolError on missing required fields", () => {
589
- const bytes = new TextEncoder().encode(JSON.stringify({ v: 1, type: "hello" }));
590
- // hello requires nodeId
591
- try {
592
- decodeMessage(bytes);
593
- throw new Error("should have thrown");
594
- } catch (err) {
595
- assert(err instanceof ProtocolError, "ProtocolError thrown");
596
- }
597
- });
598
-
599
- await test("all message types encode/decode", () => {
600
- const messages: Message[] = [
601
- { v: 1, type: "hello", nodeId: "n1", name: "Alice" },
602
- { v: 1, type: "hello_ok", nodeId: "n2", public: true },
603
- { v: 1, type: "list_playlists" },
604
- { v: 1, type: "playlists", items: [] },
605
- { v: 1, type: "knock", nodeId: "n3", message: "hi" },
606
- {
607
- v: 1,
608
- type: "knock_status",
609
- status: "accepted",
610
- grantedDocIds: ["doc1"],
611
- },
612
- { v: 1, type: "blob_request", sha256: "a".repeat(64) },
613
- {
614
- v: 1,
615
- type: "blob_ready",
616
- sha256: "a".repeat(64),
617
- blake3: "b".repeat(64),
618
- size: 12345,
619
- },
620
- { v: 1, type: "error", code: "not_found", message: "not found" },
621
- ];
622
- for (const msg of messages) {
623
- const decoded = decodeMessage(encodeMessage(msg));
624
- assertDeepEqual(decoded, msg, `round-trip for type=${msg.type}`);
625
- }
626
- });
627
-
628
- // summary
629
- console.log(`\nplaylistz: ${passed} passed, ${failed} failed\n`);
630
- return { passed, failed };
631
- }