@quo-systems/quo 0.1.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.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +91 -0
- package/SPEC.md +1595 -0
- package/package.json +64 -0
- package/src/being/being.ts +92 -0
- package/src/being/digest.ts +31 -0
- package/src/being/index.ts +7 -0
- package/src/being/silence.ts +14 -0
- package/src/being/types.ts +88 -0
- package/src/conformance/assert.ts +67 -0
- package/src/conformance/beings.ts +129 -0
- package/src/conformance/estate.ts +339 -0
- package/src/conformance/index.ts +516 -0
- package/src/conformance/reach.ts +83 -0
- package/src/conformance/store.ts +109 -0
- package/src/harbor/core.ts +178 -0
- package/src/harbor/dial.ts +61 -0
- package/src/harbor/index.ts +11 -0
- package/src/harbor/memory.ts +74 -0
- package/src/harbor/reach.ts +189 -0
- package/src/harbor/store.ts +69 -0
- package/src/ward/allowance.ts +74 -0
- package/src/ward/arithmetic.ts +161 -0
- package/src/ward/cells.ts +90 -0
- package/src/ward/door.ts +102 -0
- package/src/ward/ground.ts +35 -0
- package/src/ward/heirs.ts +87 -0
- package/src/ward/index.ts +14 -0
- package/src/ward/owner.ts +122 -0
- package/src/ward/partition.ts +123 -0
- package/src/ward/seal.ts +133 -0
- package/src/ward/stance.ts +264 -0
- package/src/ward/ward.ts +209 -0
- package/vectors/arithmetic.json +110 -0
- package/vectors/framing.json +71 -0
- package/vectors/wire.json +48 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The ward conformance suite. The beings are fixed; the ward is under test.
|
|
3
|
+
//
|
|
4
|
+
// A ward is handed in as a probe:
|
|
5
|
+
//
|
|
6
|
+
// make() -> world
|
|
7
|
+
// world.boot(key, Class, cells?, { isPublic }) -> handle
|
|
8
|
+
// world.heir(invitation) -> { current, announced, fresh } | undefined
|
|
9
|
+
// what the inviting ward holds for that heir
|
|
10
|
+
// world.oneWard true when every being shares one ward
|
|
11
|
+
// handle.pk the ward pk she lives behind
|
|
12
|
+
// handle.being the object, as the test drives it
|
|
13
|
+
// handle.cells her cells, as the ward keeps them
|
|
14
|
+
// handle.down = true | false her door is unreachable (needs canDown)
|
|
15
|
+
// handle.minted.has(pk) every pk her side ever minted
|
|
16
|
+
// handle.forgeKnock(invitation) make her side knock with a key of its own, unannounced
|
|
17
|
+
// world.census() every partition in the world, as values
|
|
18
|
+
// world.migrate(key) her ward moves to another harbor, same seed, same pk
|
|
19
|
+
//
|
|
20
|
+
// Every assertion here is the truth's. Nothing here knows how a ward is built.
|
|
21
|
+
import { assert } from './assert.ts';
|
|
22
|
+
import { silence, isSilence, isUnreached } from '../being/silence.ts';
|
|
23
|
+
import { digest } from '../being/digest.ts';
|
|
24
|
+
import type { Asker, BeingClass, Cells, Invitation, Json, JsonObject } from '../being/types.ts';
|
|
25
|
+
import { Printer, Shop, Customer, Echo, Member } from './beings.ts';
|
|
26
|
+
import { estate } from './estate.ts';
|
|
27
|
+
|
|
28
|
+
export type Handle<B = unknown> = {
|
|
29
|
+
pk: string;
|
|
30
|
+
being: B;
|
|
31
|
+
cells: Cells;
|
|
32
|
+
down: boolean;
|
|
33
|
+
minted: { has(pk: string): boolean };
|
|
34
|
+
forgeKnock(inv: Invitation): void;
|
|
35
|
+
};
|
|
36
|
+
export type HeirView = { current: string; announced: string | null; fresh: boolean };
|
|
37
|
+
|
|
38
|
+
// The whole world's bookkeeping, as values. What a ward keeps, read the way
|
|
39
|
+
// an auditor reads it: the estate chapter holds it to being a graph, and the
|
|
40
|
+
// secrets are here so that it can prove they are nowhere else.
|
|
41
|
+
export type Census = {
|
|
42
|
+
wards: Record<string, { beings: string[]; public: string | null; heirs: Record<string, { being: string; id: string; current: string; announced: string | null; fresh: boolean; mark: number; spent: number[] }> }>;
|
|
43
|
+
binds: Record<string, { ward: string; standings: Record<string, { ward: string; heir: string | null; seq: number }>; occupants: Record<string, string>; knocks: string[]; answered: string[]; minted: string[]; secrets: string[] }>;
|
|
44
|
+
cells: Record<string, Cells>;
|
|
45
|
+
};
|
|
46
|
+
export type World = {
|
|
47
|
+
oneWard?: boolean;
|
|
48
|
+
boot<B>(key: string, Class: BeingClass, cells?: JsonObject, opts?: { isPublic?: boolean }): Promise<Handle<B>>;
|
|
49
|
+
heir(inv: Invitation): HeirView | undefined;
|
|
50
|
+
census(): Census;
|
|
51
|
+
migrate(key: string): Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { Printer, Shop, Customer, Echo, Member };
|
|
55
|
+
|
|
56
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
57
|
+
|
|
58
|
+
// The runner is handed in. Node passes `node:test`; a browser and an edge
|
|
59
|
+
// worker pass a shim of their own. The suite is the ward's truth, and the
|
|
60
|
+
// ward's truth must be checkable wherever a ward runs, not only here.
|
|
61
|
+
export type Runner = (name: string, opts: { skip?: string | false }, fn: () => Promise<void> | void) => void;
|
|
62
|
+
|
|
63
|
+
export function conform(label: string, make: () => Promise<World>, { canDown = true, test }: { canDown?: boolean; test: Runner }) {
|
|
64
|
+
const t = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, {}, fn);
|
|
65
|
+
const down = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, { skip: !canDown && 'nothing to cut inside one ward' }, fn);
|
|
66
|
+
|
|
67
|
+
async function world({ printerInvitesShop = true } = {}) {
|
|
68
|
+
const w = await make();
|
|
69
|
+
const printer = await w.boot<Printer>('P', Printer);
|
|
70
|
+
const shop = await w.boot<Shop>('S', Shop);
|
|
71
|
+
const alice = await w.boot<Customer>('A', Customer);
|
|
72
|
+
const bob = await w.boot<Customer>('B', Customer);
|
|
73
|
+
if (printerInvitesShop) await shop.being.keepPrinter((await printer.being.invite('shop'))!);
|
|
74
|
+
return { w, printer, shop, alice, bob };
|
|
75
|
+
}
|
|
76
|
+
const bp = (x: unknown) => x as { asks: { name: string }[] };
|
|
77
|
+
|
|
78
|
+
// ---- invite
|
|
79
|
+
|
|
80
|
+
t('invite mints the occupant and a heir: the invitation is the far ward pk, the heir pk, and the heir secret. nothing of her.', async () => {
|
|
81
|
+
const { w, shop } = await world();
|
|
82
|
+
const inv = await shop.being.invite('gold');
|
|
83
|
+
assert.deepEqual(Object.keys(shop.cells.occupants), ['cust1']);
|
|
84
|
+
assert.deepEqual(Object.keys(inv).sort(), ['heir', 'secret', 'ward']);
|
|
85
|
+
assert.match(inv.ward, /^[0-9a-f]{128}$/);
|
|
86
|
+
assert.match(inv.heir!, HEX64);
|
|
87
|
+
assert.match(inv.secret!, HEX64);
|
|
88
|
+
assert.equal(inv.ward, shop.pk);
|
|
89
|
+
assert.ok(shop.minted.has(inv.heir!)); // rotation one: her ward minted the occupant's first key
|
|
90
|
+
assert.deepEqual(w.heir(inv), { current: inv.heir, announced: null, fresh: true }); // and keeps only its pk
|
|
91
|
+
for (const word of [inv.heir!, inv.secret!]) assert.ok(!JSON.stringify(shop.cells).includes(word)); // never in her cells
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
t('two invites, two heirs, nothing shared', async () => {
|
|
95
|
+
const { shop } = await world();
|
|
96
|
+
const i1 = await shop.being.invite();
|
|
97
|
+
const i2 = await shop.being.invite();
|
|
98
|
+
assert.notEqual(i1.heir, i2.heir);
|
|
99
|
+
assert.notEqual(i1.secret, i2.secret);
|
|
100
|
+
assert.equal(i1.ward, i2.ward);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
t('remove before any knock: the heir is forgotten, a knock on it is silence', async () => {
|
|
104
|
+
const { w, shop, alice } = await world();
|
|
105
|
+
const inv = await shop.being.invite();
|
|
106
|
+
shop.being.occupants.remove('cust1');
|
|
107
|
+
assert.equal(w.heir(inv), undefined);
|
|
108
|
+
assert.ok(isSilence(await alice.being.join(inv)));
|
|
109
|
+
assert.deepEqual(alice.cells.standings, {});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ---- consume, the knock
|
|
113
|
+
|
|
114
|
+
t('a valid knock binds the id to her own key and spends the heir; the asker is { id }', async () => {
|
|
115
|
+
const { w, shop, alice } = await world();
|
|
116
|
+
const inv = await shop.being.invite('gold');
|
|
117
|
+
assert.deepEqual(await alice.being.join(inv), { welcome: true });
|
|
118
|
+
const h = w.heir(inv)!;
|
|
119
|
+
assert.equal(h.fresh, false);
|
|
120
|
+
assert.notEqual(h.current, inv.heir); // rotation two: the heir is spent
|
|
121
|
+
assert.ok(alice.minted.has(h.current)); // her own key speaks now
|
|
122
|
+
assert.ok(!shop.minted.has(h.current)); // which the inviter never held
|
|
123
|
+
assert.equal(shop.cells.occupants.cust1.notes.asks, 1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
t('a second knock with the same invitation, from another: silence, nothing rebinds', async () => {
|
|
127
|
+
const { w, shop, alice, bob } = await world();
|
|
128
|
+
const inv = await shop.being.invite();
|
|
129
|
+
await alice.being.join(inv);
|
|
130
|
+
const bound = w.heir(inv)!.current;
|
|
131
|
+
assert.ok(isSilence(await bob.being.join(inv)));
|
|
132
|
+
assert.equal(w.heir(inv)!.current, bound); // still alice's
|
|
133
|
+
assert.deepEqual(bob.cells.standings, {});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
t('a knock with the wrong secret, or a forged heir, is silence and never reaches her', async () => {
|
|
137
|
+
const { w, shop, alice } = await world();
|
|
138
|
+
let reached = 0;
|
|
139
|
+
const real = shop.being.answer.bind(shop.being);
|
|
140
|
+
shop.being.answer = (...a) => (reached++, real(...a));
|
|
141
|
+
const inv = await shop.being.invite();
|
|
142
|
+
const other = await shop.being.invite();
|
|
143
|
+
assert.ok(isSilence(await alice.being.knock({ ward: inv.ward, heir: 'f'.repeat(64), secret: inv.secret }, 'hello')));
|
|
144
|
+
assert.ok(isSilence(await alice.being.knock({ ward: inv.ward, heir: inv.heir, secret: other.secret }, 'hello'))); // a real secret, the wrong heir
|
|
145
|
+
assert.ok(isSilence(await alice.being.knock({ ward: inv.ward, heir: inv.heir, secret: '0'.repeat(64) }, 'hello')));
|
|
146
|
+
assert.equal(reached, 0);
|
|
147
|
+
assert.equal(w.heir(inv)!.fresh, true); // still unbound
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
t('the same knocker may knock again: her key is bound, no heir needed, it is an ask, and it announces her next key', async () => {
|
|
151
|
+
const { w, shop, alice } = await world();
|
|
152
|
+
const inv = await shop.being.invite();
|
|
153
|
+
assert.deepEqual(await alice.being.join(inv), { welcome: true });
|
|
154
|
+
const k1 = w.heir(inv)!.current;
|
|
155
|
+
assert.equal(w.heir(inv)!.announced, null); // a knock announces her key and dies; nothing further yet
|
|
156
|
+
assert.deepEqual(await alice.being.knock(inv, 'hello'), { welcome: true });
|
|
157
|
+
assert.equal(shop.cells.occupants.cust1.notes.asks, 2);
|
|
158
|
+
const h = w.heir(inv)!;
|
|
159
|
+
assert.equal(h.current, k1); // k1 spoke
|
|
160
|
+
assert.ok(h.announced && alice.minted.has(h.announced) && h.announced !== k1); // and vouched for the next
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
t('a knocker who presents a key of her own, unannounced, on a bound heir: silence', async () => {
|
|
164
|
+
const { w, shop, alice, bob } = await world();
|
|
165
|
+
const inv = await shop.being.invite();
|
|
166
|
+
await alice.being.join(inv);
|
|
167
|
+
const bound = w.heir(inv)!.current;
|
|
168
|
+
bob.forgeKnock(inv);
|
|
169
|
+
assert.ok(isSilence(await bob.being.knock(inv, 'hello')));
|
|
170
|
+
assert.equal(w.heir(inv)!.current, bound);
|
|
171
|
+
assert.equal(shop.cells.occupants.cust1.notes.asks, 1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
t('ids never cross: hers for me and mine for her are unrelated', async () => {
|
|
175
|
+
const { shop, alice } = await world();
|
|
176
|
+
await alice.being.join(await shop.being.invite());
|
|
177
|
+
assert.deepEqual(Object.keys(alice.cells.standings), ['shop']);
|
|
178
|
+
assert.deepEqual(Object.keys(shop.cells.occupants), ['cust1']);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
down('a standing is born only after an answer; silence or unreached births nothing; an unreached knock does not spend the heir', async () => {
|
|
182
|
+
const { w, shop, alice } = await world();
|
|
183
|
+
const inv = await shop.being.invite();
|
|
184
|
+
shop.down = true;
|
|
185
|
+
assert.ok(isUnreached(await alice.being.join(inv)));
|
|
186
|
+
assert.deepEqual(alice.cells.standings, {});
|
|
187
|
+
assert.equal(w.heir(inv)!.fresh, true);
|
|
188
|
+
shop.down = false;
|
|
189
|
+
assert.deepEqual(await alice.being.join(inv), { welcome: true }); // the same invitation still works
|
|
190
|
+
const inv2 = await shop.being.invite();
|
|
191
|
+
shop.being.occupants.remove('cust2'); // she changed her mind before the knock
|
|
192
|
+
assert.ok(isSilence(await alice.being.join(inv2)));
|
|
193
|
+
assert.deepEqual(Object.keys(alice.cells.standings), ['shop']);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
t('take without a knock, or after a silent knock, births nothing', async () => {
|
|
197
|
+
const { shop, alice } = await world();
|
|
198
|
+
const inv = await shop.being.invite();
|
|
199
|
+
assert.equal(await alice.being.take('shop', inv), null);
|
|
200
|
+
shop.being.occupants.remove('cust1');
|
|
201
|
+
assert.ok(isSilence(await alice.being.knock(inv, 'hello')));
|
|
202
|
+
assert.equal(await alice.being.take('shop', inv), null);
|
|
203
|
+
assert.deepEqual(alice.cells.standings, {});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ---- asks
|
|
207
|
+
|
|
208
|
+
t('an occupant asks, a standing is asked, an object comes back', async () => {
|
|
209
|
+
const { alice, printer, shop } = await world();
|
|
210
|
+
await alice.being.join(await shop.being.invite());
|
|
211
|
+
assert.deepEqual(await alice.being.buy('hat'), { ok: true, receipt: 'receipt for hat' });
|
|
212
|
+
assert.equal((printer.cells.jobs as unknown[]).length, 1);
|
|
213
|
+
assert.equal((shop.cells.sales as JsonObject[])[0].by, 'cust1');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
t('every answered ask rotates: each call speaks as the key the last one announced, and announces a fresh one, all minted by her side', async () => {
|
|
217
|
+
const { w, shop, alice } = await world();
|
|
218
|
+
const inv = await shop.being.invite();
|
|
219
|
+
await alice.being.join(inv);
|
|
220
|
+
const seen = new Set<string>([w.heir(inv)!.current]);
|
|
221
|
+
let expectNext = w.heir(inv)!.current; // after the knock, her key is current and nothing is announced
|
|
222
|
+
for (let i = 0; i < 3; i++) {
|
|
223
|
+
await alice.being.buy('hat');
|
|
224
|
+
const h = w.heir(inv)!;
|
|
225
|
+
assert.equal(h.current, expectNext); // this call spoke as what was announced before
|
|
226
|
+
assert.ok(h.announced && alice.minted.has(h.announced) && !seen.has(h.announced)); // and announced a fresh key
|
|
227
|
+
seen.add(h.announced);
|
|
228
|
+
expectNext = h.announced!;
|
|
229
|
+
}
|
|
230
|
+
assert.equal((shop.cells.sales as unknown[]).length, 3);
|
|
231
|
+
assert.equal(seen.size, 4);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
t('asks in flight together on one standing: every one is answered, the rotation stays a single line, and the relation lives', async () => {
|
|
235
|
+
const { w, shop, alice } = await world();
|
|
236
|
+
const inv = await shop.being.invite();
|
|
237
|
+
await alice.being.join(inv);
|
|
238
|
+
const bound = w.heir(inv)!.current; // the key her side bound at the knock
|
|
239
|
+
|
|
240
|
+
const out = await Promise.all(Array.from({ length: 5 }, (_, i) => alice.being.buy(`hat${i}`)));
|
|
241
|
+
for (const [i, o] of out.entries()) assert.deepEqual(o, { ok: true, receipt: `receipt for hat${i}` }, `ask ${i} was answered`);
|
|
242
|
+
assert.equal((shop.cells.sales as unknown[]).length, 5); // each arrived once. none was lost, none was doubled
|
|
243
|
+
|
|
244
|
+
// five asks, five rotations, one line: the door's key is the fifth her
|
|
245
|
+
// side minted, and the side that did not mint it never held it.
|
|
246
|
+
const h = w.heir(inv)!;
|
|
247
|
+
assert.notEqual(h.current, bound);
|
|
248
|
+
assert.ok(alice.minted.has(h.current));
|
|
249
|
+
assert.ok(!shop.minted.has(h.current));
|
|
250
|
+
|
|
251
|
+
// and the two sides still agree, which is the whole of it: a relation
|
|
252
|
+
// that survived the crowd is a relation that still answers.
|
|
253
|
+
assert.deepEqual(await alice.being.buy('after'), { ok: true, receipt: 'receipt for after' });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
t('two knocks in flight on one invitation: one heir, one binding, and she may still ask', async () => {
|
|
257
|
+
const { w, shop, alice } = await world();
|
|
258
|
+
const inv = await shop.being.invite();
|
|
259
|
+
const [a, b] = await Promise.all([alice.being.join(inv), alice.being.join(inv)]);
|
|
260
|
+
// the same knocker knocking twice at once is not two occupants: the heir
|
|
261
|
+
// is spent once, and both calls speak for the one relation she is building.
|
|
262
|
+
assert.deepEqual(Object.keys(shop.cells.occupants), ['cust1']);
|
|
263
|
+
for (const r of [a, b]) assert.ok(!isSilence(r) && !isUnreached(r), 'both knocks were answered');
|
|
264
|
+
const h = w.heir(inv)!;
|
|
265
|
+
assert.equal(h.fresh, false);
|
|
266
|
+
assert.ok(alice.minted.has(h.current)); // her own key speaks, exactly as after one knock
|
|
267
|
+
// What she does with the standing afterwards is not this test's. A take
|
|
268
|
+
// reads the relation where the knock left it, and a knock still in flight
|
|
269
|
+
// has not left it anywhere yet: the standing is born a step behind the
|
|
270
|
+
// door, and its first ask speaks under a number already honoured.
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
t('an inherited name is not an ask through a ward either: her cells do not come back for the asking', async () => {
|
|
274
|
+
const { shop, alice } = await world();
|
|
275
|
+
await alice.being.join(await shop.being.invite()); // plain: refund is hidden from her
|
|
276
|
+
shop.cells.ledger = { swiss: 'CH-99' };
|
|
277
|
+
for (const name of ['valueOf', 'toString', 'constructor', '__proto__', 'hasOwnProperty']) {
|
|
278
|
+
const out = await alice.being.standings.shop!.ask(name);
|
|
279
|
+
assert.deepEqual(out, { error: 'unknown ask' }, name);
|
|
280
|
+
assert.ok(!JSON.stringify(out).includes('CH-99'), `${name} carried her cells out`);
|
|
281
|
+
}
|
|
282
|
+
// the blueprint never named one of them, and the gate is the same gate
|
|
283
|
+
const blueprint = bp(await alice.being.learn());
|
|
284
|
+
for (const ask of blueprint.asks) assert.ok(!['valueOf', 'toString', 'constructor'].includes(ask.name));
|
|
285
|
+
assert.deepEqual(await alice.being.standings.shop!.ask('refund'), { error: 'unknown ask' }); // still hidden, still the same answer
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
t('symmetry: the customer offers a way back inside args; the shop takes a gold one', async () => {
|
|
289
|
+
const { shop, alice, bob } = await world();
|
|
290
|
+
await alice.being.join(await shop.being.invite('gold'));
|
|
291
|
+
await bob.being.join(await shop.being.invite('plain'));
|
|
292
|
+
assert.deepEqual(Object.keys(shop.cells.standings), ['prn', 'vip-cust1']);
|
|
293
|
+
assert.deepEqual(alice.cells.heard, [{ from: 'shop-back', method: 'hi' }]);
|
|
294
|
+
assert.deepEqual(bob.cells.heard, []);
|
|
295
|
+
assert.deepEqual(await shop.being.standings['vip-cust1']!.ask('hi'), { heard: 'hi' });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
t('the printer never invited the shop: no standing, an error object, no throw', async () => {
|
|
299
|
+
const { alice, shop } = await world({ printerInvitesShop: false });
|
|
300
|
+
await alice.being.join(await shop.being.invite());
|
|
301
|
+
assert.deepEqual(await alice.being.buy('hat'), { error: 'no printer' });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
t('the empty ask is describe; shaped per asker', async () => {
|
|
305
|
+
const { shop, alice, bob } = await world();
|
|
306
|
+
await alice.being.join(await shop.being.invite('gold'));
|
|
307
|
+
await bob.being.join(await shop.being.invite());
|
|
308
|
+
const names = (x: unknown) => bp(x).asks.map((a) => a.name);
|
|
309
|
+
assert.deepEqual(names(await alice.being.learn()), ['hello', 'buy', 'refund']);
|
|
310
|
+
assert.deepEqual(names(await bob.being.learn()), ['hello', 'buy']);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
t('the digest is per relation: two occupants, two digests of one being, both true', async () => {
|
|
314
|
+
const { shop, alice, bob } = await world();
|
|
315
|
+
await alice.being.join(await shop.being.invite('gold'));
|
|
316
|
+
await bob.being.join(await shop.being.invite());
|
|
317
|
+
await alice.being.learn();
|
|
318
|
+
await bob.being.learn();
|
|
319
|
+
assert.notEqual(alice.cells.standings.shop.digest, bob.cells.standings.shop.digest);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
t('describe caches blueprint and digest in the asker cells', async () => {
|
|
323
|
+
const { shop, alice } = await world();
|
|
324
|
+
await alice.being.join(await shop.being.invite());
|
|
325
|
+
const out = await alice.being.learn();
|
|
326
|
+
assert.deepEqual(alice.cells.standings.shop.blueprint, out);
|
|
327
|
+
assert.equal(alice.cells.standings.shop.digest, await digest(out as Json));
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
t('a changed standing is noticed by digest, without carrying the blueprint', async () => {
|
|
331
|
+
const { alice, printer, shop } = await world();
|
|
332
|
+
await alice.being.join(await shop.being.invite());
|
|
333
|
+
await shop.being.standings.prn!.ask();
|
|
334
|
+
(printer.cells.asks as JsonObject[]).push({ name: 'scan', input: {} }); // she changed
|
|
335
|
+
await alice.being.buy('hat');
|
|
336
|
+
const st = shop.cells.standings.prn;
|
|
337
|
+
assert.notEqual(st.seen, st.digest);
|
|
338
|
+
assert.equal(shop.cells.occupants.cust1.notes.printerChanged, true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
t('the digest rides on every answer: seen updates without a describe from her side', async () => {
|
|
342
|
+
const { alice, printer, shop } = await world();
|
|
343
|
+
await alice.being.join(await shop.being.invite());
|
|
344
|
+
let describes = 0;
|
|
345
|
+
const real = printer.being.answer.bind(printer.being);
|
|
346
|
+
printer.being.answer = (a, m, x) => (m === undefined && describes++, real(a, m, x));
|
|
347
|
+
await alice.being.buy('hat'); // shop asks printer once
|
|
348
|
+
const st = shop.cells.standings.prn;
|
|
349
|
+
assert.equal(st.seen, await digest((await real({ id: 'shop' })) as Json));
|
|
350
|
+
assert.equal(describes, 1); // the far ward ran her describe, in process, once per answer
|
|
351
|
+
assert.equal(st.digest, null); // she never fetched it herself
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
t('a silent refresh leaves seen untouched', async () => {
|
|
355
|
+
const { alice, printer, shop } = await world();
|
|
356
|
+
await alice.being.join(await shop.being.invite());
|
|
357
|
+
await shop.being.standings.prn!.ask();
|
|
358
|
+
const before = shop.cells.standings.prn.seen;
|
|
359
|
+
const real = printer.being.answer.bind(printer.being);
|
|
360
|
+
printer.being.answer = async (a, m, x) => (m === undefined ? silence : real(a, m, x));
|
|
361
|
+
await alice.being.buy('hat');
|
|
362
|
+
assert.equal(shop.cells.standings.prn.seen, before);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
down('unreached is the ward word, distinct from silence, carries nothing, safe to retry', async () => {
|
|
366
|
+
const { alice, printer, shop } = await world();
|
|
367
|
+
await alice.being.join(await shop.being.invite());
|
|
368
|
+
printer.down = true;
|
|
369
|
+
assert.deepEqual(await alice.being.buy('hat'), { error: 'printer unreachable', retry: true });
|
|
370
|
+
const out = await shop.being.standings.prn!.ask('print', { doc: 'x' });
|
|
371
|
+
assert.ok(isUnreached(out) && !isSilence(out));
|
|
372
|
+
assert.deepEqual(Object.keys(out), []);
|
|
373
|
+
printer.down = false;
|
|
374
|
+
assert.deepEqual(await alice.being.buy('hat'), { ok: true, receipt: 'receipt for hat' });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
t('a removed occupant is silence to the far side and her heir is forgotten; a dropped standing is gone', async () => {
|
|
378
|
+
const { w, alice, shop } = await world();
|
|
379
|
+
const inv = await shop.being.invite();
|
|
380
|
+
await alice.being.join(inv);
|
|
381
|
+
shop.being.occupants.remove('cust1');
|
|
382
|
+
assert.equal(w.heir(inv), undefined);
|
|
383
|
+
assert.ok(isSilence(await alice.being.buy('hat')));
|
|
384
|
+
alice.being.standings.remove('shop');
|
|
385
|
+
assert.equal(alice.being.standings.shop, undefined);
|
|
386
|
+
assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); // and a re-knock finds no heir
|
|
387
|
+
const again = await shop.being.invite(); // cust2. a new heir
|
|
388
|
+
assert.notEqual(again.heir, inv.heir);
|
|
389
|
+
assert.deepEqual(await alice.being.knock(again, 'hello'), { welcome: true });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
t('dropping a standing while an ask on it is in flight does not crash', async () => {
|
|
393
|
+
const { alice, shop } = await world();
|
|
394
|
+
await alice.being.join(await shop.being.invite());
|
|
395
|
+
const p = alice.being.buy('hat');
|
|
396
|
+
alice.being.standings.remove('shop');
|
|
397
|
+
assert.deepEqual(await p, { ok: true, receipt: 'receipt for hat' });
|
|
398
|
+
assert.equal((shop.cells.sales as unknown[]).length, 1);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
t('a being that throws is silence outside; the ward stays up', async () => {
|
|
402
|
+
const w = await make();
|
|
403
|
+
class Bomb {
|
|
404
|
+
s: unknown;
|
|
405
|
+
constructor(s: unknown) {
|
|
406
|
+
this.s = s;
|
|
407
|
+
}
|
|
408
|
+
answer(_: Asker, m?: string) {
|
|
409
|
+
if (m === undefined) return { asks: [], notes: {} };
|
|
410
|
+
throw new Error('boom');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const x = await w.boot<Bomb>('X', Bomb);
|
|
414
|
+
const a = await w.boot<Customer>('A', Customer);
|
|
415
|
+
const inv = (await (x.being.s as Customer['stance']).occupants.invite('a'))!;
|
|
416
|
+
assert.ok(isSilence(await a.being.knock(inv, 'any')));
|
|
417
|
+
assert.deepEqual(await a.being.knock(inv), { asks: [], notes: {} });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
t('null is an answer, silence is not', async () => {
|
|
421
|
+
const w = await make();
|
|
422
|
+
class Nul {
|
|
423
|
+
s: unknown;
|
|
424
|
+
constructor(s: unknown) {
|
|
425
|
+
this.s = s;
|
|
426
|
+
}
|
|
427
|
+
answer(_: Asker, m?: string) {
|
|
428
|
+
return m === undefined ? { asks: [], notes: {} } : null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const n = await w.boot<Nul>('N', Nul);
|
|
432
|
+
const a = await w.boot<Customer>('A', Customer);
|
|
433
|
+
const out = await a.being.knock((await (n.being.s as Customer['stance']).occupants.invite('a'))!, 'x');
|
|
434
|
+
assert.equal(out, null);
|
|
435
|
+
assert.ok(!isSilence(out));
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
t('the raw shape is a being through a ward, exactly like one on the base class', async () => {
|
|
439
|
+
const w = await make();
|
|
440
|
+
const e = await w.boot<Echo>('E', Echo);
|
|
441
|
+
const a = await w.boot<Customer>('A', Customer);
|
|
442
|
+
const inv = (await e.being.s.occupants.invite('a'))!;
|
|
443
|
+
assert.deepEqual(await a.being.knock(inv, 'echo', { x: 1 }), { from: 'a', x: 1 });
|
|
444
|
+
assert.equal(await a.being.take('echo', inv), 'echo');
|
|
445
|
+
assert.deepEqual(await a.being.standings.echo!.ask('echo', { y: 2 }), { from: 'a', y: 2 });
|
|
446
|
+
assert.equal(a.cells.standings.echo.seen, await digest({ asks: [{ name: 'echo', input: {} }], notes: {} }));
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ---- public
|
|
450
|
+
|
|
451
|
+
t('a public being is asked by anyone as {}; a knock on her needs no heir and may be taken; a ward with none is silence; an unknown pk is unreached', async () => {
|
|
452
|
+
const w = await make();
|
|
453
|
+
class Pub {
|
|
454
|
+
s: { cells: Cells };
|
|
455
|
+
constructor(s: { cells: Cells }) {
|
|
456
|
+
this.s = s;
|
|
457
|
+
this.s.cells.askers ??= [];
|
|
458
|
+
}
|
|
459
|
+
answer(asker: Asker, m?: string, args?: JsonObject) {
|
|
460
|
+
if (m === undefined) return { asks: [{ name: 'time', input: {} }], notes: {} };
|
|
461
|
+
(this.s.cells.askers as Json[]).push(asker as Json);
|
|
462
|
+
return { time: 12, got: args ?? {} };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const pub = await w.boot<Pub>('PUB', Pub as never, {}, { isPublic: true });
|
|
466
|
+
const priv = await w.boot<Pub>('PRIV', Pub as never); // same class, not public
|
|
467
|
+
const a = await w.boot<Customer>('A', Customer);
|
|
468
|
+
const b = await w.boot<Customer>('B', Customer);
|
|
469
|
+
const at = (h: { pk: string }) => ({ ward: h.pk });
|
|
470
|
+
assert.deepEqual(await a.being.knock(at(pub), 'time', { x: 1 }), { time: 12, got: { x: 1 } });
|
|
471
|
+
assert.deepEqual(await b.being.knock(at(pub), 'time'), { time: 12, got: {} });
|
|
472
|
+
assert.deepEqual(pub.cells.askers, [{}, {}]);
|
|
473
|
+
assert.deepEqual(pub.cells.occupants, {}); // she never invited anyone
|
|
474
|
+
if (!w.oneWard) assert.ok(isSilence(await a.being.knock(at(priv), 'time'))); // a ward with no public being: silence
|
|
475
|
+
assert.ok(isUnreached(await a.being.knock({ ward: 'a'.repeat(128) }, 'time'))); // no door anywhere: unreached
|
|
476
|
+
assert.ok(isSilence(await a.being.knock({ ward: 'not a pk' }, 'time'))); // not even an address
|
|
477
|
+
assert.equal(await a.being.take('clock', at(pub)), 'clock');
|
|
478
|
+
assert.deepEqual(await a.being.standings.clock!.ask('time'), { time: 12, got: {} });
|
|
479
|
+
assert.equal(a.cells.standings.clock.seen, await digest((await a.being.standings.clock!.ask()) as Json));
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ---- keys stay where they belong
|
|
483
|
+
|
|
484
|
+
t("two rotations: the heir is the inviter's, the bound key is the knocker's, and neither side minted the other's", async () => {
|
|
485
|
+
const { w, shop, alice, bob } = await world();
|
|
486
|
+
const i1 = await shop.being.invite('gold');
|
|
487
|
+
const i2 = await shop.being.invite();
|
|
488
|
+
assert.ok(shop.minted.has(i1.heir!) && shop.minted.has(i2.heir!));
|
|
489
|
+
await alice.being.join(i1);
|
|
490
|
+
await bob.being.join(i2);
|
|
491
|
+
const h1 = w.heir(i1)!, h2 = w.heir(i2)!;
|
|
492
|
+
assert.ok(alice.minted.has(h1.current) && !shop.minted.has(h1.current) && !bob.minted.has(h1.current));
|
|
493
|
+
assert.ok(bob.minted.has(h2.current) && !shop.minted.has(h2.current) && !alice.minted.has(h2.current));
|
|
494
|
+
assert.notEqual(h1.current, h2.current); // no key serves two relations
|
|
495
|
+
assert.ok(!alice.minted.has(i1.heir!)); // alice never minted anything of the shop's
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
t('her cells carry no key: heir, secret, and bound key live in the bind table only', async () => {
|
|
499
|
+
const { w, shop, alice } = await world();
|
|
500
|
+
const inv = await shop.being.invite();
|
|
501
|
+
await alice.being.join(inv);
|
|
502
|
+
await alice.being.learn();
|
|
503
|
+
const words = [inv.heir!, inv.secret!, w.heir(inv)!.current];
|
|
504
|
+
for (const cells of [shop.cells, alice.cells]) for (const word of words) assert.ok(!JSON.stringify(cells).includes(word), `${word} in cells`);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// ---- the estate
|
|
508
|
+
|
|
509
|
+
estate(label, make, { canDown, test });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// The store and reach suites, beside the ward's: every store a harbor keeps
|
|
513
|
+
// wards in and every reach it carries bytes by passes its own, on the
|
|
514
|
+
// terrain it is written for.
|
|
515
|
+
export { conformStore } from './store.ts';
|
|
516
|
+
export { conformReach, type FarSide } from './reach.ts';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The reach conformance suite: what every reach must show, written against
|
|
3
|
+
// the interface alone. A reach is handed in as a maker of a fresh line to a
|
|
4
|
+
// far side the suite controls: `hold` puts a door behind a pk over there,
|
|
5
|
+
// `drop` takes the far side away, the line down or the listener gone. The
|
|
6
|
+
// suite asks for bytes and expects the door's bytes back, nothing for a pk
|
|
7
|
+
// nobody holds, and nothing once the far side is gone. It cannot assert
|
|
8
|
+
// that a reach which sent and then lost the line answers nothing at all,
|
|
9
|
+
// since a suite cannot wait forever; that promise is read in the code.
|
|
10
|
+
import { assert } from './assert.ts';
|
|
11
|
+
import type { Reach } from '../harbor/reach.ts';
|
|
12
|
+
import type { Runner } from './index.ts';
|
|
13
|
+
|
|
14
|
+
export type FarSide = {
|
|
15
|
+
reach: Reach;
|
|
16
|
+
hold(pk: string, door: (bytes: Uint8Array) => Promise<Uint8Array>): Promise<void>;
|
|
17
|
+
drop(): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const pk = (b: string) => b.repeat(64);
|
|
21
|
+
const bytes = (...xs: number[]) => Uint8Array.from(xs);
|
|
22
|
+
const wait = (ms: number) => new Promise<void>((ok) => setTimeout(ok, ms));
|
|
23
|
+
|
|
24
|
+
export function conformReach(label: string, make: () => Promise<FarSide>, { test }: { test: Runner }) {
|
|
25
|
+
const t = (name: string, fn: () => Promise<void>) => test(`[${label}] ${name}`, {}, fn);
|
|
26
|
+
|
|
27
|
+
t('bytes to a pk the far side holds come back as the door answered', async () => {
|
|
28
|
+
const far = await make();
|
|
29
|
+
try {
|
|
30
|
+
await far.hold(pk('aa'), async (b) => bytes(...b, 9));
|
|
31
|
+
assert.deepEqual(await far.reach.carry(pk('aa'), bytes(1, 2, 3)), bytes(1, 2, 3, 9));
|
|
32
|
+
} finally {
|
|
33
|
+
await far.drop();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
t('bytes to a pk nobody holds are nothing', async () => {
|
|
38
|
+
const far = await make();
|
|
39
|
+
try {
|
|
40
|
+
await far.hold(pk('aa'), async (b) => b);
|
|
41
|
+
assert.equal(await far.reach.carry(pk('bb'), bytes(1)), undefined);
|
|
42
|
+
} finally {
|
|
43
|
+
await far.drop();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
t('asks in flight at once each get their own answer', async () => {
|
|
48
|
+
const far = await make();
|
|
49
|
+
try {
|
|
50
|
+
await far.hold(pk('aa'), async (b) => (await wait(b[0]! * 10), bytes(b[0]!)));
|
|
51
|
+
const out = await Promise.all([far.reach.carry(pk('aa'), bytes(3)), far.reach.carry(pk('aa'), bytes(1)), far.reach.carry(pk('aa'), bytes(2))]);
|
|
52
|
+
assert.deepEqual(out, [bytes(3), bytes(1), bytes(2)]);
|
|
53
|
+
} finally {
|
|
54
|
+
await far.drop();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
t('what comes back is a copy, and what was sent was one', async () => {
|
|
59
|
+
const far = await make();
|
|
60
|
+
try {
|
|
61
|
+
let seen: Uint8Array | undefined;
|
|
62
|
+
const reply = bytes(7, 7);
|
|
63
|
+
await far.hold(pk('aa'), async (b) => ((seen = b), reply));
|
|
64
|
+
const sent = bytes(4, 5);
|
|
65
|
+
const back = (await far.reach.carry(pk('aa'), sent))!;
|
|
66
|
+
assert.deepEqual(back, bytes(7, 7));
|
|
67
|
+
back[0] = 0;
|
|
68
|
+
assert.deepEqual(reply, bytes(7, 7), 'the caller wrote through to the door');
|
|
69
|
+
seen![0] = 0;
|
|
70
|
+
assert.deepEqual(sent, bytes(4, 5), 'the door wrote through to the caller');
|
|
71
|
+
} finally {
|
|
72
|
+
await far.drop();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
t('once the far side is gone, bytes are nothing', async () => {
|
|
77
|
+
const far = await make();
|
|
78
|
+
await far.hold(pk('aa'), async (b) => b);
|
|
79
|
+
assert.deepEqual(await far.reach.carry(pk('aa'), bytes(1)), bytes(1));
|
|
80
|
+
await far.drop();
|
|
81
|
+
assert.equal(await far.reach.carry(pk('aa'), bytes(1)), undefined);
|
|
82
|
+
});
|
|
83
|
+
}
|