nervur 0.20.3 → 0.21.0

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.
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The few assertions a scene needs, with no platform under them, so the
3
+ // same scene runs in Node's runner and inside any other engine.
4
+ export type Expect = {
5
+ equal(actual: unknown, expected: unknown, message?: string): void;
6
+ same(actual: unknown, expected: unknown, message?: string): void;
7
+ ok(value: unknown, message?: string): void;
8
+ };
9
+
10
+ const fail = (message: string | undefined, detail: string): never => {
11
+ throw new Error(`${message ? `${message}: ` : ''}${detail}`);
12
+ };
13
+
14
+ export const expect: Expect = {
15
+ equal: (actual, expected, message) => {
16
+ if (!Object.is(actual, expected)) fail(message, `${String(actual)} is not ${String(expected)}`);
17
+ },
18
+ same: (actual, expected, message) => {
19
+ const [a, b] = [JSON.stringify(actual), JSON.stringify(expected)];
20
+ if (a !== b) fail(message, `${a} is not ${b}`);
21
+ },
22
+ ok: (value, message) => {
23
+ if (!value) fail(message, 'not true');
24
+ },
25
+ };
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur/proof`: the suites a body of a stranger's passes to be supported,
3
+ // the kit's own. Each is a list of scenes, a title and a function, run by
4
+ // whatever runner the stranger keeps, in the engine the body is for:
5
+ //
6
+ // for (const [title, scene] of memoryScenes('S3Memory', make, expect, half)) test(title, scene);
7
+ //
8
+ // `expect` throws on the first assertion that fails. The law's scenes stand
9
+ // harbors on a stranger's bodies and hold the unpacking law over them.
10
+ export { entropyScenes, custodyScenes, loaderScenes, memoryScenes, type Cut, type Scene } from './contracts.ts';
11
+ export { expect, type Expect } from './expect.ts';
12
+ export { law, lawScenes, type Bodies, type Terrain } from './law.ts';
@@ -0,0 +1,352 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The unpacking law, as scenes any terrain's bodies run: a harbor is one
3
+ // package. Only the seed and the terrain's bodies stand outside it, the
4
+ // terrain keeps sealed bytes it cannot read, and the seed opens the memory,
5
+ // the dock stands the catalogue, the catalogue loads the modules her cells
6
+ // name, the faculties and every ward stand from cells, and the dock lends.
7
+ // It names no runtime, so it bundles.
8
+ import { Being, Faculty, type Asker, type Invitation, type JsonObject } from '../being/index.ts';
9
+ import { Memory, type Custody, type Module } from '../contract/index.ts';
10
+ import { copyPackage, rootLine, type Harbor } from '../harbor/index.ts';
11
+ import { CryptoEntropy, HeldLoader, World } from '../pointer/index.ts';
12
+ import type { Expect } from './expect.ts';
13
+
14
+ // The bodies a harbor stands on, and the same keeping as a new process
15
+ // would build it.
16
+ export type Bodies = { custody: Custody; memory: Memory };
17
+ export type Terrain = { name: string; make(): Bodies; again(bodies: Bodies): Bodies };
18
+
19
+ abstract class Echoes extends Faculty {
20
+ static override kind = 'org.example.echoes';
21
+ }
22
+ class Echo extends Echoes {
23
+ static override kind = 'org.example.echo';
24
+ static override asks = { echo: {} };
25
+ echo(args: JsonObject, asker: Asker): JsonObject {
26
+ return { echoed: args.text ?? null, to: asker.id ?? null };
27
+ }
28
+ }
29
+
30
+ class Shop extends Being {
31
+ static override kind = 'org.example.shop';
32
+ static override cells = { sold: [] as string[] };
33
+ static override asks = { buy: {} };
34
+ buy(args: JsonObject, asker: Asker): JsonObject {
35
+ (this.cells.sold as string[]).push(`${String(args.item)} to ${asker.id ?? 'nobody'}`);
36
+ return { bought: args.item ?? null };
37
+ }
38
+ }
39
+
40
+ class Customer extends Being {
41
+ static override kind = 'org.example.customer';
42
+ static override asks = { join: {}, buy: {}, echo: {}, spoil: {} };
43
+ spoil(): JsonObject {
44
+ try {
45
+ this.cells.when = new Date() as unknown as string;
46
+ return { kept: true };
47
+ } catch {
48
+ return { refused: true };
49
+ }
50
+ }
51
+ async join(args: JsonObject): Promise<JsonObject> {
52
+ return { joined: await this.stance.standings.take('shop', args.invitation as Invitation) };
53
+ }
54
+ async buy(args: JsonObject): Promise<JsonObject> {
55
+ return { said: ((await this.stance.standings.get('shop')?.ask('buy', args)) ?? null) as JsonObject };
56
+ }
57
+ async echo(args: JsonObject): Promise<JsonObject> {
58
+ if (!this.stance.standings.get('echo')) await this.stance.lend(Echoes, 'echo');
59
+ return { said: ((await this.stance.standings.get('echo')?.ask('echo', args)) ?? null) as JsonObject };
60
+ }
61
+ }
62
+
63
+ // One set of classes for every scene in a bundle, since a bundler renames
64
+ // a second class of the same name and a row resolves a class by its name.
65
+ export const classes = [Echo, Shop, Customer];
66
+ export const law: Module = { module: 'org.example.law', version: '1', classes };
67
+
68
+ // A harbor of the world whose catalogue runs the law's module.
69
+ export const standOne = async (world: World, name: string, parts: Partial<Bodies> = {}): Promise<Harbor> => {
70
+ const harbor = await world.harbor(name, [law], parts);
71
+ await harbor.root('ask', { being: 'catalogue', method: 'add', args: { from: law.module } });
72
+ return harbor;
73
+ };
74
+ const CATALOGUE = { catalogue: { class: 'org.nervur.catalogue', public: false, absent: false } };
75
+ const NONE = { wards: {}, routes: {} };
76
+
77
+ // Every byte a memory is handed, seen from outside it.
78
+ class Watched extends Memory {
79
+ readonly inner: Memory;
80
+ readonly seen: { place: string; name: string; bytes: Uint8Array | null }[] = [];
81
+ constructor(inner: Memory) {
82
+ super();
83
+ this.inner = inner;
84
+ }
85
+ read(place: string): Promise<Map<string, Uint8Array>> {
86
+ return this.inner.read(place);
87
+ }
88
+ write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void> {
89
+ for (const [name, bytes] of entries) this.seen.push({ place, name, bytes: bytes && bytes.slice() });
90
+ return this.inner.write(place, entries);
91
+ }
92
+ places(): Promise<string[]> {
93
+ return this.inner.places();
94
+ }
95
+ forget(place: string): Promise<void> {
96
+ return this.inner.forget(place);
97
+ }
98
+ }
99
+
100
+ const utf8 = (text: string): Uint8Array => new TextEncoder().encode(text);
101
+ const holds = (bytes: Uint8Array, part: Uint8Array): boolean => {
102
+ outer: for (let at = 0; at + part.length <= bytes.length; at += 1) {
103
+ for (let k = 0; k < part.length; k += 1) if (bytes[at + k] !== part[k]) continue outer;
104
+ return true;
105
+ }
106
+ return false;
107
+ };
108
+ const hex = (bytes: Uint8Array): string => [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
109
+
110
+ const census = async (harbor: Harbor, ward?: string): Promise<unknown> => {
111
+ const out = (await (ward === undefined ? harbor.root() : harbor.ward(ward)!.root())) as { object?: unknown; notes?: unknown };
112
+ return (out as { notes?: unknown }).notes;
113
+ };
114
+
115
+ // Acme hosts a shop; home opens an echo, hosts alice under a drawn seed and
116
+ // bob under a named one, and alice takes the shop and lends the echo.
117
+ const stand = async (world: World, t: Terrain, bodies: { acme: Bodies; home: Bodies }, seed: string, expect: Expect): Promise<void> => {
118
+ const acme = await standOne(world, 'acme', bodies.acme);
119
+ const home = await standOne(world, 'home', bodies.home);
120
+ expect.same(home.names, [], `${t.name}: a new harbor hosts nobody`);
121
+ expect.same(await home.root('open', { key: 'echo', class: 'org.example.echo' }), { opened: 'echo' });
122
+ expect.same(await home.root('host', { ward: 'alice' }), { hosted: 'alice' });
123
+ expect.same(await home.root('host', { ward: 'bob', seed }), { hosted: 'bob' });
124
+ expect.same(await home.ward('alice')!.root('boot', { key: 'me', class: 'org.example.customer' }), { booted: 'me' });
125
+ expect.same(await acme.root('host', { ward: 'shop' }), { hosted: 'shop' });
126
+ expect.same(await acme.ward('shop')!.root('boot', { key: 'shop', class: 'org.example.shop' }), { booted: 'shop' });
127
+ const invitation = (await acme.ward('shop')!.root('invite', { being: 'shop', id: 'alice' })) as JsonObject;
128
+ const me = async (method: string, args: JsonObject = {}): Promise<unknown> => ((await world.get('home').ward('alice')!.root('ask', { being: 'me', method, args })) as { answer: unknown }).answer;
129
+ expect.same(await me('join', { invitation }), { joined: 'shop' });
130
+ expect.same(await me('buy', { item: 'tea' }), { said: { bought: 'tea' } });
131
+ expect.same(await me('echo', { text: 'hi' }), { said: { echoed: 'hi', to: 'lent:1' } });
132
+ };
133
+
134
+ export const lawScenes = (t: Terrain, expect: Expect): [string, () => Promise<void>][] => [
135
+ [
136
+ `law on ${t.name}: empty memory unpacks a box ward holding the dock and her catalogue, and it wakes the same`,
137
+ async () => {
138
+ const bodies = t.make();
139
+ const world = new World();
140
+ const harbor = await world.harbor('h', [], bodies);
141
+ const pk = harbor.box.pk;
142
+ expect.same(await census(harbor), { pk, beings: CATALOGUE, failed: NONE });
143
+ const again = await world.restart('h', t.again(bodies));
144
+ expect.equal(again.box.pk, pk, 'the seed is the box ward');
145
+ expect.same(await census(again), { pk, beings: CATALOGUE, failed: NONE });
146
+ },
147
+ ],
148
+ [
149
+ `law on ${t.name}: a restart without a module the catalogue names stands, its beings absent, and wakes them when it returns`,
150
+ async () => {
151
+ const bodies = t.make();
152
+ const world = new World();
153
+ const harbor = await standOne(world, 'h', bodies);
154
+ expect.same(await harbor.root('open', { key: 'echo', class: 'org.example.echo' }), { opened: 'echo' });
155
+ expect.same(await harbor.root('host', { ward: 'w' }), { hosted: 'w' });
156
+ const lost = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([]) });
157
+ expect.same(lost.names, ['w'], 'the rest of the harbor stands');
158
+ expect.same(((await census(lost)) as { beings: Record<string, { absent: boolean }> }).beings.echo?.absent, true);
159
+ expect.same(await lost.root('ask', { being: 'catalogue', method: 'modules' }), {
160
+ answer: { [law.module]: { version: '1', running: null, from: law.module, failed: `not found at ${law.module}` } },
161
+ });
162
+ const again = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([law]) });
163
+ expect.same(((await census(again)) as { beings: Record<string, { absent: boolean }> }).beings.echo?.absent, false);
164
+ expect.same(await again.root('ask', { being: 'catalogue', method: 'modules' }), { answer: { [law.module]: { version: '1', running: '1', from: law.module } } });
165
+ },
166
+ ],
167
+ [
168
+ `law on ${t.name}: a module that does not load is removed only once no being stands on the kinds kept for it`,
169
+ async () => {
170
+ const bodies = t.make();
171
+ const world = new World();
172
+ const harbor = await standOne(world, 'h', bodies);
173
+ expect.same(await harbor.root('open', { key: 'echo', class: 'org.example.echo' }), { opened: 'echo' });
174
+ const lost = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([]) });
175
+ const remove = () => lost.root('ask', { being: 'catalogue', method: 'remove', args: { module: law.module } });
176
+ expect.same(await remove(), { answer: { error: 'a being stands on this module' } });
177
+ expect.same(await lost.root('unboot', { being: 'echo' }), { unbooted: 'echo' });
178
+ expect.same(await remove(), { answer: { removed: law.module } });
179
+ const after = await world.restart('h', t.again(bodies));
180
+ expect.same(await after.root('ask', { being: 'catalogue', method: 'modules' }), { answer: {} });
181
+ },
182
+ ],
183
+ [
184
+ `law on ${t.name}: a restart where a module splits a kind runs the modules before it, and the one alone stays out`,
185
+ async () => {
186
+ const bodies = t.make();
187
+ const world = new World();
188
+ const twin: Module = { module: 'org.example.twin', version: '1', classes: [] };
189
+ const harbor = await world.harbor('h', [law, twin], bodies);
190
+ for (const from of [law.module, twin.module]) await harbor.root('ask', { being: 'catalogue', method: 'add', args: { from } });
191
+ expect.same(await harbor.root('open', { key: 'echo', class: 'org.example.echo' }), { opened: 'echo' });
192
+ const split = await world.restart('h', { ...t.again(bodies), loader: new HeldLoader([law, { ...twin, version: '2', classes }]) });
193
+ expect.same(((await census(split)) as { beings: Record<string, { absent: boolean }> }).beings.echo?.absent, false, 'the module before it runs');
194
+ const modules = ((await split.root('ask', { being: 'catalogue', method: 'modules' })) as { answer: Record<string, { running: unknown; failed?: string }> }).answer;
195
+ expect.same([modules[law.module]?.running, modules[twin.module]?.running], ['1', null]);
196
+ expect.equal(modules[twin.module]?.failed, `kind org.example.echo is in modules ${law.module} and ${twin.module}`);
197
+ },
198
+ ],
199
+ [
200
+ `law on ${t.name}: a hosted ward whose place does not open fails alone, and a box ward whose place does not open fails the harbor`,
201
+ async () => {
202
+ const bodies = t.make();
203
+ const watched = new Watched(bodies.memory);
204
+ const world = new World();
205
+ const harbor = await standOne(world, 'h', { custody: bodies.custody, memory: watched });
206
+ expect.same(await harbor.root('host', { ward: 'good' }), { hosted: 'good' });
207
+ const from = watched.seen.length;
208
+ expect.same(await harbor.root('host', { ward: 'bad' }), { hosted: 'bad' });
209
+ const box = watched.seen[0]!.place;
210
+ const bad = watched.seen.slice(from).find((s) => s.place !== box)!.place;
211
+ const places = [bad, box];
212
+ const spoil = async (place: string): Promise<void> => {
213
+ const [[name, bytes]] = [...(await watched.inner.read(place))];
214
+ const changed = bytes!.slice();
215
+ changed[0] = changed[0]! ^ 1;
216
+ await watched.inner.write(place, new Map([[name!, changed]]));
217
+ };
218
+ await spoil(places[0]!);
219
+ const again = await world.restart('h', t.again(bodies));
220
+ expect.same(again.names, ['good']);
221
+ expect.same(((await census(again)) as { failed: unknown }).failed, { wards: { bad: 'an entry the package did not seal' }, routes: {} });
222
+ expect.same(await rootLine(again, { ward: 'bad' }), { error: 'the ward did not stand', reason: 'an entry the package did not seal' });
223
+ expect.same(await again.root('host', { ward: 'bad' }), { error: 'no such ward to host' }, 'its name and seed stay kept');
224
+ await spoil(places[1]!);
225
+ let refused = '';
226
+ await world.restart('h', t.again(bodies)).catch((e: Error) => (refused = e.message));
227
+ expect.equal(refused, 'an entry the package did not seal');
228
+ },
229
+ ],
230
+ [
231
+ `law on ${t.name}: a package carried to another memory stands the same harbor under the same seed`,
232
+ async () => {
233
+ const here = t.make();
234
+ const there = t.make();
235
+ const world = new World();
236
+ const harbor = await standOne(world, 'here', here);
237
+ expect.same(await harbor.root('open', { key: 'echo', class: 'org.example.echo' }), { opened: 'echo' });
238
+ expect.same(await harbor.root('host', { ward: 'alice' }), { hosted: 'alice' });
239
+ expect.same(await harbor.ward('alice')!.root('boot', { key: 'me', class: 'org.example.customer' }), { booted: 'me' });
240
+ const pks = { box: harbor.box.pk, alice: harbor.ward('alice')!.pk };
241
+ const carried = await copyPackage(here.memory, there.memory);
242
+ expect.equal(carried.length, 2, 'the box ward and alice');
243
+ const moved = await world.harbor('there', [law], { custody: here.custody, memory: there.memory, loader: new HeldLoader([law]) });
244
+ expect.same({ box: moved.box.pk, alice: moved.ward('alice')!.pk }, pks);
245
+ expect.same(((await census(moved, 'alice')) as { beings: JsonObject }).beings, { me: { class: 'org.example.customer', public: false, absent: false } });
246
+ expect.same(((await moved.ward('alice')!.root('ask', { being: 'me', method: 'echo', args: { text: 'moved' } })) as { answer: unknown }).answer, { said: { echoed: 'moved', to: 'lent:1' } });
247
+ const stranger = await world.harbor('stranger', [law], { custody: t.make().custody, memory: there.memory, loader: new HeldLoader([law]) });
248
+ expect.same(stranger.names, [], 'the bytes carried say nothing without the seed');
249
+ },
250
+ ],
251
+ [
252
+ `law on ${t.name}: the root sweeps every place the dock does not name, and nothing it does`,
253
+ async () => {
254
+ const bodies = t.make();
255
+ const world = new World();
256
+ const harbor = await standOne(world, 'h', bodies);
257
+ expect.same(await harbor.root('host', { ward: 'w' }), { hosted: 'w' });
258
+ const mine = (await bodies.memory.places()).sort();
259
+ expect.equal(mine.length, 2, 'the box ward and the ward it hosts');
260
+ await bodies.memory.write('ff'.repeat(16), new Map([['ab', new Uint8Array([1, 2, 3])]]));
261
+ expect.same(await harbor.root('sweep'), { swept: ['ff'.repeat(16)] });
262
+ expect.same((await bodies.memory.places()).sort(), mine, 'what the dock names stays');
263
+ expect.same(await harbor.root('sweep'), { swept: [] });
264
+ const again = await world.restart('h', t.again(bodies));
265
+ expect.same(again.names, ['w']);
266
+ expect.same(await again.ward('w')!.root('boot', { key: 'me', class: 'org.example.customer' }), { booted: 'me' });
267
+ },
268
+ ],
269
+ [
270
+ `law on ${t.name}: a restart stands the dock, its faculties, every hosted ward and its beings, and lends, with nobody asking`,
271
+ async () => {
272
+ const bodies = { acme: t.make(), home: t.make() };
273
+ const world = new World();
274
+ const seed = hex(new CryptoEntropy().draw(32));
275
+ await stand(world, t, bodies, seed, expect);
276
+ const before = world.get('home');
277
+ const pks = { box: before.box.pk, alice: before.ward('alice')!.pk, bob: before.ward('bob')!.pk, shop: world.get('acme').ward('shop')!.pk };
278
+ const beings = await census(before, 'alice');
279
+ const home = await world.restart('home', t.again(bodies.home));
280
+ const acme = await world.restart('acme', t.again(bodies.acme));
281
+ expect.same(home.names, ['alice', 'bob']);
282
+ expect.same(acme.names, ['shop']);
283
+ expect.same({ box: home.box.pk, alice: home.ward('alice')!.pk, bob: home.ward('bob')!.pk, shop: acme.ward('shop')!.pk }, pks);
284
+ expect.same(await census(home, 'alice'), beings);
285
+ expect.equal(home.box.partition.being('echo')?.class, 'org.example.echo');
286
+ const me = async (method: string, args: JsonObject = {}): Promise<unknown> => ((await home.ward('alice')!.root('ask', { being: 'me', method, args })) as { answer: unknown }).answer;
287
+ expect.same(await me('buy', { item: 'cake' }), { said: { bought: 'cake' } });
288
+ expect.same(await me('echo', { text: 'again' }), { said: { echoed: 'again', to: 'lent:1' } });
289
+ expect.same(acme.ward('shop')!.partition.being('shop')?.cells, { sold: ['tea to alice', 'cake to alice'] });
290
+ },
291
+ ],
292
+ [
293
+ `law on ${t.name}: every root ask is a step a restart may follow, and each step wakes whole`,
294
+ async () => {
295
+ const bodies = t.make();
296
+ const world = new World();
297
+ await world.harbor('h', [law], bodies);
298
+ const step = async (ask: (h: Harbor) => Promise<unknown>, want: unknown): Promise<Harbor> => {
299
+ expect.same(await ask(world.get('h')), want);
300
+ return world.restart('h', t.again(bodies));
301
+ };
302
+ await step((h) => h.root('ask', { being: 'catalogue', method: 'add', args: { from: law.module } }), { answer: { added: law.module, version: law.version } });
303
+ await step((h) => h.root('open', { key: 'echo', class: 'org.example.echo' }), { opened: 'echo' });
304
+ await step((h) => h.root('host', { ward: 'alice' }), { hosted: 'alice' });
305
+ await step((h) => h.ward('alice')!.root('boot', { key: 'me', class: 'org.example.customer' }), { booted: 'me' });
306
+ const h = await step(async (x) => ((await x.ward('alice')!.root('ask', { being: 'me', method: 'echo', args: { text: 'one' } })) as { answer: unknown }).answer, { said: { echoed: 'one', to: 'lent:1' } });
307
+ expect.same(((await h.ward('alice')!.root('ask', { being: 'me', method: 'echo', args: { text: 'two' } })) as { answer: unknown }).answer, { said: { echoed: 'two', to: 'lent:1' } });
308
+ },
309
+ ],
310
+ [
311
+ `law on ${t.name}: the terrain keeps the seed and sealed bytes, and reads no name, cell or hosted seed`,
312
+ async () => {
313
+ const home = t.make();
314
+ const watched = new Watched(home.memory);
315
+ const world = new World();
316
+ const seed = hex(new CryptoEntropy().draw(32));
317
+ await stand(world, t, { acme: t.make(), home: { custody: home.custody, memory: watched } }, seed, expect);
318
+ const harbor = world.get('home');
319
+ const secrets = [seed, ...harbor.dock.hosted.map((h) => h.seed)].map(utf8);
320
+ // Names of five bytes or more: a shorter one turns up in random bytes
321
+ // of this size often enough to fail a run by chance.
322
+ const words = ['alice', 'org.example', 'hosted', 'lent:1', 'customer'].map(utf8);
323
+ expect.ok(watched.seen.length > 0, 'the harbor kept something');
324
+ for (const { place, name, bytes } of watched.seen) {
325
+ expect.ok(/^[0-9a-f]+$/.test(place) && /^[0-9a-f]+$/.test(name), 'names are the kit hex');
326
+ if (bytes === null) continue;
327
+ for (const secret of secrets) expect.ok(!holds(bytes, secret), 'no hosted seed in the clear');
328
+ for (const word of words) expect.ok(!holds(bytes, word), 'no name or cell in the clear');
329
+ expect.ok(!holds(bytes, await home.custody.seed()), 'no harbor seed in memory');
330
+ }
331
+ },
332
+ ],
333
+ [
334
+ `law on ${t.name}: another seed finds nothing of the package, and a byte changed refuses to unpack`,
335
+ async () => {
336
+ const bodies = t.make();
337
+ const watched = new Watched(bodies.memory);
338
+ const world = new World();
339
+ const harbor = await standOne(world, 'h', { custody: bodies.custody, memory: watched });
340
+ expect.same(await harbor.root('host', { ward: 'w' }), { hosted: 'w' });
341
+ const last = [...watched.seen].reverse().find((s) => s.bytes !== null && s.bytes.length > 0)!;
342
+ const stranger = await world.harbor('stranger', [law], { custody: t.make().custody, memory: watched });
343
+ expect.same(stranger.names, [], 'another seed opens another package');
344
+ const bytes = last.bytes!.slice();
345
+ bytes[0] = bytes[0]! ^ 1;
346
+ await watched.inner.write(last.place, new Map([[last.name, bytes]]));
347
+ let refused = false;
348
+ await world.restart('h', t.again(bodies)).catch(() => (refused = true));
349
+ expect.ok(refused, 'a changed byte does not unpack');
350
+ },
351
+ ],
352
+ ];