opc-agent 1.4.0 → 2.0.1

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 (198) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -32
  3. package/dist/channels/email.d.ts +32 -26
  4. package/dist/channels/email.js +239 -62
  5. package/dist/channels/feishu.d.ts +21 -6
  6. package/dist/channels/feishu.js +225 -126
  7. package/dist/channels/telegram.d.ts +30 -9
  8. package/dist/channels/telegram.js +125 -33
  9. package/dist/channels/websocket.d.ts +46 -3
  10. package/dist/channels/websocket.js +306 -37
  11. package/dist/channels/wechat.d.ts +33 -13
  12. package/dist/channels/wechat.js +229 -42
  13. package/dist/cli.js +1127 -19
  14. package/dist/core/a2a.d.ts +17 -0
  15. package/dist/core/a2a.js +43 -1
  16. package/dist/core/agent.d.ts +39 -0
  17. package/dist/core/agent.js +228 -3
  18. package/dist/core/runtime.d.ts +7 -0
  19. package/dist/core/runtime.js +205 -2
  20. package/dist/core/sandbox.d.ts +26 -0
  21. package/dist/core/sandbox.js +117 -0
  22. package/dist/core/scheduler.d.ts +52 -0
  23. package/dist/core/scheduler.js +168 -0
  24. package/dist/core/subagent.d.ts +28 -0
  25. package/dist/core/subagent.js +65 -0
  26. package/dist/core/workflow-graph.d.ts +93 -0
  27. package/dist/core/workflow-graph.js +247 -0
  28. package/dist/daemon.d.ts +3 -0
  29. package/dist/daemon.js +134 -0
  30. package/dist/doctor.d.ts +15 -0
  31. package/dist/doctor.js +183 -0
  32. package/dist/eval/index.d.ts +65 -0
  33. package/dist/eval/index.js +191 -0
  34. package/dist/index.d.ts +37 -6
  35. package/dist/index.js +75 -3
  36. package/dist/plugins/content-filter.d.ts +7 -0
  37. package/dist/plugins/content-filter.js +25 -0
  38. package/dist/plugins/index.d.ts +42 -0
  39. package/dist/plugins/index.js +108 -2
  40. package/dist/plugins/logger.d.ts +6 -0
  41. package/dist/plugins/logger.js +20 -0
  42. package/dist/plugins/rate-limiter.d.ts +7 -0
  43. package/dist/plugins/rate-limiter.js +35 -0
  44. package/dist/protocols/a2a/client.d.ts +25 -0
  45. package/dist/protocols/a2a/client.js +115 -0
  46. package/dist/protocols/a2a/index.d.ts +6 -0
  47. package/dist/protocols/a2a/index.js +12 -0
  48. package/dist/protocols/a2a/server.d.ts +41 -0
  49. package/dist/protocols/a2a/server.js +295 -0
  50. package/dist/protocols/a2a/types.d.ts +91 -0
  51. package/dist/protocols/a2a/types.js +15 -0
  52. package/dist/protocols/a2a/utils.d.ts +6 -0
  53. package/dist/protocols/a2a/utils.js +47 -0
  54. package/dist/protocols/agui/client.d.ts +10 -0
  55. package/dist/protocols/agui/client.js +75 -0
  56. package/dist/protocols/agui/index.d.ts +4 -0
  57. package/dist/protocols/agui/index.js +25 -0
  58. package/dist/protocols/agui/server.d.ts +37 -0
  59. package/dist/protocols/agui/server.js +191 -0
  60. package/dist/protocols/agui/types.d.ts +107 -0
  61. package/dist/protocols/agui/types.js +17 -0
  62. package/dist/protocols/index.d.ts +2 -0
  63. package/dist/protocols/index.js +19 -0
  64. package/dist/protocols/mcp/agent-tools.d.ts +11 -0
  65. package/dist/protocols/mcp/agent-tools.js +129 -0
  66. package/dist/protocols/mcp/index.d.ts +5 -0
  67. package/dist/protocols/mcp/index.js +11 -0
  68. package/dist/protocols/mcp/server.d.ts +31 -0
  69. package/dist/protocols/mcp/server.js +248 -0
  70. package/dist/protocols/mcp/types.d.ts +92 -0
  71. package/dist/protocols/mcp/types.js +17 -0
  72. package/dist/providers/index.d.ts +5 -1
  73. package/dist/providers/index.js +16 -9
  74. package/dist/publish/index.d.ts +45 -0
  75. package/dist/publish/index.js +350 -0
  76. package/dist/schema/oad.d.ts +859 -67
  77. package/dist/schema/oad.js +47 -3
  78. package/dist/security/approval.d.ts +36 -0
  79. package/dist/security/approval.js +113 -0
  80. package/dist/security/index.d.ts +4 -0
  81. package/dist/security/index.js +8 -0
  82. package/dist/security/keys.d.ts +16 -0
  83. package/dist/security/keys.js +117 -0
  84. package/dist/skills/auto-learn.d.ts +28 -0
  85. package/dist/skills/auto-learn.js +257 -0
  86. package/dist/studio/server.d.ts +63 -0
  87. package/dist/studio/server.js +625 -0
  88. package/dist/studio-ui/index.html +662 -0
  89. package/dist/telemetry/index.d.ts +93 -0
  90. package/dist/telemetry/index.js +285 -0
  91. package/dist/tools/builtin/datetime.d.ts +3 -0
  92. package/dist/tools/builtin/datetime.js +44 -0
  93. package/dist/tools/builtin/file.d.ts +3 -0
  94. package/dist/tools/builtin/file.js +151 -0
  95. package/dist/tools/builtin/index.d.ts +15 -0
  96. package/dist/tools/builtin/index.js +30 -0
  97. package/dist/tools/builtin/shell.d.ts +3 -0
  98. package/dist/tools/builtin/shell.js +43 -0
  99. package/dist/tools/builtin/web.d.ts +3 -0
  100. package/dist/tools/builtin/web.js +37 -0
  101. package/dist/tools/mcp-client.d.ts +24 -0
  102. package/dist/tools/mcp-client.js +119 -0
  103. package/package.json +5 -3
  104. package/scripts/install.ps1 +31 -0
  105. package/scripts/install.sh +40 -0
  106. package/src/channels/email.ts +351 -177
  107. package/src/channels/feishu.ts +349 -236
  108. package/src/channels/telegram.ts +212 -90
  109. package/src/channels/websocket.ts +399 -87
  110. package/src/channels/wechat.ts +329 -149
  111. package/src/cli.ts +1201 -20
  112. package/src/core/a2a.ts +60 -0
  113. package/src/core/agent.ts +420 -152
  114. package/src/core/runtime.ts +174 -0
  115. package/src/core/sandbox.ts +143 -0
  116. package/src/core/scheduler.ts +187 -0
  117. package/src/core/subagent.ts +98 -0
  118. package/src/core/workflow-graph.ts +365 -0
  119. package/src/daemon.ts +96 -0
  120. package/src/doctor.ts +156 -0
  121. package/src/eval/index.ts +211 -0
  122. package/src/eval/suites/basic.json +16 -0
  123. package/src/eval/suites/memory.json +12 -0
  124. package/src/eval/suites/safety.json +14 -0
  125. package/src/index.ts +65 -6
  126. package/src/plugins/content-filter.ts +23 -0
  127. package/src/plugins/index.ts +133 -2
  128. package/src/plugins/logger.ts +18 -0
  129. package/src/plugins/rate-limiter.ts +38 -0
  130. package/src/protocols/a2a/client.ts +132 -0
  131. package/src/protocols/a2a/index.ts +8 -0
  132. package/src/protocols/a2a/server.ts +333 -0
  133. package/src/protocols/a2a/types.ts +88 -0
  134. package/src/protocols/a2a/utils.ts +50 -0
  135. package/src/protocols/agui/client.ts +83 -0
  136. package/src/protocols/agui/index.ts +4 -0
  137. package/src/protocols/agui/server.ts +218 -0
  138. package/src/protocols/agui/types.ts +153 -0
  139. package/src/protocols/index.ts +2 -0
  140. package/src/protocols/mcp/agent-tools.ts +134 -0
  141. package/src/protocols/mcp/index.ts +8 -0
  142. package/src/protocols/mcp/server.ts +262 -0
  143. package/src/protocols/mcp/types.ts +69 -0
  144. package/src/providers/index.ts +354 -339
  145. package/src/publish/index.ts +376 -0
  146. package/src/schema/oad.ts +204 -154
  147. package/src/security/approval.ts +131 -0
  148. package/src/security/index.ts +3 -0
  149. package/src/security/keys.ts +87 -0
  150. package/src/skills/auto-learn.ts +262 -0
  151. package/src/studio/server.ts +629 -0
  152. package/src/studio-ui/index.html +662 -0
  153. package/src/telemetry/index.ts +324 -0
  154. package/src/tools/builtin/datetime.ts +41 -0
  155. package/src/tools/builtin/file.ts +107 -0
  156. package/src/tools/builtin/index.ts +28 -0
  157. package/src/tools/builtin/shell.ts +43 -0
  158. package/src/tools/builtin/web.ts +35 -0
  159. package/src/tools/mcp-client.ts +131 -0
  160. package/src/types/agent-workstation.d.ts +2 -0
  161. package/tests/a2a-protocol.test.ts +285 -0
  162. package/tests/agui-protocol.test.ts +246 -0
  163. package/tests/auto-learn.test.ts +105 -0
  164. package/tests/builtin-tools.test.ts +83 -0
  165. package/tests/channels/discord.test.ts +79 -0
  166. package/tests/channels/email.test.ts +148 -0
  167. package/tests/channels/feishu.test.ts +123 -0
  168. package/tests/channels/telegram.test.ts +129 -0
  169. package/tests/channels/websocket.test.ts +53 -0
  170. package/tests/channels/wechat.test.ts +170 -0
  171. package/tests/chat-cli.test.ts +160 -0
  172. package/tests/cli.test.ts +46 -0
  173. package/tests/daemon.test.ts +135 -0
  174. package/tests/deepbrain-wire.test.ts +234 -0
  175. package/tests/doctor.test.ts +38 -0
  176. package/tests/eval.test.ts +173 -0
  177. package/tests/init-role.test.ts +124 -0
  178. package/tests/mcp-client.test.ts +92 -0
  179. package/tests/mcp-server.test.ts +178 -0
  180. package/tests/plugin-a2a-enhanced.test.ts +230 -0
  181. package/tests/publish.test.ts +231 -0
  182. package/tests/scheduler.test.ts +200 -0
  183. package/tests/security-enhanced.test.ts +233 -0
  184. package/tests/skill-learner.test.ts +161 -0
  185. package/tests/studio.test.ts +229 -0
  186. package/tests/subagent.test.ts +193 -0
  187. package/tests/telegram-discord.test.ts +60 -0
  188. package/tests/telemetry.test.ts +186 -0
  189. package/tests/tools/builtin-extended.test.ts +138 -0
  190. package/tests/workflow-graph.test.ts +279 -0
  191. package/tutorial/customer-service-agent/README.md +612 -0
  192. package/tutorial/customer-service-agent/SOUL.md +26 -0
  193. package/tutorial/customer-service-agent/agent.yaml +63 -0
  194. package/tutorial/customer-service-agent/package.json +19 -0
  195. package/tutorial/customer-service-agent/src/index.ts +69 -0
  196. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
  197. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
  198. package/tutorial/customer-service-agent/tsconfig.json +14 -0
@@ -0,0 +1,178 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { MCPServer } from '../src/protocols/mcp/server';
3
+ import { agentToMCPTools, agentToMCPResources } from '../src/protocols/mcp/agent-tools';
4
+ import type { MCPServerToolDefinition, JsonRpcRequest } from '../src/protocols/mcp/types';
5
+
6
+ function makeRequest(method: string, params?: any, id: number = 1): JsonRpcRequest {
7
+ return { jsonrpc: '2.0', id, method, params };
8
+ }
9
+
10
+ describe('MCPServer', () => {
11
+ let server: MCPServer;
12
+ const echoTool: MCPServerToolDefinition = {
13
+ name: 'echo',
14
+ description: 'Echo back input',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: { text: { type: 'string' } },
18
+ required: ['text'],
19
+ },
20
+ handler: async (args) => `Echo: ${args.text}`,
21
+ };
22
+
23
+ beforeEach(() => {
24
+ server = new MCPServer({ name: 'test-server', version: '1.0.0', tools: [echoTool] });
25
+ });
26
+
27
+ it('initialize response has capabilities', async () => {
28
+ const res = await server.handleMessage(makeRequest('initialize', {
29
+ protocolVersion: '2024-11-05',
30
+ capabilities: {},
31
+ clientInfo: { name: 'test', version: '1.0' },
32
+ }));
33
+ expect(res).toBeDefined();
34
+ expect(res!.result.protocolVersion).toBe('2024-11-05');
35
+ expect(res!.result.capabilities.tools).toBeDefined();
36
+ expect(res!.result.serverInfo.name).toBe('test-server');
37
+ });
38
+
39
+ it('tools/list returns registered tools', async () => {
40
+ const res = await server.handleMessage(makeRequest('tools/list'));
41
+ expect(res!.result.tools).toHaveLength(1);
42
+ expect(res!.result.tools[0].name).toBe('echo');
43
+ expect(res!.result.tools[0].description).toBe('Echo back input');
44
+ expect(res!.result.tools[0].inputSchema).toBeDefined();
45
+ });
46
+
47
+ it('tools/call executes handler and returns content', async () => {
48
+ const res = await server.handleMessage(makeRequest('tools/call', {
49
+ name: 'echo', arguments: { text: 'hello' },
50
+ }));
51
+ expect(res!.result.content).toHaveLength(1);
52
+ expect(res!.result.content[0].text).toBe('Echo: hello');
53
+ });
54
+
55
+ it('tools/call returns error for missing tool', async () => {
56
+ const res = await server.handleMessage(makeRequest('tools/call', {
57
+ name: 'nonexistent', arguments: {},
58
+ }));
59
+ expect(res!.error).toBeDefined();
60
+ expect(res!.error!.code).toBe(-32001);
61
+ });
62
+
63
+ it('tools/call validates required params', async () => {
64
+ const res = await server.handleMessage(makeRequest('tools/call', {
65
+ name: 'echo', arguments: {},
66
+ }));
67
+ expect(res!.error).toBeDefined();
68
+ expect(res!.error!.code).toBe(-32602);
69
+ expect(res!.error!.message).toContain('text');
70
+ });
71
+
72
+ it('resources/list returns registered resources', async () => {
73
+ server.addResource({
74
+ uri: 'test://doc', name: 'TestDoc', description: 'A doc', mimeType: 'text/plain',
75
+ handler: async () => 'Hello content',
76
+ });
77
+ const res = await server.handleMessage(makeRequest('resources/list'));
78
+ expect(res!.result.resources).toHaveLength(1);
79
+ expect(res!.result.resources[0].uri).toBe('test://doc');
80
+ });
81
+
82
+ it('resources/read returns resource content', async () => {
83
+ server.addResource({
84
+ uri: 'test://doc', name: 'TestDoc', mimeType: 'text/plain',
85
+ handler: async () => 'Hello content',
86
+ });
87
+ const res = await server.handleMessage(makeRequest('resources/read', { uri: 'test://doc' }));
88
+ expect(res!.result.contents[0].text).toBe('Hello content');
89
+ });
90
+
91
+ it('resources/read returns error for unknown resource', async () => {
92
+ const res = await server.handleMessage(makeRequest('resources/read', { uri: 'nope://x' }));
93
+ expect(res!.error).toBeDefined();
94
+ expect(res!.error!.code).toBe(-32002);
95
+ });
96
+
97
+ it('unknown method returns METHOD_NOT_FOUND', async () => {
98
+ const res = await server.handleMessage(makeRequest('foo/bar'));
99
+ expect(res!.error).toBeDefined();
100
+ expect(res!.error!.code).toBe(-32601);
101
+ });
102
+
103
+ it('JSON-RPC format: response has jsonrpc 2.0 and matching id', async () => {
104
+ const res = await server.handleMessage(makeRequest('tools/list', {}, 42));
105
+ expect(res!.jsonrpc).toBe('2.0');
106
+ expect(res!.id).toBe(42);
107
+ });
108
+
109
+ it('notification (no id) returns null', async () => {
110
+ const res = await server.handleMessage({
111
+ jsonrpc: '2.0', method: 'notifications/initialized', params: {},
112
+ } as any);
113
+ expect(res).toBeNull();
114
+ });
115
+
116
+ it('prompts/list returns prompts', async () => {
117
+ server.addPrompt({
118
+ name: 'summarize', description: 'Summarize text',
119
+ arguments: [{ name: 'text', required: true }],
120
+ });
121
+ const res = await server.handleMessage(makeRequest('prompts/list'));
122
+ expect(res!.result.prompts).toHaveLength(1);
123
+ expect(res!.result.prompts[0].name).toBe('summarize');
124
+ });
125
+
126
+ it('addTool/removeTool works dynamically', () => {
127
+ expect(server.getToolCount()).toBe(1);
128
+ server.addTool({ name: 'test2', description: 't', inputSchema: {}, handler: async () => 'ok' });
129
+ expect(server.getToolCount()).toBe(2);
130
+ server.removeTool('test2');
131
+ expect(server.getToolCount()).toBe(1);
132
+ });
133
+ });
134
+
135
+ describe('agentToMCPTools', () => {
136
+ it('generates correct tools from agent', () => {
137
+ const agent = { name: 'test-agent' };
138
+ const tools = agentToMCPTools(agent);
139
+ expect(tools.length).toBeGreaterThanOrEqual(3);
140
+ const names = tools.map(t => t.name);
141
+ expect(names).toContain('chat');
142
+ expect(names).toContain('memory_search');
143
+ expect(names).toContain('memory_store');
144
+ });
145
+
146
+ it('chat tool has correct schema', () => {
147
+ const tools = agentToMCPTools({ name: 'bot' });
148
+ const chat = tools.find(t => t.name === 'chat')!;
149
+ expect(chat.inputSchema.required).toContain('message');
150
+ expect(chat.description).toContain('bot');
151
+ });
152
+
153
+ it('exposes agent skills as tools', () => {
154
+ const agent = {
155
+ name: 'skilled',
156
+ skills: [{ name: 'translate', description: 'Translate text', execute: async () => 'done' }],
157
+ };
158
+ const tools = agentToMCPTools(agent);
159
+ const skillTool = tools.find(t => t.name === 'skill_translate');
160
+ expect(skillTool).toBeDefined();
161
+ expect(skillTool!.description).toContain('Translate');
162
+ });
163
+ });
164
+
165
+ describe('agentToMCPResources', () => {
166
+ it('returns empty array for non-existent dir', () => {
167
+ const resources = agentToMCPResources({}, '/tmp/nonexistent-agent-dir-xyz');
168
+ expect(resources).toEqual([]);
169
+ });
170
+
171
+ it('exposes agent files that exist', () => {
172
+ // Use current project dir which has files
173
+ const path = require('path');
174
+ const resources = agentToMCPResources({}, path.resolve(__dirname, '..'));
175
+ // At minimum no crash; might find some files
176
+ expect(Array.isArray(resources)).toBe(true);
177
+ });
178
+ });
@@ -0,0 +1,230 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { PluginManager } from '../src/plugins';
3
+ import type { Plugin } from '../src/plugins';
4
+ import { loggerPlugin } from '../src/plugins/logger';
5
+ import { rateLimiterPlugin, createRateLimiterPlugin } from '../src/plugins/rate-limiter';
6
+ import { contentFilterPlugin, createContentFilterPlugin } from '../src/plugins/content-filter';
7
+ import { AgentCardRegistry } from '../src/core/a2a';
8
+ import type { AgentCard } from '../src/core/a2a';
9
+
10
+ // ── PluginManager Enhanced Tests ────────────────────────────
11
+
12
+ describe('PluginManager (enhanced middleware)', () => {
13
+ let pm: PluginManager;
14
+
15
+ beforeEach(() => {
16
+ pm = new PluginManager();
17
+ });
18
+
19
+ it('should register and list enhanced plugins', () => {
20
+ const p: Plugin = { name: 'test', version: '1.0.0' };
21
+ pm.registerEnhanced(p);
22
+ expect(pm.has('test')).toBe(true);
23
+ expect(pm.listEnhanced()).toHaveLength(1);
24
+ });
25
+
26
+ it('should unregister enhanced plugins', () => {
27
+ pm.registerEnhanced({ name: 'tmp', version: '1.0.0' });
28
+ pm.unregisterEnhanced('tmp');
29
+ expect(pm.getEnhanced('tmp')).toBeUndefined();
30
+ });
31
+
32
+ it('should get enhanced plugin by name', () => {
33
+ pm.registerEnhanced({ name: 'foo', version: '2.0.0' });
34
+ expect(pm.getEnhanced('foo')?.version).toBe('2.0.0');
35
+ });
36
+
37
+ it('should run message middleware chain in order', async () => {
38
+ const order: number[] = [];
39
+ pm.registerEnhanced({
40
+ name: 'p1', version: '1.0.0',
41
+ onMessage: async (msg, next) => { order.push(1); return next({ ...msg, p1: true }); },
42
+ });
43
+ pm.registerEnhanced({
44
+ name: 'p2', version: '1.0.0',
45
+ onMessage: async (msg, next) => { order.push(2); return next({ ...msg, p2: true }); },
46
+ });
47
+
48
+ const result = await pm.runMessageMiddleware({ content: 'hi' });
49
+ expect(order).toEqual([1, 2]);
50
+ expect(result.p1).toBe(true);
51
+ expect(result.p2).toBe(true);
52
+ });
53
+
54
+ it('should run response middleware chain', async () => {
55
+ pm.registerEnhanced({
56
+ name: 'r1', version: '1.0.0',
57
+ onResponse: async (res, next) => next({ ...res, tagged: true }),
58
+ });
59
+
60
+ const result = await pm.runResponseMiddleware({ content: 'reply' });
61
+ expect(result.tagged).toBe(true);
62
+ });
63
+
64
+ it('should pass through when no middleware registered', async () => {
65
+ const msg = { content: 'hello' };
66
+ const result = await pm.runMessageMiddleware(msg);
67
+ expect(result).toEqual(msg);
68
+ });
69
+
70
+ it('middleware can short-circuit by not calling next', async () => {
71
+ pm.registerEnhanced({
72
+ name: 'blocker', version: '1.0.0',
73
+ onMessage: async (_msg, _next) => ({ content: 'blocked' }),
74
+ });
75
+ pm.registerEnhanced({
76
+ name: 'never', version: '1.0.0',
77
+ onMessage: async (msg, next) => { throw new Error('should not reach'); },
78
+ });
79
+
80
+ const result = await pm.runMessageMiddleware({ content: 'test' });
81
+ expect(result.content).toBe('blocked');
82
+ });
83
+
84
+ it('initAll calls onInit for enhanced plugins', async () => {
85
+ const inited: string[] = [];
86
+ pm.registerEnhanced({
87
+ name: 'init-test', version: '1.0.0',
88
+ onInit: async () => { inited.push('init-test'); },
89
+ });
90
+ await pm.initAll({});
91
+ expect(inited).toContain('init-test');
92
+ });
93
+
94
+ it('shutdownAll calls onShutdown for enhanced plugins', async () => {
95
+ const shutdown: string[] = [];
96
+ pm.registerEnhanced({
97
+ name: 'sd-test', version: '1.0.0',
98
+ onShutdown: async () => { shutdown.push('sd-test'); },
99
+ });
100
+ await pm.shutdownAll();
101
+ expect(shutdown).toContain('sd-test');
102
+ });
103
+
104
+ it('list() includes both legacy and enhanced plugins', () => {
105
+ pm.register({ name: 'legacy', version: '1.0.0' });
106
+ pm.registerEnhanced({ name: 'enhanced', version: '1.0.0' });
107
+ expect(pm.list()).toHaveLength(2);
108
+ });
109
+ });
110
+
111
+ // ── Built-in Plugin Tests ───────────────────────────────────
112
+
113
+ describe('loggerPlugin', () => {
114
+ it('should log and pass through messages', async () => {
115
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
116
+ const msg = { content: 'hello world' };
117
+ const result = await loggerPlugin.onMessage!(msg, async (m) => m);
118
+ expect(spy).toHaveBeenCalled();
119
+ expect(result.content).toBe('hello world');
120
+ spy.mockRestore();
121
+ });
122
+
123
+ it('should log and pass through responses', async () => {
124
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
125
+ const res = { content: 'response text' };
126
+ const result = await loggerPlugin.onResponse!(res, async (r) => r);
127
+ expect(spy).toHaveBeenCalled();
128
+ expect(result.content).toBe('response text');
129
+ spy.mockRestore();
130
+ });
131
+ });
132
+
133
+ describe('rateLimiterPlugin', () => {
134
+ it('should allow messages under limit', async () => {
135
+ const plugin = createRateLimiterPlugin(5);
136
+ const msg = { content: 'test', id: 'session-1' };
137
+ for (let i = 0; i < 5; i++) {
138
+ await plugin.onMessage!(msg, async (m) => m);
139
+ }
140
+ });
141
+
142
+ it('should reject messages over limit', async () => {
143
+ const plugin = createRateLimiterPlugin(2);
144
+ const msg = { content: 'test', id: 'session-rl' };
145
+ await plugin.onMessage!(msg, async (m) => m);
146
+ await plugin.onMessage!(msg, async (m) => m);
147
+ await expect(plugin.onMessage!(msg, async (m) => m)).rejects.toThrow('Rate limit exceeded');
148
+ });
149
+ });
150
+
151
+ describe('contentFilterPlugin', () => {
152
+ it('should pass clean messages', async () => {
153
+ const plugin = createContentFilterPlugin(['spam', 'abuse']);
154
+ const result = await plugin.onMessage!({ content: 'hello' }, async (m) => m);
155
+ expect(result.content).toBe('hello');
156
+ });
157
+
158
+ it('should block messages with blocked words', async () => {
159
+ const plugin = createContentFilterPlugin(['spam']);
160
+ await expect(
161
+ plugin.onMessage!({ content: 'this is spam' }, async (m) => m)
162
+ ).rejects.toThrow('Content blocked');
163
+ });
164
+ });
165
+
166
+ // ── AgentCardRegistry Tests ─────────────────────────────────
167
+
168
+ describe('AgentCardRegistry', () => {
169
+ let registry: AgentCardRegistry;
170
+
171
+ beforeEach(() => {
172
+ registry = new AgentCardRegistry();
173
+ });
174
+
175
+ it('should register and list agent cards', () => {
176
+ registry.register({ name: 'bot', description: 'A bot', capabilities: ['chat'] });
177
+ expect(registry.list()).toHaveLength(1);
178
+ });
179
+
180
+ it('should unregister agent cards', () => {
181
+ registry.register({ name: 'tmp', description: 'Temp', capabilities: [] });
182
+ registry.unregister('tmp');
183
+ expect(registry.list()).toHaveLength(0);
184
+ });
185
+
186
+ it('should get agent by name', () => {
187
+ registry.register({ name: 'a1', description: 'Agent 1', capabilities: [] });
188
+ expect(registry.get('a1')?.name).toBe('a1');
189
+ expect(registry.get('missing')).toBeUndefined();
190
+ });
191
+
192
+ it('should find by name', () => {
193
+ registry.register({ name: 'search-bot', description: 'Searches', capabilities: ['search'] });
194
+ registry.register({ name: 'chat-bot', description: 'Chats', capabilities: ['chat'] });
195
+ expect(registry.find('search')).toHaveLength(1);
196
+ expect(registry.find('search')[0].name).toBe('search-bot');
197
+ });
198
+
199
+ it('should find by capability', () => {
200
+ registry.register({ name: 'translator', description: 'Translates', capabilities: ['translate', 'detect-lang'] });
201
+ const found = registry.find('translate');
202
+ expect(found).toHaveLength(1);
203
+ expect(found[0].name).toBe('translator');
204
+ });
205
+
206
+ it('should find by description', () => {
207
+ registry.register({ name: 'x', description: 'Summarizes documents', capabilities: [] });
208
+ expect(registry.find('summarize')).toHaveLength(1);
209
+ });
210
+
211
+ it('should send to local handler', async () => {
212
+ registry.register({
213
+ name: 'echo',
214
+ description: 'Echo agent',
215
+ capabilities: ['echo'],
216
+ handler: async (msg) => `echo: ${msg}`,
217
+ });
218
+ const result = await registry.send('echo', 'hello');
219
+ expect(result).toBe('echo: hello');
220
+ });
221
+
222
+ it('should throw when sending to unknown agent', async () => {
223
+ await expect(registry.send('ghost', 'hi')).rejects.toThrow("Agent 'ghost' not found");
224
+ });
225
+
226
+ it('should throw when agent has no handler or endpoint', async () => {
227
+ registry.register({ name: 'empty', description: 'No handler', capabilities: [] });
228
+ await expect(registry.send('empty', 'hi')).rejects.toThrow('has no handler or endpoint');
229
+ });
230
+ });
@@ -0,0 +1,231 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import * as crypto from 'crypto';
6
+ import { AgentPackager, AgentPublisher, AgentInstaller } from '../src/publish';
7
+
8
+ function makeTempDir(): string {
9
+ const dir = path.join(os.tmpdir(), `opc-test-${crypto.randomBytes(6).toString('hex')}`);
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ return dir;
12
+ }
13
+
14
+ function writeAgentYaml(dir: string, overrides: Record<string, any> = {}) {
15
+ const config = {
16
+ apiVersion: 'opc/v1',
17
+ kind: 'Agent',
18
+ metadata: { name: 'test-agent', version: '1.0.0', description: 'Test agent', author: 'Test', license: 'MIT', ...overrides.metadata },
19
+ spec: { model: 'gpt-4', provider: { default: 'openai' }, channels: [{ type: 'web' }], skills: [{ name: 'echo' }], tools: [], ...overrides.spec },
20
+ };
21
+ const yaml = require('js-yaml');
22
+ fs.writeFileSync(path.join(dir, 'agent.yaml'), yaml.dump(config));
23
+ }
24
+
25
+ function writePackageJson(dir: string, overrides: Record<string, any> = {}) {
26
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'test-agent', version: '1.0.0', ...overrides }, null, 2));
27
+ }
28
+
29
+ function setupValidProject(dir: string) {
30
+ writeAgentYaml(dir);
31
+ writePackageJson(dir);
32
+ fs.writeFileSync(path.join(dir, 'SOUL.md'), '# Soul');
33
+ fs.writeFileSync(path.join(dir, 'README.md'), '# Readme');
34
+ fs.writeFileSync(path.join(dir, 'src.ts'), 'console.log("hello")');
35
+ }
36
+
37
+ describe('AgentPackager', () => {
38
+ let tmpDir: string;
39
+ const packager = new AgentPackager();
40
+
41
+ beforeEach(() => { tmpDir = makeTempDir(); });
42
+ afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
43
+
44
+ // 1. validate: missing agent.yaml → error
45
+ it('validate: missing agent.yaml produces error', async () => {
46
+ writePackageJson(tmpDir);
47
+ const result = await packager.validate(tmpDir);
48
+ expect(result.valid).toBe(false);
49
+ expect(result.errors).toContain('Missing agent.yaml');
50
+ });
51
+
52
+ // 2. validate: missing package.json → error
53
+ it('validate: missing package.json produces error', async () => {
54
+ writeAgentYaml(tmpDir);
55
+ const result = await packager.validate(tmpDir);
56
+ expect(result.valid).toBe(false);
57
+ expect(result.errors).toContain('Missing package.json');
58
+ });
59
+
60
+ // 3. validate: valid project → no errors
61
+ it('validate: valid project has no errors', async () => {
62
+ setupValidProject(tmpDir);
63
+ const result = await packager.validate(tmpDir);
64
+ expect(result.valid).toBe(true);
65
+ expect(result.errors).toHaveLength(0);
66
+ });
67
+
68
+ // 4. validate: missing SOUL.md → warning
69
+ it('validate: missing SOUL.md produces warning', async () => {
70
+ writeAgentYaml(tmpDir);
71
+ writePackageJson(tmpDir);
72
+ const result = await packager.validate(tmpDir);
73
+ expect(result.warnings).toContain('Missing SOUL.md (recommended)');
74
+ });
75
+
76
+ // 5. validate: missing README.md → warning
77
+ it('validate: missing README.md produces warning', async () => {
78
+ writeAgentYaml(tmpDir);
79
+ writePackageJson(tmpDir);
80
+ const result = await packager.validate(tmpDir);
81
+ expect(result.warnings).toContain('Missing README.md');
82
+ });
83
+
84
+ // 6. validate: uppercase name → error
85
+ it('validate: uppercase name in agent.yaml produces error', async () => {
86
+ writeAgentYaml(tmpDir, { metadata: { name: 'MyAgent', version: '1.0.0' } });
87
+ writePackageJson(tmpDir);
88
+ const result = await packager.validate(tmpDir);
89
+ expect(result.valid).toBe(false);
90
+ expect(result.errors.some(e => e.includes('lowercase'))).toBe(true);
91
+ });
92
+
93
+ // 7. validate: invalid version → error
94
+ it('validate: invalid version format produces error', async () => {
95
+ writeAgentYaml(tmpDir, { metadata: { name: 'test', version: 'bad' } });
96
+ writePackageJson(tmpDir);
97
+ const result = await packager.validate(tmpDir);
98
+ expect(result.valid).toBe(false);
99
+ expect(result.errors.some(e => e.includes('version'))).toBe(true);
100
+ });
101
+
102
+ // 8. listFiles: excludes node_modules
103
+ it('listFiles: excludes node_modules', async () => {
104
+ setupValidProject(tmpDir);
105
+ fs.mkdirSync(path.join(tmpDir, 'node_modules', 'foo'), { recursive: true });
106
+ fs.writeFileSync(path.join(tmpDir, 'node_modules', 'foo', 'index.js'), '');
107
+ const files = await packager.listFiles(tmpDir);
108
+ expect(files.every(f => !f.includes('node_modules'))).toBe(true);
109
+ });
110
+
111
+ // 9. listFiles: excludes .env
112
+ it('listFiles: excludes .env', async () => {
113
+ setupValidProject(tmpDir);
114
+ fs.writeFileSync(path.join(tmpDir, '.env'), 'SECRET=x');
115
+ const files = await packager.listFiles(tmpDir);
116
+ expect(files).not.toContain('.env');
117
+ });
118
+
119
+ // 10. listFiles: excludes .git
120
+ it('listFiles: excludes .git directory', async () => {
121
+ setupValidProject(tmpDir);
122
+ fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true });
123
+ fs.writeFileSync(path.join(tmpDir, '.git', 'config'), '');
124
+ const files = await packager.listFiles(tmpDir);
125
+ expect(files.every(f => !f.includes('.git'))).toBe(true);
126
+ });
127
+
128
+ // 11. listFiles: respects .opcignore
129
+ it('listFiles: respects .opcignore', async () => {
130
+ setupValidProject(tmpDir);
131
+ fs.writeFileSync(path.join(tmpDir, 'secret.txt'), 'hidden');
132
+ fs.writeFileSync(path.join(tmpDir, '.opcignore'), 'secret.txt\n');
133
+ const files = await packager.listFiles(tmpDir);
134
+ expect(files).not.toContain('secret.txt');
135
+ });
136
+
137
+ // 12. pack: creates .opc.tgz file
138
+ it('pack: creates .opc.tgz file', async () => {
139
+ setupValidProject(tmpDir);
140
+ const result = await packager.pack(tmpDir);
141
+ expect(fs.existsSync(result.path)).toBe(true);
142
+ expect(result.path).toMatch(/\.opc\.tgz$/);
143
+ // cleanup
144
+ fs.unlinkSync(result.path);
145
+ });
146
+
147
+ // 13. pack: manifest contains correct fields
148
+ it('pack: manifest contains correct fields', async () => {
149
+ setupValidProject(tmpDir);
150
+ const result = await packager.pack(tmpDir);
151
+ expect(result.manifest.name).toBe('test-agent');
152
+ expect(result.manifest.version).toBe('1.0.0');
153
+ expect(result.manifest.author).toBe('Test');
154
+ expect(result.manifest.license).toBe('MIT');
155
+ expect(result.manifest.agent.model).toBe('gpt-4');
156
+ expect(result.manifest.agent.provider).toBe('openai');
157
+ expect(result.manifest.agent.channels).toContain('web');
158
+ expect(result.manifest.files.length).toBeGreaterThan(0);
159
+ expect(result.manifest.checksum).toBeTruthy();
160
+ expect(result.manifest.createdAt).toBeTruthy();
161
+ fs.unlinkSync(result.path);
162
+ });
163
+
164
+ // 14. manifest checksum is valid sha256
165
+ it('pack: checksum is valid sha256', async () => {
166
+ setupValidProject(tmpDir);
167
+ const result = await packager.pack(tmpDir);
168
+ expect(result.manifest.checksum).toMatch(/^[a-f0-9]{64}$/);
169
+ // Verify checksum matches file
170
+ const fileHash = crypto.createHash('sha256').update(fs.readFileSync(result.path)).digest('hex');
171
+ expect(result.manifest.checksum).toBe(fileHash);
172
+ fs.unlinkSync(result.path);
173
+ });
174
+
175
+ // 15. pack: validation failure throws
176
+ it('pack: throws on invalid project', async () => {
177
+ // Empty dir — no agent.yaml
178
+ await expect(packager.pack(tmpDir)).rejects.toThrow('Validation failed');
179
+ });
180
+ });
181
+
182
+ describe('AgentPublisher', () => {
183
+ // 16. dry-run doesn't throw
184
+ it('dry-run returns success without error', async () => {
185
+ const publisher = new AgentPublisher();
186
+ const manifest = {
187
+ name: 'test', version: '1.0.0', description: '', author: '', license: 'MIT',
188
+ agent: { model: '', provider: '', channels: [], skills: [], tools: [] },
189
+ files: ['a.ts'], checksum: 'abc', createdAt: new Date().toISOString(),
190
+ };
191
+ const result = await publisher.publish('/fake/path.opc.tgz', manifest, { dryRun: true });
192
+ expect(result.success).toBe(true);
193
+ });
194
+ });
195
+
196
+ describe('AgentInstaller', () => {
197
+ let tmpDir: string;
198
+ let installDir: string;
199
+
200
+ beforeEach(() => {
201
+ tmpDir = makeTempDir();
202
+ installDir = makeTempDir();
203
+ });
204
+ afterEach(() => {
205
+ fs.rmSync(tmpDir, { recursive: true, force: true });
206
+ fs.rmSync(installDir, { recursive: true, force: true });
207
+ });
208
+
209
+ // 17. install from tarball extracts correctly
210
+ it('install from .opc.tgz extracts files', async () => {
211
+ // Create a valid project and pack it
212
+ setupValidProject(tmpDir);
213
+ const packager = new AgentPackager();
214
+ const { path: pkgPath } = await packager.pack(tmpDir);
215
+
216
+ const installer = new AgentInstaller();
217
+ await installer.install(pkgPath, installDir);
218
+
219
+ // Check that files were extracted
220
+ const extractedFiles = fs.readdirSync(installDir);
221
+ expect(extractedFiles.length).toBeGreaterThan(0);
222
+
223
+ fs.unlinkSync(pkgPath);
224
+ });
225
+
226
+ // 18. install from missing file throws
227
+ it('install from missing file throws', async () => {
228
+ const installer = new AgentInstaller();
229
+ await expect(installer.install('/nonexistent/pkg.opc.tgz', installDir)).rejects.toThrow();
230
+ });
231
+ });