opc-agent 2.0.0 → 2.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.
Files changed (157) hide show
  1. package/README.md +545 -365
  2. package/dist/channels/email.d.ts +32 -26
  3. package/dist/channels/email.js +239 -62
  4. package/dist/channels/feishu.d.ts +21 -6
  5. package/dist/channels/feishu.js +225 -126
  6. package/dist/channels/websocket.d.ts +46 -3
  7. package/dist/channels/websocket.js +306 -37
  8. package/dist/channels/wechat.d.ts +33 -13
  9. package/dist/channels/wechat.js +229 -42
  10. package/dist/cli.js +712 -11
  11. package/dist/core/a2a.d.ts +17 -0
  12. package/dist/core/a2a.js +43 -1
  13. package/dist/core/agent.d.ts +16 -0
  14. package/dist/core/agent.js +108 -0
  15. package/dist/core/runtime.d.ts +6 -0
  16. package/dist/core/runtime.js +161 -2
  17. package/dist/core/sandbox.d.ts +26 -0
  18. package/dist/core/sandbox.js +117 -0
  19. package/dist/core/workflow-graph.d.ts +93 -0
  20. package/dist/core/workflow-graph.js +247 -0
  21. package/dist/doctor.d.ts +15 -0
  22. package/dist/doctor.js +183 -0
  23. package/dist/eval/index.d.ts +65 -0
  24. package/dist/eval/index.js +191 -0
  25. package/dist/index.d.ts +32 -6
  26. package/dist/index.js +63 -4
  27. package/dist/plugins/content-filter.d.ts +7 -0
  28. package/dist/plugins/content-filter.js +25 -0
  29. package/dist/plugins/index.d.ts +42 -0
  30. package/dist/plugins/index.js +108 -2
  31. package/dist/plugins/logger.d.ts +6 -0
  32. package/dist/plugins/logger.js +20 -0
  33. package/dist/plugins/rate-limiter.d.ts +7 -0
  34. package/dist/plugins/rate-limiter.js +35 -0
  35. package/dist/protocols/a2a/client.d.ts +25 -0
  36. package/dist/protocols/a2a/client.js +115 -0
  37. package/dist/protocols/a2a/index.d.ts +6 -0
  38. package/dist/protocols/a2a/index.js +12 -0
  39. package/dist/protocols/a2a/server.d.ts +41 -0
  40. package/dist/protocols/a2a/server.js +295 -0
  41. package/dist/protocols/a2a/types.d.ts +91 -0
  42. package/dist/protocols/a2a/types.js +15 -0
  43. package/dist/protocols/a2a/utils.d.ts +6 -0
  44. package/dist/protocols/a2a/utils.js +47 -0
  45. package/dist/protocols/agui/client.d.ts +10 -0
  46. package/dist/protocols/agui/client.js +75 -0
  47. package/dist/protocols/agui/index.d.ts +4 -0
  48. package/dist/protocols/agui/index.js +25 -0
  49. package/dist/protocols/agui/server.d.ts +37 -0
  50. package/dist/protocols/agui/server.js +191 -0
  51. package/dist/protocols/agui/types.d.ts +107 -0
  52. package/dist/protocols/agui/types.js +17 -0
  53. package/dist/protocols/index.d.ts +2 -0
  54. package/dist/protocols/index.js +19 -0
  55. package/dist/protocols/mcp/agent-tools.d.ts +11 -0
  56. package/dist/protocols/mcp/agent-tools.js +129 -0
  57. package/dist/protocols/mcp/index.d.ts +5 -0
  58. package/dist/protocols/mcp/index.js +11 -0
  59. package/dist/protocols/mcp/server.d.ts +31 -0
  60. package/dist/protocols/mcp/server.js +248 -0
  61. package/dist/protocols/mcp/types.d.ts +92 -0
  62. package/dist/protocols/mcp/types.js +17 -0
  63. package/dist/publish/index.d.ts +45 -0
  64. package/dist/publish/index.js +350 -0
  65. package/dist/schema/oad.d.ts +682 -65
  66. package/dist/schema/oad.js +36 -3
  67. package/dist/security/approval.d.ts +36 -0
  68. package/dist/security/approval.js +113 -0
  69. package/dist/security/index.d.ts +4 -0
  70. package/dist/security/index.js +8 -0
  71. package/dist/security/keys.d.ts +16 -0
  72. package/dist/security/keys.js +117 -0
  73. package/dist/studio/server.d.ts +63 -0
  74. package/dist/studio/server.js +625 -0
  75. package/dist/studio-ui/index.html +662 -0
  76. package/dist/telemetry/index.d.ts +93 -0
  77. package/dist/telemetry/index.js +285 -0
  78. package/package.json +5 -3
  79. package/scripts/install.ps1 +31 -0
  80. package/scripts/install.sh +40 -0
  81. package/src/channels/email.ts +351 -177
  82. package/src/channels/feishu.ts +349 -236
  83. package/src/channels/websocket.ts +399 -87
  84. package/src/channels/wechat.ts +329 -149
  85. package/src/cli.ts +783 -12
  86. package/src/core/a2a.ts +60 -0
  87. package/src/core/agent.ts +125 -0
  88. package/src/core/runtime.ts +127 -0
  89. package/src/core/sandbox.ts +143 -0
  90. package/src/core/workflow-graph.ts +365 -0
  91. package/src/doctor.ts +156 -0
  92. package/src/eval/index.ts +211 -0
  93. package/src/eval/suites/basic.json +16 -0
  94. package/src/eval/suites/memory.json +12 -0
  95. package/src/eval/suites/safety.json +14 -0
  96. package/src/index.ts +58 -6
  97. package/src/plugins/content-filter.ts +23 -0
  98. package/src/plugins/index.ts +133 -2
  99. package/src/plugins/logger.ts +18 -0
  100. package/src/plugins/rate-limiter.ts +38 -0
  101. package/src/protocols/a2a/client.ts +132 -0
  102. package/src/protocols/a2a/index.ts +8 -0
  103. package/src/protocols/a2a/server.ts +333 -0
  104. package/src/protocols/a2a/types.ts +88 -0
  105. package/src/protocols/a2a/utils.ts +50 -0
  106. package/src/protocols/agui/client.ts +83 -0
  107. package/src/protocols/agui/index.ts +4 -0
  108. package/src/protocols/agui/server.ts +218 -0
  109. package/src/protocols/agui/types.ts +153 -0
  110. package/src/protocols/index.ts +2 -0
  111. package/src/protocols/mcp/agent-tools.ts +134 -0
  112. package/src/protocols/mcp/index.ts +8 -0
  113. package/src/protocols/mcp/server.ts +262 -0
  114. package/src/protocols/mcp/types.ts +69 -0
  115. package/src/publish/index.ts +376 -0
  116. package/src/schema/oad.ts +39 -2
  117. package/src/security/approval.ts +131 -0
  118. package/src/security/index.ts +3 -0
  119. package/src/security/keys.ts +87 -0
  120. package/src/studio/server.ts +629 -0
  121. package/src/studio-ui/index.html +662 -0
  122. package/src/telemetry/index.ts +324 -0
  123. package/src/types/agent-workstation.d.ts +2 -0
  124. package/tests/a2a-protocol.test.ts +285 -0
  125. package/tests/agui-protocol.test.ts +246 -0
  126. package/tests/channels/discord.test.ts +79 -0
  127. package/tests/channels/email.test.ts +148 -0
  128. package/tests/channels/feishu.test.ts +123 -0
  129. package/tests/channels/telegram.test.ts +129 -0
  130. package/tests/channels/websocket.test.ts +53 -0
  131. package/tests/channels/wechat.test.ts +170 -0
  132. package/tests/chat-cli.test.ts +160 -0
  133. package/tests/daemon.test.ts +135 -0
  134. package/tests/deepbrain-wire.test.ts +234 -0
  135. package/tests/doctor.test.ts +38 -0
  136. package/tests/eval.test.ts +173 -0
  137. package/tests/init-role.test.ts +124 -0
  138. package/tests/mcp-client.test.ts +92 -0
  139. package/tests/mcp-server.test.ts +178 -0
  140. package/tests/plugin-a2a-enhanced.test.ts +230 -0
  141. package/tests/publish.test.ts +231 -0
  142. package/tests/scheduler.test.ts +200 -0
  143. package/tests/security-enhanced.test.ts +233 -0
  144. package/tests/skill-learner.test.ts +161 -0
  145. package/tests/studio.test.ts +229 -0
  146. package/tests/subagent.test.ts +63 -0
  147. package/tests/telemetry.test.ts +186 -0
  148. package/tests/tools/builtin-extended.test.ts +138 -0
  149. package/tests/workflow-graph.test.ts +279 -0
  150. package/tutorial/customer-service-agent/README.md +612 -0
  151. package/tutorial/customer-service-agent/SOUL.md +26 -0
  152. package/tutorial/customer-service-agent/agent.yaml +63 -0
  153. package/tutorial/customer-service-agent/package.json +19 -0
  154. package/tutorial/customer-service-agent/src/index.ts +69 -0
  155. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
  156. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
  157. package/tutorial/customer-service-agent/tsconfig.json +14 -0
@@ -0,0 +1,234 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { BaseAgent } from '../src/core/agent';
3
+ import type { Message } from '../src/core/types';
4
+
5
+ // Mock LLM provider
6
+ const mockProvider = {
7
+ chat: vi.fn().mockResolvedValue('Hello from LLM'),
8
+ chatStream: vi.fn(),
9
+ listModels: vi.fn(),
10
+ };
11
+
12
+ vi.mock('../src/providers', () => ({
13
+ createProvider: () => mockProvider,
14
+ }));
15
+
16
+ function createAgent(opts?: any) {
17
+ return new BaseAgent({
18
+ name: 'test-agent',
19
+ systemPrompt: 'You are a test agent.',
20
+ ...opts,
21
+ });
22
+ }
23
+
24
+ function createMessage(content: string): Message {
25
+ return {
26
+ id: `msg_${Date.now()}`,
27
+ role: 'user',
28
+ content,
29
+ timestamp: Date.now(),
30
+ };
31
+ }
32
+
33
+ describe('BaseAgent long-term memory', () => {
34
+ let agent: BaseAgent;
35
+ let mockBrain: any;
36
+
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ agent = createAgent();
40
+ mockBrain = {
41
+ recall: vi.fn().mockResolvedValue([{ content: 'remembered fact' }]),
42
+ learn: vi.fn().mockResolvedValue(undefined),
43
+ };
44
+ });
45
+
46
+ it('setLongTermMemory sets the brain', () => {
47
+ agent.setLongTermMemory(mockBrain);
48
+ expect(agent.getLongTermMemory()).toBe(mockBrain);
49
+ });
50
+
51
+ it('setLongTermMemory with config sets autoLearn/autoRecall', () => {
52
+ agent.setLongTermMemory(mockBrain, { autoLearn: false, autoRecall: true });
53
+ const config = agent.getLongTermMemoryConfig();
54
+ expect(config.autoLearn).toBe(false);
55
+ expect(config.autoRecall).toBe(true);
56
+ });
57
+
58
+ it('handleMessage calls recall before LLM when autoRecall is true', async () => {
59
+ agent.setLongTermMemory(mockBrain);
60
+ const callOrder: string[] = [];
61
+ mockBrain.recall.mockImplementation(async () => {
62
+ callOrder.push('recall');
63
+ return [{ content: 'memory' }];
64
+ });
65
+ mockProvider.chat.mockImplementation(async () => {
66
+ callOrder.push('llm');
67
+ return 'response';
68
+ });
69
+
70
+ await agent.init();
71
+ await agent.handleMessage(createMessage('hello'));
72
+
73
+ expect(callOrder[0]).toBe('recall');
74
+ expect(callOrder[1]).toBe('llm');
75
+ expect(mockBrain.recall).toHaveBeenCalledWith('hello');
76
+ });
77
+
78
+ it('handleMessage calls learn after LLM when autoLearn is true', async () => {
79
+ agent.setLongTermMemory(mockBrain);
80
+ const callOrder: string[] = [];
81
+ mockProvider.chat.mockImplementation(async () => {
82
+ callOrder.push('llm');
83
+ return 'response';
84
+ });
85
+ mockBrain.learn.mockImplementation(async () => {
86
+ callOrder.push('learn');
87
+ });
88
+
89
+ await agent.init();
90
+ await agent.handleMessage(createMessage('hello'));
91
+
92
+ expect(callOrder.indexOf('llm')).toBeLessThan(callOrder.indexOf('learn'));
93
+ expect(mockBrain.learn).toHaveBeenCalled();
94
+ });
95
+
96
+ it('recall failure does not crash handleMessage', async () => {
97
+ mockBrain.recall.mockRejectedValue(new Error('DB down'));
98
+ agent.setLongTermMemory(mockBrain);
99
+
100
+ await agent.init();
101
+ const response = await agent.handleMessage(createMessage('hello'));
102
+ expect(response.content).toBeDefined();
103
+ });
104
+
105
+ it('learn failure does not crash handleMessage', async () => {
106
+ mockBrain.learn.mockRejectedValue(new Error('DB down'));
107
+ agent.setLongTermMemory(mockBrain);
108
+
109
+ await agent.init();
110
+ const response = await agent.handleMessage(createMessage('hello'));
111
+ expect(response.content).toBeDefined();
112
+ });
113
+
114
+ it('does not recall when autoRecall is false', async () => {
115
+ agent.setLongTermMemory(mockBrain, { autoRecall: false, autoLearn: true });
116
+
117
+ await agent.init();
118
+ await agent.handleMessage(createMessage('hello'));
119
+
120
+ expect(mockBrain.recall).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it('does not learn when autoLearn is false', async () => {
124
+ agent.setLongTermMemory(mockBrain, { autoRecall: true, autoLearn: false });
125
+
126
+ await agent.init();
127
+ await agent.handleMessage(createMessage('hello'));
128
+
129
+ expect(mockBrain.learn).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it('injects memory context into LLM prompt', async () => {
133
+ mockBrain.recall.mockResolvedValue([{ content: 'user likes cats' }]);
134
+ agent.setLongTermMemory(mockBrain);
135
+
136
+ await agent.init();
137
+ await agent.handleMessage(createMessage('hello'));
138
+
139
+ const systemPromptArg = mockProvider.chat.mock.calls[0][1];
140
+ expect(systemPromptArg).toContain('[Relevant memories]');
141
+ expect(systemPromptArg).toContain('user likes cats');
142
+ });
143
+
144
+ it('handles empty recall array gracefully', async () => {
145
+ mockBrain.recall.mockResolvedValue([]);
146
+ agent.setLongTermMemory(mockBrain);
147
+
148
+ await agent.init();
149
+ await agent.handleMessage(createMessage('hello'));
150
+
151
+ const systemPromptArg = mockProvider.chat.mock.calls[0][1];
152
+ expect(systemPromptArg).not.toContain('[Relevant memories]');
153
+ });
154
+
155
+ it('handles string recall results', async () => {
156
+ mockBrain.recall.mockResolvedValue('a single memory string');
157
+ agent.setLongTermMemory(mockBrain);
158
+
159
+ await agent.init();
160
+ await agent.handleMessage(createMessage('hello'));
161
+
162
+ const systemPromptArg = mockProvider.chat.mock.calls[0][1];
163
+ expect(systemPromptArg).toContain('a single memory string');
164
+ });
165
+
166
+ it('learn includes user and assistant content', async () => {
167
+ mockProvider.chat.mockResolvedValue('I am fine');
168
+ agent.setLongTermMemory(mockBrain);
169
+
170
+ await agent.init();
171
+ await agent.handleMessage(createMessage('how are you'));
172
+
173
+ const learnArg = mockBrain.learn.mock.calls[0][0];
174
+ expect(learnArg).toContain('User: how are you');
175
+ expect(learnArg).toContain('Assistant: I am fine');
176
+ });
177
+
178
+ it('no long-term memory means no recall/learn calls', async () => {
179
+ // No setLongTermMemory called
180
+ await agent.init();
181
+ await agent.handleMessage(createMessage('hello'));
182
+
183
+ // Should work fine without any brain
184
+ expect(mockBrain.recall).not.toHaveBeenCalled();
185
+ expect(mockBrain.learn).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it('getLongTermMemory returns undefined when not set', () => {
189
+ expect(agent.getLongTermMemory()).toBeUndefined();
190
+ });
191
+
192
+ it('default config has autoLearn and autoRecall true', () => {
193
+ const config = agent.getLongTermMemoryConfig();
194
+ expect(config.autoLearn).toBe(true);
195
+ expect(config.autoRecall).toBe(true);
196
+ });
197
+
198
+ it('recall with compiled_truth format', async () => {
199
+ mockBrain.recall.mockResolvedValue([{ compiled_truth: 'agent is helpful' }]);
200
+ agent.setLongTermMemory(mockBrain);
201
+
202
+ await agent.init();
203
+ await agent.handleMessage(createMessage('hello'));
204
+
205
+ const systemPromptArg = mockProvider.chat.mock.calls[0][1];
206
+ expect(systemPromptArg).toContain('agent is helpful');
207
+ });
208
+ });
209
+
210
+ describe('OAD LongTerm memory schema', () => {
211
+ it('accepts deepbrain config with new fields', async () => {
212
+ const { LongTermMemorySchema } = await import('../src/schema/oad');
213
+ const result = LongTermMemorySchema.parse({
214
+ provider: 'deepbrain',
215
+ collection: 'test',
216
+ config: {
217
+ database: './data/brain.db',
218
+ embeddingProvider: 'ollama',
219
+ autoLearn: true,
220
+ autoRecall: true,
221
+ evolveInterval: 3600000,
222
+ },
223
+ });
224
+ expect(result.provider).toBe('deepbrain');
225
+ expect(result.config?.database).toBe('./data/brain.db');
226
+ expect(result.config?.evolveInterval).toBe(3600000);
227
+ });
228
+
229
+ it('accepts minimal config', async () => {
230
+ const { LongTermMemorySchema } = await import('../src/schema/oad');
231
+ const result = LongTermMemorySchema.parse({ provider: 'deepbrain' });
232
+ expect(result.provider).toBe('deepbrain');
233
+ });
234
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getDoctorChecks, runDoctor } from '../src/doctor';
3
+
4
+ describe('opc doctor', () => {
5
+ it('runDoctor runs without error', async () => {
6
+ // Suppress console output
7
+ const orig = console.log;
8
+ console.log = () => {};
9
+ const result = await runDoctor();
10
+ console.log = orig;
11
+ expect(result).toHaveProperty('passed');
12
+ expect(result).toHaveProperty('total');
13
+ expect(result.total).toBeGreaterThan(0);
14
+ expect(result.passed).toBeGreaterThanOrEqual(0);
15
+ expect(result.passed).toBeLessThanOrEqual(result.total);
16
+ });
17
+
18
+ it('Node.js check passes (we are running in Node)', async () => {
19
+ const checks = getDoctorChecks();
20
+ const nodeCheck = checks.find(c => c.name === 'Node.js version');
21
+ expect(nodeCheck).toBeDefined();
22
+ const result = await nodeCheck!.check();
23
+ expect(result.ok).toBe(true);
24
+ expect(result.detail).toMatch(/^v\d+/);
25
+ });
26
+
27
+ it('check result format has ok and detail fields', async () => {
28
+ const checks = getDoctorChecks();
29
+ for (const check of checks) {
30
+ const result = await check.check();
31
+ expect(typeof result.ok).toBe('boolean');
32
+ expect(typeof result.detail).toBe('string');
33
+ if (result.fix !== undefined) {
34
+ expect(typeof result.fix).toBe('string');
35
+ }
36
+ }
37
+ });
38
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { AgentEvaluator } from '../src/eval';
3
+ import type { EvalCase, EvalSuite, EvalReport } from '../src/eval';
4
+ import * as path from 'path';
5
+ import * as fs from 'fs';
6
+ import * as os from 'os';
7
+
8
+ // Mock agent
9
+ const mockAgent = {
10
+ chat: async (input: string) => {
11
+ if (!input) return 'Hello! How can I help?';
12
+ if (input.includes('capital of France')) return 'The capital of France is Paris.';
13
+ if (input.includes('Hello')) return 'Hello there! How can I help you?';
14
+ return `Response to: ${input}`;
15
+ },
16
+ };
17
+
18
+ describe('AgentEvaluator', () => {
19
+ const evaluator = new AgentEvaluator(mockAgent);
20
+
21
+ it('should score exact match correctly', async () => {
22
+ const result = await evaluator.evalCase({
23
+ id: 'test-1',
24
+ input: 'What is the capital of France?',
25
+ expectedOutput: 'The capital of France is Paris.',
26
+ });
27
+ expect(result.scores.exact_match).toBe(1);
28
+ expect(result.passed).toBe(true);
29
+ });
30
+
31
+ it('should score exact match failure', async () => {
32
+ const result = await evaluator.evalCase({
33
+ id: 'test-2',
34
+ input: 'Hello!',
35
+ expectedOutput: 'Goodbye!',
36
+ });
37
+ expect(result.scores.exact_match).toBe(0);
38
+ expect(result.passed).toBe(false);
39
+ });
40
+
41
+ it('should score contains correctly', async () => {
42
+ const result = await evaluator.evalCase({
43
+ id: 'test-3',
44
+ input: 'What is the capital of France?',
45
+ expectedContains: ['Paris', 'capital'],
46
+ });
47
+ expect(result.scores.contains).toBe(1);
48
+ expect(result.passed).toBe(true);
49
+ });
50
+
51
+ it('should score partial contains', async () => {
52
+ const result = await evaluator.evalCase({
53
+ id: 'test-4',
54
+ input: 'What is the capital of France?',
55
+ expectedContains: ['Paris', 'London'],
56
+ });
57
+ expect(result.scores.contains).toBe(0.5);
58
+ expect(result.passed).toBe(true); // 0.5 >= 0.5 threshold
59
+ });
60
+
61
+ it('should score not_contains correctly', async () => {
62
+ const result = await evaluator.evalCase({
63
+ id: 'test-5',
64
+ input: 'Hello!',
65
+ expectedNotContains: ['error', 'crash'],
66
+ });
67
+ expect(result.scores.not_contains).toBe(1);
68
+ expect(result.passed).toBe(true);
69
+ });
70
+
71
+ it('should score not_contains failure', async () => {
72
+ const result = await evaluator.evalCase({
73
+ id: 'test-6',
74
+ input: 'Hello!',
75
+ expectedNotContains: ['Hello', 'crash'],
76
+ });
77
+ expect(result.scores.not_contains).toBe(0.5);
78
+ });
79
+
80
+ it('should load suite from JSON', () => {
81
+ const suitePath = path.join(__dirname, '..', 'src', 'eval', 'suites', 'basic.json');
82
+ const suite = AgentEvaluator.loadSuite(suitePath);
83
+ expect(suite.name).toBe('basic');
84
+ expect(suite.cases.length).toBe(10);
85
+ });
86
+
87
+ it('should load all built-in suites', () => {
88
+ const suites = AgentEvaluator.builtinSuites();
89
+ expect(suites.length).toBeGreaterThanOrEqual(3);
90
+ const names = suites.map(s => s.name);
91
+ expect(names).toContain('basic');
92
+ expect(names).toContain('safety');
93
+ expect(names).toContain('memory');
94
+ });
95
+
96
+ it('should have correct case counts for built-in suites', () => {
97
+ const suites = AgentEvaluator.builtinSuites();
98
+ const basic = suites.find(s => s.name === 'basic');
99
+ const safety = suites.find(s => s.name === 'safety');
100
+ const memory = suites.find(s => s.name === 'memory');
101
+ expect(basic?.caseCount).toBe(10);
102
+ expect(safety?.caseCount).toBe(8);
103
+ expect(memory?.caseCount).toBe(6);
104
+ });
105
+
106
+ it('should compare reports and detect regression', () => {
107
+ const baseline: EvalReport = {
108
+ suite: 'test', timestamp: '', totalCases: 2, passed: 2, failed: 0, passRate: 1, avgLatency: 10, p95Latency: 15, summary: '',
109
+ results: [
110
+ { caseId: 'a', input: '', output: '', scores: { latency_ms: 10 }, passed: true },
111
+ { caseId: 'b', input: '', output: '', scores: { latency_ms: 10 }, passed: true },
112
+ ],
113
+ };
114
+ const current: EvalReport = {
115
+ suite: 'test', timestamp: '', totalCases: 2, passed: 1, failed: 1, passRate: 0.5, avgLatency: 10, p95Latency: 15, summary: '',
116
+ results: [
117
+ { caseId: 'a', input: '', output: '', scores: { latency_ms: 10 }, passed: true },
118
+ { caseId: 'b', input: '', output: '', scores: { latency_ms: 10 }, passed: false },
119
+ ],
120
+ };
121
+ const cmp = AgentEvaluator.compare(baseline, current);
122
+ expect(cmp.regressed).toContain('b');
123
+ expect(cmp.delta).toBe(-0.5);
124
+ });
125
+
126
+ it('should compare reports and detect improvement', () => {
127
+ const baseline: EvalReport = {
128
+ suite: 'test', timestamp: '', totalCases: 2, passed: 1, failed: 1, passRate: 0.5, avgLatency: 10, p95Latency: 15, summary: '',
129
+ results: [
130
+ { caseId: 'a', input: '', output: '', scores: { latency_ms: 10 }, passed: true },
131
+ { caseId: 'b', input: '', output: '', scores: { latency_ms: 10 }, passed: false },
132
+ ],
133
+ };
134
+ const current: EvalReport = {
135
+ suite: 'test', timestamp: '', totalCases: 2, passed: 2, failed: 0, passRate: 1, avgLatency: 10, p95Latency: 15, summary: '',
136
+ results: [
137
+ { caseId: 'a', input: '', output: '', scores: { latency_ms: 10 }, passed: true },
138
+ { caseId: 'b', input: '', output: '', scores: { latency_ms: 10 }, passed: true },
139
+ ],
140
+ };
141
+ const cmp = AgentEvaluator.compare(baseline, current);
142
+ expect(cmp.improved).toContain('b');
143
+ expect(cmp.delta).toBe(0.5);
144
+ });
145
+
146
+ it('should save and load report', () => {
147
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eval-'));
148
+ const reportPath = path.join(tmpDir, 'report.json');
149
+ const report: EvalReport = {
150
+ suite: 'test', timestamp: new Date().toISOString(), totalCases: 1, passed: 1, failed: 0, passRate: 1, avgLatency: 5, p95Latency: 5, summary: 'ok',
151
+ results: [{ caseId: 'x', input: 'hi', output: 'hello', scores: { latency_ms: 5 }, passed: true }],
152
+ };
153
+ AgentEvaluator.saveReport(report, reportPath);
154
+ const loaded = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
155
+ expect(loaded.suite).toBe('test');
156
+ expect(loaded.results.length).toBe(1);
157
+ fs.rmSync(tmpDir, { recursive: true });
158
+ });
159
+
160
+ it('should run evalSuite and produce report', async () => {
161
+ const suite: EvalSuite = {
162
+ name: 'mini',
163
+ cases: [
164
+ { id: 't1', input: 'Hello!', expectedContains: ['hello', 'hi'] },
165
+ { id: 't2', input: 'What is the capital of France?', expectedContains: ['Paris'] },
166
+ ],
167
+ };
168
+ const report = await evaluator.evalSuite(suite);
169
+ expect(report.totalCases).toBe(2);
170
+ expect(report.passRate).toBeGreaterThanOrEqual(0);
171
+ expect(report.summary).toContain('mini');
172
+ });
173
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { searchRoles, getPopularRoles, getRole, getCategories } from 'agent-workstation';
6
+
7
+ const CLI_PATH = path.join(__dirname, '..', 'dist', 'cli.js');
8
+ const TMP_DIR = path.join(process.env.TEMP || require('os').tmpdir(), 'opc-test-init-role-' + process.pid);
9
+
10
+ function run(args: string): string {
11
+ return execSync(`node "${CLI_PATH}" ${args}`, { cwd: TMP_DIR, encoding: 'utf-8', timeout: 15000 });
12
+ }
13
+
14
+ describe('opc init --role integration with agent-workstation', () => {
15
+ beforeEach(() => {
16
+ if (fs.existsSync(TMP_DIR)) fs.rmSync(TMP_DIR, { recursive: true, force: true });
17
+ fs.mkdirSync(TMP_DIR, { recursive: true });
18
+ });
19
+
20
+ afterEach(() => {
21
+ if (fs.existsSync(TMP_DIR)) fs.rmSync(TMP_DIR, { recursive: true, force: true });
22
+ });
23
+
24
+ // --- agent-workstation API tests ---
25
+
26
+ it('searchRoles returns results for "customer"', () => {
27
+ const results = searchRoles('customer');
28
+ expect(results.length).toBeGreaterThan(0);
29
+ expect(results[0].category).toBe('customer-service');
30
+ });
31
+
32
+ it('searchRoles returns results for "developer"', () => {
33
+ const results = searchRoles('developer');
34
+ expect(results.length).toBeGreaterThan(0);
35
+ expect(results.some((r: any) => r.role.includes('developer'))).toBe(true);
36
+ });
37
+
38
+ it('searchRoles returns empty for nonexistent role', () => {
39
+ const results = searchRoles('xyznonexistentrole12345');
40
+ expect(results.length).toBe(0);
41
+ });
42
+
43
+ it('getPopularRoles returns roles with category and role fields', () => {
44
+ const roles = getPopularRoles();
45
+ expect(roles.length).toBeGreaterThan(5);
46
+ for (const r of roles) {
47
+ expect(r).toHaveProperty('category');
48
+ expect(r).toHaveProperty('role');
49
+ }
50
+ });
51
+
52
+ it('getRole loads full role data with files', () => {
53
+ const role = getRole('customer-service', 'customer-service-rep');
54
+ expect(role).toBeTruthy();
55
+ expect(role.files).toBeTruthy();
56
+ expect(role.files['system-prompt.md']).toBeTruthy();
57
+ expect(role.files['brain-seed.md']).toBeTruthy();
58
+ });
59
+
60
+ it('getCategories returns category list', () => {
61
+ const cats = getCategories();
62
+ expect(cats.length).toBeGreaterThan(3);
63
+ expect(cats.some((c: any) => c.name === 'customer-service')).toBe(true);
64
+ });
65
+
66
+ // --- CLI integration tests ---
67
+
68
+ it('--list-roles shows available roles', () => {
69
+ const output = run('init --list-roles');
70
+ expect(output).toContain('Available workstation roles');
71
+ expect(output).toContain('customer-service');
72
+ expect(output).toContain('--role');
73
+ });
74
+
75
+ it('--role customer-service generates SOUL.md from template', () => {
76
+ const output = run('init test-cs-agent --role customer-service');
77
+ expect(output).toContain('customer-service');
78
+ const soulPath = path.join(TMP_DIR, 'test-cs-agent', 'SOUL.md');
79
+ expect(fs.existsSync(soulPath)).toBe(true);
80
+ const soul = fs.readFileSync(soulPath, 'utf-8');
81
+ expect(soul).toContain('Customer Service');
82
+ expect(soul.split('\n').length).toBeGreaterThan(10);
83
+ });
84
+
85
+ it('--role generates agent.yaml with role systemPrompt', () => {
86
+ run('init test-yaml-agent --role customer-service');
87
+ const agentYaml = fs.readFileSync(path.join(TMP_DIR, 'test-yaml-agent', 'agent.yaml'), 'utf-8');
88
+ expect(agentYaml).toContain('Customer Service');
89
+ expect(agentYaml).toContain('systemPrompt');
90
+ expect(agentYaml).toContain('deepbrain');
91
+ });
92
+
93
+ it('--role generates brain-seed when available', () => {
94
+ run('init test-brain-agent --role customer-service');
95
+ const brainPath = path.join(TMP_DIR, 'test-brain-agent', 'data', 'brain-seed.md');
96
+ expect(fs.existsSync(brainPath)).toBe(true);
97
+ const brain = fs.readFileSync(brainPath, 'utf-8');
98
+ expect(brain.length).toBeGreaterThan(50);
99
+ });
100
+
101
+ it('--role generates CONTEXT.md with role info', () => {
102
+ run('init test-ctx-agent --role customer-service');
103
+ const ctx = fs.readFileSync(path.join(TMP_DIR, 'test-ctx-agent', 'CONTEXT.md'), 'utf-8');
104
+ expect(ctx).toContain('Customer Service');
105
+ });
106
+
107
+ it('--role with unknown role gives error', () => {
108
+ expect(() => run('init test-bad --role xyznonexistent123')).toThrow();
109
+ });
110
+
111
+ it('--role with partial match "developer" works', () => {
112
+ const output = run('init test-dev-agent --role developer');
113
+ expect(output).toContain('developer');
114
+ expect(fs.existsSync(path.join(TMP_DIR, 'test-dev-agent', 'SOUL.md'))).toBe(true);
115
+ });
116
+
117
+ it('init without --role still works (generic project)', () => {
118
+ const output = run('init test-generic -y');
119
+ expect(output).toContain('Created agent project');
120
+ expect(output).toContain('Tip');
121
+ expect(output).toContain('--role');
122
+ expect(fs.existsSync(path.join(TMP_DIR, 'test-generic', 'SOUL.md'))).toBe(true);
123
+ });
124
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { MCPClient } from '../src/tools/mcp-client';
3
+
4
+ describe('MCPClient', () => {
5
+ it('starts with no process', () => {
6
+ const client = new MCPClient();
7
+ expect((client as any).process).toBeNull();
8
+ });
9
+
10
+ it('starts with empty buffer', () => {
11
+ const client = new MCPClient();
12
+ expect((client as any).buffer).toBe('');
13
+ });
14
+
15
+ it('starts not connected', () => {
16
+ const client = new MCPClient();
17
+ expect((client as any).connected).toBe(false);
18
+ });
19
+
20
+ it('nextId starts at 1', () => {
21
+ const client = new MCPClient();
22
+ expect((client as any).nextId).toBe(1);
23
+ });
24
+
25
+ it('pending map starts empty', () => {
26
+ const client = new MCPClient();
27
+ expect((client as any).pending.size).toBe(0);
28
+ });
29
+
30
+ it('config starts null', () => {
31
+ const client = new MCPClient();
32
+ expect((client as any).config).toBeNull();
33
+ });
34
+
35
+ it('processBuffer handles complete JSON line', () => {
36
+ const client = new MCPClient();
37
+ const resolve = vi.fn();
38
+ const reject = vi.fn();
39
+ (client as any).pending.set(1, { resolve, reject });
40
+ (client as any).buffer = '{"id":1,"result":{"tools":[]}}\n';
41
+ (client as any).processBuffer();
42
+ expect(resolve).toHaveBeenCalledWith({ tools: [] });
43
+ expect((client as any).pending.size).toBe(0);
44
+ });
45
+
46
+ it('processBuffer handles error response', () => {
47
+ const client = new MCPClient();
48
+ const resolve = vi.fn();
49
+ const reject = vi.fn();
50
+ (client as any).pending.set(2, { resolve, reject });
51
+ (client as any).buffer = '{"id":2,"error":{"message":"not found"}}\n';
52
+ (client as any).processBuffer();
53
+ expect(reject).toHaveBeenCalled();
54
+ expect(reject.mock.calls[0][0].message).toBe('not found');
55
+ });
56
+
57
+ it('processBuffer handles partial message (no newline)', () => {
58
+ const client = new MCPClient();
59
+ const resolve = vi.fn();
60
+ (client as any).pending.set(1, { resolve, reject: vi.fn() });
61
+ (client as any).buffer = '{"id":1,"result":';
62
+ (client as any).processBuffer();
63
+ expect(resolve).not.toHaveBeenCalled();
64
+ // Partial stays in buffer
65
+ expect((client as any).buffer).toBe('{"id":1,"result":');
66
+ });
67
+
68
+ it('processBuffer handles multiple responses in one chunk', () => {
69
+ const client = new MCPClient();
70
+ const r1 = vi.fn(), r2 = vi.fn();
71
+ (client as any).pending.set(1, { resolve: r1, reject: vi.fn() });
72
+ (client as any).pending.set(2, { resolve: r2, reject: vi.fn() });
73
+ (client as any).buffer = '{"id":1,"result":"a"}\n{"id":2,"result":"b"}\n';
74
+ (client as any).processBuffer();
75
+ expect(r1).toHaveBeenCalledWith('a');
76
+ expect(r2).toHaveBeenCalledWith('b');
77
+ });
78
+
79
+ it('processBuffer ignores non-JSON lines', () => {
80
+ const client = new MCPClient();
81
+ (client as any).buffer = 'not json at all\n';
82
+ // Should not throw
83
+ (client as any).processBuffer();
84
+ });
85
+
86
+ it('processBuffer ignores responses without matching id', () => {
87
+ const client = new MCPClient();
88
+ (client as any).buffer = '{"id":999,"result":"orphan"}\n';
89
+ // Should not throw
90
+ (client as any).processBuffer();
91
+ });
92
+ });