@phronesis-io/openclaw-eigenflux 0.0.1 → 0.0.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.
@@ -1,437 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import http from 'http';
5
- import { WebSocketServer } from 'ws';
6
-
7
- function waitFor(condition: () => boolean, timeoutMs = 8000): Promise<void> {
8
- const startedAt = Date.now();
9
- return new Promise((resolve, reject) => {
10
- const timer = setInterval(() => {
11
- if (condition()) {
12
- clearInterval(timer);
13
- resolve();
14
- return;
15
- }
16
- if (Date.now() - startedAt > timeoutMs) {
17
- clearInterval(timer);
18
- reject(new Error('condition wait timeout'));
19
- }
20
- }, 50);
21
- });
22
- }
23
-
24
- describe('register integration', () => {
25
- let homeDir: string;
26
- let originalHome: string | undefined;
27
- let workdir: string;
28
-
29
- let apiHttpServer: http.Server;
30
- let apiPort: number;
31
- let apiRequestCount: number;
32
- let apiAuthHeader: string | undefined;
33
- let apiUserAgentHeader: string | undefined;
34
- let apiFeedItems: Array<{
35
- item_id: string;
36
- group_id?: string;
37
- broadcast_type: string;
38
- updated_at: number;
39
- }>;
40
-
41
- let gatewayHttpServer: http.Server;
42
- let gatewayWss: WebSocketServer;
43
- let gatewayPort: number;
44
-
45
- beforeEach(async () => {
46
- originalHome = process.env.HOME;
47
- homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-openclaw-home-'));
48
- process.env.HOME = homeDir;
49
- workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-openclaw-workdir-'));
50
- fs.writeFileSync(
51
- path.join(workdir, 'credentials.json'),
52
- JSON.stringify({ access_token: 'at_integration_token' }),
53
- 'utf-8'
54
- );
55
-
56
- apiRequestCount = 0;
57
- apiAuthHeader = undefined;
58
- apiUserAgentHeader = undefined;
59
- apiFeedItems = [
60
- {
61
- item_id: '501',
62
- group_id: 'group-int-1',
63
- broadcast_type: 'info',
64
- updated_at: 1760000000000,
65
- },
66
- ];
67
- apiHttpServer = http.createServer((req, res) => {
68
- if (req.url?.startsWith('/api/v1/items/feed')) {
69
- apiRequestCount++;
70
- apiAuthHeader = req.headers.authorization;
71
- apiUserAgentHeader = req.headers['user-agent'];
72
- res.writeHead(200, { 'Content-Type': 'application/json' });
73
- res.end(
74
- JSON.stringify({
75
- code: 0,
76
- msg: 'success',
77
- data: {
78
- items: apiFeedItems,
79
- has_more: false,
80
- notifications: [],
81
- },
82
- })
83
- );
84
- return;
85
- }
86
-
87
- res.writeHead(404);
88
- res.end();
89
- });
90
- await new Promise<void>((resolve) => {
91
- apiHttpServer.listen(0, '127.0.0.1', () => {
92
- apiPort = (apiHttpServer.address() as any).port;
93
- resolve();
94
- });
95
- });
96
-
97
- gatewayHttpServer = http.createServer();
98
- gatewayWss = new WebSocketServer({ server: gatewayHttpServer });
99
- await new Promise<void>((resolve) => {
100
- gatewayHttpServer.listen(0, '127.0.0.1', () => {
101
- gatewayPort = (gatewayHttpServer.address() as any).port;
102
- resolve();
103
- });
104
- });
105
- });
106
-
107
- afterEach(async () => {
108
- if (originalHome === undefined) {
109
- delete process.env.HOME;
110
- } else {
111
- process.env.HOME = originalHome;
112
- }
113
- fs.rmSync(homeDir, { recursive: true, force: true });
114
- fs.rmSync(workdir, { recursive: true, force: true });
115
- await new Promise<void>((resolve) => apiHttpServer.close(() => resolve()));
116
- await new Promise<void>((resolve) => gatewayWss.close(() => resolve()));
117
- await new Promise<void>((resolve) => gatewayHttpServer.close(() => resolve()));
118
- });
119
-
120
- test('falls back to gateway rpc agent when polling feed returns new items', async () => {
121
- jest.resetModules();
122
- const sessionStorePath = path.join(
123
- homeDir,
124
- '.openclaw',
125
- 'agents',
126
- 'main',
127
- 'sessions',
128
- 'sessions.json'
129
- );
130
- const { default: plugin } = await import('./index');
131
- const services: any[] = [];
132
- const gatewayMethods: string[] = [];
133
- const agentParams: any[] = [];
134
-
135
- gatewayWss.on('connection', (socket) => {
136
- socket.send(
137
- JSON.stringify({
138
- type: 'event',
139
- event: 'connect.challenge',
140
- payload: { nonce: 'nonce-integration' },
141
- })
142
- );
143
-
144
- socket.on('message', (raw) => {
145
- const frame = JSON.parse(raw.toString());
146
- gatewayMethods.push(String(frame.method || ''));
147
-
148
- if (frame.type !== 'req') {
149
- return;
150
- }
151
-
152
- if (frame.method === 'connect') {
153
- socket.send(
154
- JSON.stringify({
155
- type: 'res',
156
- id: frame.id,
157
- ok: true,
158
- payload: {
159
- type: 'hello-ok',
160
- protocol: 3,
161
- server: { version: 'test', connId: 'conn-test' },
162
- features: { methods: ['agent'], events: [] },
163
- snapshot: { ts: Date.now() },
164
- policy: { maxPayload: 1000000, maxBufferedBytes: 1000000, tickIntervalMs: 30000 },
165
- },
166
- })
167
- );
168
- return;
169
- }
170
-
171
- if (frame.method === 'agent') {
172
- agentParams.push(frame.params);
173
- socket.send(
174
- JSON.stringify({
175
- type: 'res',
176
- id: frame.id,
177
- ok: true,
178
- payload: {
179
- runId: 'run-integration-1',
180
- status: 'started',
181
- },
182
- })
183
- );
184
- }
185
- });
186
- });
187
-
188
- plugin.register({
189
- config: {
190
- gateway: {
191
- auth: {
192
- token: 'gw_test_token',
193
- },
194
- },
195
- },
196
- pluginConfig: {
197
- gatewayUrl: `ws://127.0.0.1:${gatewayPort}`,
198
- servers: [
199
- {
200
- name: 'eigenflux',
201
- endpoint: `http://127.0.0.1:${apiPort}`,
202
- workdir,
203
- pollInterval: 60,
204
- sessionStorePath,
205
- },
206
- ],
207
- },
208
- runtime: {},
209
- logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
210
- registerService: (service: any) => {
211
- services.push(service);
212
- },
213
- } as any);
214
-
215
- expect(services).toHaveLength(1);
216
- await services[0].start();
217
- await waitFor(() => agentParams.length === 1);
218
-
219
- expect(apiRequestCount).toBeGreaterThanOrEqual(1);
220
- expect(apiAuthHeader).toBe('Bearer at_integration_token');
221
- expect(apiUserAgentHeader).toContain('eigenflux-plugin');
222
- expect(apiUserAgentHeader).toContain('node/');
223
- expect(gatewayMethods).toEqual(['connect', 'agent']);
224
- expect(agentParams[0]).toEqual(
225
- expect.objectContaining({
226
- agentId: 'main',
227
- sessionKey: 'main',
228
- message: expect.stringContaining('[EIGENFLUX_FEED_PAYLOAD]'),
229
- deliver: true,
230
- })
231
- );
232
- expect(String(agentParams[0].message)).toContain('"item_id": "501"');
233
- expect(String(agentParams[0].message)).toContain('"group_id": "group-int-1"');
234
- expect(String(agentParams[0].message)).toContain('network=eigenflux');
235
- expect(String(agentParams[0].message)).toContain(`workdir=${workdir}`);
236
- expect(String(agentParams[0].message)).toContain(
237
- `skill_file=http://127.0.0.1:${apiPort}/skill.md`
238
- );
239
- expect(String(agentParams[0].message)).toContain(
240
- 'submit the corresponding feedback scores through the normal EigenFlux workflow'
241
- );
242
- expect(typeof agentParams[0].idempotencyKey).toBe('string');
243
- expect(agentParams[0].idempotencyKey.length).toBeGreaterThan(0);
244
-
245
- await services[0].stop();
246
- });
247
-
248
- test('dispatches the entire feed payload in a single gateway agent message', async () => {
249
- apiFeedItems = [
250
- {
251
- item_id: '601',
252
- group_id: 'group-dup-1',
253
- broadcast_type: 'info',
254
- updated_at: 1760000000100,
255
- },
256
- {
257
- item_id: '602',
258
- group_id: 'group-dup-1',
259
- broadcast_type: 'info',
260
- updated_at: 1760000000200,
261
- },
262
- ];
263
-
264
- jest.resetModules();
265
- const sessionStorePath = path.join(
266
- homeDir,
267
- '.openclaw',
268
- 'agents',
269
- 'main',
270
- 'sessions',
271
- 'sessions.json'
272
- );
273
- const { default: plugin } = await import('./index');
274
- const services: any[] = [];
275
- const agentParams: any[] = [];
276
-
277
- gatewayWss.on('connection', (socket) => {
278
- socket.send(
279
- JSON.stringify({
280
- type: 'event',
281
- event: 'connect.challenge',
282
- payload: { nonce: 'nonce-duplicate-group' },
283
- })
284
- );
285
-
286
- socket.on('message', (raw) => {
287
- const frame = JSON.parse(raw.toString());
288
- if (frame.type !== 'req') {
289
- return;
290
- }
291
-
292
- if (frame.method === 'connect') {
293
- socket.send(
294
- JSON.stringify({
295
- type: 'res',
296
- id: frame.id,
297
- ok: true,
298
- payload: {
299
- type: 'hello-ok',
300
- protocol: 3,
301
- server: { version: 'test', connId: 'conn-dup' },
302
- features: { methods: ['agent'], events: [] },
303
- snapshot: { ts: Date.now() },
304
- policy: { maxPayload: 1000000, maxBufferedBytes: 1000000, tickIntervalMs: 30000 },
305
- },
306
- })
307
- );
308
- return;
309
- }
310
-
311
- if (frame.method === 'agent') {
312
- agentParams.push(frame.params);
313
- socket.send(
314
- JSON.stringify({
315
- type: 'res',
316
- id: frame.id,
317
- ok: true,
318
- payload: {
319
- runId: `run-dup-${agentParams.length}`,
320
- status: 'started',
321
- },
322
- })
323
- );
324
- }
325
- });
326
- });
327
-
328
- plugin.register({
329
- config: {
330
- gateway: {
331
- auth: {
332
- token: 'gw_test_token',
333
- },
334
- },
335
- },
336
- pluginConfig: {
337
- gatewayUrl: `ws://127.0.0.1:${gatewayPort}`,
338
- servers: [
339
- {
340
- name: 'eigenflux',
341
- endpoint: `http://127.0.0.1:${apiPort}`,
342
- workdir,
343
- pollInterval: 60,
344
- sessionStorePath,
345
- },
346
- ],
347
- },
348
- runtime: {},
349
- logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
350
- registerService: (service: any) => {
351
- services.push(service);
352
- },
353
- } as any);
354
-
355
- expect(services).toHaveLength(1);
356
- await services[0].start();
357
- await waitFor(() => agentParams.length === 1);
358
-
359
- expect(agentParams).toHaveLength(1);
360
- expect(String(agentParams[0].message)).toContain('"item_id": "601"');
361
- expect(String(agentParams[0].message)).toContain('"item_id": "602"');
362
- expect(String(agentParams[0].message)).toContain('"group_id": "group-dup-1"');
363
-
364
- await services[0].stop();
365
- });
366
-
367
- test('routes mocked feed notifications to the freshest external session for runtime.subagent', async () => {
368
- jest.resetModules();
369
- const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-home-'));
370
- const sessionStoreDir = path.join(homeDir, '.openclaw', 'agents', 'main', 'sessions');
371
- fs.mkdirSync(sessionStoreDir, { recursive: true });
372
- const sessionStorePath = path.join(sessionStoreDir, 'sessions.json');
373
- fs.writeFileSync(
374
- sessionStorePath,
375
- JSON.stringify({
376
- 'agent:main:main': {
377
- updatedAt: 100,
378
- deliveryContext: {
379
- channel: 'webchat',
380
- },
381
- },
382
- 'agent:main:feishu:direct:ou_feed_target': {
383
- updatedAt: 200,
384
- deliveryContext: {
385
- channel: 'feishu',
386
- to: 'user:ou_feed_target',
387
- accountId: 'default',
388
- },
389
- },
390
- }),
391
- 'utf-8'
392
- );
393
-
394
- const { default: plugin } = await import('./index');
395
- const services: any[] = [];
396
- const subagentRun = jest.fn().mockResolvedValue({ runId: 'run-subagent-feed' });
397
-
398
- plugin.register({
399
- config: {},
400
- pluginConfig: {
401
- servers: [
402
- {
403
- name: 'eigenflux',
404
- endpoint: `http://127.0.0.1:${apiPort}`,
405
- workdir,
406
- pollInterval: 60,
407
- sessionStorePath,
408
- },
409
- ],
410
- },
411
- runtime: {
412
- subagent: {
413
- run: subagentRun,
414
- },
415
- },
416
- logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
417
- registerService: (service: any) => {
418
- services.push(service);
419
- },
420
- } as any);
421
-
422
- expect(services).toHaveLength(1);
423
- await services[0].start();
424
- await waitFor(() => subagentRun.mock.calls.length === 1);
425
-
426
- expect(subagentRun).toHaveBeenCalledWith({
427
- sessionKey: 'agent:main:feishu:direct:ou_feed_target',
428
- message: expect.stringContaining('[EIGENFLUX_FEED_PAYLOAD]'),
429
- deliver: true,
430
- idempotencyKey: expect.any(String),
431
- });
432
- expect(String(subagentRun.mock.calls[0]?.[0]?.message)).toContain('"item_id": "501"');
433
-
434
- await services[0].stop();
435
- fs.rmSync(homeDir, { recursive: true, force: true });
436
- });
437
- });