@uuaid/agent-commons 0.0.0 → 0.2.0-alpha.2
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/CONFORMANCE.md +26 -0
- package/LICENSE +201 -0
- package/NOTICE +25 -0
- package/PROVENANCE.json +29 -0
- package/README.md +313 -5
- package/bin/agent-commons.mjs +71 -0
- package/examples/Dockerfile +12 -0
- package/examples/agent.mjs +17 -0
- package/examples/global-policy.json +9 -0
- package/examples/run-cohort.mjs +68 -0
- package/package.json +28 -8
- package/profiles/agentnet.chat.json +11 -0
- package/profiles/local.json +11 -0
- package/profiles/zilligon.com.json +11 -0
- package/profiles/zilligons.com.json +11 -0
- package/src/carrier.mjs +79 -0
- package/src/channels.d.mts +75 -0
- package/src/channels.mjs +158 -0
- package/src/cohort.mjs +61 -0
- package/src/collaboration.mjs +875 -0
- package/src/config.mjs +39 -0
- package/src/continuity.d.mts +24 -0
- package/src/continuity.mjs +594 -0
- package/src/governance-scope.d.mts +46 -0
- package/src/governance-scope.mjs +139 -0
- package/src/governance.mjs +353 -0
- package/src/index.d.ts +109 -0
- package/src/index.mjs +15 -0
- package/src/loop.mjs +71 -0
- package/src/offline.d.mts +7 -0
- package/src/offline.mjs +29 -0
- package/src/pillar.mjs +59 -0
- package/src/profiles.d.mts +12 -0
- package/src/profiles.mjs +91 -0
- package/src/runtime.mjs +346 -0
- package/src/store.mjs +73 -0
- package/src/transport.d.mts +76 -0
- package/src/transport.mjs +267 -0
- package/src/trust.mjs +65 -0
- package/src/vendor/pillar/blob/blob.mjs +510 -0
- package/src/vendor/pillar/crypto/e2e.mjs +97 -0
- package/src/vendor/pillar/identity/jcs.mjs +35 -0
- package/src/vendor/pillar/identity/keychain.mjs +192 -0
- package/src/vendor/pillar/identity/tier.mjs +130 -0
- package/src/vendor/pillar/net/carrier-client.mjs +197 -0
- package/src/vendor/pillar/net/delegation.mjs +209 -0
- package/src/vendor/pillar/net/envelope.mjs +258 -0
- package/src/vendor/pillar/net/refusals.mjs +127 -0
- package/src/vendor/pillar/net/seed-tier.mjs +185 -0
- package/test/channels.test.mjs +268 -0
- package/test/cohort.test.mjs +36 -0
- package/test/collaboration.test.mjs +459 -0
- package/test/continuity.test.mjs +229 -0
- package/test/custody.negative.test.mjs +165 -0
- package/test/derivation-parity.test.mjs +19 -0
- package/test/fixtures/derivation-vectors.json +26 -0
- package/test/fixtures/ed25519-small-order.vectors.json +15 -0
- package/test/governance-expected-red.expected-red.mjs +241 -0
- package/test/governance-scope.test.mjs +142 -0
- package/test/governance-t1-t10.test.mjs +477 -0
- package/test/governance.test.mjs +514 -0
- package/test/hold-admission.test.mjs +1185 -0
- package/test/keychain-strict-ed25519.test.mjs +240 -0
- package/test/offline.test.mjs +152 -0
- package/test/runtime-channels.test.mjs +1140 -0
- package/test/runtime.test.mjs +201 -0
- package/test/security-runtime.test.mjs +104 -0
- package/test/strict-ed25519-consumers.test.mjs +234 -0
- package/test/transport.test.mjs +353 -0
|
@@ -0,0 +1,1140 @@
|
|
|
1
|
+
// L3 Slice 2 v6 §3 / §8: Property 1-5 Two-Pole Test Suite
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
AgentCommons,
|
|
9
|
+
CommonsStore,
|
|
10
|
+
Keychain,
|
|
11
|
+
createProfile,
|
|
12
|
+
verifyProfile,
|
|
13
|
+
MemoryLoopbackTransport,
|
|
14
|
+
PillarCarrierTransport,
|
|
15
|
+
classifyThread,
|
|
16
|
+
validateTenantBinding,
|
|
17
|
+
} from "../src/index.mjs";
|
|
18
|
+
import { jcs, envelopeSha, seal } from "../src/pillar.mjs";
|
|
19
|
+
import { encode, digest } from "../src/profiles.mjs";
|
|
20
|
+
|
|
21
|
+
function makeKey() {
|
|
22
|
+
const k = new Keychain("unused");
|
|
23
|
+
k._identity = Keychain.generate();
|
|
24
|
+
return k;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeEnv(tempDir) {
|
|
28
|
+
const atlasKey = makeKey();
|
|
29
|
+
const lyraKey = makeKey();
|
|
30
|
+
const atlasId = atlasKey._identity.uuaid;
|
|
31
|
+
const lyraId = lyraKey._identity.uuaid;
|
|
32
|
+
const channelUri = "channel://local/community/general";
|
|
33
|
+
const tenantId = "tenant-a";
|
|
34
|
+
|
|
35
|
+
const policy = {
|
|
36
|
+
mode: "local",
|
|
37
|
+
offline: true,
|
|
38
|
+
channelsEnabled: true,
|
|
39
|
+
channels: {
|
|
40
|
+
[channelUri]: {
|
|
41
|
+
members: [atlasId, lyraId],
|
|
42
|
+
allowForwarding: false,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
agents: {
|
|
46
|
+
[atlasId]: {
|
|
47
|
+
kind: "agent",
|
|
48
|
+
publicKey: atlasKey._identity.publicKeyHex,
|
|
49
|
+
capabilities: ["commons:message", "commons:relay"],
|
|
50
|
+
tenantId,
|
|
51
|
+
},
|
|
52
|
+
[lyraId]: {
|
|
53
|
+
kind: "agent",
|
|
54
|
+
publicKey: lyraKey._identity.publicKeyHex,
|
|
55
|
+
capabilities: ["commons:message"],
|
|
56
|
+
tenantId,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
tenantProfiles: {},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const profile = createProfile({
|
|
63
|
+
namespace: "local/test",
|
|
64
|
+
name: "Test Profile",
|
|
65
|
+
scope: "local",
|
|
66
|
+
fixtures: ["semantic equivalence A", "semantic equivalence B"],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
policy.tenantProfiles[profile.id] = tenantId;
|
|
70
|
+
|
|
71
|
+
const dbPath = tempDir ? join(tempDir, "store.db") : ":memory:";
|
|
72
|
+
const atlasStore = new CommonsStore(dbPath);
|
|
73
|
+
const lyraStore = new CommonsStore(":memory:");
|
|
74
|
+
const transport = new MemoryLoopbackTransport();
|
|
75
|
+
|
|
76
|
+
const atlasRuntime = new AgentCommons({
|
|
77
|
+
keychain: atlasKey,
|
|
78
|
+
store: atlasStore,
|
|
79
|
+
policy,
|
|
80
|
+
transport,
|
|
81
|
+
});
|
|
82
|
+
atlasRuntime.addProfile(profile);
|
|
83
|
+
|
|
84
|
+
const lyraRuntime = new AgentCommons({
|
|
85
|
+
keychain: lyraKey,
|
|
86
|
+
store: lyraStore,
|
|
87
|
+
policy,
|
|
88
|
+
transport,
|
|
89
|
+
});
|
|
90
|
+
lyraRuntime.addProfile(profile);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
atlasKey,
|
|
94
|
+
lyraKey,
|
|
95
|
+
atlasId,
|
|
96
|
+
lyraId,
|
|
97
|
+
channelUri,
|
|
98
|
+
tenantId,
|
|
99
|
+
policy,
|
|
100
|
+
profile,
|
|
101
|
+
atlasStore,
|
|
102
|
+
lyraStore,
|
|
103
|
+
transport,
|
|
104
|
+
atlasRuntime,
|
|
105
|
+
lyraRuntime,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =========================================================================
|
|
110
|
+
// Property 1: Pluggable Transport Wiring & Receipt Acceptance (§3.1, §3.3, §3.5)
|
|
111
|
+
// =========================================================================
|
|
112
|
+
test("Property 1: CONTROL A - Memory transport auto-flush delivers and consumes outbox row", async () => {
|
|
113
|
+
const env = makeEnv();
|
|
114
|
+
const fixtureReceipt = {
|
|
115
|
+
carrier: "memory://loopback/test",
|
|
116
|
+
seq: 1,
|
|
117
|
+
sha: "fixture-sha",
|
|
118
|
+
duplicate: false,
|
|
119
|
+
all: [],
|
|
120
|
+
accepted: true,
|
|
121
|
+
};
|
|
122
|
+
env.transport.deliver = async (envelope) => fixtureReceipt;
|
|
123
|
+
|
|
124
|
+
const res = await env.atlasRuntime.send({
|
|
125
|
+
recipient: env.lyraId,
|
|
126
|
+
profileId: env.profile.id,
|
|
127
|
+
thread: env.channelUri,
|
|
128
|
+
body: "hello",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
assert.equal(res.result.length, 1);
|
|
132
|
+
assert.equal(res.result[0].state, "carrier-accepted");
|
|
133
|
+
assert.deepEqual(res.result[0].receipt, fixtureReceipt);
|
|
134
|
+
|
|
135
|
+
const pendingRows = env.atlasStore.pending();
|
|
136
|
+
assert.equal(pendingRows.length, 0);
|
|
137
|
+
|
|
138
|
+
const subsequentFlush = await env.atlasRuntime.flush();
|
|
139
|
+
assert.deepEqual(subsequentFlush, []);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("Property 1: CONTROL B - Omitted transport fallback preserves pending row without error", async () => {
|
|
143
|
+
const atlasKey = makeKey();
|
|
144
|
+
const atlasId = atlasKey._identity.uuaid;
|
|
145
|
+
const lyraKey = makeKey();
|
|
146
|
+
const lyraId = lyraKey._identity.uuaid;
|
|
147
|
+
const channelUri = "channel://local/community/general";
|
|
148
|
+
const tenantId = "tenant-a";
|
|
149
|
+
|
|
150
|
+
const policy = {
|
|
151
|
+
mode: "local",
|
|
152
|
+
offline: true,
|
|
153
|
+
channelsEnabled: true,
|
|
154
|
+
channels: { [channelUri]: { members: [atlasId, lyraId] } },
|
|
155
|
+
agents: {
|
|
156
|
+
[atlasId]: { kind: "agent", publicKey: atlasKey._identity.publicKeyHex, capabilities: ["commons:message"], tenantId },
|
|
157
|
+
[lyraId]: { kind: "agent", publicKey: lyraKey._identity.publicKeyHex, capabilities: ["commons:message"], tenantId },
|
|
158
|
+
},
|
|
159
|
+
tenantProfiles: {},
|
|
160
|
+
};
|
|
161
|
+
const profile = createProfile({
|
|
162
|
+
namespace: "local/test",
|
|
163
|
+
name: "Test Profile",
|
|
164
|
+
scope: "local",
|
|
165
|
+
fixtures: ["semantic equivalence A", "semantic equivalence B"],
|
|
166
|
+
});
|
|
167
|
+
policy.tenantProfiles[profile.id] = tenantId;
|
|
168
|
+
|
|
169
|
+
const atlasStore = new CommonsStore(":memory:");
|
|
170
|
+
const atlasRuntime = new AgentCommons({
|
|
171
|
+
keychain: atlasKey,
|
|
172
|
+
store: atlasStore,
|
|
173
|
+
policy,
|
|
174
|
+
carriers: [], // Omitted transport and empty carriers
|
|
175
|
+
});
|
|
176
|
+
atlasRuntime.addProfile(profile);
|
|
177
|
+
|
|
178
|
+
const res = await atlasRuntime.send({
|
|
179
|
+
recipient: lyraId,
|
|
180
|
+
profileId: profile.id,
|
|
181
|
+
thread: channelUri,
|
|
182
|
+
body: "hello",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
assert.equal(res.state, "pending");
|
|
186
|
+
assert.equal(res.reason, "No configured carrier");
|
|
187
|
+
|
|
188
|
+
const flushRes = await atlasRuntime.flush();
|
|
189
|
+
assert.deepEqual(flushRes, []);
|
|
190
|
+
|
|
191
|
+
const pendingRows = atlasStore.pending();
|
|
192
|
+
assert.equal(pendingRows.length, 1);
|
|
193
|
+
assert.equal(pendingRows[0].state, "pending");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("Property 1: MUTANT A - Unaccepted receipt refusal retains pending with backoff", async () => {
|
|
197
|
+
const env = makeEnv();
|
|
198
|
+
const rejectedReceipt = {
|
|
199
|
+
carrier: "memory://loopback/test",
|
|
200
|
+
seq: 1,
|
|
201
|
+
sha: "fixture-sha",
|
|
202
|
+
duplicate: false,
|
|
203
|
+
all: [],
|
|
204
|
+
accepted: false,
|
|
205
|
+
};
|
|
206
|
+
env.transport.deliver = async () => rejectedReceipt;
|
|
207
|
+
|
|
208
|
+
const res = await env.atlasRuntime.send({
|
|
209
|
+
recipient: env.lyraId,
|
|
210
|
+
profileId: env.profile.id,
|
|
211
|
+
thread: env.channelUri,
|
|
212
|
+
body: "hello",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
assert.equal(res.result.length, 1);
|
|
216
|
+
assert.equal(res.result[0].state, "retry-or-quarantine");
|
|
217
|
+
assert.equal(res.result[0].error, "Carrier rejected envelope");
|
|
218
|
+
|
|
219
|
+
const pendingRows = env.atlasStore.pending();
|
|
220
|
+
assert.equal(pendingRows.length, 0); // next_at is in future (now + 2000), so pending() filters it out
|
|
221
|
+
|
|
222
|
+
const allRows = env.atlasStore.db.prepare("SELECT * FROM outbox").all();
|
|
223
|
+
assert.equal(allRows.length, 1);
|
|
224
|
+
assert.equal(allRows[0].state, "pending");
|
|
225
|
+
assert.equal(allRows[0].attempts, 1);
|
|
226
|
+
assert.equal(allRows[0].error, "Carrier rejected envelope");
|
|
227
|
+
|
|
228
|
+
const flushRes = await env.atlasRuntime.flush();
|
|
229
|
+
assert.deepEqual(flushRes, []);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("Property 1: Five-attempt quarantine boundary with controlled clock", async () => {
|
|
233
|
+
const env = makeEnv();
|
|
234
|
+
env.transport.deliver = async () => ({ accepted: false, carrier: "mem", sha: "s" });
|
|
235
|
+
|
|
236
|
+
await env.atlasRuntime.send({
|
|
237
|
+
recipient: env.lyraId,
|
|
238
|
+
profileId: env.profile.id,
|
|
239
|
+
thread: env.channelUri,
|
|
240
|
+
body: "retry-test",
|
|
241
|
+
deferFlush: true,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const row = env.atlasStore.pending()[0];
|
|
245
|
+
assert.ok(row);
|
|
246
|
+
|
|
247
|
+
// Simulate attempts 1 to 4 with store.failure()
|
|
248
|
+
for (let i = 1; i <= 4; i++) {
|
|
249
|
+
env.atlasStore.failure(row.id, `error-${i}`);
|
|
250
|
+
const r = env.atlasStore.db.prepare("SELECT * FROM outbox WHERE id = ?").get(row.id);
|
|
251
|
+
assert.equal(r.state, "pending");
|
|
252
|
+
assert.equal(r.attempts, i);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Attempt 5 triggers quarantine
|
|
256
|
+
env.atlasStore.failure(row.id, "error-5");
|
|
257
|
+
const quarantinedRow = env.atlasStore.db.prepare("SELECT * FROM outbox WHERE id = ?").get(row.id);
|
|
258
|
+
assert.equal(quarantinedRow.state, "quarantined");
|
|
259
|
+
assert.equal(quarantinedRow.attempts, 5);
|
|
260
|
+
assert.equal(env.atlasStore.pending().length, 0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("Property 1: MUTANT B - Offline non-loopback carrier catch returns retry-or-quarantine", async () => {
|
|
264
|
+
const origOffline = process.env.AGENT_COMMONS_OFFLINE;
|
|
265
|
+
process.env.AGENT_COMMONS_OFFLINE = "1";
|
|
266
|
+
try {
|
|
267
|
+
const env = makeEnv();
|
|
268
|
+
const mockCarrierClient = {
|
|
269
|
+
deliver: async () => {
|
|
270
|
+
throw new Error("Carrier refused loopback requirement: https://carrier.example.com");
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const pillarTransport = new PillarCarrierTransport({
|
|
274
|
+
carriers: ["https://carrier.example.com"],
|
|
275
|
+
client: mockCarrierClient,
|
|
276
|
+
});
|
|
277
|
+
env.atlasRuntime.transport = pillarTransport;
|
|
278
|
+
|
|
279
|
+
const res = await env.atlasRuntime.send({
|
|
280
|
+
recipient: env.lyraId,
|
|
281
|
+
profileId: env.profile.id,
|
|
282
|
+
thread: env.channelUri,
|
|
283
|
+
body: "offline-test",
|
|
284
|
+
deferFlush: true,
|
|
285
|
+
});
|
|
286
|
+
assert.equal(res.state, "pending");
|
|
287
|
+
|
|
288
|
+
const flushRes = await env.atlasRuntime.flush();
|
|
289
|
+
assert.equal(flushRes.length, 1);
|
|
290
|
+
assert.equal(flushRes[0].state, "retry-or-quarantine");
|
|
291
|
+
assert.match(flushRes[0].error, /offline|loopback/i);
|
|
292
|
+
|
|
293
|
+
const pendingInDb = env.atlasStore.db.prepare("SELECT * FROM outbox").all();
|
|
294
|
+
assert.equal(pendingInDb.length, 1);
|
|
295
|
+
assert.equal(pendingInDb[0].state, "pending");
|
|
296
|
+
} finally {
|
|
297
|
+
if (origOffline === undefined) delete process.env.AGENT_COMMONS_OFFLINE;
|
|
298
|
+
else process.env.AGENT_COMMONS_OFFLINE = origOffline;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// =========================================================================
|
|
303
|
+
// Property 2: Structured Channel Send Admission & Validation Precedence (§4.2, §4.2.3)
|
|
304
|
+
// =========================================================================
|
|
305
|
+
test("Property 2: CONTROL - Structured channel send succeeds when sender and recipient are admitted members", async () => {
|
|
306
|
+
const env = makeEnv();
|
|
307
|
+
const res = await env.atlasRuntime.send({
|
|
308
|
+
recipient: env.lyraId,
|
|
309
|
+
profileId: env.profile.id,
|
|
310
|
+
thread: env.channelUri,
|
|
311
|
+
body: "passing-channel-msg",
|
|
312
|
+
});
|
|
313
|
+
assert.equal(res.result[0].state, "carrier-accepted");
|
|
314
|
+
assert.ok(res.envelope);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("Property 2: MUTANT A - Unauthorized sender throws UNAUTHORIZED_CHANNEL_SENDER with zero writes", async () => {
|
|
318
|
+
const env = makeEnv();
|
|
319
|
+
env.policy.channels[env.channelUri].members = [env.lyraId]; // Atlas removed
|
|
320
|
+
|
|
321
|
+
await assert.rejects(
|
|
322
|
+
() =>
|
|
323
|
+
env.atlasRuntime.send({
|
|
324
|
+
recipient: env.lyraId,
|
|
325
|
+
profileId: env.profile.id,
|
|
326
|
+
thread: env.channelUri,
|
|
327
|
+
body: "unauthorized-sender",
|
|
328
|
+
}),
|
|
329
|
+
(err) => err.code === "UNAUTHORIZED_CHANNEL_SENDER" || /UNAUTHORIZED_CHANNEL_SENDER|Sender atlas is unauthorized/.test(err.message)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
|
|
333
|
+
assert.equal(outboxRows.count, 0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("Property 2: MUTANT B - Unauthorized recipient throws UNAUTHORIZED_CHANNEL_RECIPIENT with zero writes", async () => {
|
|
337
|
+
const env = makeEnv();
|
|
338
|
+
env.policy.channels[env.channelUri].members = [env.atlasId]; // Lyra removed
|
|
339
|
+
|
|
340
|
+
await assert.rejects(
|
|
341
|
+
() =>
|
|
342
|
+
env.atlasRuntime.send({
|
|
343
|
+
recipient: env.lyraId,
|
|
344
|
+
profileId: env.profile.id,
|
|
345
|
+
thread: env.channelUri,
|
|
346
|
+
body: "unauthorized-recipient",
|
|
347
|
+
}),
|
|
348
|
+
(err) => err.code === "UNAUTHORIZED_CHANNEL_RECIPIENT" || /UNAUTHORIZED_CHANNEL_RECIPIENT|Receiver lyra is unauthorized/.test(err.message)
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
|
|
352
|
+
assert.equal(outboxRows.count, 0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("Property 2: MUTANT C - Reserved-prefix malformed / uppercase URI throws INVALID_CHANNEL_URI", async () => {
|
|
356
|
+
const env = makeEnv();
|
|
357
|
+
env.policy.channelsEnabled = false; // Even when channels disabled, reserved prefix takes precedence
|
|
358
|
+
|
|
359
|
+
for (const badUri of ["channel://LOCAL/COMMUNITY/GENERAL", "channel:malformed"]) {
|
|
360
|
+
await assert.rejects(
|
|
361
|
+
() =>
|
|
362
|
+
env.atlasRuntime.send({
|
|
363
|
+
recipient: env.lyraId,
|
|
364
|
+
profileId: env.profile.id,
|
|
365
|
+
thread: badUri,
|
|
366
|
+
body: "bad-uri",
|
|
367
|
+
}),
|
|
368
|
+
(err) => err.code === "INVALID_CHANNEL_URI" || /INVALID_CHANNEL_URI/.test(err.message)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
|
|
373
|
+
assert.equal(outboxRows.count, 0);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("Property 2: MUTANT D - Unconfigured channel throws UNCONFIGURED_CHANNEL with zero writes", async () => {
|
|
377
|
+
const env = makeEnv();
|
|
378
|
+
await assert.rejects(
|
|
379
|
+
() =>
|
|
380
|
+
env.atlasRuntime.send({
|
|
381
|
+
recipient: env.lyraId,
|
|
382
|
+
profileId: env.profile.id,
|
|
383
|
+
thread: "channel://local/community/unconfigured",
|
|
384
|
+
body: "unconfigured-channel",
|
|
385
|
+
}),
|
|
386
|
+
(err) => err.code === "UNCONFIGURED_CHANNEL" || /UNCONFIGURED_CHANNEL|Channel is unconfigured/.test(err.message)
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
|
|
390
|
+
assert.equal(outboxRows.count, 0);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("Property 2: MUTANT E - Strict mode legacy thread throws STRUCTURED_CHANNELS_REQUIRED with zero writes", async () => {
|
|
394
|
+
const env = makeEnv();
|
|
395
|
+
env.policy.channelsEnabled = true;
|
|
396
|
+
|
|
397
|
+
await assert.rejects(
|
|
398
|
+
() =>
|
|
399
|
+
env.atlasRuntime.send({
|
|
400
|
+
recipient: env.lyraId,
|
|
401
|
+
profileId: env.profile.id,
|
|
402
|
+
thread: "commons",
|
|
403
|
+
body: "legacy-thread",
|
|
404
|
+
}),
|
|
405
|
+
(err) => err.code === "STRUCTURED_CHANNELS_REQUIRED" || /STRUCTURED_CHANNELS_REQUIRED/.test(err.message)
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
|
|
409
|
+
assert.equal(outboxRows.count, 0);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// =========================================================================
|
|
413
|
+
// Property 3: Structured Channel Inbound Receive Admission & Quarantine (§4.2, §4.2.3)
|
|
414
|
+
// =========================================================================
|
|
415
|
+
test("Property 3: CONTROL - Inbound receive and poll succeed for admitted member envelope", async () => {
|
|
416
|
+
const env = makeEnv();
|
|
417
|
+
// 1. Direct receive on envelope 1
|
|
418
|
+
const sent1 = await env.atlasRuntime.send({
|
|
419
|
+
recipient: env.lyraId,
|
|
420
|
+
profileId: env.profile.id,
|
|
421
|
+
thread: env.channelUri,
|
|
422
|
+
body: "hello-lyra-direct",
|
|
423
|
+
deferFlush: true,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const recv = await env.lyraRuntime.receive(sent1.envelope);
|
|
427
|
+
assert.equal(recv.receipt.kind, "host-accepted");
|
|
428
|
+
assert.equal(recv.body, "hello-lyra-direct");
|
|
429
|
+
assert.equal(recv.accepted, true);
|
|
430
|
+
|
|
431
|
+
const processedCount1 = env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c;
|
|
432
|
+
assert.equal(processedCount1, 1);
|
|
433
|
+
|
|
434
|
+
// 2. Poll on envelope 2 delivered to transport
|
|
435
|
+
const sent2 = await env.atlasRuntime.send({
|
|
436
|
+
recipient: env.lyraId,
|
|
437
|
+
profileId: env.profile.id,
|
|
438
|
+
thread: env.channelUri,
|
|
439
|
+
body: "hello-lyra-poll",
|
|
440
|
+
deferFlush: true,
|
|
441
|
+
});
|
|
442
|
+
await env.transport.deliver(sent2.envelope);
|
|
443
|
+
|
|
444
|
+
const polled = await env.lyraRuntime.poll();
|
|
445
|
+
assert.equal(polled.length, 1);
|
|
446
|
+
assert.equal(polled[0].accepted, true);
|
|
447
|
+
assert.equal(polled[0].body, "hello-lyra-poll");
|
|
448
|
+
|
|
449
|
+
const processedCount2 = env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c;
|
|
450
|
+
assert.equal(processedCount2, 2);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("Property 3: MUTANT A & B - Unauthorized inbound sender fails receive() and poll()", async () => {
|
|
454
|
+
const env = makeEnv();
|
|
455
|
+
const sent = await env.atlasRuntime.send({
|
|
456
|
+
recipient: env.lyraId,
|
|
457
|
+
profileId: env.profile.id,
|
|
458
|
+
thread: env.channelUri,
|
|
459
|
+
body: "sender-unauth-test",
|
|
460
|
+
deferFlush: true,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Mutate receiver policy only
|
|
464
|
+
env.lyraRuntime.policy.channels[env.channelUri].members = [env.lyraId]; // Atlas removed
|
|
465
|
+
|
|
466
|
+
// MUTANT A: direct receive() throws
|
|
467
|
+
await assert.rejects(
|
|
468
|
+
() => env.lyraRuntime.receive(sent.envelope),
|
|
469
|
+
(err) => err.code === "UNAUTHORIZED_CHANNEL_SENDER" || /UNAUTHORIZED_CHANNEL_SENDER|Sender .* is unauthorized for channel/.test(err.message)
|
|
470
|
+
);
|
|
471
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
472
|
+
assert.equal(env.lyraStore.audit().filter(e => JSON.parse(e.body).kind === "control-received").length, 0);
|
|
473
|
+
|
|
474
|
+
// MUTANT B: poll() quarantines with exact reason and advances cursor
|
|
475
|
+
await env.transport.deliver(sent.envelope);
|
|
476
|
+
const polled = await env.lyraRuntime.poll();
|
|
477
|
+
assert.equal(polled.length, 1);
|
|
478
|
+
assert.equal(polled[0].rejected, true);
|
|
479
|
+
assert.equal(polled[0].reason, `Sender ${env.atlasId} is unauthorized for channel ${env.channelUri}`);
|
|
480
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
481
|
+
assert.equal(env.lyraStore.audit().filter(e => JSON.parse(e.body).kind === "control-received").length, 0);
|
|
482
|
+
|
|
483
|
+
// Second poll returns empty because cursor advanced
|
|
484
|
+
const secondPoll = await env.lyraRuntime.poll();
|
|
485
|
+
assert.equal(secondPoll.length, 0);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("Property 3: MUTANT C & D - Unauthorized inbound receiver fails receive() and poll()", async () => {
|
|
489
|
+
const env = makeEnv();
|
|
490
|
+
const sent = await env.atlasRuntime.send({
|
|
491
|
+
recipient: env.lyraId,
|
|
492
|
+
profileId: env.profile.id,
|
|
493
|
+
thread: env.channelUri,
|
|
494
|
+
body: "recv-unauth-test",
|
|
495
|
+
deferFlush: true,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Mutate receiver policy only: Lyra removed
|
|
499
|
+
env.lyraRuntime.policy.channels[env.channelUri].members = [env.atlasId];
|
|
500
|
+
|
|
501
|
+
// MUTANT C: direct receive() throws
|
|
502
|
+
await assert.rejects(
|
|
503
|
+
() => env.lyraRuntime.receive(sent.envelope),
|
|
504
|
+
(err) => err.code === "UNAUTHORIZED_CHANNEL_RECEIVER" || /UNAUTHORIZED_CHANNEL_RECEIVER|Receiver .* is unauthorized for channel/.test(err.message)
|
|
505
|
+
);
|
|
506
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
507
|
+
|
|
508
|
+
// MUTANT D: poll() quarantines with exact reason and advances cursor
|
|
509
|
+
await env.transport.deliver(sent.envelope);
|
|
510
|
+
const polled = await env.lyraRuntime.poll();
|
|
511
|
+
assert.equal(polled.length, 1);
|
|
512
|
+
assert.equal(polled[0].rejected, true);
|
|
513
|
+
assert.equal(polled[0].reason, `Receiver ${env.lyraId} is unauthorized for channel ${env.channelUri}`);
|
|
514
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
515
|
+
|
|
516
|
+
const secondPoll = await env.lyraRuntime.poll();
|
|
517
|
+
assert.equal(secondPoll.length, 0);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("Property 3: MUTANT E - Strict mode inbound legacy rejection fails receive() and poll()", async () => {
|
|
521
|
+
const env = makeEnv();
|
|
522
|
+
// Sender queues legacy envelope under channelsEnabled: false
|
|
523
|
+
env.atlasRuntime.policy.channelsEnabled = false;
|
|
524
|
+
const sent = await env.atlasRuntime.send({
|
|
525
|
+
recipient: env.lyraId,
|
|
526
|
+
profileId: env.profile.id,
|
|
527
|
+
thread: "commons",
|
|
528
|
+
body: "legacy-inbound",
|
|
529
|
+
deferFlush: true,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Receiver has channelsEnabled: true (strict)
|
|
533
|
+
env.lyraRuntime.policy.channelsEnabled = true;
|
|
534
|
+
|
|
535
|
+
// Direct receive
|
|
536
|
+
await assert.rejects(
|
|
537
|
+
() => env.lyraRuntime.receive(sent.envelope),
|
|
538
|
+
(err) => err.code === "STRUCTURED_CHANNELS_REQUIRED" || /STRUCTURED_CHANNELS_REQUIRED/.test(err.message)
|
|
539
|
+
);
|
|
540
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
541
|
+
|
|
542
|
+
// Poll
|
|
543
|
+
await env.transport.deliver(sent.envelope);
|
|
544
|
+
const polled = await env.lyraRuntime.poll();
|
|
545
|
+
assert.equal(polled.length, 1);
|
|
546
|
+
assert.equal(polled[0].rejected, true);
|
|
547
|
+
assert.match(polled[0].reason, /STRUCTURED_CHANNELS_REQUIRED/);
|
|
548
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("Property 3: MUTANT F - Inbound reserved-malformed rejection fails receive() and poll()", async () => {
|
|
552
|
+
const env = makeEnv();
|
|
553
|
+
const bodyText = "malformed-thread-body";
|
|
554
|
+
const payload = {
|
|
555
|
+
v: "agent-commons/1",
|
|
556
|
+
id: "00000000-0000-0000-0000-000000000001",
|
|
557
|
+
profileId: env.profile.id,
|
|
558
|
+
thread: "channel:malformed",
|
|
559
|
+
kind: "message",
|
|
560
|
+
wire: encode(bodyText, env.profile.lexicon),
|
|
561
|
+
bodyHash: digest(bodyText),
|
|
562
|
+
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
563
|
+
};
|
|
564
|
+
const sealedEnvelope = seal(env.atlasKey, {
|
|
565
|
+
recipient: env.lyraId,
|
|
566
|
+
recipientPublicKey: env.lyraKey._identity.publicKeyHex,
|
|
567
|
+
kind: "agent-commons/1",
|
|
568
|
+
payload,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Direct receive
|
|
572
|
+
await assert.rejects(
|
|
573
|
+
() => env.lyraRuntime.receive(sealedEnvelope),
|
|
574
|
+
(err) => err.code === "INVALID_CHANNEL_URI" || /INVALID_CHANNEL_URI/.test(err.message)
|
|
575
|
+
);
|
|
576
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
577
|
+
|
|
578
|
+
// Poll
|
|
579
|
+
await env.transport.deliver(sealedEnvelope);
|
|
580
|
+
const polled = await env.lyraRuntime.poll();
|
|
581
|
+
assert.equal(polled.length, 1);
|
|
582
|
+
assert.equal(polled[0].rejected, true);
|
|
583
|
+
assert.match(polled[0].reason, /INVALID_CHANNEL_URI/);
|
|
584
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// =========================================================================
|
|
588
|
+
// Property 4: Tenant Boundary Fail-Closed Across All Scopes & Durable Flush (§5.1, §5.2)
|
|
589
|
+
// =========================================================================
|
|
590
|
+
test("Property 4: CONTROL A & B - Tenant matching and unbound local exemption", async () => {
|
|
591
|
+
const env = makeEnv();
|
|
592
|
+
// CONTROL A: Local profile bound to tenant-a, matching sender and receiver
|
|
593
|
+
const resA = await env.atlasRuntime.send({
|
|
594
|
+
recipient: env.lyraId,
|
|
595
|
+
profileId: env.profile.id,
|
|
596
|
+
thread: env.channelUri,
|
|
597
|
+
body: "tenant-match",
|
|
598
|
+
});
|
|
599
|
+
assert.equal(resA.result[0].state, "carrier-accepted");
|
|
600
|
+
|
|
601
|
+
// CONTROL B: Unbound local profile exemption
|
|
602
|
+
const unboundLocalProfile = createProfile({
|
|
603
|
+
namespace: "local/unbound",
|
|
604
|
+
name: "Unbound Local Profile",
|
|
605
|
+
scope: "local",
|
|
606
|
+
fixtures: ["fixture one local", "fixture two local"],
|
|
607
|
+
});
|
|
608
|
+
env.atlasRuntime.addProfile(unboundLocalProfile);
|
|
609
|
+
env.lyraRuntime.addProfile(unboundLocalProfile);
|
|
610
|
+
delete env.policy.tenantProfiles[unboundLocalProfile.id];
|
|
611
|
+
|
|
612
|
+
const resB = await env.atlasRuntime.send({
|
|
613
|
+
recipient: env.lyraId,
|
|
614
|
+
profileId: unboundLocalProfile.id,
|
|
615
|
+
thread: env.channelUri,
|
|
616
|
+
body: "unbound-local-pass",
|
|
617
|
+
});
|
|
618
|
+
assert.equal(resB.result[0].state, "carrier-accepted");
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("Property 4: MUTANT A & B - Bound local tenant mismatch throws TENANT_NAMESPACE_MISMATCH", async () => {
|
|
622
|
+
const envA = makeEnv();
|
|
623
|
+
envA.policy.agents[envA.lyraId].tenantId = "tenant-b"; // Recipient mismatch
|
|
624
|
+
await assert.rejects(
|
|
625
|
+
() =>
|
|
626
|
+
envA.atlasRuntime.send({
|
|
627
|
+
recipient: envA.lyraId,
|
|
628
|
+
profileId: envA.profile.id,
|
|
629
|
+
thread: envA.channelUri,
|
|
630
|
+
body: "msg",
|
|
631
|
+
}),
|
|
632
|
+
(err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const envB = makeEnv();
|
|
636
|
+
envB.policy.agents[envB.atlasId].tenantId = "tenant-b"; // Sender mismatch
|
|
637
|
+
await assert.rejects(
|
|
638
|
+
() =>
|
|
639
|
+
envB.atlasRuntime.send({
|
|
640
|
+
recipient: envB.lyraId,
|
|
641
|
+
profileId: envB.profile.id,
|
|
642
|
+
thread: envB.channelUri,
|
|
643
|
+
body: "msg",
|
|
644
|
+
}),
|
|
645
|
+
(err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test("Property 4: MUTANT C & D - Missing tenant binding throws TENANT_NAMESPACE_MISMATCH", async () => {
|
|
650
|
+
const envC = makeEnv();
|
|
651
|
+
delete envC.policy.agents[envC.atlasId].tenantId; // Missing sender binding
|
|
652
|
+
await assert.rejects(
|
|
653
|
+
() =>
|
|
654
|
+
envC.atlasRuntime.send({
|
|
655
|
+
recipient: envC.lyraId,
|
|
656
|
+
profileId: envC.profile.id,
|
|
657
|
+
thread: envC.channelUri,
|
|
658
|
+
body: "msg",
|
|
659
|
+
}),
|
|
660
|
+
(err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const envD = makeEnv();
|
|
664
|
+
delete envD.policy.agents[envD.lyraId].tenantId; // Missing recipient binding
|
|
665
|
+
await assert.rejects(
|
|
666
|
+
() =>
|
|
667
|
+
envD.atlasRuntime.send({
|
|
668
|
+
recipient: envD.lyraId,
|
|
669
|
+
profileId: envD.profile.id,
|
|
670
|
+
thread: envD.channelUri,
|
|
671
|
+
body: "msg",
|
|
672
|
+
}),
|
|
673
|
+
(err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
|
|
674
|
+
);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("Property 4: MUTANT E, F, G - Inbound receive tenant mismatches and missing bindings fail closed", async () => {
|
|
678
|
+
const env = makeEnv();
|
|
679
|
+
const sent = await env.atlasRuntime.send({
|
|
680
|
+
recipient: env.lyraId,
|
|
681
|
+
profileId: env.profile.id,
|
|
682
|
+
thread: env.channelUri,
|
|
683
|
+
body: "inbound-tenant-test",
|
|
684
|
+
deferFlush: true,
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// MUTANT E: Receiver has tenant-b
|
|
688
|
+
env.lyraRuntime.policy.agents[env.lyraId].tenantId = "tenant-b";
|
|
689
|
+
await assert.rejects(
|
|
690
|
+
() => env.lyraRuntime.receive(sent.envelope),
|
|
691
|
+
(err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
|
|
692
|
+
);
|
|
693
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
694
|
+
|
|
695
|
+
// Reset Lyra to tenant-a, then MUTANT F: Sender tenantId removed from receiver policy
|
|
696
|
+
env.lyraRuntime.policy.agents[env.lyraId].tenantId = "tenant-a";
|
|
697
|
+
delete env.lyraRuntime.policy.agents[env.atlasId].tenantId;
|
|
698
|
+
await assert.rejects(
|
|
699
|
+
() => env.lyraRuntime.receive(sent.envelope),
|
|
700
|
+
(err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
|
|
701
|
+
);
|
|
702
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
703
|
+
|
|
704
|
+
// Restore sender tenantId, then MUTANT G: Receiver tenantId removed from receiver policy
|
|
705
|
+
env.lyraRuntime.policy.agents[env.atlasId].tenantId = "tenant-a";
|
|
706
|
+
delete env.lyraRuntime.policy.agents[env.lyraId].tenantId;
|
|
707
|
+
await assert.rejects(
|
|
708
|
+
() => env.lyraRuntime.receive(sent.envelope),
|
|
709
|
+
(err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
|
|
710
|
+
);
|
|
711
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("Property 4: MUTANT H & I - Unbound tenant and global scope profiles throw UNBOUND_PROFILE_TENANT", async () => {
|
|
715
|
+
const env = makeEnv();
|
|
716
|
+
|
|
717
|
+
// MUTANT H: scope tenant without tenantProfiles entry
|
|
718
|
+
const tenantProfile = createProfile({
|
|
719
|
+
namespace: "tenant/org",
|
|
720
|
+
name: "Org Profile",
|
|
721
|
+
scope: "tenant",
|
|
722
|
+
fixtures: ["tenant fixture alpha", "tenant fixture beta"],
|
|
723
|
+
});
|
|
724
|
+
env.atlasRuntime.addProfile(tenantProfile);
|
|
725
|
+
delete env.policy.tenantProfiles[tenantProfile.id];
|
|
726
|
+
|
|
727
|
+
await assert.rejects(
|
|
728
|
+
() =>
|
|
729
|
+
env.atlasRuntime.send({
|
|
730
|
+
recipient: env.lyraId,
|
|
731
|
+
profileId: tenantProfile.id,
|
|
732
|
+
thread: env.channelUri,
|
|
733
|
+
body: "tenant-unbound",
|
|
734
|
+
}),
|
|
735
|
+
(err) => err.code === "UNBOUND_PROFILE_TENANT" || /UNBOUND_PROFILE_TENANT|Unbound tenant profile/.test(err.message)
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
// MUTANT I: scope global without tenantProfiles entry, with local trust stubs
|
|
739
|
+
const globalProfile = createProfile({
|
|
740
|
+
namespace: "global/standard",
|
|
741
|
+
name: "Standard Global Profile",
|
|
742
|
+
scope: "global",
|
|
743
|
+
fixtures: ["global fixture alpha", "global fixture beta"],
|
|
744
|
+
});
|
|
745
|
+
const pins = env.atlasRuntime.store.get("globalProfilePins", {});
|
|
746
|
+
pins[globalProfile.id] = "IAASO-1001";
|
|
747
|
+
env.atlasRuntime.store.set("globalProfilePins", pins);
|
|
748
|
+
env.atlasRuntime.store.set("profiles", [...env.atlasRuntime.profiles(), globalProfile]);
|
|
749
|
+
delete env.policy.tenantProfiles[globalProfile.id];
|
|
750
|
+
// Local stubs preventing external network
|
|
751
|
+
env.atlasRuntime.trust = {
|
|
752
|
+
publishedProfilePin: async () => ({ code: "IAASO-1001", hash: globalProfile.fixtureHash }),
|
|
753
|
+
authorize: async () => ({ kind: "agent", capabilities: ["commons:message"] }),
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
await assert.rejects(
|
|
757
|
+
() =>
|
|
758
|
+
env.atlasRuntime.send({
|
|
759
|
+
recipient: env.lyraId,
|
|
760
|
+
profileId: globalProfile.id,
|
|
761
|
+
thread: env.channelUri,
|
|
762
|
+
body: "global-unbound",
|
|
763
|
+
}),
|
|
764
|
+
(err) => err.code === "UNBOUND_PROFILE_TENANT" || /UNBOUND_PROFILE_TENANT|Unbound tenant profile/.test(err.message)
|
|
765
|
+
);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("Property 4: Scope Separation Pole (R3) - verifyProfile validator vs validateTenantBinding", () => {
|
|
769
|
+
const base = createProfile({
|
|
770
|
+
namespace: "local/rebuilt",
|
|
771
|
+
name: "Rebuilt Local",
|
|
772
|
+
scope: "local",
|
|
773
|
+
fixtures: ["fixture one local", "fixture two local"],
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// 1. Unrecognized scope rejected at verifyProfile seam with 'Invalid profile scope'
|
|
777
|
+
assert.throws(
|
|
778
|
+
() => verifyProfile({ ...base, scope: "unknown" }),
|
|
779
|
+
/Invalid profile scope/
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
// 2. Local profile with scope: undefined is rebuilt as 'local' by verifyProfile
|
|
783
|
+
const withoutScope = { ...base };
|
|
784
|
+
delete withoutScope.scope;
|
|
785
|
+
const verified = verifyProfile(withoutScope);
|
|
786
|
+
assert.equal(verified.scope, "local");
|
|
787
|
+
|
|
788
|
+
// 3. At validateTenantBinding with unbound tenant profile, fails closed with UNBOUND_PROFILE_TENANT
|
|
789
|
+
assert.throws(
|
|
790
|
+
() => validateTenantBinding("atlas", "lyra", { id: "some-id", scope: "tenant" }, {}),
|
|
791
|
+
(err) => err.code === "UNBOUND_PROFILE_TENANT" || /UNBOUND_PROFILE_TENANT|Unbound tenant profile/.test(err.message)
|
|
792
|
+
);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("Property 4: Inbound unbound tenant scope refusal via receive()", async () => {
|
|
796
|
+
const env = makeEnv();
|
|
797
|
+
const tenantProfile = createProfile({
|
|
798
|
+
namespace: "tenant/org",
|
|
799
|
+
name: "Org Profile",
|
|
800
|
+
scope: "tenant",
|
|
801
|
+
fixtures: ["tenant fixture 1", "tenant fixture 2"],
|
|
802
|
+
});
|
|
803
|
+
env.policy.tenantProfiles[tenantProfile.id] = "tenant-a";
|
|
804
|
+
env.atlasRuntime.addProfile(tenantProfile);
|
|
805
|
+
env.lyraRuntime.addProfile(tenantProfile);
|
|
806
|
+
|
|
807
|
+
const sent = await env.atlasRuntime.send({
|
|
808
|
+
recipient: env.lyraId,
|
|
809
|
+
profileId: tenantProfile.id,
|
|
810
|
+
thread: env.channelUri,
|
|
811
|
+
body: "unbound-inbound",
|
|
812
|
+
deferFlush: true,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Remove profile from receiver tenantProfiles
|
|
816
|
+
delete env.lyraRuntime.policy.tenantProfiles[tenantProfile.id];
|
|
817
|
+
|
|
818
|
+
await assert.rejects(
|
|
819
|
+
() => env.lyraRuntime.receive(sent.envelope),
|
|
820
|
+
(err) => err.code === "UNBOUND_PROFILE_TENANT" || /UNBOUND_PROFILE_TENANT|Unbound tenant profile/.test(err.message)
|
|
821
|
+
);
|
|
822
|
+
assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
test("Property 4: Durable Flush Rechecks & Retained-Store Recovery (CONTROL 1 & MUTANTS 1-7)", async () => {
|
|
826
|
+
const tempDir = mkdtempSync(join(tmpdir(), "ac-p4-flush-"));
|
|
827
|
+
try {
|
|
828
|
+
const env = makeEnv(tempDir);
|
|
829
|
+
// Queue message with valid matching policy
|
|
830
|
+
const sent = await env.atlasRuntime.send({
|
|
831
|
+
recipient: env.lyraId,
|
|
832
|
+
profileId: env.profile.id,
|
|
833
|
+
thread: env.channelUri,
|
|
834
|
+
body: "durable-flush-msg",
|
|
835
|
+
deferFlush: true,
|
|
836
|
+
});
|
|
837
|
+
const msgId = sent.envelope.id;
|
|
838
|
+
|
|
839
|
+
// CONTROL 1: Retained Store Recovery
|
|
840
|
+
// Close store and runtime
|
|
841
|
+
env.atlasStore.close();
|
|
842
|
+
const recoveredStore = new CommonsStore(join(tempDir, "store.db"));
|
|
843
|
+
const recoveredRuntime = new AgentCommons({
|
|
844
|
+
keychain: env.atlasKey,
|
|
845
|
+
store: recoveredStore,
|
|
846
|
+
policy: env.policy,
|
|
847
|
+
transport: env.transport,
|
|
848
|
+
});
|
|
849
|
+
recoveredRuntime.addProfile(env.profile);
|
|
850
|
+
|
|
851
|
+
const flushControl = await recoveredRuntime.flush();
|
|
852
|
+
assert.equal(flushControl.length, 1);
|
|
853
|
+
assert.equal(flushControl[0].state, "carrier-accepted");
|
|
854
|
+
recoveredStore.close();
|
|
855
|
+
|
|
856
|
+
// Setup for MUTANT 1: Thread-only corruption
|
|
857
|
+
const storeM1 = new CommonsStore(join(tempDir, "store.db"));
|
|
858
|
+
const runtimeM1 = new AgentCommons({ keychain: env.atlasKey, store: storeM1, policy: env.policy, transport: env.transport });
|
|
859
|
+
runtimeM1.addProfile(env.profile);
|
|
860
|
+
const sentM1 = await runtimeM1.send({
|
|
861
|
+
recipient: env.lyraId,
|
|
862
|
+
profileId: env.profile.id,
|
|
863
|
+
thread: env.channelUri,
|
|
864
|
+
body: "m1-msg",
|
|
865
|
+
deferFlush: true,
|
|
866
|
+
});
|
|
867
|
+
// Tamper meta.thread in SQLite kv table
|
|
868
|
+
const metaM1 = storeM1.get(`outbox_meta:${sentM1.envelope.id}`);
|
|
869
|
+
metaM1.thread = "commons";
|
|
870
|
+
storeM1.set(`outbox_meta:${sentM1.envelope.id}`, metaM1);
|
|
871
|
+
|
|
872
|
+
const flushM1 = await runtimeM1.flush();
|
|
873
|
+
assert.equal(flushM1.length, 1);
|
|
874
|
+
assert.equal(flushM1[0].state, "retry-or-quarantine");
|
|
875
|
+
assert.equal(flushM1[0].error, "INVALID_OUTBOX_METADATA");
|
|
876
|
+
storeM1.close();
|
|
877
|
+
|
|
878
|
+
// Setup for MUTANT 2: Sender Revocation after queueing
|
|
879
|
+
const storeM2 = new CommonsStore(join(tempDir, "store.db"));
|
|
880
|
+
const runtimeM2 = new AgentCommons({ keychain: env.atlasKey, store: storeM2, policy: env.policy, transport: env.transport });
|
|
881
|
+
runtimeM2.addProfile(env.profile);
|
|
882
|
+
await runtimeM2.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m2-msg", deferFlush: true });
|
|
883
|
+
// Revoke sender
|
|
884
|
+
runtimeM2.policy.channels[env.channelUri].members = [env.lyraId];
|
|
885
|
+
const flushM2 = await runtimeM2.flush();
|
|
886
|
+
assert.equal(flushM2.length, 1);
|
|
887
|
+
assert.equal(flushM2[0].state, "retry-or-quarantine");
|
|
888
|
+
assert.match(flushM2[0].error, /UNAUTHORIZED_CHANNEL_SENDER|Sender atlas is unauthorized/);
|
|
889
|
+
runtimeM2.policy.channels[env.channelUri].members = [env.atlasId, env.lyraId]; // restore
|
|
890
|
+
storeM2.close();
|
|
891
|
+
|
|
892
|
+
// Setup for MUTANT 3: Recipient Revocation after queueing
|
|
893
|
+
const storeM3 = new CommonsStore(join(tempDir, "store.db"));
|
|
894
|
+
const runtimeM3 = new AgentCommons({ keychain: env.atlasKey, store: storeM3, policy: env.policy, transport: env.transport });
|
|
895
|
+
runtimeM3.addProfile(env.profile);
|
|
896
|
+
await runtimeM3.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m3-msg", deferFlush: true });
|
|
897
|
+
// Revoke recipient
|
|
898
|
+
runtimeM3.policy.channels[env.channelUri].members = [env.atlasId];
|
|
899
|
+
const flushM3 = await runtimeM3.flush();
|
|
900
|
+
assert.equal(flushM3.length, 1);
|
|
901
|
+
assert.equal(flushM3[0].state, "retry-or-quarantine");
|
|
902
|
+
assert.match(flushM3[0].error, /UNAUTHORIZED_CHANNEL_RECIPIENT|Receiver lyra is unauthorized/);
|
|
903
|
+
runtimeM3.policy.channels[env.channelUri].members = [env.atlasId, env.lyraId]; // restore
|
|
904
|
+
storeM3.close();
|
|
905
|
+
|
|
906
|
+
// Setup for MUTANT 4: Peer tenant revocation after queueing
|
|
907
|
+
const storeM4 = new CommonsStore(join(tempDir, "store.db"));
|
|
908
|
+
const runtimeM4 = new AgentCommons({ keychain: env.atlasKey, store: storeM4, policy: env.policy, transport: env.transport });
|
|
909
|
+
runtimeM4.addProfile(env.profile);
|
|
910
|
+
await runtimeM4.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m4-msg", deferFlush: true });
|
|
911
|
+
runtimeM4.policy.agents[env.lyraId].tenantId = "tenant-b";
|
|
912
|
+
const flushM4 = await runtimeM4.flush();
|
|
913
|
+
assert.equal(flushM4.length, 1);
|
|
914
|
+
assert.equal(flushM4[0].state, "retry-or-quarantine");
|
|
915
|
+
assert.match(flushM4[0].error, /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i);
|
|
916
|
+
runtimeM4.policy.agents[env.lyraId].tenantId = "tenant-a"; // restore
|
|
917
|
+
storeM4.close();
|
|
918
|
+
|
|
919
|
+
// Setup for MUTANT 5: Missing admission record
|
|
920
|
+
const storeM5 = new CommonsStore(join(tempDir, "store.db"));
|
|
921
|
+
const runtimeM5 = new AgentCommons({ keychain: env.atlasKey, store: storeM5, policy: env.policy, transport: env.transport });
|
|
922
|
+
runtimeM5.addProfile(env.profile);
|
|
923
|
+
const sentM5 = await runtimeM5.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m5-msg", deferFlush: true });
|
|
924
|
+
storeM5.db.prepare("DELETE FROM kv WHERE k = ?").run(`outbox_meta:${sentM5.envelope.id}`);
|
|
925
|
+
const flushM5 = await runtimeM5.flush();
|
|
926
|
+
assert.equal(flushM5.length, 1);
|
|
927
|
+
assert.equal(flushM5[0].state, "retry-or-quarantine");
|
|
928
|
+
assert.equal(flushM5[0].error, "INVALID_OUTBOX_METADATA");
|
|
929
|
+
storeM5.close();
|
|
930
|
+
|
|
931
|
+
// Setup for MUTANT 6: Strict toggle after queueing
|
|
932
|
+
const storeM6 = new CommonsStore(join(tempDir, "store.db"));
|
|
933
|
+
const runtimeM6 = new AgentCommons({ keychain: env.atlasKey, store: storeM6, policy: { ...env.policy, channelsEnabled: false }, transport: env.transport });
|
|
934
|
+
runtimeM6.addProfile(env.profile);
|
|
935
|
+
await runtimeM6.send({ recipient: env.lyraId, profileId: env.profile.id, thread: "commons", body: "m6-msg", deferFlush: true });
|
|
936
|
+
// Toggle strict mode
|
|
937
|
+
runtimeM6.policy.channelsEnabled = true;
|
|
938
|
+
const flushM6 = await runtimeM6.flush();
|
|
939
|
+
assert.equal(flushM6.length, 1);
|
|
940
|
+
assert.equal(flushM6[0].state, "retry-or-quarantine");
|
|
941
|
+
assert.equal(flushM6[0].error, "STRUCTURED_CHANNELS_REQUIRED");
|
|
942
|
+
storeM6.close();
|
|
943
|
+
|
|
944
|
+
// Setup for MUTANT 7: Authenticated reserved-malformed flush refusal (R1b / R3)
|
|
945
|
+
const storeM7 = new CommonsStore(join(tempDir, "store.db"));
|
|
946
|
+
const runtimeM7 = new AgentCommons({ keychain: env.atlasKey, store: storeM7, policy: env.policy, transport: env.transport });
|
|
947
|
+
runtimeM7.addProfile(env.profile);
|
|
948
|
+
// Queue valid message first
|
|
949
|
+
const sentM7 = await runtimeM7.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m7-msg", deferFlush: true });
|
|
950
|
+
// Construct signed tuple matching runtime.mjs:161 exactly:
|
|
951
|
+
const sha = envelopeSha(sentM7.envelope);
|
|
952
|
+
const tuple7 = {
|
|
953
|
+
id: sentM7.envelope.id,
|
|
954
|
+
sha,
|
|
955
|
+
thread: "channel:malformed",
|
|
956
|
+
profileId: env.profile.id,
|
|
957
|
+
sender: env.atlasId,
|
|
958
|
+
recipient: env.lyraId,
|
|
959
|
+
};
|
|
960
|
+
const sig7 = env.atlasKey.sign(Buffer.from(jcs(tuple7))).toString("hex");
|
|
961
|
+
const meta7 = { ...tuple7, signature: sig7 };
|
|
962
|
+
storeM7.set(`outbox_meta:${sentM7.envelope.id}`, meta7);
|
|
963
|
+
|
|
964
|
+
const flushM7 = await runtimeM7.flush();
|
|
965
|
+
assert.equal(flushM7.length, 1);
|
|
966
|
+
assert.equal(flushM7[0].state, "retry-or-quarantine");
|
|
967
|
+
assert.equal(flushM7[0].error, "INVALID_CHANNEL_URI");
|
|
968
|
+
|
|
969
|
+
const rowM7 = storeM7.db.prepare("SELECT * FROM outbox WHERE id = ?").get(sentM7.envelope.id);
|
|
970
|
+
assert.equal(rowM7.attempts, 1);
|
|
971
|
+
assert.equal(rowM7.error, "INVALID_CHANNEL_URI");
|
|
972
|
+
storeM7.close();
|
|
973
|
+
} finally {
|
|
974
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// =========================================================================
|
|
979
|
+
// Property 5: Legacy Thread Compatibility & Reserved-Prefix Priority (§4.2)
|
|
980
|
+
// =========================================================================
|
|
981
|
+
test("Property 5: CONTROL - Legacy thread succeeds under channelsEnabled !== true", async () => {
|
|
982
|
+
const env = makeEnv();
|
|
983
|
+
env.policy.channelsEnabled = false;
|
|
984
|
+
|
|
985
|
+
const res = await env.atlasRuntime.send({
|
|
986
|
+
recipient: env.lyraId,
|
|
987
|
+
profileId: env.profile.id,
|
|
988
|
+
thread: "commons",
|
|
989
|
+
body: "legacy-pass",
|
|
990
|
+
});
|
|
991
|
+
assert.equal(res.result[0].state, "carrier-accepted");
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
test("Property 5: MUTANT A - Legacy thread denied under channelsEnabled === true", async () => {
|
|
995
|
+
const env = makeEnv();
|
|
996
|
+
env.policy.channelsEnabled = true;
|
|
997
|
+
|
|
998
|
+
await assert.rejects(
|
|
999
|
+
() =>
|
|
1000
|
+
env.atlasRuntime.send({
|
|
1001
|
+
recipient: env.lyraId,
|
|
1002
|
+
profileId: env.profile.id,
|
|
1003
|
+
thread: "commons",
|
|
1004
|
+
body: "legacy-fail",
|
|
1005
|
+
}),
|
|
1006
|
+
(err) => err.code === "STRUCTURED_CHANNELS_REQUIRED" || /STRUCTURED_CHANNELS_REQUIRED/.test(err.message)
|
|
1007
|
+
);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
test("Property 5: MUTANT B - Reserved prefix takes precedence over disabled channels", async () => {
|
|
1011
|
+
const env = makeEnv();
|
|
1012
|
+
env.policy.channelsEnabled = false;
|
|
1013
|
+
|
|
1014
|
+
// Even though channelsEnabled is false, channel:// is classified as structured and enforces channel configuration
|
|
1015
|
+
await assert.rejects(
|
|
1016
|
+
() =>
|
|
1017
|
+
env.atlasRuntime.send({
|
|
1018
|
+
recipient: env.lyraId,
|
|
1019
|
+
profileId: env.profile.id,
|
|
1020
|
+
thread: "channel://local/community/unconfigured",
|
|
1021
|
+
body: "reserved-precedence",
|
|
1022
|
+
}),
|
|
1023
|
+
(err) => err.code === "UNCONFIGURED_CHANNEL" || /UNCONFIGURED_CHANNEL|Channel is unconfigured/.test(err.message)
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
// And malformed channel URI throws INVALID_CHANNEL_URI
|
|
1027
|
+
await assert.rejects(
|
|
1028
|
+
() =>
|
|
1029
|
+
env.atlasRuntime.send({
|
|
1030
|
+
recipient: env.lyraId,
|
|
1031
|
+
profileId: env.profile.id,
|
|
1032
|
+
thread: "channel:malformed",
|
|
1033
|
+
body: "reserved-malformed",
|
|
1034
|
+
}),
|
|
1035
|
+
(err) => err.code === "INVALID_CHANNEL_URI" || /INVALID_CHANNEL_URI/.test(err.message)
|
|
1036
|
+
);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
// =========================================================================
|
|
1040
|
+
// Property 4: Reviewer Atomicity and Ordering Fault Oracles
|
|
1041
|
+
// =========================================================================
|
|
1042
|
+
test("Property 4: Atomic rollback oracle on send metadata fault", async () => {
|
|
1043
|
+
const a = new Keychain("unused");
|
|
1044
|
+
const b = new Keychain("unused");
|
|
1045
|
+
a._identity = Keychain.generate();
|
|
1046
|
+
b._identity = Keychain.generate();
|
|
1047
|
+
const store = new CommonsStore(":memory:");
|
|
1048
|
+
const policy = {
|
|
1049
|
+
mode: "local",
|
|
1050
|
+
offline: true,
|
|
1051
|
+
agents: Object.fromEntries(
|
|
1052
|
+
[a, b].map((k) => [
|
|
1053
|
+
k._identity.uuaid,
|
|
1054
|
+
{ kind: "agent", publicKey: k._identity.publicKeyHex, capabilities: ["commons:message"] },
|
|
1055
|
+
])
|
|
1056
|
+
),
|
|
1057
|
+
};
|
|
1058
|
+
const runtime = new AgentCommons({ keychain: a, store, policy });
|
|
1059
|
+
const profile = createProfile({
|
|
1060
|
+
namespace: "local/review",
|
|
1061
|
+
name: "Atomic rollback",
|
|
1062
|
+
scope: "local",
|
|
1063
|
+
fixtures: ["first fixture", "second fixture"],
|
|
1064
|
+
});
|
|
1065
|
+
runtime.addProfile(profile);
|
|
1066
|
+
const set = store.set.bind(store);
|
|
1067
|
+
let faults = 0;
|
|
1068
|
+
store.set = (key, value) => {
|
|
1069
|
+
if (key.startsWith("outbox_meta:")) {
|
|
1070
|
+
faults++;
|
|
1071
|
+
throw Error("AC2_META_WRITE_FAULT");
|
|
1072
|
+
}
|
|
1073
|
+
return set(key, value);
|
|
1074
|
+
};
|
|
1075
|
+
try {
|
|
1076
|
+
await assert.rejects(
|
|
1077
|
+
runtime.send({ recipient: b._identity.uuaid, profileId: profile.id, body: "atomic", deferFlush: true }),
|
|
1078
|
+
/AC2_META_WRITE_FAULT/
|
|
1079
|
+
);
|
|
1080
|
+
const outbox = store.db.prepare("SELECT COUNT(*) AS n FROM outbox").get().n;
|
|
1081
|
+
const admission = store.audit().filter((e) => JSON.parse(e.body).kind === "outbox-admission").length;
|
|
1082
|
+
assert.equal(faults, 1);
|
|
1083
|
+
assert.equal(outbox, 0);
|
|
1084
|
+
assert.equal(admission, 0);
|
|
1085
|
+
} finally {
|
|
1086
|
+
store.close();
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("Property 4: Actual trust-failure ordering oracle on receive path", async () => {
|
|
1091
|
+
const keys = Array.from({ length: 3 }, () => {
|
|
1092
|
+
const k = new Keychain("unused");
|
|
1093
|
+
k._identity = Keychain.generate();
|
|
1094
|
+
return k;
|
|
1095
|
+
});
|
|
1096
|
+
const policy = {
|
|
1097
|
+
mode: "local",
|
|
1098
|
+
offline: true,
|
|
1099
|
+
agents: Object.fromEntries(
|
|
1100
|
+
keys.map((k) => [
|
|
1101
|
+
k._identity.uuaid,
|
|
1102
|
+
{ kind: "agent", publicKey: k._identity.publicKeyHex, capabilities: ["commons:message"] },
|
|
1103
|
+
])
|
|
1104
|
+
),
|
|
1105
|
+
};
|
|
1106
|
+
const transport = new MemoryLoopbackTransport();
|
|
1107
|
+
const stores = [new CommonsStore(":memory:"), new CommonsStore(":memory:")];
|
|
1108
|
+
const [a, b] = keys.slice(0, 2).map((keychain, i) => new AgentCommons({ keychain, store: stores[i], policy, transport }));
|
|
1109
|
+
const profile = createProfile({
|
|
1110
|
+
namespace: "local/review",
|
|
1111
|
+
name: "Transient order",
|
|
1112
|
+
scope: "local",
|
|
1113
|
+
fixtures: ["first fixture", "second fixture"],
|
|
1114
|
+
});
|
|
1115
|
+
a.addProfile(profile);
|
|
1116
|
+
b.addProfile(profile);
|
|
1117
|
+
try {
|
|
1118
|
+
for (const recipient of [b.uuaid, b.uuaid, keys[2]._identity.uuaid]) {
|
|
1119
|
+
const { envelope } = await a.send({ recipient, profileId: profile.id, body: "trust-order", deferFlush: true });
|
|
1120
|
+
await transport.deliver(envelope);
|
|
1121
|
+
}
|
|
1122
|
+
let calls = 0;
|
|
1123
|
+
b.trust.authorize = async () => {
|
|
1124
|
+
calls++;
|
|
1125
|
+
const e = Error("AC2_TRANSIENT");
|
|
1126
|
+
e.transient = true;
|
|
1127
|
+
e.code = "trust-unavailable";
|
|
1128
|
+
throw e;
|
|
1129
|
+
};
|
|
1130
|
+
const result = await b.poll();
|
|
1131
|
+
const cursor = b.store.get("cursors", {})[transport.sourceId] ?? 0;
|
|
1132
|
+
assert.equal(result.length, 1);
|
|
1133
|
+
assert.equal(calls, 1);
|
|
1134
|
+
assert.equal(cursor, 0);
|
|
1135
|
+
assert.equal(result[0].deferred, true);
|
|
1136
|
+
} finally {
|
|
1137
|
+
await transport.close();
|
|
1138
|
+
for (const s of stores) s.close();
|
|
1139
|
+
}
|
|
1140
|
+
});
|