@serverkgg/bridge 0.0.8 → 0.0.9

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.
Files changed (48) hide show
  1. package/package.json +3 -3
  2. package/src/cli/validate.ts +1 -0
  3. package/src/context/bridgeContext.ts +10 -0
  4. package/src/context/index.ts +1 -0
  5. package/src/core/bridgeDownloadError.ts +10 -0
  6. package/src/core/bridgeHold.ts +12 -0
  7. package/src/core/bridgeKind.ts +2 -0
  8. package/src/core/bridgeUserError.test.ts +23 -0
  9. package/src/core/bridgeUserError.ts +12 -0
  10. package/src/core/index.ts +3 -0
  11. package/src/manifest/index.ts +1 -0
  12. package/src/manifest/manifest.test.ts +85 -0
  13. package/src/manifest/manifest.ts +21 -0
  14. package/src/manifest/presence.ts +48 -0
  15. package/src/manifest/primitives.ts +6 -0
  16. package/src/modules/bridge.ts +45 -2
  17. package/src/modules/bridgeDriver.ts +2 -0
  18. package/src/modules/bridgeModule.ts +25 -3
  19. package/src/panel/bridgeDetail.ts +49 -0
  20. package/src/panel/bridgeLayout.ts +1 -0
  21. package/src/panel/bridgePanel.ts +30 -0
  22. package/src/panel/bridgeUploadMode.ts +4 -0
  23. package/src/panel/index.ts +2 -0
  24. package/src/protocol/bridgeEgress.ts +8 -1
  25. package/src/protocol/bridgeEvents.ts +4 -0
  26. package/src/protocol/bridgeFrame.ts +28 -1
  27. package/src/protocol/bridgeLimits.ts +10 -0
  28. package/src/runtime/bridgeActions.ts +7 -0
  29. package/src/runtime/bridgeDriver.test.ts +477 -2
  30. package/src/runtime/bridgeDriver.ts +116 -3
  31. package/src/runtime/bridgeEgress.ts +3 -0
  32. package/src/runtime/bridgeEvents.ts +7 -5
  33. package/src/runtime/bridgeFiles.test.ts +625 -15
  34. package/src/runtime/bridgeFiles.ts +152 -45
  35. package/src/runtime/bridgeInstall.test.ts +118 -0
  36. package/src/runtime/bridgeInstall.ts +55 -0
  37. package/src/runtime/bridgeNet.ts +32 -3
  38. package/src/runtime/bridgePending.test.ts +148 -0
  39. package/src/runtime/bridgePending.ts +43 -0
  40. package/src/runtime/bridgeRegistry.test.ts +134 -0
  41. package/src/runtime/bridgeRegistry.ts +26 -4
  42. package/src/runtime/bridgeServer.test.ts +338 -0
  43. package/src/runtime/bridgeServer.ts +25 -3
  44. package/src/runtime/bridgeSupervisor.ts +18 -0
  45. package/src/runtime/bridgeZip.ts +179 -58
  46. package/src/runtime/serve.ts +64 -4
  47. package/src/wire/panel.test.ts +166 -0
  48. package/src/wire/panel.ts +32 -0
@@ -0,0 +1,148 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BRIDGE_PENDING_FILES_LIMIT, type BridgePendingFile } from "../protocol";
3
+ import type { BridgeLeveledLogger } from "./bridgeLog";
4
+ import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
5
+
6
+ const file = (id: string): BridgePendingFile => ({
7
+ id,
8
+ title: {
9
+ ar: id,
10
+ en: id,
11
+ },
12
+ fileName: `${id}.jar`,
13
+ sizeBytes: null,
14
+ digest: null,
15
+ sourceUrl: null,
16
+ note: null,
17
+ });
18
+
19
+ const files = (count: number) => {
20
+ return Array.from({
21
+ length: count,
22
+ }).map((_entry, index) => file(`file-${index}`));
23
+ };
24
+
25
+ const recorder = () => {
26
+ const warnings: string[] = [];
27
+
28
+ const onLog: BridgeLeveledLogger = Object.assign(
29
+ () => {
30
+ return;
31
+ },
32
+ {
33
+ warn: (message: string) => {
34
+ warnings.push(message);
35
+ },
36
+ error: () => {
37
+ return;
38
+ },
39
+ },
40
+ );
41
+
42
+ return {
43
+ onLog,
44
+ warnings,
45
+ };
46
+ };
47
+
48
+ describe("unsupportedPending", () => {
49
+ test("answers the sentinel a driver without a pending module gets", () => {
50
+ expect(unsupportedPending()).toEqual({
51
+ supported: false,
52
+ staging: "",
53
+ extensions: [],
54
+ files: [],
55
+ rejected: null,
56
+ });
57
+ });
58
+ });
59
+
60
+ describe("pendingResult", () => {
61
+ test("carries the staging path, the extensions and the files the driver listed", () => {
62
+ const { onLog, warnings } = recorder();
63
+
64
+ expect(
65
+ pendingResult(
66
+ ".serverk-staging/pending-upload",
67
+ [
68
+ "jar",
69
+ ],
70
+ files(2),
71
+ onLog,
72
+ ),
73
+ ).toEqual({
74
+ supported: true,
75
+ staging: ".serverk-staging/pending-upload",
76
+ extensions: [
77
+ "jar",
78
+ ],
79
+ files: files(2),
80
+ rejected: null,
81
+ });
82
+
83
+ expect(warnings).toEqual([]);
84
+ });
85
+
86
+ test("truncates a list longer than the platform takes and warns instead of throwing", () => {
87
+ const { onLog, warnings } = recorder();
88
+
89
+ const result = pendingResult(
90
+ ".serverk-staging/pending-upload",
91
+ [
92
+ "jar",
93
+ ],
94
+ files(BRIDGE_PENDING_FILES_LIMIT + 5),
95
+ onLog,
96
+ );
97
+
98
+ expect(result.files).toHaveLength(BRIDGE_PENDING_FILES_LIMIT);
99
+ expect(result.files.at(0)?.id).toBe("file-0");
100
+ expect(warnings).toEqual([
101
+ "pending list truncated",
102
+ ]);
103
+ });
104
+ });
105
+
106
+ describe("rejectedPending", () => {
107
+ test("names every file still pending as what the upload was expected to be", () => {
108
+ const { onLog } = recorder();
109
+
110
+ const result = rejectedPending(
111
+ pendingResult(
112
+ ".serverk-staging/pending-upload",
113
+ [
114
+ "jar",
115
+ ],
116
+ files(2),
117
+ onLog,
118
+ ),
119
+ );
120
+
121
+ expect(result.rejected).toEqual({
122
+ expected: [
123
+ "file-0.jar",
124
+ "file-1.jar",
125
+ ],
126
+ });
127
+ expect(result.files).toHaveLength(2);
128
+ });
129
+
130
+ test("leaves an empty expectation when the driver has nothing pending left", () => {
131
+ const { onLog } = recorder();
132
+
133
+ expect(
134
+ rejectedPending(
135
+ pendingResult(
136
+ ".serverk-staging/pending-upload",
137
+ [
138
+ "jar",
139
+ ],
140
+ [],
141
+ onLog,
142
+ ),
143
+ ).rejected,
144
+ ).toEqual({
145
+ expected: [],
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,43 @@
1
+ import { BRIDGE_PENDING_FILES_LIMIT, type BridgePendingFile, type BridgePendingResult } from "../protocol";
2
+ import type { BridgeLeveledLogger } from "./bridgeLog";
3
+
4
+ export const unsupportedPending = (): BridgePendingResult => {
5
+ return {
6
+ supported: false,
7
+ staging: "",
8
+ extensions: [],
9
+ files: [],
10
+ rejected: null,
11
+ };
12
+ };
13
+
14
+ export const pendingResult = (
15
+ staging: string,
16
+ extensions: string[],
17
+ files: BridgePendingFile[],
18
+ onLog: BridgeLeveledLogger,
19
+ ): BridgePendingResult => {
20
+ if (files.length > BRIDGE_PENDING_FILES_LIMIT) {
21
+ onLog.warn("pending list truncated", {
22
+ files: files.length,
23
+ limit: BRIDGE_PENDING_FILES_LIMIT,
24
+ });
25
+ }
26
+
27
+ return {
28
+ supported: true,
29
+ staging,
30
+ extensions,
31
+ files: files.slice(0, BRIDGE_PENDING_FILES_LIMIT),
32
+ rejected: null,
33
+ };
34
+ };
35
+
36
+ export const rejectedPending = (result: BridgePendingResult): BridgePendingResult => {
37
+ return {
38
+ ...result,
39
+ rejected: {
40
+ expected: result.files.map((file) => file.fileName),
41
+ },
42
+ };
43
+ };
@@ -426,3 +426,137 @@ describe("preview modules", () => {
426
426
  );
427
427
  });
428
428
  });
429
+
430
+ describe("detail modules", () => {
431
+ test("lists read and its own action ids in the manifest", () => {
432
+ const registry = registryFor({
433
+ ...baseDriver(),
434
+ modules: {
435
+ modpackStatus: {
436
+ kind: BridgeKind.Detail,
437
+ read: async () => null,
438
+ actions: {
439
+ update: async () => null,
440
+ },
441
+ },
442
+ },
443
+ });
444
+
445
+ expect(registry.manifest().modules).toEqual([
446
+ {
447
+ id: "modpackStatus",
448
+ kind: BridgeKind.Detail,
449
+ requiresRunning: false,
450
+ refreshSeconds: null,
451
+ pageSize: null,
452
+ dependsOn: [],
453
+ actions: [
454
+ BridgeAction.Read,
455
+ "update",
456
+ ],
457
+ },
458
+ ]);
459
+ });
460
+
461
+ test("returns what read answers", async () => {
462
+ const registry = registryFor({
463
+ ...baseDriver(),
464
+ modules: {
465
+ modpackStatus: {
466
+ kind: BridgeKind.Detail,
467
+ read: async () => ({
468
+ id: "modrinth:abc",
469
+ title: "Better MC",
470
+ subtitle: "1.21.1 · NeoForge",
471
+ description: null,
472
+ image: null,
473
+ badges: [],
474
+ stats: [],
475
+ links: [],
476
+ stale: false,
477
+ actions: [],
478
+ }),
479
+ },
480
+ },
481
+ });
482
+
483
+ expect(await registry.invoke("modpackStatus", BridgeAction.Read)).toMatchObject({
484
+ id: "modrinth:abc",
485
+ title: "Better MC",
486
+ });
487
+ });
488
+
489
+ test("hands the argument bag to an action and returns its outcome", async () => {
490
+ const received: BridgeValues[] = [];
491
+
492
+ const registry = registryFor({
493
+ ...baseDriver(),
494
+ modules: {
495
+ modpackStatus: {
496
+ kind: BridgeKind.Detail,
497
+ read: async () => null,
498
+ actions: {
499
+ update: async (_context, args) => {
500
+ received.push(args);
501
+
502
+ return {
503
+ variables: {
504
+ MODPACK: "modrinth:abc:v2",
505
+ },
506
+ };
507
+ },
508
+ },
509
+ },
510
+ },
511
+ });
512
+
513
+ expect(
514
+ await registry.invoke("modpackStatus", "update", {
515
+ id: "modrinth:abc",
516
+ }),
517
+ ).toEqual({
518
+ variables: {
519
+ MODPACK: "modrinth:abc:v2",
520
+ },
521
+ });
522
+
523
+ expect(received).toEqual([
524
+ {
525
+ id: "modrinth:abc",
526
+ },
527
+ ]);
528
+ });
529
+
530
+ test("refuses an action named after a read", () => {
531
+ expect(() =>
532
+ registryFor({
533
+ ...baseDriver(),
534
+ modules: {
535
+ modpackStatus: {
536
+ kind: BridgeKind.Detail,
537
+ read: async () => null,
538
+ actions: {
539
+ read: async () => null,
540
+ },
541
+ },
542
+ },
543
+ }),
544
+ ).toThrow('module "modpackStatus" declares action "read", which is reserved for reads');
545
+ });
546
+
547
+ test("refuses an action the module does not answer", async () => {
548
+ const registry = registryFor({
549
+ ...baseDriver(),
550
+ modules: {
551
+ modpackStatus: {
552
+ kind: BridgeKind.Detail,
553
+ read: async () => null,
554
+ },
555
+ },
556
+ });
557
+
558
+ await expect(registry.invoke("modpackStatus", "update")).rejects.toThrow(
559
+ 'module "modpackStatus" does not support "update"',
560
+ );
561
+ });
562
+ });
@@ -18,7 +18,11 @@ interface CacheEntry {
18
18
  }
19
19
 
20
20
  const requiresRunning = (module: BridgePanelModule) => {
21
- if (module.kind === BridgeKind.Actions || module.kind === BridgeKind.Collection) {
21
+ if (
22
+ module.kind === BridgeKind.Actions
23
+ || module.kind === BridgeKind.Collection
24
+ || module.kind === BridgeKind.Detail
25
+ ) {
22
26
  return module.requiresRunning ?? false;
23
27
  }
24
28
 
@@ -26,7 +30,11 @@ const requiresRunning = (module: BridgePanelModule) => {
26
30
  };
27
31
 
28
32
  const refreshOf = (module: BridgePanelModule) => {
29
- return module.kind === BridgeKind.Collection ? (module.refreshSeconds ?? null) : null;
33
+ if (module.kind === BridgeKind.Collection || module.kind === BridgeKind.Detail) {
34
+ return module.refreshSeconds ?? null;
35
+ }
36
+
37
+ return null;
30
38
  };
31
39
 
32
40
  const dependsOf = (module: BridgePanelModule) => {
@@ -138,11 +146,11 @@ export const createRegistry = (
138
146
  const modules = driver.modules ?? {};
139
147
 
140
148
  for (const [id, module] of Object.entries(modules)) {
141
- if (module.kind !== BridgeKind.Actions) {
149
+ if (module.kind !== BridgeKind.Actions && module.kind !== BridgeKind.Detail) {
142
150
  continue;
143
151
  }
144
152
 
145
- for (const action of Object.keys(module.actions)) {
153
+ for (const action of Object.keys(module.actions ?? {})) {
146
154
  if (isReadAction(action)) {
147
155
  throw new BridgeDriverError(
148
156
  `module "${id}" declares action "${action}", which is reserved for reads — pick another id`,
@@ -290,6 +298,20 @@ export const createRegistry = (
290
298
  break;
291
299
  }
292
300
 
301
+ case BridgeKind.Detail: {
302
+ if (action === BridgeAction.Read) {
303
+ return await module.read(context);
304
+ }
305
+
306
+ const detailAction = module.actions?.[action];
307
+
308
+ if (detailAction) {
309
+ return await detailAction(context, args);
310
+ }
311
+
312
+ break;
313
+ }
314
+
293
315
  case BridgeKind.Catalog: {
294
316
  if (action === BridgeAction.Search) {
295
317
  return await module.search(context, {
@@ -0,0 +1,338 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { BridgeDesired, BridgeFailureCode, BridgeFailureError, BridgeRuntime, BridgeUserError } from "../core";
6
+ import { BridgeMethod, type BridgePendingResult, type BridgeResponse } from "../protocol";
7
+ import { createServer, type ServerOptions } from "./bridgeServer";
8
+
9
+ const TOKEN = "test-token";
10
+
11
+ const unreachable = (what: string) => {
12
+ return () => {
13
+ throw new Error(`${what} is not part of this test`);
14
+ };
15
+ };
16
+
17
+ const baseOptions = (socket: string): ServerOptions => ({
18
+ socket,
19
+ token: TOKEN,
20
+ onStatus: () => ({
21
+ version: "0.0.0",
22
+ runtime: BridgeRuntime.Stopped,
23
+ desired: BridgeDesired.Stopped,
24
+ exitCode: null,
25
+ outOfMemory: false,
26
+ restarts: 0,
27
+ installError: null,
28
+ installHold: null,
29
+ }),
30
+ onManifest: unreachable("manifest"),
31
+ onStart: unreachable("start"),
32
+ onStop: unreachable("stop"),
33
+ onCommand: unreachable("command"),
34
+ onInvoke: unreachable("invoke"),
35
+ onEvents: unreachable("events"),
36
+ onSample: unreachable("sample"),
37
+ onQuiesce: unreachable("quiesce"),
38
+ onRelease: unreachable("release"),
39
+ onDescribe: unreachable("describe"),
40
+ onAnnounce: unreachable("announce"),
41
+ onPendingList: unreachable("pendingList"),
42
+ onPendingAdd: unreachable("pendingAdd"),
43
+ onLog: () => {
44
+ return;
45
+ },
46
+ });
47
+
48
+ const call = async (socket: string, frame: Record<string, unknown>): Promise<BridgeResponse> => {
49
+ return await new Promise<BridgeResponse>((resolve, reject) => {
50
+ void Bun.connect<{
51
+ buffer: string;
52
+ }>({
53
+ unix: socket,
54
+ socket: {
55
+ open(connection) {
56
+ connection.data = {
57
+ buffer: "",
58
+ };
59
+
60
+ connection.write(`${JSON.stringify(frame)}\n`);
61
+ },
62
+
63
+ data(connection, chunk) {
64
+ connection.data.buffer += chunk.toString();
65
+
66
+ const [line] = connection.data.buffer.split("\n");
67
+
68
+ if (!line) {
69
+ return;
70
+ }
71
+
72
+ connection.end();
73
+
74
+ resolve(JSON.parse(line) as BridgeResponse);
75
+ },
76
+
77
+ error(_connection, error) {
78
+ reject(error);
79
+ },
80
+ },
81
+ }).catch(reject);
82
+ });
83
+ };
84
+
85
+ describe("pending dispatch", () => {
86
+ let dir = "";
87
+ let socket = "";
88
+
89
+ beforeEach(async () => {
90
+ dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
91
+ socket = join(dir, "bridge.sock");
92
+ });
93
+
94
+ afterEach(async () => {
95
+ await rm(dir, {
96
+ force: true,
97
+ recursive: true,
98
+ });
99
+ });
100
+
101
+ const listed: BridgePendingResult = {
102
+ supported: true,
103
+ staging: ".serverk-staging/pending-upload",
104
+ extensions: [
105
+ "jar",
106
+ ],
107
+ files: [],
108
+ rejected: null,
109
+ };
110
+
111
+ test("answers pendingList with what the driver lists", async () => {
112
+ const server = createServer({
113
+ ...baseOptions(socket),
114
+ onPendingList: async () => listed,
115
+ });
116
+
117
+ const response = await call(socket, {
118
+ id: 1,
119
+ token: TOKEN,
120
+ method: BridgeMethod.PendingList,
121
+ });
122
+
123
+ server.stop(true);
124
+
125
+ expect(response).toEqual({
126
+ id: 1,
127
+ ok: true,
128
+ result: listed,
129
+ });
130
+ });
131
+
132
+ test("answers pendingList with the unsupported sentinel when the driver declares no pending module", async () => {
133
+ const server = createServer({
134
+ ...baseOptions(socket),
135
+ onPendingList: async () => ({
136
+ supported: false,
137
+ staging: "",
138
+ extensions: [],
139
+ files: [],
140
+ rejected: null,
141
+ }),
142
+ });
143
+
144
+ const response = await call(socket, {
145
+ id: 2,
146
+ token: TOKEN,
147
+ method: BridgeMethod.PendingList,
148
+ });
149
+
150
+ server.stop(true);
151
+
152
+ expect(response).toEqual({
153
+ id: 2,
154
+ ok: true,
155
+ result: {
156
+ supported: false,
157
+ staging: "",
158
+ extensions: [],
159
+ files: [],
160
+ rejected: null,
161
+ },
162
+ });
163
+ });
164
+
165
+ test("passes the uploaded path to pendingAdd and answers with the refreshed list", async () => {
166
+ const received: string[] = [];
167
+
168
+ const server = createServer({
169
+ ...baseOptions(socket),
170
+ onPendingAdd: async (input) => {
171
+ received.push(input);
172
+
173
+ return listed;
174
+ },
175
+ });
176
+
177
+ const response = await call(socket, {
178
+ id: 3,
179
+ token: TOKEN,
180
+ method: BridgeMethod.PendingAdd,
181
+ input: ".serverk-staging/pending-upload/patch.jar",
182
+ });
183
+
184
+ server.stop(true);
185
+
186
+ expect(received).toEqual([
187
+ ".serverk-staging/pending-upload/patch.jar",
188
+ ]);
189
+ expect(response).toEqual({
190
+ id: 3,
191
+ ok: true,
192
+ result: listed,
193
+ });
194
+ });
195
+
196
+ test("refuses a pendingAdd carrying no path", async () => {
197
+ const server = createServer({
198
+ ...baseOptions(socket),
199
+ onPendingAdd: unreachable("pendingAdd"),
200
+ });
201
+
202
+ const response = await call(socket, {
203
+ id: 4,
204
+ token: TOKEN,
205
+ method: BridgeMethod.PendingAdd,
206
+ });
207
+
208
+ server.stop(true);
209
+
210
+ expect(response).toEqual({
211
+ id: 4,
212
+ ok: false,
213
+ error: "pendingAdd needs an input path",
214
+ });
215
+ });
216
+
217
+ test("answers pendingAdd with the rejection the driver reported beside the refreshed list", async () => {
218
+ const rejected: BridgePendingResult = {
219
+ ...listed,
220
+ rejected: {
221
+ expected: [
222
+ "patch.jar",
223
+ ],
224
+ },
225
+ };
226
+
227
+ const server = createServer({
228
+ ...baseOptions(socket),
229
+ onPendingAdd: async () => rejected,
230
+ });
231
+
232
+ const response = await call(socket, {
233
+ id: 6,
234
+ token: TOKEN,
235
+ method: BridgeMethod.PendingAdd,
236
+ input: ".serverk-staging/pending-upload/wrong.jar",
237
+ });
238
+
239
+ server.stop(true);
240
+
241
+ expect(response).toEqual({
242
+ id: 6,
243
+ ok: true,
244
+ result: rejected,
245
+ });
246
+ });
247
+
248
+ test("names an unknown method so an older agent can degrade", async () => {
249
+ const server = createServer(baseOptions(socket));
250
+
251
+ const response = await call(socket, {
252
+ id: 5,
253
+ token: TOKEN,
254
+ method: "pendingRemove",
255
+ });
256
+
257
+ server.stop(true);
258
+
259
+ expect(response).toEqual({
260
+ id: 5,
261
+ ok: false,
262
+ error: 'unknown method "pendingRemove"',
263
+ });
264
+ });
265
+ });
266
+
267
+ describe("error serialisation", () => {
268
+ let dir = "";
269
+ let socket = "";
270
+
271
+ beforeEach(async () => {
272
+ dir = await mkdtemp(join(tmpdir(), "bridge-server-"));
273
+ socket = join(dir, "bridge.sock");
274
+ });
275
+
276
+ afterEach(async () => {
277
+ await rm(dir, {
278
+ force: true,
279
+ recursive: true,
280
+ });
281
+ });
282
+
283
+ const thrown = async (error: unknown) => {
284
+ const server = createServer({
285
+ ...baseOptions(socket),
286
+ onPendingAdd: async () => {
287
+ throw error;
288
+ },
289
+ });
290
+
291
+ const response = await call(socket, {
292
+ id: 1,
293
+ token: TOKEN,
294
+ method: BridgeMethod.PendingAdd,
295
+ input: ".serverk-staging/pending-upload/patch.jar",
296
+ });
297
+
298
+ server.stop(true);
299
+
300
+ return response;
301
+ };
302
+
303
+ test("carries the localized text of a user error beside the message", async () => {
304
+ expect(
305
+ await thrown(
306
+ new BridgeUserError({
307
+ ar: "هذا مو الملف اللي ننتظره",
308
+ en: "this is not a file we are waiting for",
309
+ }),
310
+ ),
311
+ ).toEqual({
312
+ id: 1,
313
+ ok: false,
314
+ error: "this is not a file we are waiting for",
315
+ text: {
316
+ ar: "هذا مو الملف اللي ننتظره",
317
+ en: "this is not a file we are waiting for",
318
+ },
319
+ });
320
+ });
321
+
322
+ test("carries the failure code of a failure error and no text", async () => {
323
+ expect(await thrown(new BridgeFailureError(BridgeFailureCode.CatalogRestricted, "the author blocked it"))).toEqual({
324
+ id: 1,
325
+ ok: false,
326
+ error: "the author blocked it",
327
+ failure: BridgeFailureCode.CatalogRestricted,
328
+ });
329
+ });
330
+
331
+ test("carries the message alone for an ordinary error", async () => {
332
+ expect(await thrown(new Error("the file could not be read"))).toEqual({
333
+ id: 1,
334
+ ok: false,
335
+ error: "the file could not be read",
336
+ });
337
+ });
338
+ });