@elizaos/interop 2.0.0-alpha

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +436 -0
  3. package/dist/packages/interop/tsconfig.tsbuildinfo +1 -0
  4. package/dist/tsconfig.tsbuildinfo +1 -0
  5. package/dist/typescript/index.d.ts +33 -0
  6. package/dist/typescript/index.d.ts.map +1 -0
  7. package/dist/typescript/index.js +121 -0
  8. package/dist/typescript/python-bridge.d.ts +80 -0
  9. package/dist/typescript/python-bridge.d.ts.map +1 -0
  10. package/dist/typescript/python-bridge.js +334 -0
  11. package/dist/typescript/types.d.ts +301 -0
  12. package/dist/typescript/types.d.ts.map +1 -0
  13. package/dist/typescript/types.js +10 -0
  14. package/dist/typescript/wasm-loader.d.ts +32 -0
  15. package/dist/typescript/wasm-loader.d.ts.map +1 -0
  16. package/dist/typescript/wasm-loader.js +269 -0
  17. package/package.json +43 -0
  18. package/python/__init__.py +50 -0
  19. package/python/__pycache__/__init__.cpython-313.pyc +0 -0
  20. package/python/__pycache__/rust_ffi.cpython-313.pyc +0 -0
  21. package/python/__pycache__/ts_bridge.cpython-313.pyc +0 -0
  22. package/python/__pycache__/wasm_loader.cpython-313.pyc +0 -0
  23. package/python/bridge_server.py +505 -0
  24. package/python/rust_ffi.py +418 -0
  25. package/python/tests/__init__.py +1 -0
  26. package/python/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/python/tests/__pycache__/test_bridge_server.cpython-313-pytest-9.0.2.pyc +0 -0
  28. package/python/tests/__pycache__/test_interop.cpython-313-pytest-9.0.2.pyc +0 -0
  29. package/python/tests/__pycache__/test_rust_ffi.cpython-313-pytest-9.0.2.pyc +0 -0
  30. package/python/tests/__pycache__/test_rust_ffi_loader.cpython-313-pytest-9.0.2.pyc +0 -0
  31. package/python/tests/test_bridge_server.py +525 -0
  32. package/python/tests/test_interop.py +319 -0
  33. package/python/tests/test_rust_ffi.py +352 -0
  34. package/python/tests/test_rust_ffi_loader.py +71 -0
  35. package/python/ts_bridge.py +452 -0
  36. package/python/ts_bridge_runner.mjs +284 -0
  37. package/python/wasm_loader.py +517 -0
  38. package/rust/ffi_exports.rs +362 -0
  39. package/rust/mod.rs +21 -0
  40. package/rust/py_loader.rs +412 -0
  41. package/rust/ts_loader.rs +467 -0
  42. package/rust/wasm_plugin.rs +320 -0
@@ -0,0 +1,525 @@
1
+ """
2
+ Tests for Python Plugin Bridge Server
3
+
4
+ These tests validate the IPC protocol handling and message processing.
5
+ """
6
+
7
+ import json
8
+ import pytest
9
+
10
+
11
+ class TestIPCProtocol:
12
+ """Test IPC message protocol."""
13
+
14
+ def test_ready_message_format(self):
15
+ """Test ready message with manifest format."""
16
+ manifest = {
17
+ "name": "python-plugin",
18
+ "description": "Test Python plugin",
19
+ "version": "2.0.0-alpha",
20
+ "language": "python",
21
+ "actions": [{"name": "TEST_ACTION", "description": "Test"}],
22
+ }
23
+
24
+ ready_msg = {"type": "ready", "manifest": manifest}
25
+
26
+ json_str = json.dumps(ready_msg)
27
+ parsed = json.loads(json_str)
28
+
29
+ assert parsed["type"] == "ready"
30
+ assert parsed["manifest"]["name"] == "python-plugin"
31
+
32
+ def test_action_invoke_request(self):
33
+ """Test action.invoke request parsing."""
34
+ request = {
35
+ "type": "action.invoke",
36
+ "id": "req-123",
37
+ "action": "HELLO_PYTHON",
38
+ "memory": {"content": {"text": "Hello"}},
39
+ "state": {"values": {}},
40
+ "options": {"timeout": 5000},
41
+ }
42
+
43
+ json_str = json.dumps(request)
44
+ parsed = json.loads(json_str)
45
+
46
+ assert parsed["type"] == "action.invoke"
47
+ assert parsed["action"] == "HELLO_PYTHON"
48
+ assert parsed["memory"]["content"]["text"] == "Hello"
49
+
50
+ def test_action_validate_request(self):
51
+ """Test action.validate request parsing."""
52
+ request = {
53
+ "type": "action.validate",
54
+ "id": "req-124",
55
+ "action": "TEST_ACTION",
56
+ "memory": {"content": {"text": "Test"}},
57
+ "state": None,
58
+ }
59
+
60
+ json_str = json.dumps(request)
61
+ parsed = json.loads(json_str)
62
+
63
+ assert parsed["type"] == "action.validate"
64
+ assert parsed["state"] is None
65
+
66
+ def test_provider_get_request(self):
67
+ """Test provider.get request parsing."""
68
+ request = {
69
+ "type": "provider.get",
70
+ "id": "req-125",
71
+ "provider": "PYTHON_INFO",
72
+ "memory": {"content": {}},
73
+ "state": {"values": {}},
74
+ }
75
+
76
+ json_str = json.dumps(request)
77
+ parsed = json.loads(json_str)
78
+
79
+ assert parsed["type"] == "provider.get"
80
+ assert parsed["provider"] == "PYTHON_INFO"
81
+
82
+ def test_plugin_init_request(self):
83
+ """Test plugin.init request parsing."""
84
+ request = {
85
+ "type": "plugin.init",
86
+ "id": "req-126",
87
+ "config": {"API_KEY": "test-key", "DEBUG": "true"},
88
+ }
89
+
90
+ json_str = json.dumps(request)
91
+ parsed = json.loads(json_str)
92
+
93
+ assert parsed["type"] == "plugin.init"
94
+ assert parsed["config"]["API_KEY"] == "test-key"
95
+
96
+
97
+ class TestIPCResponses:
98
+ """Test IPC response formatting."""
99
+
100
+ def test_action_result_success(self):
101
+ """Test successful action result response."""
102
+ response = {
103
+ "type": "action.result",
104
+ "id": "req-123",
105
+ "result": {
106
+ "success": True,
107
+ "text": "Hello from Python! 🐍",
108
+ "data": {"language": "python"},
109
+ },
110
+ }
111
+
112
+ json_str = json.dumps(response)
113
+ parsed = json.loads(json_str)
114
+
115
+ assert parsed["type"] == "action.result"
116
+ assert parsed["result"]["success"] is True
117
+ assert "🐍" in parsed["result"]["text"]
118
+
119
+ def test_action_result_failure(self):
120
+ """Test failed action result response."""
121
+ response = {
122
+ "type": "action.result",
123
+ "id": "req-123",
124
+ "result": {"success": False, "error": "Action failed"},
125
+ }
126
+
127
+ json_str = json.dumps(response)
128
+ parsed = json.loads(json_str)
129
+
130
+ assert parsed["result"]["success"] is False
131
+ assert parsed["result"]["error"] == "Action failed"
132
+
133
+ def test_validate_result(self):
134
+ """Test validation result response."""
135
+ response = {"type": "validate.result", "id": "req-124", "valid": True}
136
+
137
+ json_str = json.dumps(response)
138
+ parsed = json.loads(json_str)
139
+
140
+ assert parsed["type"] == "validate.result"
141
+ assert parsed["valid"] is True
142
+
143
+ def test_provider_result(self):
144
+ """Test provider result response."""
145
+ response = {
146
+ "type": "provider.result",
147
+ "id": "req-125",
148
+ "result": {
149
+ "text": "Python environment info",
150
+ "values": {"version": "3.11"},
151
+ "data": {"platform": "linux"},
152
+ },
153
+ }
154
+
155
+ json_str = json.dumps(response)
156
+ parsed = json.loads(json_str)
157
+
158
+ assert parsed["result"]["text"] == "Python environment info"
159
+ assert parsed["result"]["values"]["version"] == "3.11"
160
+
161
+ def test_init_result(self):
162
+ """Test plugin init result response."""
163
+ response = {"type": "plugin.init.result", "id": "req-126", "success": True}
164
+
165
+ json_str = json.dumps(response)
166
+ parsed = json.loads(json_str)
167
+
168
+ assert parsed["type"] == "plugin.init.result"
169
+ assert parsed["success"] is True
170
+
171
+ def test_error_response(self):
172
+ """Test error response format."""
173
+ response = {
174
+ "type": "error",
175
+ "id": "req-error",
176
+ "error": "Module not found",
177
+ "details": "Traceback (most recent call last):\n File...",
178
+ }
179
+
180
+ json_str = json.dumps(response)
181
+ parsed = json.loads(json_str)
182
+
183
+ assert parsed["type"] == "error"
184
+ assert parsed["error"] == "Module not found"
185
+
186
+
187
+ class TestRequestHandling:
188
+ """Test request handling logic."""
189
+
190
+ def test_route_action_invoke(self):
191
+ """Test routing action.invoke requests."""
192
+ request = {"type": "action.invoke", "action": "TEST"}
193
+
194
+ # Route based on type
195
+ handlers = {
196
+ "action.invoke": lambda r: {"handled": "action.invoke"},
197
+ "action.validate": lambda r: {"handled": "action.validate"},
198
+ "provider.get": lambda r: {"handled": "provider.get"},
199
+ }
200
+
201
+ result = handlers.get(request["type"], lambda r: {"error": "unknown"})(request)
202
+ assert result["handled"] == "action.invoke"
203
+
204
+ def test_route_unknown_type(self):
205
+ """Test routing unknown request types."""
206
+ request = {"type": "unknown.type"}
207
+
208
+ handlers = {
209
+ "action.invoke": lambda r: {"handled": "action.invoke"},
210
+ }
211
+
212
+ result = handlers.get(request["type"], lambda r: {"type": "error", "error": "Unknown type"})(
213
+ request
214
+ )
215
+ assert result["type"] == "error"
216
+
217
+ def test_extract_request_id(self):
218
+ """Test extracting request ID from messages."""
219
+ request = {"type": "action.invoke", "id": "unique-id-123", "action": "TEST"}
220
+
221
+ request_id = request.get("id", "")
222
+ assert request_id == "unique-id-123"
223
+
224
+ def test_missing_request_id(self):
225
+ """Test handling missing request ID."""
226
+ request = {"type": "action.invoke", "action": "TEST"}
227
+
228
+ request_id = request.get("id", "")
229
+ assert request_id == ""
230
+
231
+
232
+ class TestManifestGeneration:
233
+ """Test manifest generation for plugins."""
234
+
235
+ def test_generate_manifest_with_actions(self):
236
+ """Test generating manifest with actions."""
237
+ # Simulate plugin attributes
238
+ plugin = {
239
+ "name": "test-plugin",
240
+ "description": "Test description",
241
+ "version": "2.0.0-alpha",
242
+ "actions": [
243
+ {"name": "ACTION_1", "description": "First action"},
244
+ {"name": "ACTION_2", "description": "Second action", "similes": ["A2"]},
245
+ ],
246
+ }
247
+
248
+ manifest = {
249
+ "name": plugin["name"],
250
+ "description": plugin["description"],
251
+ "version": plugin.get("version", "1.0.0"),
252
+ "language": "python",
253
+ "actions": [
254
+ {
255
+ "name": a["name"],
256
+ "description": a["description"],
257
+ "similes": a.get("similes"),
258
+ }
259
+ for a in plugin.get("actions", [])
260
+ ],
261
+ }
262
+
263
+ assert manifest["name"] == "test-plugin"
264
+ assert len(manifest["actions"]) == 2
265
+ assert manifest["actions"][1]["similes"] == ["A2"]
266
+
267
+ def test_generate_manifest_with_providers(self):
268
+ """Test generating manifest with providers."""
269
+ plugin = {
270
+ "name": "provider-plugin",
271
+ "description": "Provider test",
272
+ "providers": [
273
+ {
274
+ "name": "PROVIDER_1",
275
+ "description": "First provider",
276
+ "dynamic": True,
277
+ "position": 5,
278
+ "private": False,
279
+ }
280
+ ],
281
+ }
282
+
283
+ manifest = {
284
+ "name": plugin["name"],
285
+ "description": plugin["description"],
286
+ "version": "2.0.0-alpha",
287
+ "language": "python",
288
+ "providers": [
289
+ {
290
+ "name": p["name"],
291
+ "description": p.get("description"),
292
+ "dynamic": p.get("dynamic"),
293
+ "position": p.get("position"),
294
+ "private": p.get("private"),
295
+ }
296
+ for p in plugin.get("providers", [])
297
+ ],
298
+ }
299
+
300
+ assert len(manifest["providers"]) == 1
301
+ assert manifest["providers"][0]["dynamic"] is True
302
+
303
+
304
+ class TestMessageBuffering:
305
+ """Test message buffering for stdin/stdout communication."""
306
+
307
+ def test_newline_delimited_messages(self):
308
+ """Test parsing newline-delimited messages."""
309
+ messages = [
310
+ {"type": "action.invoke", "id": "1", "action": "A"},
311
+ {"type": "action.invoke", "id": "2", "action": "B"},
312
+ {"type": "action.invoke", "id": "3", "action": "C"},
313
+ ]
314
+
315
+ # Simulate buffered input
316
+ buffer = "\n".join(json.dumps(m) for m in messages) + "\n"
317
+ lines = buffer.strip().split("\n")
318
+
319
+ assert len(lines) == 3
320
+ for i, line in enumerate(lines):
321
+ parsed = json.loads(line)
322
+ assert parsed["id"] == str(i + 1)
323
+
324
+ def test_partial_message_buffering(self):
325
+ """Test handling partial messages in buffer."""
326
+ message = {"type": "test", "data": "complete"}
327
+ full_json = json.dumps(message)
328
+
329
+ # Simulate partial reads
330
+ part1 = full_json[:10]
331
+ part2 = full_json[10:]
332
+
333
+ buffer = ""
334
+ buffer += part1
335
+ # Can't parse yet
336
+ with pytest.raises(json.JSONDecodeError):
337
+ json.loads(buffer)
338
+
339
+ buffer += part2
340
+ # Now can parse
341
+ parsed = json.loads(buffer)
342
+ assert parsed["type"] == "test"
343
+
344
+
345
+ class TestErrorHandling:
346
+ """Test error handling in bridge server."""
347
+
348
+ def test_malformed_json(self):
349
+ """Test handling malformed JSON input."""
350
+ malformed = '{ type: "test" }'
351
+
352
+ with pytest.raises(json.JSONDecodeError):
353
+ json.loads(malformed)
354
+
355
+ def test_missing_required_field(self):
356
+ """Test handling missing required fields."""
357
+ request = {"type": "action.invoke"}
358
+ # Missing action field
359
+
360
+ action = request.get("action")
361
+ assert action is None
362
+
363
+ def test_exception_serialization(self):
364
+ """Test serializing exceptions in error responses."""
365
+ try:
366
+ raise ValueError("Test error")
367
+ except Exception as e:
368
+ response = {
369
+ "type": "error",
370
+ "id": "req-err",
371
+ "error": str(e),
372
+ "details": type(e).__name__,
373
+ }
374
+
375
+ json_str = json.dumps(response)
376
+ parsed = json.loads(json_str)
377
+
378
+ assert parsed["error"] == "Test error"
379
+ assert parsed["details"] == "ValueError"
380
+
381
+
382
+ class TestAsyncOperations:
383
+ """Test async operation handling."""
384
+
385
+ @pytest.mark.asyncio
386
+ async def test_async_action_handler(self):
387
+ """Test async action handler execution."""
388
+
389
+ async def mock_handler(memory, state, options):
390
+ # Simulate async work
391
+ await asyncio.sleep(0.01)
392
+ return {"success": True, "text": "Async result"}
393
+
394
+ import asyncio
395
+
396
+ result = await mock_handler({}, {}, {})
397
+ assert result["success"] is True
398
+
399
+ @pytest.mark.asyncio
400
+ async def test_async_provider_get(self):
401
+ """Test async provider get execution."""
402
+
403
+ async def mock_get(memory, state):
404
+ return {"text": "Async provider data", "values": {}}
405
+
406
+
407
+ result = await mock_get({}, {})
408
+ assert result["text"] == "Async provider data"
409
+
410
+
411
+ class TestServiceHandling:
412
+ """Test service handling in bridge server."""
413
+
414
+ def test_service_start_request(self):
415
+ """Test service.start request format."""
416
+ request = {
417
+ "type": "service.start",
418
+ "id": "req-456",
419
+ "serviceType": "CUSTOM_SERVICE",
420
+ }
421
+
422
+ json_str = json.dumps(request)
423
+ parsed = json.loads(json_str)
424
+
425
+ assert parsed["type"] == "service.start"
426
+ assert parsed["serviceType"] == "CUSTOM_SERVICE"
427
+
428
+ def test_service_stop_request(self):
429
+ """Test service.stop request format."""
430
+ request = {
431
+ "type": "service.stop",
432
+ "id": "req-789",
433
+ "serviceType": "CUSTOM_SERVICE",
434
+ }
435
+
436
+ json_str = json.dumps(request)
437
+ parsed = json.loads(json_str)
438
+
439
+ assert parsed["type"] == "service.stop"
440
+ assert parsed["serviceType"] == "CUSTOM_SERVICE"
441
+
442
+ def test_service_manifest_entry(self):
443
+ """Test service manifest entry format."""
444
+ service_entry = {
445
+ "type": "CUSTOM_SERVICE",
446
+ "description": "A custom service for testing",
447
+ }
448
+
449
+ manifest = {
450
+ "name": "service-plugin",
451
+ "services": [service_entry],
452
+ }
453
+
454
+ assert manifest["services"][0]["type"] == "CUSTOM_SERVICE"
455
+ assert manifest["services"][0]["description"] == "A custom service for testing"
456
+
457
+
458
+ class TestRouteHandling:
459
+ """Test route handling in bridge server."""
460
+
461
+ def test_route_handle_request(self):
462
+ """Test route.handle request format."""
463
+ request = {
464
+ "type": "route.handle",
465
+ "id": "req-101",
466
+ "path": "/api/test",
467
+ "request": {
468
+ "method": "GET",
469
+ "body": {},
470
+ "params": {},
471
+ "query": {"limit": "10"},
472
+ "headers": {"authorization": "Bearer token"},
473
+ },
474
+ }
475
+
476
+ json_str = json.dumps(request)
477
+ parsed = json.loads(json_str)
478
+
479
+ assert parsed["type"] == "route.handle"
480
+ assert parsed["path"] == "/api/test"
481
+ assert parsed["request"]["query"]["limit"] == "10"
482
+
483
+ def test_route_result_success(self):
484
+ """Test route.result success format."""
485
+ response = {
486
+ "type": "route.result",
487
+ "id": "req-101",
488
+ "status": 200,
489
+ "body": {"data": [1, 2, 3]},
490
+ "headers": {"content-type": "application/json"},
491
+ }
492
+
493
+ assert response["status"] == 200
494
+ assert response["body"]["data"] == [1, 2, 3]
495
+
496
+ def test_route_result_error(self):
497
+ """Test route.result error format."""
498
+ response = {
499
+ "type": "route.result",
500
+ "id": "req-101",
501
+ "status": 404,
502
+ "body": {"error": "Not found"},
503
+ }
504
+
505
+ assert response["status"] == 404
506
+ assert response["body"]["error"] == "Not found"
507
+
508
+ def test_route_manifest_entry(self):
509
+ """Test route manifest entry format."""
510
+ route_entry = {
511
+ "path": "/api/users",
512
+ "type": "GET",
513
+ "public": True,
514
+ "name": "get_users",
515
+ }
516
+
517
+ manifest = {
518
+ "name": "route-plugin",
519
+ "routes": [route_entry],
520
+ }
521
+
522
+ assert manifest["routes"][0]["path"] == "/api/users"
523
+ assert manifest["routes"][0]["type"] == "GET"
524
+ assert manifest["routes"][0]["public"] is True
525
+