@olane/o-leader 0.7.10 → 0.7.11
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/dist/test/address-resolution-integration.spec.d.ts +2 -0
- package/dist/test/address-resolution-integration.spec.d.ts.map +1 -0
- package/dist/test/address-resolution-integration.spec.js +704 -0
- package/dist/test/registry-memory.spec.d.ts +2 -0
- package/dist/test/registry-memory.spec.d.ts.map +1 -0
- package/dist/test/registry-memory.spec.js +528 -0
- package/package.json +7 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address-resolution-integration.spec.d.ts","sourceRoot":"","sources":["../../test/address-resolution-integration.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { oLeaderNode } from '../src/leader.node.js';
|
|
3
|
+
import { oNode } from '@olane/o-node';
|
|
4
|
+
import { oNodeAddress } from '@olane/o-node';
|
|
5
|
+
import { oAddress, RestrictedAddresses, NodeState } from '@olane/o-core';
|
|
6
|
+
describe('Address Resolution Integration Tests', () => {
|
|
7
|
+
describe('Leader + Registry + Resolver Integration', () => {
|
|
8
|
+
let leader;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
leader = new oLeaderNode({
|
|
11
|
+
name: 'test-leader',
|
|
12
|
+
parent: null,
|
|
13
|
+
leader: null,
|
|
14
|
+
network: {
|
|
15
|
+
listeners: [],
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
await leader.start();
|
|
19
|
+
});
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
if (leader && leader.state === NodeState.RUNNING) {
|
|
22
|
+
await leader.stop();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
it('should start leader with registry as child', async () => {
|
|
26
|
+
expect(leader.state).to.equal(NodeState.RUNNING);
|
|
27
|
+
expect(leader.address.value).to.equal(RestrictedAddresses.LEADER);
|
|
28
|
+
// Verify registry is a child
|
|
29
|
+
const registryChild = leader.hierarchyManager.getChild(new oAddress(RestrictedAddresses.REGISTRY));
|
|
30
|
+
expect(registryChild).to.not.be.null;
|
|
31
|
+
expect(registryChild?.value).to.equal(RestrictedAddresses.REGISTRY);
|
|
32
|
+
});
|
|
33
|
+
it('should be able to register a node in the registry', async () => {
|
|
34
|
+
const registrationParams = {
|
|
35
|
+
_connectionId: 'test-connection',
|
|
36
|
+
_requestMethod: 'test-method',
|
|
37
|
+
peerId: 'peer-embeddings',
|
|
38
|
+
address: 'o://leader/services/embeddings',
|
|
39
|
+
staticAddress: 'o://embeddings-text',
|
|
40
|
+
protocols: ['/embeddings/1.0.0'],
|
|
41
|
+
transports: ['/ip4/127.0.0.1/tcp/6001'],
|
|
42
|
+
};
|
|
43
|
+
const result = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
44
|
+
method: 'commit',
|
|
45
|
+
params: registrationParams,
|
|
46
|
+
});
|
|
47
|
+
expect(result.result.success).to.be.true;
|
|
48
|
+
});
|
|
49
|
+
it('should be able to search for registered nodes', async () => {
|
|
50
|
+
// Register a node
|
|
51
|
+
const registrationParams = {
|
|
52
|
+
_connectionId: 'test-connection',
|
|
53
|
+
_requestMethod: 'test-method',
|
|
54
|
+
peerId: 'peer-search-test',
|
|
55
|
+
address: 'o://leader/services/search-test',
|
|
56
|
+
staticAddress: 'o://search-test',
|
|
57
|
+
protocols: ['/test/1.0.0'],
|
|
58
|
+
transports: ['/ip4/127.0.0.1/tcp/7001'],
|
|
59
|
+
};
|
|
60
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
61
|
+
method: 'commit',
|
|
62
|
+
params: registrationParams,
|
|
63
|
+
});
|
|
64
|
+
// Search for the node
|
|
65
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
66
|
+
method: 'search',
|
|
67
|
+
params: { staticAddress: 'o://search-test' },
|
|
68
|
+
});
|
|
69
|
+
expect(searchResult.result.data).to.be.an('array');
|
|
70
|
+
expect(searchResult.result.data).to.have.lengthOf(1);
|
|
71
|
+
expect(searchResult.result.data[0].peerId).to.equal('peer-search-test');
|
|
72
|
+
});
|
|
73
|
+
it('should resolve address using search resolver', async () => {
|
|
74
|
+
// Register a service
|
|
75
|
+
const registrationParams = {
|
|
76
|
+
_connectionId: 'test-connection',
|
|
77
|
+
_requestMethod: 'test-method',
|
|
78
|
+
peerId: 'peer-embeddings',
|
|
79
|
+
address: 'o://leader/services/embeddings',
|
|
80
|
+
staticAddress: 'o://embeddings-text',
|
|
81
|
+
protocols: ['/embeddings/1.0.0'],
|
|
82
|
+
transports: ['/ip4/127.0.0.1/tcp/6001'],
|
|
83
|
+
};
|
|
84
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
85
|
+
method: 'commit',
|
|
86
|
+
params: registrationParams,
|
|
87
|
+
});
|
|
88
|
+
// Try to use the service by static address
|
|
89
|
+
// The resolver should find it in the registry
|
|
90
|
+
try {
|
|
91
|
+
const result = await leader.use(new oAddress('o://embeddings-text'), {
|
|
92
|
+
method: 'ping', // Some method that may not exist, but routing should work
|
|
93
|
+
params: {},
|
|
94
|
+
});
|
|
95
|
+
// If we get here without error, routing worked
|
|
96
|
+
// The actual method call may fail, but that's expected
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Expected - the service doesn't actually exist, but we should have resolved the address
|
|
100
|
+
// Check that the error is about the method, not about routing
|
|
101
|
+
expect(error.message).to.not.include('No route');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
it('should handle multiple services with same protocol', async () => {
|
|
105
|
+
// Register two embedding services
|
|
106
|
+
const service1 = {
|
|
107
|
+
_connectionId: 'test-connection',
|
|
108
|
+
_requestMethod: 'test-method',
|
|
109
|
+
peerId: 'peer-embeddings-1',
|
|
110
|
+
address: 'o://leader/services/embeddings-text',
|
|
111
|
+
staticAddress: 'o://embeddings-text',
|
|
112
|
+
protocols: ['/embeddings/1.0.0'],
|
|
113
|
+
transports: ['/ip4/127.0.0.1/tcp/6001'],
|
|
114
|
+
registeredAt: 1000,
|
|
115
|
+
};
|
|
116
|
+
const service2 = {
|
|
117
|
+
_connectionId: 'test-connection',
|
|
118
|
+
_requestMethod: 'test-method',
|
|
119
|
+
peerId: 'peer-embeddings-2',
|
|
120
|
+
address: 'o://leader/services/embeddings-image',
|
|
121
|
+
staticAddress: 'o://embeddings-image',
|
|
122
|
+
protocols: ['/embeddings/1.0.0'],
|
|
123
|
+
transports: ['/ip4/127.0.0.1/tcp/6002'],
|
|
124
|
+
registeredAt: 2000, // More recent
|
|
125
|
+
};
|
|
126
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
127
|
+
method: 'commit',
|
|
128
|
+
params: service1,
|
|
129
|
+
});
|
|
130
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
131
|
+
method: 'commit',
|
|
132
|
+
params: service2,
|
|
133
|
+
});
|
|
134
|
+
// Search by protocol - should return both, sorted by registeredAt
|
|
135
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
136
|
+
method: 'search',
|
|
137
|
+
params: { protocols: ['/embeddings/1.0.0'] },
|
|
138
|
+
});
|
|
139
|
+
expect(searchResult.result.data).to.have.lengthOf(2);
|
|
140
|
+
expect(searchResult.result.data[0].peerId).to.equal('peer-embeddings-2'); // Most recent
|
|
141
|
+
expect(searchResult.result.data[1].peerId).to.equal('peer-embeddings-1');
|
|
142
|
+
});
|
|
143
|
+
it('should handle service removal', async () => {
|
|
144
|
+
const registrationParams = {
|
|
145
|
+
_connectionId: 'test-connection',
|
|
146
|
+
_requestMethod: 'test-method',
|
|
147
|
+
peerId: 'peer-temporary',
|
|
148
|
+
address: 'o://leader/services/temporary',
|
|
149
|
+
staticAddress: 'o://temporary',
|
|
150
|
+
protocols: ['/temp/1.0.0'],
|
|
151
|
+
transports: ['/memory'],
|
|
152
|
+
};
|
|
153
|
+
// Register
|
|
154
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
155
|
+
method: 'commit',
|
|
156
|
+
params: registrationParams,
|
|
157
|
+
});
|
|
158
|
+
// Verify it exists
|
|
159
|
+
let searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
160
|
+
method: 'search',
|
|
161
|
+
params: { staticAddress: 'o://temporary' },
|
|
162
|
+
});
|
|
163
|
+
expect(searchResult.result.data).to.have.lengthOf(1);
|
|
164
|
+
// Remove
|
|
165
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
166
|
+
method: 'remove',
|
|
167
|
+
params: { peerId: 'peer-temporary' },
|
|
168
|
+
});
|
|
169
|
+
// Verify it's gone
|
|
170
|
+
searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
171
|
+
method: 'search',
|
|
172
|
+
params: { staticAddress: 'o://temporary' },
|
|
173
|
+
});
|
|
174
|
+
expect(searchResult.result.data).to.have.lengthOf(0);
|
|
175
|
+
});
|
|
176
|
+
it('should list all registered nodes', async () => {
|
|
177
|
+
// Register multiple nodes
|
|
178
|
+
const nodes = [
|
|
179
|
+
{
|
|
180
|
+
_connectionId: 'test-connection',
|
|
181
|
+
_requestMethod: 'test-method',
|
|
182
|
+
peerId: 'peer-1',
|
|
183
|
+
address: 'o://node-1',
|
|
184
|
+
protocols: [],
|
|
185
|
+
transports: ['/memory'],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
_connectionId: 'test-connection',
|
|
189
|
+
_requestMethod: 'test-method',
|
|
190
|
+
peerId: 'peer-2',
|
|
191
|
+
address: 'o://node-2',
|
|
192
|
+
protocols: [],
|
|
193
|
+
transports: ['/memory'],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
_connectionId: 'test-connection',
|
|
197
|
+
_requestMethod: 'test-method',
|
|
198
|
+
peerId: 'peer-3',
|
|
199
|
+
address: 'o://node-3',
|
|
200
|
+
protocols: [],
|
|
201
|
+
transports: ['/memory'],
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
for (const node of nodes) {
|
|
205
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
206
|
+
method: 'commit',
|
|
207
|
+
params: node,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Get all nodes
|
|
211
|
+
const result = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
212
|
+
method: 'find_all',
|
|
213
|
+
params: {},
|
|
214
|
+
});
|
|
215
|
+
expect(result.result.data).to.be.an('array');
|
|
216
|
+
expect(result.result.data.length).to.be.at.least(3); // At least our 3 nodes
|
|
217
|
+
const peerIds = result.result.data.map((n) => n.peerId);
|
|
218
|
+
expect(peerIds).to.include('peer-1');
|
|
219
|
+
expect(peerIds).to.include('peer-2');
|
|
220
|
+
expect(peerIds).to.include('peer-3');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe('Multi-node scenarios', () => {
|
|
224
|
+
let leader;
|
|
225
|
+
let childNode;
|
|
226
|
+
beforeEach(async () => {
|
|
227
|
+
leader = new oLeaderNode({
|
|
228
|
+
name: 'test-leader',
|
|
229
|
+
parent: null,
|
|
230
|
+
leader: null,
|
|
231
|
+
network: {
|
|
232
|
+
listeners: [],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
await leader.start();
|
|
236
|
+
});
|
|
237
|
+
afterEach(async () => {
|
|
238
|
+
if (childNode && childNode.state === NodeState.RUNNING) {
|
|
239
|
+
await childNode.stop();
|
|
240
|
+
}
|
|
241
|
+
if (leader && leader.state === NodeState.RUNNING) {
|
|
242
|
+
await leader.stop();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
it('should handle hierarchical node registration', async () => {
|
|
246
|
+
// Create child node
|
|
247
|
+
childNode = new oNode({
|
|
248
|
+
address: new oNodeAddress('o://leader/services'),
|
|
249
|
+
parent: leader.address,
|
|
250
|
+
leader: leader.address,
|
|
251
|
+
network: {
|
|
252
|
+
listeners: [],
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
await childNode.start();
|
|
256
|
+
// Register the child in the registry
|
|
257
|
+
const registrationParams = {
|
|
258
|
+
_connectionId: 'test-connection',
|
|
259
|
+
_requestMethod: 'test-method',
|
|
260
|
+
peerId: 'peer-services',
|
|
261
|
+
address: 'o://leader/services',
|
|
262
|
+
protocols: ['/services/1.0.0'],
|
|
263
|
+
transports: ['/memory'],
|
|
264
|
+
};
|
|
265
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
266
|
+
method: 'commit',
|
|
267
|
+
params: registrationParams,
|
|
268
|
+
});
|
|
269
|
+
// Search for it
|
|
270
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
271
|
+
method: 'search',
|
|
272
|
+
params: { address: 'o://leader/services' },
|
|
273
|
+
});
|
|
274
|
+
expect(searchResult.result.data).to.have.lengthOf(1);
|
|
275
|
+
expect(searchResult.result.data[0].address).to.equal('o://leader/services');
|
|
276
|
+
});
|
|
277
|
+
it('should support service discovery by protocol', async () => {
|
|
278
|
+
// Register services with different protocols
|
|
279
|
+
const services = [
|
|
280
|
+
{
|
|
281
|
+
_connectionId: 'test-connection',
|
|
282
|
+
_requestMethod: 'test-method',
|
|
283
|
+
peerId: 'peer-embeddings',
|
|
284
|
+
address: 'o://leader/ai/embeddings',
|
|
285
|
+
protocols: ['/ai/embeddings/1.0.0', '/text/1.0.0'],
|
|
286
|
+
transports: ['/memory'],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
_connectionId: 'test-connection',
|
|
290
|
+
_requestMethod: 'test-method',
|
|
291
|
+
peerId: 'peer-chat',
|
|
292
|
+
address: 'o://leader/ai/chat',
|
|
293
|
+
protocols: ['/ai/chat/1.0.0', '/text/1.0.0'],
|
|
294
|
+
transports: ['/memory'],
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
_connectionId: 'test-connection',
|
|
298
|
+
_requestMethod: 'test-method',
|
|
299
|
+
peerId: 'peer-vision',
|
|
300
|
+
address: 'o://leader/ai/vision',
|
|
301
|
+
protocols: ['/ai/vision/1.0.0', '/image/1.0.0'],
|
|
302
|
+
transports: ['/memory'],
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
for (const service of services) {
|
|
306
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
307
|
+
method: 'commit',
|
|
308
|
+
params: service,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Find all text-based services
|
|
312
|
+
const textServices = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
313
|
+
method: 'search',
|
|
314
|
+
params: { protocols: ['/text/1.0.0'] },
|
|
315
|
+
});
|
|
316
|
+
expect(textServices.result.data).to.have.lengthOf(2);
|
|
317
|
+
const peerIds = textServices.result.data.map((s) => s.peerId);
|
|
318
|
+
expect(peerIds).to.include.members(['peer-embeddings', 'peer-chat']);
|
|
319
|
+
// Find embeddings service specifically
|
|
320
|
+
const embeddingsService = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
321
|
+
method: 'search',
|
|
322
|
+
params: { protocols: ['/ai/embeddings/1.0.0'] },
|
|
323
|
+
});
|
|
324
|
+
expect(embeddingsService.result.data).to.have.lengthOf(1);
|
|
325
|
+
expect(embeddingsService.result.data[0].peerId).to.equal('peer-embeddings');
|
|
326
|
+
});
|
|
327
|
+
it('should handle service updates on re-registration', async () => {
|
|
328
|
+
const initialRegistration = {
|
|
329
|
+
_connectionId: 'test-connection',
|
|
330
|
+
_requestMethod: 'test-method',
|
|
331
|
+
peerId: 'peer-dynamic',
|
|
332
|
+
address: 'o://leader/services/dynamic',
|
|
333
|
+
protocols: ['/service/1.0.0'],
|
|
334
|
+
transports: ['/ip4/127.0.0.1/tcp/4001'],
|
|
335
|
+
registeredAt: 1000,
|
|
336
|
+
};
|
|
337
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
338
|
+
method: 'commit',
|
|
339
|
+
params: initialRegistration,
|
|
340
|
+
});
|
|
341
|
+
// Simulate service update (new transport)
|
|
342
|
+
const updatedRegistration = {
|
|
343
|
+
_connectionId: 'test-connection',
|
|
344
|
+
_requestMethod: 'test-method',
|
|
345
|
+
peerId: 'peer-dynamic',
|
|
346
|
+
address: 'o://leader/services/dynamic',
|
|
347
|
+
protocols: ['/service/1.0.0', '/service/2.0.0'], // Added new protocol
|
|
348
|
+
transports: ['/ip4/192.168.1.1/tcp/4001'], // New transport
|
|
349
|
+
registeredAt: 2000,
|
|
350
|
+
};
|
|
351
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
352
|
+
method: 'commit',
|
|
353
|
+
params: updatedRegistration,
|
|
354
|
+
});
|
|
355
|
+
// Verify update
|
|
356
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
357
|
+
method: 'search',
|
|
358
|
+
params: { address: 'o://leader/services/dynamic' },
|
|
359
|
+
});
|
|
360
|
+
expect(searchResult.result.data).to.have.lengthOf(1);
|
|
361
|
+
expect(searchResult.result.data[0].protocols).to.include('/service/2.0.0');
|
|
362
|
+
expect(searchResult.result.data[0].transports[0]).to.equal('/ip4/192.168.1.1/tcp/4001');
|
|
363
|
+
expect(searchResult.result.data[0].registeredAt).to.equal(2000);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
describe('Real-world routing scenarios', () => {
|
|
367
|
+
let leader;
|
|
368
|
+
beforeEach(async () => {
|
|
369
|
+
leader = new oLeaderNode({
|
|
370
|
+
name: 'test-leader',
|
|
371
|
+
parent: null,
|
|
372
|
+
leader: null,
|
|
373
|
+
network: {
|
|
374
|
+
listeners: [],
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
await leader.start();
|
|
378
|
+
});
|
|
379
|
+
afterEach(async () => {
|
|
380
|
+
if (leader && leader.state === NodeState.RUNNING) {
|
|
381
|
+
await leader.stop();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
it('should route to static address via registry lookup', async () => {
|
|
385
|
+
// Register service with static address
|
|
386
|
+
const registrationParams = {
|
|
387
|
+
_connectionId: 'test-connection',
|
|
388
|
+
_requestMethod: 'test-method',
|
|
389
|
+
peerId: 'peer-static-service',
|
|
390
|
+
address: 'o://leader/services/my-service',
|
|
391
|
+
staticAddress: 'o://my-service',
|
|
392
|
+
protocols: ['/service/1.0.0'],
|
|
393
|
+
transports: ['/memory'],
|
|
394
|
+
};
|
|
395
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
396
|
+
method: 'commit',
|
|
397
|
+
params: registrationParams,
|
|
398
|
+
});
|
|
399
|
+
// Verify static address search works
|
|
400
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
401
|
+
method: 'search',
|
|
402
|
+
params: { staticAddress: 'o://my-service' },
|
|
403
|
+
});
|
|
404
|
+
expect(searchResult.result.data).to.have.lengthOf(1);
|
|
405
|
+
expect(searchResult.result.data[0].address).to.equal('o://leader/services/my-service');
|
|
406
|
+
expect(searchResult.result.data[0].staticAddress).to.equal('o://my-service');
|
|
407
|
+
});
|
|
408
|
+
it('should handle hierarchical routing through services', async () => {
|
|
409
|
+
// Register nested services
|
|
410
|
+
const services = [
|
|
411
|
+
{
|
|
412
|
+
_connectionId: 'test-connection',
|
|
413
|
+
_requestMethod: 'test-method',
|
|
414
|
+
peerId: 'peer-services-parent',
|
|
415
|
+
address: 'o://leader/services',
|
|
416
|
+
protocols: ['/services/1.0.0'],
|
|
417
|
+
transports: ['/memory'],
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
_connectionId: 'test-connection',
|
|
421
|
+
_requestMethod: 'test-method',
|
|
422
|
+
peerId: 'peer-embeddings-child',
|
|
423
|
+
address: 'o://leader/services/embeddings',
|
|
424
|
+
protocols: ['/embeddings/1.0.0'],
|
|
425
|
+
transports: ['/memory'],
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
_connectionId: 'test-connection',
|
|
429
|
+
_requestMethod: 'test-method',
|
|
430
|
+
peerId: 'peer-embeddings-text-leaf',
|
|
431
|
+
address: 'o://leader/services/embeddings/text',
|
|
432
|
+
staticAddress: 'o://embeddings-text',
|
|
433
|
+
protocols: ['/embeddings/text/1.0.0'],
|
|
434
|
+
transports: ['/memory'],
|
|
435
|
+
},
|
|
436
|
+
];
|
|
437
|
+
for (const service of services) {
|
|
438
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
439
|
+
method: 'commit',
|
|
440
|
+
params: service,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Verify all are registered
|
|
444
|
+
const allServices = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
445
|
+
method: 'find_all',
|
|
446
|
+
params: {},
|
|
447
|
+
});
|
|
448
|
+
const peerIds = allServices.result.data.map((s) => s.peerId);
|
|
449
|
+
expect(peerIds).to.include.members([
|
|
450
|
+
'peer-services-parent',
|
|
451
|
+
'peer-embeddings-child',
|
|
452
|
+
'peer-embeddings-text-leaf',
|
|
453
|
+
]);
|
|
454
|
+
// Test routing through hierarchy using oAddress.next()
|
|
455
|
+
const currentAddress = new oAddress('o://leader');
|
|
456
|
+
const targetAddress = new oAddress('o://leader/services/embeddings/text');
|
|
457
|
+
// First hop: leader -> leader/services
|
|
458
|
+
const firstHop = oAddress.next(currentAddress, targetAddress);
|
|
459
|
+
expect(firstHop.toString()).to.equal('o://leader/services');
|
|
460
|
+
// Second hop: leader/services -> leader/services/embeddings
|
|
461
|
+
const secondHop = oAddress.next(firstHop, targetAddress);
|
|
462
|
+
expect(secondHop.toString()).to.equal('o://leader/services/embeddings');
|
|
463
|
+
// Third hop: leader/services/embeddings -> leader/services/embeddings/text
|
|
464
|
+
const thirdHop = oAddress.next(secondHop, targetAddress);
|
|
465
|
+
expect(thirdHop.toString()).to.equal('o://leader/services/embeddings/text');
|
|
466
|
+
// Fourth hop: at destination
|
|
467
|
+
const fourthHop = oAddress.next(thirdHop, targetAddress);
|
|
468
|
+
expect(fourthHop.toString()).to.equal('o://leader/services/embeddings/text');
|
|
469
|
+
});
|
|
470
|
+
it('should support searching with combined parameters', async () => {
|
|
471
|
+
// Register service with multiple attributes
|
|
472
|
+
const registrationParams = {
|
|
473
|
+
_connectionId: 'test-connection',
|
|
474
|
+
_requestMethod: 'test-method',
|
|
475
|
+
peerId: 'peer-complex',
|
|
476
|
+
address: 'o://leader/ai/embeddings',
|
|
477
|
+
staticAddress: 'o://embeddings-text',
|
|
478
|
+
protocols: ['/embeddings/1.0.0', '/text/1.0.0', '/ai/1.0.0'],
|
|
479
|
+
transports: ['/ip4/127.0.0.1/tcp/6001'],
|
|
480
|
+
};
|
|
481
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
482
|
+
method: 'commit',
|
|
483
|
+
params: registrationParams,
|
|
484
|
+
});
|
|
485
|
+
// Search by static address
|
|
486
|
+
const byStatic = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
487
|
+
method: 'search',
|
|
488
|
+
params: { staticAddress: 'o://embeddings-text' },
|
|
489
|
+
});
|
|
490
|
+
expect(byStatic.result.data).to.have.lengthOf(1);
|
|
491
|
+
// Search by exact address
|
|
492
|
+
const byAddress = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
493
|
+
method: 'search',
|
|
494
|
+
params: { address: 'o://leader/ai/embeddings' },
|
|
495
|
+
});
|
|
496
|
+
expect(byAddress.result.data).to.have.lengthOf(1);
|
|
497
|
+
// Search by multiple protocols (AND logic)
|
|
498
|
+
const byProtocols = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
499
|
+
method: 'search',
|
|
500
|
+
params: { protocols: ['/embeddings/1.0.0', '/text/1.0.0'] },
|
|
501
|
+
});
|
|
502
|
+
expect(byProtocols.result.data).to.have.lengthOf(1);
|
|
503
|
+
// Search by single protocol
|
|
504
|
+
const bySingleProtocol = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
505
|
+
method: 'search',
|
|
506
|
+
params: { protocols: ['/ai/1.0.0'] },
|
|
507
|
+
});
|
|
508
|
+
expect(bySingleProtocol.result.data).to.have.lengthOf(1);
|
|
509
|
+
});
|
|
510
|
+
it('should handle registry preventing infinite loops', async () => {
|
|
511
|
+
// Try to register registry itself (should work, but search should filter it)
|
|
512
|
+
const registryRegistration = {
|
|
513
|
+
_connectionId: 'test-connection',
|
|
514
|
+
_requestMethod: 'test-method',
|
|
515
|
+
peerId: 'peer-registry',
|
|
516
|
+
address: RestrictedAddresses.REGISTRY,
|
|
517
|
+
staticAddress: RestrictedAddresses.REGISTRY,
|
|
518
|
+
protocols: ['/registry/1.0.0'],
|
|
519
|
+
transports: ['/memory'],
|
|
520
|
+
};
|
|
521
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
522
|
+
method: 'commit',
|
|
523
|
+
params: registryRegistration,
|
|
524
|
+
});
|
|
525
|
+
// Verify it's in the registry
|
|
526
|
+
const allNodes = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
527
|
+
method: 'find_all',
|
|
528
|
+
params: {},
|
|
529
|
+
});
|
|
530
|
+
const registryNode = allNodes.result.data.find((n) => n.peerId === 'peer-registry');
|
|
531
|
+
expect(registryNode).to.exist;
|
|
532
|
+
// But search resolver should filter it out to prevent loops
|
|
533
|
+
// This is tested in the resolver unit tests
|
|
534
|
+
});
|
|
535
|
+
it('should handle empty registry gracefully', async () => {
|
|
536
|
+
// Search empty registry
|
|
537
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
538
|
+
method: 'search',
|
|
539
|
+
params: { staticAddress: 'o://nonexistent' },
|
|
540
|
+
});
|
|
541
|
+
expect(searchResult.result.data).to.be.an('array');
|
|
542
|
+
expect(searchResult.result.data).to.have.lengthOf(0);
|
|
543
|
+
});
|
|
544
|
+
it('should handle multiple transports per service', async () => {
|
|
545
|
+
const registrationParams = {
|
|
546
|
+
_connectionId: 'test-connection',
|
|
547
|
+
_requestMethod: 'test-method',
|
|
548
|
+
peerId: 'peer-multi-transport',
|
|
549
|
+
address: 'o://leader/services/multi',
|
|
550
|
+
staticAddress: 'o://multi-transport',
|
|
551
|
+
protocols: ['/multi/1.0.0'],
|
|
552
|
+
transports: [
|
|
553
|
+
'/ip4/127.0.0.1/tcp/4001',
|
|
554
|
+
'/ip4/192.168.1.1/tcp/4001',
|
|
555
|
+
'/ip6/::1/tcp/4001',
|
|
556
|
+
],
|
|
557
|
+
};
|
|
558
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
559
|
+
method: 'commit',
|
|
560
|
+
params: registrationParams,
|
|
561
|
+
});
|
|
562
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
563
|
+
method: 'search',
|
|
564
|
+
params: { staticAddress: 'o://multi-transport' },
|
|
565
|
+
});
|
|
566
|
+
expect(searchResult.result.data[0].transports).to.have.lengthOf(3);
|
|
567
|
+
expect(searchResult.result.data[0].transports).to.include('/ip4/127.0.0.1/tcp/4001');
|
|
568
|
+
expect(searchResult.result.data[0].transports).to.include('/ip4/192.168.1.1/tcp/4001');
|
|
569
|
+
expect(searchResult.result.data[0].transports).to.include('/ip6/::1/tcp/4001');
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
describe('Edge cases and error handling', () => {
|
|
573
|
+
let leader;
|
|
574
|
+
beforeEach(async () => {
|
|
575
|
+
leader = new oLeaderNode({
|
|
576
|
+
name: 'test-leader',
|
|
577
|
+
parent: null,
|
|
578
|
+
leader: null,
|
|
579
|
+
network: {
|
|
580
|
+
listeners: [],
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
await leader.start();
|
|
584
|
+
});
|
|
585
|
+
afterEach(async () => {
|
|
586
|
+
if (leader && leader.state === NodeState.RUNNING) {
|
|
587
|
+
await leader.stop();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
it('should handle service with long address path', async () => {
|
|
591
|
+
const registrationParams = {
|
|
592
|
+
_connectionId: 'test-connection',
|
|
593
|
+
_requestMethod: 'test-method',
|
|
594
|
+
peerId: 'peer-deep',
|
|
595
|
+
address: 'o://leader/a/b/c/d/e/f/g/h/i/j/service',
|
|
596
|
+
protocols: ['/deep/1.0.0'],
|
|
597
|
+
transports: ['/memory'],
|
|
598
|
+
};
|
|
599
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
600
|
+
method: 'commit',
|
|
601
|
+
params: registrationParams,
|
|
602
|
+
});
|
|
603
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
604
|
+
method: 'search',
|
|
605
|
+
params: { address: 'o://leader/a/b/c/d/e/f/g/h/i/j/service' },
|
|
606
|
+
});
|
|
607
|
+
expect(searchResult.result.data).to.have.lengthOf(1);
|
|
608
|
+
});
|
|
609
|
+
it('should handle service with special characters', async () => {
|
|
610
|
+
const registrationParams = {
|
|
611
|
+
_connectionId: 'test-connection',
|
|
612
|
+
_requestMethod: 'test-method',
|
|
613
|
+
peerId: 'peer-special-chars',
|
|
614
|
+
address: 'o://leader/my-service_v2.0',
|
|
615
|
+
staticAddress: 'o://my-service_v2.0',
|
|
616
|
+
protocols: ['/service/2.0.0'],
|
|
617
|
+
transports: ['/memory'],
|
|
618
|
+
};
|
|
619
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
620
|
+
method: 'commit',
|
|
621
|
+
params: registrationParams,
|
|
622
|
+
});
|
|
623
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
624
|
+
method: 'search',
|
|
625
|
+
params: { staticAddress: 'o://my-service_v2.0' },
|
|
626
|
+
});
|
|
627
|
+
expect(searchResult.result.data).to.have.lengthOf(1);
|
|
628
|
+
expect(searchResult.result.data[0].address).to.equal('o://leader/my-service_v2.0');
|
|
629
|
+
});
|
|
630
|
+
it('should handle service with empty protocols array', async () => {
|
|
631
|
+
const registrationParams = {
|
|
632
|
+
_connectionId: 'test-connection',
|
|
633
|
+
_requestMethod: 'test-method',
|
|
634
|
+
peerId: 'peer-no-protocols',
|
|
635
|
+
address: 'o://leader/services/no-protocols',
|
|
636
|
+
protocols: [],
|
|
637
|
+
transports: ['/memory'],
|
|
638
|
+
};
|
|
639
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
640
|
+
method: 'commit',
|
|
641
|
+
params: registrationParams,
|
|
642
|
+
});
|
|
643
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
644
|
+
method: 'search',
|
|
645
|
+
params: { address: 'o://leader/services/no-protocols' },
|
|
646
|
+
});
|
|
647
|
+
expect(searchResult.result.data).to.have.lengthOf(1);
|
|
648
|
+
expect(searchResult.result.data[0].protocols).to.be.an('array')
|
|
649
|
+
.that.is.empty;
|
|
650
|
+
});
|
|
651
|
+
it('should handle concurrent registrations', async () => {
|
|
652
|
+
const registrations = [];
|
|
653
|
+
for (let i = 0; i < 10; i++) {
|
|
654
|
+
registrations.push({
|
|
655
|
+
_connectionId: 'test-connection',
|
|
656
|
+
_requestMethod: 'test-method',
|
|
657
|
+
peerId: `peer-concurrent-${i}`,
|
|
658
|
+
address: `o://leader/services/concurrent-${i}`,
|
|
659
|
+
protocols: ['/concurrent/1.0.0'],
|
|
660
|
+
transports: ['/memory'],
|
|
661
|
+
registeredAt: Date.now() + i,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
// Register concurrently
|
|
665
|
+
const promises = registrations.map((params) => leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
666
|
+
method: 'commit',
|
|
667
|
+
params,
|
|
668
|
+
}));
|
|
669
|
+
await Promise.all(promises);
|
|
670
|
+
// Verify all were registered
|
|
671
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
672
|
+
method: 'search',
|
|
673
|
+
params: { protocols: ['/concurrent/1.0.0'] },
|
|
674
|
+
});
|
|
675
|
+
expect(searchResult.result.data).to.have.lengthOf(10);
|
|
676
|
+
});
|
|
677
|
+
it('should maintain sort order after multiple operations', async () => {
|
|
678
|
+
const nodes = [
|
|
679
|
+
{ peerId: 'peer-1', registeredAt: 1000 },
|
|
680
|
+
{ peerId: 'peer-2', registeredAt: 2000 },
|
|
681
|
+
{ peerId: 'peer-3', registeredAt: 3000 },
|
|
682
|
+
];
|
|
683
|
+
for (const node of nodes) {
|
|
684
|
+
await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
685
|
+
method: 'commit',
|
|
686
|
+
params: {
|
|
687
|
+
...node,
|
|
688
|
+
address: `o://${node.peerId}`,
|
|
689
|
+
protocols: ['/test/1.0.0'],
|
|
690
|
+
transports: ['/memory'],
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const searchResult = await leader.use(new oAddress(RestrictedAddresses.REGISTRY), {
|
|
695
|
+
method: 'search',
|
|
696
|
+
params: { protocols: ['/test/1.0.0'] },
|
|
697
|
+
});
|
|
698
|
+
// Should be sorted by registeredAt descending (most recent first)
|
|
699
|
+
expect(searchResult.result.data[0].peerId).to.equal('peer-3');
|
|
700
|
+
expect(searchResult.result.data[1].peerId).to.equal('peer-2');
|
|
701
|
+
expect(searchResult.result.data[2].peerId).to.equal('peer-1');
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-memory.spec.d.ts","sourceRoot":"","sources":["../../test/registry-memory.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { RegistryMemoryTool } from '../src/registry/registry-memory.tool.js';
|
|
3
|
+
import { oAddress } from '@olane/o-core';
|
|
4
|
+
describe('RegistryMemoryTool', () => {
|
|
5
|
+
let registry;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
registry = new RegistryMemoryTool({
|
|
8
|
+
name: 'test-registry',
|
|
9
|
+
parent: oAddress.leader(),
|
|
10
|
+
leader: oAddress.leader(),
|
|
11
|
+
network: {
|
|
12
|
+
listeners: [],
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe('_tool_commit()', () => {
|
|
17
|
+
it('should register a node with all fields', async () => {
|
|
18
|
+
const params = {
|
|
19
|
+
_connectionId: 'test-connection',
|
|
20
|
+
_requestMethod: 'test-method',
|
|
21
|
+
peerId: 'peer-123',
|
|
22
|
+
address: 'o://leader/services/embeddings',
|
|
23
|
+
protocols: ['/embeddings/1.0.0', '/text/1.0.0'],
|
|
24
|
+
transports: ['/ip4/127.0.0.1/tcp/4001'],
|
|
25
|
+
staticAddress: 'o://embeddings-text',
|
|
26
|
+
registeredAt: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
const request = {
|
|
29
|
+
params,
|
|
30
|
+
};
|
|
31
|
+
const result = await registry._tool_commit(request);
|
|
32
|
+
expect(result.success).to.be.true;
|
|
33
|
+
// Verify node is in registry
|
|
34
|
+
const allNodes = await registry._tool_find_all({});
|
|
35
|
+
expect(allNodes).to.have.lengthOf(1);
|
|
36
|
+
expect(allNodes[0].peerId).to.equal('peer-123');
|
|
37
|
+
expect(allNodes[0].address).to.equal('o://leader/services/embeddings');
|
|
38
|
+
});
|
|
39
|
+
it('should register a node with minimal fields', async () => {
|
|
40
|
+
const params = {
|
|
41
|
+
_connectionId: 'test-connection',
|
|
42
|
+
_requestMethod: 'test-method',
|
|
43
|
+
peerId: 'peer-456',
|
|
44
|
+
address: 'o://worker',
|
|
45
|
+
protocols: [],
|
|
46
|
+
transports: ['/memory'],
|
|
47
|
+
};
|
|
48
|
+
const request = {
|
|
49
|
+
params,
|
|
50
|
+
};
|
|
51
|
+
const result = await registry._tool_commit(request);
|
|
52
|
+
expect(result.success).to.be.true;
|
|
53
|
+
const allNodes = await registry._tool_find_all({});
|
|
54
|
+
expect(allNodes).to.have.lengthOf(1);
|
|
55
|
+
expect(allNodes[0].peerId).to.equal('peer-456');
|
|
56
|
+
});
|
|
57
|
+
it('should update an existing node on re-registration', async () => {
|
|
58
|
+
const initialParams = {
|
|
59
|
+
_connectionId: 'test-connection',
|
|
60
|
+
_requestMethod: 'test-method',
|
|
61
|
+
peerId: 'peer-789',
|
|
62
|
+
address: 'o://service-v1',
|
|
63
|
+
protocols: ['/service/1.0.0'],
|
|
64
|
+
transports: ['/ip4/127.0.0.1/tcp/4001'],
|
|
65
|
+
};
|
|
66
|
+
await registry._tool_commit({
|
|
67
|
+
params: initialParams,
|
|
68
|
+
});
|
|
69
|
+
// Re-register with updated info
|
|
70
|
+
const updatedParams = {
|
|
71
|
+
_connectionId: 'test-connection',
|
|
72
|
+
_requestMethod: 'test-method',
|
|
73
|
+
peerId: 'peer-789',
|
|
74
|
+
address: 'o://service-v2',
|
|
75
|
+
protocols: ['/service/2.0.0'],
|
|
76
|
+
transports: ['/ip4/127.0.0.1/tcp/4002'],
|
|
77
|
+
};
|
|
78
|
+
await registry._tool_commit({
|
|
79
|
+
params: updatedParams,
|
|
80
|
+
});
|
|
81
|
+
const allNodes = await registry._tool_find_all({});
|
|
82
|
+
expect(allNodes).to.have.lengthOf(1);
|
|
83
|
+
expect(allNodes[0].address).to.equal('o://service-v2');
|
|
84
|
+
expect(allNodes[0].protocols).to.deep.equal(['/service/2.0.0']);
|
|
85
|
+
});
|
|
86
|
+
it('should register multiple nodes with same protocols', async () => {
|
|
87
|
+
const node1 = {
|
|
88
|
+
_connectionId: 'test-connection',
|
|
89
|
+
_requestMethod: 'test-method',
|
|
90
|
+
peerId: 'peer-001',
|
|
91
|
+
address: 'o://embeddings-node-1',
|
|
92
|
+
protocols: ['/embeddings/1.0.0'],
|
|
93
|
+
transports: ['/ip4/127.0.0.1/tcp/5001'],
|
|
94
|
+
};
|
|
95
|
+
const node2 = {
|
|
96
|
+
_connectionId: 'test-connection',
|
|
97
|
+
_requestMethod: 'test-method',
|
|
98
|
+
peerId: 'peer-002',
|
|
99
|
+
address: 'o://embeddings-node-2',
|
|
100
|
+
protocols: ['/embeddings/1.0.0'],
|
|
101
|
+
transports: ['/ip4/127.0.0.1/tcp/5002'],
|
|
102
|
+
};
|
|
103
|
+
await registry._tool_commit({ params: node1 });
|
|
104
|
+
await registry._tool_commit({ params: node2 });
|
|
105
|
+
const allNodes = await registry._tool_find_all({});
|
|
106
|
+
expect(allNodes).to.have.lengthOf(2);
|
|
107
|
+
});
|
|
108
|
+
it('should set registeredAt timestamp if not provided', async () => {
|
|
109
|
+
const params = {
|
|
110
|
+
_connectionId: 'test-connection',
|
|
111
|
+
_requestMethod: 'test-method',
|
|
112
|
+
peerId: 'peer-timestamp',
|
|
113
|
+
address: 'o://test',
|
|
114
|
+
protocols: [],
|
|
115
|
+
transports: ['/memory'],
|
|
116
|
+
};
|
|
117
|
+
const beforeTime = Date.now();
|
|
118
|
+
await registry._tool_commit({ params });
|
|
119
|
+
const afterTime = Date.now();
|
|
120
|
+
const allNodes = await registry._tool_find_all({});
|
|
121
|
+
const registeredNode = allNodes[0];
|
|
122
|
+
expect(registeredNode.registeredAt).to.be.a('number');
|
|
123
|
+
expect(registeredNode.registeredAt).to.be.at.least(beforeTime);
|
|
124
|
+
expect(registeredNode.registeredAt).to.be.at.most(afterTime);
|
|
125
|
+
});
|
|
126
|
+
it('should preserve custom registeredAt timestamp', async () => {
|
|
127
|
+
const customTimestamp = 1234567890;
|
|
128
|
+
const params = {
|
|
129
|
+
_connectionId: 'test-connection',
|
|
130
|
+
_requestMethod: 'test-method',
|
|
131
|
+
peerId: 'peer-custom-time',
|
|
132
|
+
address: 'o://test',
|
|
133
|
+
protocols: [],
|
|
134
|
+
transports: ['/memory'],
|
|
135
|
+
registeredAt: customTimestamp,
|
|
136
|
+
};
|
|
137
|
+
await registry._tool_commit({ params });
|
|
138
|
+
const allNodes = await registry._tool_find_all({});
|
|
139
|
+
expect(allNodes[0].registeredAt).to.equal(customTimestamp);
|
|
140
|
+
});
|
|
141
|
+
it('should update protocol mapping on registration', async () => {
|
|
142
|
+
const params = {
|
|
143
|
+
_connectionId: 'test-connection',
|
|
144
|
+
_requestMethod: 'test-method',
|
|
145
|
+
peerId: 'peer-protocol-map',
|
|
146
|
+
address: 'o://test',
|
|
147
|
+
protocols: ['/embeddings/1.0.0', '/chat/1.0.0'],
|
|
148
|
+
transports: ['/memory'],
|
|
149
|
+
};
|
|
150
|
+
await registry._tool_commit({ params });
|
|
151
|
+
// Search by protocol to verify mapping
|
|
152
|
+
const searchResult = await registry._tool_search({
|
|
153
|
+
params: { protocols: ['/embeddings/1.0.0'] },
|
|
154
|
+
});
|
|
155
|
+
expect(searchResult).to.have.lengthOf(1);
|
|
156
|
+
expect(searchResult[0].peerId).to.equal('peer-protocol-map');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('_tool_search()', () => {
|
|
160
|
+
beforeEach(async () => {
|
|
161
|
+
// Setup test data
|
|
162
|
+
const nodes = [
|
|
163
|
+
{
|
|
164
|
+
_connectionId: 'test-connection',
|
|
165
|
+
_requestMethod: 'test-method',
|
|
166
|
+
peerId: 'peer-embeddings-1',
|
|
167
|
+
address: 'o://leader/services/embeddings-text',
|
|
168
|
+
staticAddress: 'o://embeddings-text',
|
|
169
|
+
protocols: ['/embeddings/1.0.0', '/text/1.0.0'],
|
|
170
|
+
transports: ['/ip4/127.0.0.1/tcp/6001'],
|
|
171
|
+
registeredAt: 1000,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
_connectionId: 'test-connection',
|
|
175
|
+
_requestMethod: 'test-method',
|
|
176
|
+
peerId: 'peer-embeddings-2',
|
|
177
|
+
address: 'o://leader/services/embeddings-image',
|
|
178
|
+
staticAddress: 'o://embeddings-image',
|
|
179
|
+
protocols: ['/embeddings/1.0.0', '/image/1.0.0'],
|
|
180
|
+
transports: ['/ip4/127.0.0.1/tcp/6002'],
|
|
181
|
+
registeredAt: 2000,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
_connectionId: 'test-connection',
|
|
185
|
+
_requestMethod: 'test-method',
|
|
186
|
+
peerId: 'peer-chat',
|
|
187
|
+
address: 'o://leader/services/chat',
|
|
188
|
+
staticAddress: 'o://chat',
|
|
189
|
+
protocols: ['/chat/1.0.0'],
|
|
190
|
+
transports: ['/ip4/127.0.0.1/tcp/6003'],
|
|
191
|
+
registeredAt: 3000,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
for (const node of nodes) {
|
|
195
|
+
await registry._tool_commit({ params: node });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
it('should search by staticAddress (exact match)', async () => {
|
|
199
|
+
const result = await registry._tool_search({
|
|
200
|
+
params: { staticAddress: 'o://embeddings-text' },
|
|
201
|
+
});
|
|
202
|
+
expect(result).to.have.lengthOf(1);
|
|
203
|
+
expect(result[0].peerId).to.equal('peer-embeddings-1');
|
|
204
|
+
expect(result[0].staticAddress).to.equal('o://embeddings-text');
|
|
205
|
+
});
|
|
206
|
+
it('should search by address (exact match)', async () => {
|
|
207
|
+
const result = await registry._tool_search({
|
|
208
|
+
params: { address: 'o://leader/services/chat' },
|
|
209
|
+
});
|
|
210
|
+
expect(result).to.have.lengthOf(1);
|
|
211
|
+
expect(result[0].peerId).to.equal('peer-chat');
|
|
212
|
+
});
|
|
213
|
+
it('should search by single protocol', async () => {
|
|
214
|
+
const result = await registry._tool_search({
|
|
215
|
+
params: { protocols: ['/chat/1.0.0'] },
|
|
216
|
+
});
|
|
217
|
+
expect(result).to.have.lengthOf(1);
|
|
218
|
+
expect(result[0].peerId).to.equal('peer-chat');
|
|
219
|
+
});
|
|
220
|
+
it('should search by multiple protocols (AND logic)', async () => {
|
|
221
|
+
const result = await registry._tool_search({
|
|
222
|
+
params: { protocols: ['/embeddings/1.0.0', '/text/1.0.0'] },
|
|
223
|
+
});
|
|
224
|
+
expect(result).to.have.lengthOf(1);
|
|
225
|
+
expect(result[0].peerId).to.equal('peer-embeddings-1');
|
|
226
|
+
});
|
|
227
|
+
it('should return multiple nodes matching protocol', async () => {
|
|
228
|
+
const result = await registry._tool_search({
|
|
229
|
+
params: { protocols: ['/embeddings/1.0.0'] },
|
|
230
|
+
});
|
|
231
|
+
expect(result).to.have.lengthOf(2);
|
|
232
|
+
expect(result[0].peerId).to.equal('peer-embeddings-2'); // More recent
|
|
233
|
+
expect(result[1].peerId).to.equal('peer-embeddings-1');
|
|
234
|
+
});
|
|
235
|
+
it('should return empty array when no results found', async () => {
|
|
236
|
+
const result = await registry._tool_search({
|
|
237
|
+
params: { staticAddress: 'o://nonexistent' },
|
|
238
|
+
});
|
|
239
|
+
expect(result).to.be.an('array');
|
|
240
|
+
expect(result).to.have.lengthOf(0);
|
|
241
|
+
});
|
|
242
|
+
it('should return empty array for non-matching protocol', async () => {
|
|
243
|
+
const result = await registry._tool_search({
|
|
244
|
+
params: { protocols: ['/nonexistent/1.0.0'] },
|
|
245
|
+
});
|
|
246
|
+
expect(result).to.have.lengthOf(0);
|
|
247
|
+
});
|
|
248
|
+
it('should sort results by registeredAt (most recent first)', async () => {
|
|
249
|
+
const result = await registry._tool_search({
|
|
250
|
+
params: { protocols: ['/embeddings/1.0.0'] },
|
|
251
|
+
});
|
|
252
|
+
expect(result).to.have.lengthOf(2);
|
|
253
|
+
expect(result[0].registeredAt).to.equal(2000);
|
|
254
|
+
expect(result[1].registeredAt).to.equal(1000);
|
|
255
|
+
expect(result[0].registeredAt).to.be.greaterThan(result[1].registeredAt);
|
|
256
|
+
});
|
|
257
|
+
it('should handle combined search parameters', async () => {
|
|
258
|
+
const result = await registry._tool_search({
|
|
259
|
+
params: {
|
|
260
|
+
staticAddress: 'o://embeddings-text',
|
|
261
|
+
protocols: ['/embeddings/1.0.0'],
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
// Should return the node that matches staticAddress
|
|
265
|
+
// Note: Current implementation concatenates results, may have duplicates
|
|
266
|
+
expect(result.length).to.be.at.least(1);
|
|
267
|
+
const textEmbedding = result.find((r) => r.peerId === 'peer-embeddings-1');
|
|
268
|
+
expect(textEmbedding).to.exist;
|
|
269
|
+
});
|
|
270
|
+
it('should handle search with no parameters', async () => {
|
|
271
|
+
const result = await registry._tool_search({
|
|
272
|
+
params: {},
|
|
273
|
+
});
|
|
274
|
+
// Empty search should return empty array
|
|
275
|
+
expect(result).to.be.an('array');
|
|
276
|
+
expect(result).to.have.lengthOf(0);
|
|
277
|
+
});
|
|
278
|
+
it('should handle partial protocol match failure', async () => {
|
|
279
|
+
// Search for both protocols when node only has one
|
|
280
|
+
const result = await registry._tool_search({
|
|
281
|
+
params: { protocols: ['/chat/1.0.0', '/embeddings/1.0.0'] },
|
|
282
|
+
});
|
|
283
|
+
// AND logic: node must have ALL protocols
|
|
284
|
+
expect(result).to.have.lengthOf(0);
|
|
285
|
+
});
|
|
286
|
+
it('should handle nodes without registeredAt timestamp', async () => {
|
|
287
|
+
const params = {
|
|
288
|
+
_connectionId: 'test-connection',
|
|
289
|
+
_requestMethod: 'test-method',
|
|
290
|
+
peerId: 'peer-no-timestamp',
|
|
291
|
+
address: 'o://test-no-timestamp',
|
|
292
|
+
protocols: ['/test/1.0.0'],
|
|
293
|
+
transports: ['/memory'],
|
|
294
|
+
// No registeredAt
|
|
295
|
+
};
|
|
296
|
+
await registry._tool_commit({ params });
|
|
297
|
+
const result = await registry._tool_search({
|
|
298
|
+
params: { protocols: ['/test/1.0.0'] },
|
|
299
|
+
});
|
|
300
|
+
expect(result).to.have.lengthOf(1);
|
|
301
|
+
expect(result[0].peerId).to.equal('peer-no-timestamp');
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
describe('_tool_find_all()', () => {
|
|
305
|
+
it('should return empty array for empty registry', async () => {
|
|
306
|
+
const result = await registry._tool_find_all({});
|
|
307
|
+
expect(result).to.be.an('array');
|
|
308
|
+
expect(result).to.have.lengthOf(0);
|
|
309
|
+
});
|
|
310
|
+
it('should return single node', async () => {
|
|
311
|
+
const params = {
|
|
312
|
+
_connectionId: 'test-connection',
|
|
313
|
+
_requestMethod: 'test-method',
|
|
314
|
+
peerId: 'single-peer',
|
|
315
|
+
address: 'o://single',
|
|
316
|
+
protocols: [],
|
|
317
|
+
transports: ['/memory'],
|
|
318
|
+
};
|
|
319
|
+
await registry._tool_commit({ params });
|
|
320
|
+
const result = await registry._tool_find_all({});
|
|
321
|
+
expect(result).to.have.lengthOf(1);
|
|
322
|
+
expect(result[0].peerId).to.equal('single-peer');
|
|
323
|
+
});
|
|
324
|
+
it('should return all registered nodes', async () => {
|
|
325
|
+
const nodes = [
|
|
326
|
+
{
|
|
327
|
+
peerId: 'peer-1',
|
|
328
|
+
address: 'o://node-1',
|
|
329
|
+
protocols: [],
|
|
330
|
+
transports: ['/memory'],
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
peerId: 'peer-2',
|
|
334
|
+
address: 'o://node-2',
|
|
335
|
+
protocols: [],
|
|
336
|
+
transports: ['/memory'],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
peerId: 'peer-3',
|
|
340
|
+
address: 'o://node-3',
|
|
341
|
+
protocols: [],
|
|
342
|
+
transports: ['/memory'],
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
for (const node of nodes) {
|
|
346
|
+
await registry._tool_commit({
|
|
347
|
+
params: node,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const result = await registry._tool_find_all({});
|
|
351
|
+
expect(result).to.have.lengthOf(3);
|
|
352
|
+
const peerIds = result.map((r) => r.peerId);
|
|
353
|
+
expect(peerIds).to.include.members(['peer-1', 'peer-2', 'peer-3']);
|
|
354
|
+
});
|
|
355
|
+
it('should return nodes with all their properties', async () => {
|
|
356
|
+
const params = {
|
|
357
|
+
_connectionId: 'test-connection',
|
|
358
|
+
_requestMethod: 'test-method',
|
|
359
|
+
peerId: 'full-peer',
|
|
360
|
+
address: 'o://full-node',
|
|
361
|
+
staticAddress: 'o://static',
|
|
362
|
+
protocols: ['/protocol/1.0.0'],
|
|
363
|
+
transports: ['/ip4/127.0.0.1/tcp/4001'],
|
|
364
|
+
ttl: 3600,
|
|
365
|
+
registeredAt: 12345,
|
|
366
|
+
};
|
|
367
|
+
await registry._tool_commit({ params });
|
|
368
|
+
const result = await registry._tool_find_all({});
|
|
369
|
+
expect(result[0]).to.deep.include({
|
|
370
|
+
peerId: 'full-peer',
|
|
371
|
+
address: 'o://full-node',
|
|
372
|
+
staticAddress: 'o://static',
|
|
373
|
+
ttl: 3600,
|
|
374
|
+
registeredAt: 12345,
|
|
375
|
+
});
|
|
376
|
+
expect(result[0].protocols).to.deep.equal(['/protocol/1.0.0']);
|
|
377
|
+
expect(result[0].transports).to.deep.equal(['/ip4/127.0.0.1/tcp/4001']);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
describe('_tool_remove()', () => {
|
|
381
|
+
it('should remove an existing node', async () => {
|
|
382
|
+
const params = {
|
|
383
|
+
_connectionId: 'test-connection',
|
|
384
|
+
_requestMethod: 'test-method',
|
|
385
|
+
peerId: 'peer-to-remove',
|
|
386
|
+
address: 'o://removable',
|
|
387
|
+
protocols: ['/test/1.0.0'],
|
|
388
|
+
transports: ['/memory'],
|
|
389
|
+
};
|
|
390
|
+
await registry._tool_commit({ params });
|
|
391
|
+
let allNodes = await registry._tool_find_all({});
|
|
392
|
+
expect(allNodes).to.have.lengthOf(1);
|
|
393
|
+
const result = await registry._tool_remove({
|
|
394
|
+
params,
|
|
395
|
+
});
|
|
396
|
+
expect(result.success).to.be.true;
|
|
397
|
+
allNodes = await registry._tool_find_all({});
|
|
398
|
+
expect(allNodes).to.have.lengthOf(0);
|
|
399
|
+
});
|
|
400
|
+
it('should handle removing non-existent node', async () => {
|
|
401
|
+
const params = {
|
|
402
|
+
_connectionId: 'test-connection',
|
|
403
|
+
_requestMethod: 'test-method',
|
|
404
|
+
peerId: 'nonexistent-peer',
|
|
405
|
+
address: 'o://nonexistent',
|
|
406
|
+
protocols: [],
|
|
407
|
+
transports: [],
|
|
408
|
+
};
|
|
409
|
+
const result = await registry._tool_remove({
|
|
410
|
+
params,
|
|
411
|
+
});
|
|
412
|
+
expect(result.success).to.be.true; // Should succeed silently
|
|
413
|
+
});
|
|
414
|
+
it('should remove correct node when multiple exist', async () => {
|
|
415
|
+
const nodes = [
|
|
416
|
+
{
|
|
417
|
+
peerId: 'peer-1',
|
|
418
|
+
address: 'o://node-1',
|
|
419
|
+
protocols: [],
|
|
420
|
+
transports: ['/memory'],
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
peerId: 'peer-2',
|
|
424
|
+
address: 'o://node-2',
|
|
425
|
+
protocols: [],
|
|
426
|
+
transports: ['/memory'],
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
peerId: 'peer-3',
|
|
430
|
+
address: 'o://node-3',
|
|
431
|
+
protocols: [],
|
|
432
|
+
transports: ['/memory'],
|
|
433
|
+
},
|
|
434
|
+
];
|
|
435
|
+
for (const node of nodes) {
|
|
436
|
+
await registry._tool_commit({
|
|
437
|
+
params: node,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
await registry._tool_remove({
|
|
441
|
+
params: { peerId: 'peer-2' },
|
|
442
|
+
});
|
|
443
|
+
const allNodes = await registry._tool_find_all({});
|
|
444
|
+
expect(allNodes).to.have.lengthOf(2);
|
|
445
|
+
const peerIds = allNodes.map((r) => r.peerId);
|
|
446
|
+
expect(peerIds).to.not.include('peer-2');
|
|
447
|
+
expect(peerIds).to.include.members(['peer-1', 'peer-3']);
|
|
448
|
+
});
|
|
449
|
+
it('should not affect search results after removal', async () => {
|
|
450
|
+
const params = {
|
|
451
|
+
_connectionId: 'test-connection',
|
|
452
|
+
_requestMethod: 'test-method',
|
|
453
|
+
peerId: 'peer-search-remove',
|
|
454
|
+
address: 'o://test',
|
|
455
|
+
staticAddress: 'o://test-static',
|
|
456
|
+
protocols: ['/test/1.0.0'],
|
|
457
|
+
transports: ['/memory'],
|
|
458
|
+
};
|
|
459
|
+
await registry._tool_commit({ params });
|
|
460
|
+
let searchResult = await registry._tool_search({
|
|
461
|
+
params: { staticAddress: 'o://test-static' },
|
|
462
|
+
});
|
|
463
|
+
expect(searchResult).to.have.lengthOf(1);
|
|
464
|
+
await registry._tool_remove({ params });
|
|
465
|
+
searchResult = await registry._tool_search({
|
|
466
|
+
params: { staticAddress: 'o://test-static' },
|
|
467
|
+
});
|
|
468
|
+
expect(searchResult).to.have.lengthOf(0);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
describe('Edge cases and error scenarios', () => {
|
|
472
|
+
it('should handle registry with large number of nodes', async () => {
|
|
473
|
+
const nodeCount = 100;
|
|
474
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
475
|
+
const params = {
|
|
476
|
+
_connectionId: 'test-connection',
|
|
477
|
+
_requestMethod: 'test-method',
|
|
478
|
+
peerId: `peer-${i}`,
|
|
479
|
+
address: `o://node-${i}`,
|
|
480
|
+
protocols: ['/test/1.0.0'],
|
|
481
|
+
transports: ['/memory'],
|
|
482
|
+
registeredAt: i,
|
|
483
|
+
};
|
|
484
|
+
await registry._tool_commit({ params });
|
|
485
|
+
}
|
|
486
|
+
const allNodes = await registry._tool_find_all({});
|
|
487
|
+
expect(allNodes).to.have.lengthOf(nodeCount);
|
|
488
|
+
// Verify sorting still works
|
|
489
|
+
const searchResult = await registry._tool_search({
|
|
490
|
+
params: { protocols: ['/test/1.0.0'] },
|
|
491
|
+
});
|
|
492
|
+
expect(searchResult[0].registeredAt).to.equal(nodeCount - 1); // Most recent
|
|
493
|
+
});
|
|
494
|
+
it('should handle nodes with empty protocols array', async () => {
|
|
495
|
+
const params = {
|
|
496
|
+
_connectionId: 'test-connection',
|
|
497
|
+
_requestMethod: 'test-method',
|
|
498
|
+
peerId: 'peer-no-protocols',
|
|
499
|
+
address: 'o://no-protocols',
|
|
500
|
+
protocols: [],
|
|
501
|
+
transports: ['/memory'],
|
|
502
|
+
};
|
|
503
|
+
await registry._tool_commit({ params });
|
|
504
|
+
const result = await registry._tool_search({
|
|
505
|
+
params: { address: 'o://no-protocols' },
|
|
506
|
+
});
|
|
507
|
+
expect(result).to.have.lengthOf(1);
|
|
508
|
+
expect(result[0].protocols).to.be.an('array').that.is.empty;
|
|
509
|
+
});
|
|
510
|
+
it('should handle special characters in addresses', async () => {
|
|
511
|
+
const params = {
|
|
512
|
+
_connectionId: 'test-connection',
|
|
513
|
+
_requestMethod: 'test-method',
|
|
514
|
+
peerId: 'peer-special',
|
|
515
|
+
address: 'o://service-name_v2.0',
|
|
516
|
+
staticAddress: 'o://service-name_v2.0',
|
|
517
|
+
protocols: [],
|
|
518
|
+
transports: ['/memory'],
|
|
519
|
+
};
|
|
520
|
+
await registry._tool_commit({ params });
|
|
521
|
+
const result = await registry._tool_search({
|
|
522
|
+
params: { staticAddress: 'o://service-name_v2.0' },
|
|
523
|
+
});
|
|
524
|
+
expect(result).to.have.lengthOf(1);
|
|
525
|
+
expect(result[0].address).to.equal('o://service-name_v2.0');
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olane/o-leader",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"typescript": "5.4.5"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@olane/o-config": "^0.7.
|
|
59
|
-
"@olane/o-core": "^0.7.
|
|
60
|
-
"@olane/o-gateway-olane": "^0.7.
|
|
61
|
-
"@olane/o-lane": "^0.7.
|
|
62
|
-
"@olane/o-protocol": "^0.7.
|
|
63
|
-
"@olane/o-tool": "^0.7.
|
|
58
|
+
"@olane/o-config": "^0.7.11",
|
|
59
|
+
"@olane/o-core": "^0.7.11",
|
|
60
|
+
"@olane/o-gateway-olane": "^0.7.11",
|
|
61
|
+
"@olane/o-lane": "^0.7.11",
|
|
62
|
+
"@olane/o-protocol": "^0.7.11",
|
|
63
|
+
"@olane/o-tool": "^0.7.11"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"debug": "^4.4.1",
|