@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,319 @@
1
+ """
2
+ Cross-language interop tests for elizaOS Python.
3
+
4
+ Tests verify that plugins can be loaded from other languages
5
+ and that type structures are consistent across runtimes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+
12
+ class TestTypeCompatibility:
13
+ """Tests for type compatibility across languages."""
14
+
15
+ def test_action_result_structure(self) -> None:
16
+ """ActionResult should have consistent structure."""
17
+ action_result = {
18
+ "success": True,
19
+ "text": "Action completed",
20
+ "error": None,
21
+ "data": {"key": "value"},
22
+ "values": {"setting": True},
23
+ }
24
+
25
+ assert "success" in action_result
26
+ assert "text" in action_result
27
+ assert isinstance(action_result["success"], bool)
28
+
29
+ def test_provider_result_structure(self) -> None:
30
+ """ProviderResult should have consistent structure."""
31
+ provider_result = {
32
+ "text": "Provider output",
33
+ "values": {"key": "value"},
34
+ "data": {"structured": "data"},
35
+ }
36
+
37
+ assert "text" in provider_result
38
+ assert "values" in provider_result
39
+ assert "data" in provider_result
40
+
41
+ def test_memory_structure(self) -> None:
42
+ """Memory should have consistent structure."""
43
+ memory = {
44
+ "id": "mem-uuid",
45
+ "entityId": "entity-uuid",
46
+ "agentId": "agent-uuid",
47
+ "roomId": "room-uuid",
48
+ "content": {"text": "Message content"},
49
+ "createdAt": 1704067200000,
50
+ "unique": False,
51
+ "metadata": {"type": "messages"},
52
+ }
53
+
54
+ assert "id" in memory
55
+ assert "entityId" in memory
56
+ assert "roomId" in memory
57
+ assert "content" in memory
58
+ assert "text" in memory["content"]
59
+
60
+ def test_state_structure(self) -> None:
61
+ """State should have consistent structure."""
62
+ state = {
63
+ "values": {"key": "value"},
64
+ "data": {"structured": "data"},
65
+ "text": "Context text",
66
+ }
67
+
68
+ assert "values" in state
69
+ assert "data" in state
70
+ assert "text" in state
71
+
72
+
73
+ class TestPluginManifest:
74
+ """Tests for plugin manifest structure."""
75
+
76
+ def test_manifest_required_fields(self) -> None:
77
+ """Manifest should have required fields."""
78
+ manifest = {
79
+ "name": "test-plugin",
80
+ "description": "Test plugin",
81
+ "version": "2.0.0-alpha",
82
+ "language": "python",
83
+ }
84
+
85
+ assert "name" in manifest
86
+ assert "description" in manifest
87
+ assert isinstance(manifest["name"], str)
88
+ assert isinstance(manifest["description"], str)
89
+
90
+ def test_manifest_action_structure(self) -> None:
91
+ """Action manifest should have consistent structure."""
92
+ action = {
93
+ "name": "TEST_ACTION",
94
+ "description": "A test action",
95
+ "similes": ["similar action"],
96
+ }
97
+
98
+ assert "name" in action
99
+ assert "description" in action
100
+
101
+ def test_manifest_provider_structure(self) -> None:
102
+ """Provider manifest should have consistent structure."""
103
+ provider = {
104
+ "name": "TEST_PROVIDER",
105
+ "description": "A test provider",
106
+ "dynamic": True,
107
+ "position": 10,
108
+ "private": False,
109
+ }
110
+
111
+ assert "name" in provider
112
+ assert isinstance(provider.get("dynamic"), (bool, type(None)))
113
+ assert isinstance(provider.get("position"), (int, type(None)))
114
+
115
+ def test_manifest_evaluator_structure(self) -> None:
116
+ """Evaluator manifest should have consistent structure."""
117
+ evaluator = {
118
+ "name": "TEST_EVALUATOR",
119
+ "description": "A test evaluator",
120
+ "alwaysRun": False,
121
+ "similes": ["similar evaluator"],
122
+ }
123
+
124
+ assert "name" in evaluator
125
+ assert "description" in evaluator
126
+
127
+
128
+ class TestIPCMessages:
129
+ """Tests for IPC message structure."""
130
+
131
+ def test_action_invoke_message(self) -> None:
132
+ """Action invoke message should have correct structure."""
133
+ message = {
134
+ "type": "action.invoke",
135
+ "id": "req-123",
136
+ "action": "TEST_ACTION",
137
+ "memory": {
138
+ "id": "mem-1",
139
+ "entityId": "entity-1",
140
+ "roomId": "room-1",
141
+ "content": {"text": "test"},
142
+ },
143
+ "state": None,
144
+ "options": None,
145
+ }
146
+
147
+ assert message["type"] == "action.invoke"
148
+ assert "id" in message
149
+ assert "action" in message
150
+ assert "memory" in message
151
+
152
+ def test_action_result_message(self) -> None:
153
+ """Action result message should have correct structure."""
154
+ message = {
155
+ "type": "action.result",
156
+ "id": "req-123",
157
+ "result": {
158
+ "success": True,
159
+ "text": "Action completed",
160
+ "data": {"key": "value"},
161
+ },
162
+ }
163
+
164
+ assert message["type"] == "action.result"
165
+ assert "id" in message
166
+ assert "result" in message
167
+ assert message["result"]["success"] is True
168
+
169
+ def test_provider_get_message(self) -> None:
170
+ """Provider get message should have correct structure."""
171
+ message = {
172
+ "type": "provider.get",
173
+ "id": "req-123",
174
+ "provider": "TEST_PROVIDER",
175
+ "memory": {"id": "mem-1", "content": {"text": "test"}},
176
+ "state": {"values": {}, "data": {}},
177
+ }
178
+
179
+ assert message["type"] == "provider.get"
180
+ assert "provider" in message
181
+ assert "memory" in message
182
+ assert "state" in message
183
+
184
+ def test_provider_result_message(self) -> None:
185
+ """Provider result message should have correct structure."""
186
+ message = {
187
+ "type": "provider.result",
188
+ "id": "req-123",
189
+ "result": {
190
+ "text": "Provider output",
191
+ "values": {"key": "value"},
192
+ "data": {"structured": "data"},
193
+ },
194
+ }
195
+
196
+ assert message["type"] == "provider.result"
197
+ assert "result" in message
198
+
199
+
200
+ class TestElizaClassicParity:
201
+ """Tests for ELIZA Classic cross-language parity."""
202
+
203
+ def test_core_patterns_exist(self) -> None:
204
+ """Core patterns should exist in all implementations."""
205
+ core_patterns = [
206
+ {"keyword": "hello", "weight": 0},
207
+ {"keyword": "sorry", "weight": 1},
208
+ {"keyword": "remember", "weight": 5},
209
+ {"keyword": "if", "weight": 3},
210
+ {"keyword": "dream", "weight": 3},
211
+ {"keyword": "computer", "weight": 50},
212
+ {"keyword": "my", "weight": 2},
213
+ {"keyword": "everyone", "weight": 2},
214
+ {"keyword": "always", "weight": 1},
215
+ ]
216
+
217
+ for pattern in core_patterns:
218
+ assert "keyword" in pattern
219
+ assert "weight" in pattern
220
+ assert isinstance(pattern["weight"], int)
221
+
222
+ def test_pronoun_reflections(self) -> None:
223
+ """Pronoun reflections should be consistent."""
224
+ reflections = {
225
+ "am": "are",
226
+ "was": "were",
227
+ "i": "you",
228
+ "i'd": "you would",
229
+ "i've": "you have",
230
+ "i'll": "you will",
231
+ "my": "your",
232
+ "are": "am",
233
+ "you've": "I have",
234
+ "you'll": "I will",
235
+ "your": "my",
236
+ "yours": "mine",
237
+ "you": "me",
238
+ "me": "you",
239
+ "myself": "yourself",
240
+ "yourself": "myself",
241
+ "i'm": "you are",
242
+ }
243
+
244
+ assert reflections["i"] == "you"
245
+ assert reflections["my"] == "your"
246
+ assert reflections["you"] == "me"
247
+ assert reflections["me"] == "you"
248
+
249
+ def test_default_responses(self) -> None:
250
+ """Default responses should be consistent."""
251
+ default_responses = [
252
+ "Very interesting.",
253
+ "I am not sure I understand you fully.",
254
+ "What does that suggest to you?",
255
+ "Please continue.",
256
+ "Go on.",
257
+ "Do you feel strongly about discussing such things?",
258
+ "Tell me more.",
259
+ "That is quite interesting.",
260
+ "Can you elaborate on that?",
261
+ "Why do you say that?",
262
+ "I see.",
263
+ "What does that mean to you?",
264
+ "How does that make you feel?",
265
+ "Let's explore that further.",
266
+ "Interesting. Please go on.",
267
+ ]
268
+
269
+ assert len(default_responses) >= 10
270
+ for response in default_responses:
271
+ assert isinstance(response, str)
272
+ assert len(response) > 0
273
+
274
+
275
+ class TestInteropProtocols:
276
+ """Tests for interop protocol support."""
277
+
278
+ def test_wasm_protocol(self) -> None:
279
+ """WASM protocol config should be valid."""
280
+ wasm_config = {
281
+ "protocol": "wasm",
282
+ "wasmPath": "./dist/plugin.wasm",
283
+ }
284
+
285
+ assert wasm_config["protocol"] == "wasm"
286
+ assert "wasmPath" in wasm_config
287
+
288
+ def test_ipc_protocol(self) -> None:
289
+ """IPC protocol config should be valid."""
290
+ ipc_config = {
291
+ "protocol": "ipc",
292
+ "ipcCommand": "python3 -m plugin_module",
293
+ }
294
+
295
+ assert ipc_config["protocol"] == "ipc"
296
+
297
+ def test_ffi_protocol(self) -> None:
298
+ """FFI protocol config should be valid."""
299
+ ffi_config = {
300
+ "protocol": "ffi",
301
+ "sharedLibPath": "./dist/libplugin.so",
302
+ }
303
+
304
+ assert ffi_config["protocol"] == "ffi"
305
+ assert "sharedLibPath" in ffi_config
306
+
307
+ def test_native_protocol(self) -> None:
308
+ """Native protocol config should be valid."""
309
+ native_config = {
310
+ "protocol": "native",
311
+ }
312
+
313
+ assert native_config["protocol"] == "native"
314
+
315
+
316
+
317
+
318
+
319
+
@@ -0,0 +1,352 @@
1
+ """
2
+ Tests for Rust FFI Plugin Loader
3
+
4
+ These tests validate the FFI interface without requiring an actual Rust library.
5
+ They test JSON serialization, type handling, and the expected protocol.
6
+ """
7
+
8
+ import json
9
+ import pytest
10
+
11
+
12
+ class TestFFIProtocol:
13
+ """Test the FFI protocol and data serialization."""
14
+
15
+ def test_manifest_json_format(self):
16
+ """Test that manifest JSON format matches expected structure."""
17
+ manifest = {
18
+ "name": "test-rust-plugin",
19
+ "description": "A test plugin from Rust",
20
+ "version": "2.0.0-alpha",
21
+ "language": "rust",
22
+ "actions": [
23
+ {
24
+ "name": "RUST_ACTION",
25
+ "description": "Action from Rust",
26
+ "similes": ["TEST_RUST"],
27
+ }
28
+ ],
29
+ "providers": [
30
+ {
31
+ "name": "RUST_PROVIDER",
32
+ "description": "Provider from Rust",
33
+ "dynamic": True,
34
+ }
35
+ ],
36
+ }
37
+
38
+ json_str = json.dumps(manifest)
39
+ parsed = json.loads(json_str)
40
+
41
+ assert parsed["name"] == "test-rust-plugin"
42
+ assert parsed["language"] == "rust"
43
+ assert len(parsed["actions"]) == 1
44
+ assert parsed["actions"][0]["name"] == "RUST_ACTION"
45
+
46
+ def test_action_result_serialization(self):
47
+ """Test ActionResult serialization format."""
48
+ result = {
49
+ "success": True,
50
+ "text": "Hello from Rust! πŸ¦€",
51
+ "data": {"language": "rust", "version": "2.0.0-alpha"},
52
+ "values": {"key": "value"},
53
+ }
54
+
55
+ json_str = json.dumps(result)
56
+ parsed = json.loads(json_str)
57
+
58
+ assert parsed["success"] is True
59
+ assert parsed["text"] == "Hello from Rust! πŸ¦€"
60
+ assert parsed["data"]["language"] == "rust"
61
+
62
+ def test_action_result_failure(self):
63
+ """Test ActionResult failure format."""
64
+ result = {"success": False, "error": "Something went wrong"}
65
+
66
+ json_str = json.dumps(result)
67
+ parsed = json.loads(json_str)
68
+
69
+ assert parsed["success"] is False
70
+ assert parsed["error"] == "Something went wrong"
71
+
72
+ def test_provider_result_serialization(self):
73
+ """Test ProviderResult serialization format."""
74
+ result = {
75
+ "text": "Rust plugin context",
76
+ "values": {"rust_version": "1.75.0"},
77
+ "data": {"build_info": {"target": "x86_64"}},
78
+ }
79
+
80
+ json_str = json.dumps(result)
81
+ parsed = json.loads(json_str)
82
+
83
+ assert parsed["text"] == "Rust plugin context"
84
+ assert parsed["values"]["rust_version"] == "1.75.0"
85
+
86
+ def test_memory_serialization(self):
87
+ """Test Memory object serialization for FFI."""
88
+ memory = {
89
+ "id": "123e4567-e89b-12d3-a456-426614174000",
90
+ "agentId": "123e4567-e89b-12d3-a456-426614174001",
91
+ "content": {"text": "Hello from Python", "actions": ["ACTION_1"]},
92
+ "createdAt": 1704067200000,
93
+ }
94
+
95
+ json_str = json.dumps(memory)
96
+ parsed = json.loads(json_str)
97
+
98
+ assert parsed["id"] == memory["id"]
99
+ assert parsed["content"]["text"] == "Hello from Python"
100
+
101
+ def test_state_serialization(self):
102
+ """Test State object serialization for FFI."""
103
+ state = {
104
+ "text": "Current conversation context",
105
+ "values": {"agentName": "TestAgent", "count": 5},
106
+ "data": {"providers": {"time": {"hour": 12}}},
107
+ }
108
+
109
+ json_str = json.dumps(state)
110
+ parsed = json.loads(json_str)
111
+
112
+ assert parsed["text"] == "Current conversation context"
113
+ assert parsed["values"]["count"] == 5
114
+
115
+
116
+ class TestFFIFunctionSignatures:
117
+ """Test expected FFI function signatures."""
118
+
119
+ def test_elizaos_get_manifest_signature(self):
120
+ """Test elizaos_get_manifest expected return format."""
121
+ # Simulate what the Rust function would return
122
+ manifest_json = json.dumps(
123
+ {"name": "test", "description": "Test", "version": "2.0.0-alpha", "language": "rust"}
124
+ )
125
+
126
+ # Should return a valid JSON string
127
+ parsed = json.loads(manifest_json)
128
+ assert "name" in parsed
129
+ assert "description" in parsed
130
+
131
+ def test_elizaos_init_signature(self):
132
+ """Test elizaos_init expected config format."""
133
+ config = {"API_KEY": "test-key", "DEBUG": "true"}
134
+
135
+ config_json = json.dumps(config)
136
+ parsed = json.loads(config_json)
137
+
138
+ assert parsed["API_KEY"] == "test-key"
139
+
140
+ def test_elizaos_validate_action_signature(self):
141
+ """Test elizaos_validate_action parameter format."""
142
+ action_name = "TEST_ACTION"
143
+ memory_json = json.dumps({"content": {"text": "Hello"}})
144
+ state_json = json.dumps({"values": {}})
145
+
146
+ # These would be passed to the FFI function
147
+ assert isinstance(action_name, str)
148
+ assert isinstance(memory_json, str)
149
+ assert isinstance(state_json, str)
150
+
151
+ def test_elizaos_invoke_action_signature(self):
152
+ """Test elizaos_invoke_action parameter and return format."""
153
+ # Test parameter types used in FFI calls
154
+ _action_name = "TEST_ACTION"
155
+ _memory_json = json.dumps({"content": {"text": "Hello"}})
156
+ _state_json = json.dumps({"values": {}})
157
+ _options_json = json.dumps({"timeout": 5000})
158
+
159
+ # Simulate return value
160
+ result_json = json.dumps({"success": True, "text": "Done!"})
161
+
162
+ parsed = json.loads(result_json)
163
+ assert parsed["success"] is True
164
+
165
+ def test_elizaos_get_provider_signature(self):
166
+ """Test elizaos_get_provider parameter and return format."""
167
+ # Test parameter types used in FFI calls
168
+ _provider_name = "TEST_PROVIDER"
169
+ _memory_json = json.dumps({"content": {}})
170
+ _state_json = json.dumps({"values": {}})
171
+
172
+ # Simulate return value
173
+ result_json = json.dumps({"text": "Provider data", "values": {"key": "value"}})
174
+
175
+ parsed = json.loads(result_json)
176
+ assert parsed["text"] == "Provider data"
177
+
178
+
179
+ class TestPlatformDetection:
180
+ """Test platform-specific library handling."""
181
+
182
+ def test_lib_extension_detection(self):
183
+ """Test library extension detection logic."""
184
+ import platform
185
+
186
+ system = platform.system()
187
+
188
+ if system == "Darwin":
189
+ expected = ".dylib"
190
+ elif system == "Windows":
191
+ expected = ".dll"
192
+ else:
193
+ expected = ".so"
194
+
195
+ # This tests the logic that would be in get_lib_extension()
196
+ assert expected in [".so", ".dylib", ".dll"]
197
+
198
+ def test_lib_prefix_detection(self):
199
+ """Test library prefix detection logic."""
200
+ import platform
201
+
202
+ system = platform.system()
203
+
204
+ if system == "Windows":
205
+ expected = ""
206
+ else:
207
+ expected = "lib"
208
+
209
+ assert expected in ["", "lib"]
210
+
211
+
212
+ class TestErrorHandling:
213
+ """Test error handling in FFI operations."""
214
+
215
+ def test_invalid_json_handling(self):
216
+ """Test handling of invalid JSON from FFI."""
217
+ invalid_json = "{ invalid json }"
218
+
219
+ with pytest.raises(json.JSONDecodeError):
220
+ json.loads(invalid_json)
221
+
222
+ def test_null_result_handling(self):
223
+ """Test handling of null results from FFI."""
224
+ null_json = "null"
225
+ parsed = json.loads(null_json)
226
+ assert parsed is None
227
+
228
+ def test_empty_result_handling(self):
229
+ """Test handling of empty results."""
230
+ empty_result = {"text": None, "values": None, "data": None}
231
+
232
+ json_str = json.dumps(empty_result)
233
+ parsed = json.loads(json_str)
234
+
235
+ assert parsed["text"] is None
236
+
237
+
238
+ class TestComplexDataTypes:
239
+ """Test handling of complex data types."""
240
+
241
+ def test_nested_data_serialization(self):
242
+ """Test deeply nested data structures."""
243
+ data = {
244
+ "level1": {
245
+ "level2": {
246
+ "level3": {"value": "deep"},
247
+ "array": [1, 2, {"nested": True}],
248
+ }
249
+ }
250
+ }
251
+
252
+ json_str = json.dumps(data)
253
+ parsed = json.loads(json_str)
254
+
255
+ assert parsed["level1"]["level2"]["level3"]["value"] == "deep"
256
+ assert parsed["level1"]["level2"]["array"][2]["nested"] is True
257
+
258
+ def test_unicode_handling(self):
259
+ """Test Unicode string handling."""
260
+ data = {"text": "Hello δΈ–η•Œ! πŸ¦€ Ω…Ψ±Ψ­Ψ¨Ψ§ Χ©ΦΈΧΧœΧ•ΦΉΧ", "emoji": "πŸŽ‰πŸŽŠπŸŽˆ"}
261
+
262
+ json_str = json.dumps(data, ensure_ascii=False)
263
+ parsed = json.loads(json_str)
264
+
265
+ assert "δΈ–η•Œ" in parsed["text"]
266
+ assert "πŸ¦€" in parsed["text"]
267
+ assert parsed["emoji"] == "πŸŽ‰πŸŽŠπŸŽˆ"
268
+
269
+ def test_large_payload_handling(self):
270
+ """Test handling of large payloads."""
271
+ large_text = "x" * 100000
272
+ data = {"content": {"text": large_text}}
273
+
274
+ json_str = json.dumps(data)
275
+ parsed = json.loads(json_str)
276
+
277
+ assert len(parsed["content"]["text"]) == 100000
278
+
279
+ def test_special_characters(self):
280
+ """Test handling of special characters."""
281
+ data = {
282
+ "text": 'Quotes: "hello" and \'world\'',
283
+ "newlines": "Line 1\nLine 2\r\nLine 3",
284
+ "tabs": "Col1\tCol2\tCol3",
285
+ "backslash": "path\\to\\file",
286
+ }
287
+
288
+ json_str = json.dumps(data)
289
+ parsed = json.loads(json_str)
290
+
291
+ assert '"hello"' in parsed["text"]
292
+ assert "\n" in parsed["newlines"]
293
+ assert "\t" in parsed["tabs"]
294
+ assert "\\" in parsed["backslash"]
295
+
296
+
297
+ class TestPluginAdapterCreation:
298
+ """Test creating Plugin adapters from FFI."""
299
+
300
+ def test_action_wrapper_creation(self):
301
+ """Test creating action wrappers from manifest."""
302
+ manifest = {
303
+ "actions": [
304
+ {"name": "ACTION_1", "description": "First action", "similes": ["A1"]},
305
+ {"name": "ACTION_2", "description": "Second action"},
306
+ ]
307
+ }
308
+
309
+ # Simulate wrapper creation
310
+ actions = []
311
+ for action_def in manifest["actions"]:
312
+ actions.append(
313
+ {
314
+ "name": action_def["name"],
315
+ "description": action_def["description"],
316
+ "similes": action_def.get("similes", []),
317
+ }
318
+ )
319
+
320
+ assert len(actions) == 2
321
+ assert actions[0]["name"] == "ACTION_1"
322
+ assert actions[0]["similes"] == ["A1"]
323
+ assert actions[1]["similes"] == []
324
+
325
+ def test_provider_wrapper_creation(self):
326
+ """Test creating provider wrappers from manifest."""
327
+ manifest = {
328
+ "providers": [
329
+ {
330
+ "name": "PROVIDER_1",
331
+ "description": "First provider",
332
+ "dynamic": True,
333
+ "position": 5,
334
+ }
335
+ ]
336
+ }
337
+
338
+ providers = []
339
+ for provider_def in manifest["providers"]:
340
+ providers.append(
341
+ {
342
+ "name": provider_def["name"],
343
+ "description": provider_def.get("description"),
344
+ "dynamic": provider_def.get("dynamic", False),
345
+ "position": provider_def.get("position"),
346
+ }
347
+ )
348
+
349
+ assert len(providers) == 1
350
+ assert providers[0]["dynamic"] is True
351
+ assert providers[0]["position"] == 5
352
+