opc-agent 2.1.0 → 3.0.0
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.
- package/README.md +603 -545
- package/dist/channels/voice.d.ts +59 -0
- package/dist/channels/voice.js +351 -1
- package/dist/cli.js +172 -1
- package/dist/core/agent.d.ts +4 -0
- package/dist/core/agent.js +35 -0
- package/dist/core/collaboration.d.ts +89 -0
- package/dist/core/collaboration.js +201 -0
- package/dist/deploy/index.d.ts +40 -0
- package/dist/deploy/index.js +261 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +47 -3
- package/dist/mcp/servers/calculator-mcp.d.ts +3 -0
- package/dist/mcp/servers/calculator-mcp.js +65 -0
- package/dist/mcp/servers/crypto-mcp.d.ts +3 -0
- package/dist/mcp/servers/crypto-mcp.js +108 -0
- package/dist/mcp/servers/database-mcp.d.ts +3 -0
- package/dist/mcp/servers/database-mcp.js +73 -0
- package/dist/mcp/servers/datetime-mcp.d.ts +3 -0
- package/dist/mcp/servers/datetime-mcp.js +71 -0
- package/dist/mcp/servers/filesystem.d.ts +3 -0
- package/dist/mcp/servers/filesystem.js +101 -0
- package/dist/mcp/servers/github-mcp.d.ts +3 -0
- package/dist/mcp/servers/github-mcp.js +60 -0
- package/dist/mcp/servers/index.d.ts +21 -0
- package/dist/mcp/servers/index.js +50 -0
- package/dist/mcp/servers/json-mcp.d.ts +3 -0
- package/dist/mcp/servers/json-mcp.js +126 -0
- package/dist/mcp/servers/memory-mcp.d.ts +3 -0
- package/dist/mcp/servers/memory-mcp.js +60 -0
- package/dist/mcp/servers/regex-mcp.d.ts +3 -0
- package/dist/mcp/servers/regex-mcp.js +56 -0
- package/dist/mcp/servers/web-mcp.d.ts +3 -0
- package/dist/mcp/servers/web-mcp.js +51 -0
- package/dist/schema/oad.d.ts +292 -12
- package/dist/schema/oad.js +12 -1
- package/dist/security/guardrails.d.ts +50 -0
- package/dist/security/guardrails.js +197 -0
- package/dist/studio/server.d.ts +31 -1
- package/dist/studio/server.js +154 -3
- package/dist/studio-ui/index.html +1278 -662
- package/dist/tools/integrations/calendar.d.ts +3 -0
- package/dist/tools/integrations/calendar.js +73 -0
- package/dist/tools/integrations/code-exec.d.ts +3 -0
- package/dist/tools/integrations/code-exec.js +42 -0
- package/dist/tools/integrations/csv-analyzer.d.ts +3 -0
- package/dist/tools/integrations/csv-analyzer.js +142 -0
- package/dist/tools/integrations/database.d.ts +3 -0
- package/dist/tools/integrations/database.js +44 -0
- package/dist/tools/integrations/email-send.d.ts +3 -0
- package/dist/tools/integrations/email-send.js +104 -0
- package/dist/tools/integrations/git-tool.d.ts +3 -0
- package/dist/tools/integrations/git-tool.js +49 -0
- package/dist/tools/integrations/github-tool.d.ts +3 -0
- package/dist/tools/integrations/github-tool.js +77 -0
- package/dist/tools/integrations/image-gen.d.ts +3 -0
- package/dist/tools/integrations/image-gen.js +58 -0
- package/dist/tools/integrations/index.d.ts +30 -0
- package/dist/tools/integrations/index.js +107 -0
- package/dist/tools/integrations/jira.d.ts +3 -0
- package/dist/tools/integrations/jira.js +85 -0
- package/dist/tools/integrations/notion.d.ts +3 -0
- package/dist/tools/integrations/notion.js +71 -0
- package/dist/tools/integrations/npm-tool.d.ts +3 -0
- package/dist/tools/integrations/npm-tool.js +49 -0
- package/dist/tools/integrations/pdf-reader.d.ts +3 -0
- package/dist/tools/integrations/pdf-reader.js +91 -0
- package/dist/tools/integrations/slack.d.ts +3 -0
- package/dist/tools/integrations/slack.js +67 -0
- package/dist/tools/integrations/summarizer.d.ts +3 -0
- package/dist/tools/integrations/summarizer.js +49 -0
- package/dist/tools/integrations/translator.d.ts +3 -0
- package/dist/tools/integrations/translator.js +48 -0
- package/dist/tools/integrations/trello.d.ts +3 -0
- package/dist/tools/integrations/trello.js +60 -0
- package/dist/tools/integrations/vector-search.d.ts +3 -0
- package/dist/tools/integrations/vector-search.js +44 -0
- package/dist/tools/integrations/web-scraper.d.ts +3 -0
- package/dist/tools/integrations/web-scraper.js +48 -0
- package/dist/tools/integrations/web-search.d.ts +3 -0
- package/dist/tools/integrations/web-search.js +60 -0
- package/dist/tools/integrations/webhook.d.ts +3 -0
- package/dist/tools/integrations/webhook.js +39 -0
- package/dist/ui/components.d.ts +10 -0
- package/dist/ui/components.js +123 -0
- package/package.json +1 -1
- package/src/channels/voice.ts +365 -0
- package/src/cli.ts +176 -2
- package/src/core/agent.ts +38 -0
- package/src/core/collaboration.ts +275 -0
- package/src/deploy/index.ts +255 -0
- package/src/index.ts +21 -1
- package/src/mcp/servers/calculator-mcp.ts +65 -0
- package/src/mcp/servers/crypto-mcp.ts +73 -0
- package/src/mcp/servers/database-mcp.ts +72 -0
- package/src/mcp/servers/datetime-mcp.ts +69 -0
- package/src/mcp/servers/filesystem.ts +66 -0
- package/src/mcp/servers/github-mcp.ts +58 -0
- package/src/mcp/servers/index.ts +63 -0
- package/src/mcp/servers/json-mcp.ts +102 -0
- package/src/mcp/servers/memory-mcp.ts +56 -0
- package/src/mcp/servers/regex-mcp.ts +53 -0
- package/src/mcp/servers/web-mcp.ts +49 -0
- package/src/schema/oad.ts +13 -0
- package/src/security/guardrails.ts +248 -0
- package/src/studio/server.ts +166 -4
- package/src/studio-ui/index.html +1278 -662
- package/src/tools/integrations/calendar.ts +73 -0
- package/src/tools/integrations/code-exec.ts +39 -0
- package/src/tools/integrations/csv-analyzer.ts +92 -0
- package/src/tools/integrations/database.ts +44 -0
- package/src/tools/integrations/email-send.ts +76 -0
- package/src/tools/integrations/git-tool.ts +42 -0
- package/src/tools/integrations/github-tool.ts +76 -0
- package/src/tools/integrations/image-gen.ts +56 -0
- package/src/tools/integrations/index.ts +92 -0
- package/src/tools/integrations/jira.ts +83 -0
- package/src/tools/integrations/notion.ts +71 -0
- package/src/tools/integrations/npm-tool.ts +48 -0
- package/src/tools/integrations/pdf-reader.ts +58 -0
- package/src/tools/integrations/slack.ts +65 -0
- package/src/tools/integrations/summarizer.ts +49 -0
- package/src/tools/integrations/translator.ts +48 -0
- package/src/tools/integrations/trello.ts +60 -0
- package/src/tools/integrations/vector-search.ts +42 -0
- package/src/tools/integrations/web-scraper.ts +47 -0
- package/src/tools/integrations/web-search.ts +58 -0
- package/src/tools/integrations/webhook.ts +38 -0
- package/src/ui/components.ts +127 -0
- package/tests/brain-seed-extended.test.ts +490 -0
- package/tests/collaboration.test.ts +319 -0
- package/tests/deploy-and-dag.test.ts +196 -0
- package/tests/guardrails.test.ts +177 -0
- package/tests/integrations.test.ts +249 -0
- package/tests/mcp-servers.test.ts +260 -0
- package/tests/voice-enhanced.test.ts +169 -0
- package/dist/dtv/data.d.ts +0 -18
- package/dist/dtv/data.js +0 -25
- package/dist/dtv/trust.d.ts +0 -19
- package/dist/dtv/trust.js +0 -40
- package/dist/dtv/value.d.ts +0 -23
- package/dist/dtv/value.js +0 -38
- package/dist/marketplace/index.d.ts +0 -34
- package/dist/marketplace/index.js +0 -202
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DebatePattern, VotingPattern, PipelinePattern, HierarchyPattern, SharedContext, ConversationProtocol } from '../src/core/collaboration';
|
|
3
|
+
import { BaseAgent } from '../src/core/agent';
|
|
4
|
+
|
|
5
|
+
// Mock provider that returns deterministic responses
|
|
6
|
+
function createMockAgent(name: string, responses: string[] | ((prompt: string) => string)): BaseAgent {
|
|
7
|
+
const agent = new BaseAgent({ name, provider: 'echo' });
|
|
8
|
+
let callIndex = 0;
|
|
9
|
+
|
|
10
|
+
// Override handleMessage to return mock responses
|
|
11
|
+
agent.handleMessage = async (message) => {
|
|
12
|
+
let content: string;
|
|
13
|
+
if (typeof responses === 'function') {
|
|
14
|
+
content = responses(message.content);
|
|
15
|
+
} else {
|
|
16
|
+
content = responses[callIndex % responses.length];
|
|
17
|
+
callIndex++;
|
|
18
|
+
}
|
|
19
|
+
return { id: `resp-${Date.now()}`, role: 'assistant', content, timestamp: Date.now() };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return agent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── DebatePattern ──────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe('DebatePattern', () => {
|
|
28
|
+
it('2 agents debate for 2 rounds', async () => {
|
|
29
|
+
const alice = createMockAgent('alice', ['AI is beneficial', 'AI creates jobs']);
|
|
30
|
+
const bob = createMockAgent('bob', ['AI is risky', 'AI displaces workers']);
|
|
31
|
+
const judge = createMockAgent('judge', ['Both sides made good points. AI is nuanced.']);
|
|
32
|
+
|
|
33
|
+
const pattern = new DebatePattern([alice, bob], 2);
|
|
34
|
+
const result = await pattern.debate('Is AI good?', judge);
|
|
35
|
+
|
|
36
|
+
expect(result.topic).toBe('Is AI good?');
|
|
37
|
+
expect(result.rounds.length).toBe(4); // 2 agents × 2 rounds
|
|
38
|
+
expect(result.rounds[0].agent).toBe('alice');
|
|
39
|
+
expect(result.rounds[1].agent).toBe('bob');
|
|
40
|
+
expect(result.summary).toBeTruthy();
|
|
41
|
+
expect(result.judge).toBe('judge');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('debate without explicit judge uses first agent', async () => {
|
|
45
|
+
const a1 = createMockAgent('a1', ['arg1', 'summary']);
|
|
46
|
+
const a2 = createMockAgent('a2', ['arg2']);
|
|
47
|
+
|
|
48
|
+
const pattern = new DebatePattern([a1, a2], 1);
|
|
49
|
+
const result = await pattern.debate('topic');
|
|
50
|
+
|
|
51
|
+
expect(result.judge).toBe('a1');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('single round debate', async () => {
|
|
55
|
+
const a = createMockAgent('a', ['yes']);
|
|
56
|
+
const b = createMockAgent('b', ['no']);
|
|
57
|
+
const pattern = new DebatePattern([a, b], 1);
|
|
58
|
+
const result = await pattern.debate('test');
|
|
59
|
+
|
|
60
|
+
expect(result.rounds.length).toBe(2);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ─── VotingPattern ──────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
describe('VotingPattern', () => {
|
|
67
|
+
it('3 agents vote, majority wins', async () => {
|
|
68
|
+
const a1 = createMockAgent('v1', ['TypeScript']);
|
|
69
|
+
const a2 = createMockAgent('v2', ['TypeScript']);
|
|
70
|
+
const a3 = createMockAgent('v3', ['Python']);
|
|
71
|
+
|
|
72
|
+
const pattern = new VotingPattern([a1, a2, a3]);
|
|
73
|
+
const result = await pattern.vote('Best language?', ['TypeScript', 'Python', 'Rust']);
|
|
74
|
+
|
|
75
|
+
expect(result.winner).toBe('TypeScript');
|
|
76
|
+
expect(result.tally['TypeScript']).toBe(2);
|
|
77
|
+
expect(result.tally['Python']).toBe(1);
|
|
78
|
+
expect(result.votes.length).toBe(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('unanimous vote', async () => {
|
|
82
|
+
const agents = [1, 2, 3].map(i => createMockAgent(`u${i}`, ['Rust']));
|
|
83
|
+
const pattern = new VotingPattern(agents);
|
|
84
|
+
const result = await pattern.vote('Pick one', ['Rust', 'Go']);
|
|
85
|
+
|
|
86
|
+
expect(result.winner).toBe('Rust');
|
|
87
|
+
expect(result.tally['Rust']).toBe(3);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('weighted vote with confidence', async () => {
|
|
91
|
+
const a1 = createMockAgent('w1', ['TypeScript|0.9']);
|
|
92
|
+
const a2 = createMockAgent('w2', ['Python|0.3']);
|
|
93
|
+
const a3 = createMockAgent('w3', ['Python|0.4']);
|
|
94
|
+
|
|
95
|
+
const pattern = new VotingPattern([a1, a2, a3]);
|
|
96
|
+
const result = await pattern.weightedVote('Best?', ['TypeScript', 'Python']);
|
|
97
|
+
|
|
98
|
+
expect(result.winner).toBe('TypeScript'); // 0.9 > 0.3+0.4=0.7
|
|
99
|
+
expect(result.votes[0].confidence).toBeCloseTo(0.9);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('weighted vote Python wins with higher total confidence', async () => {
|
|
103
|
+
const a1 = createMockAgent('w1', ['TypeScript|0.3']);
|
|
104
|
+
const a2 = createMockAgent('w2', ['Python|0.8']);
|
|
105
|
+
const a3 = createMockAgent('w3', ['Python|0.9']);
|
|
106
|
+
|
|
107
|
+
const pattern = new VotingPattern([a1, a2, a3]);
|
|
108
|
+
const result = await pattern.weightedVote('Best?', ['TypeScript', 'Python']);
|
|
109
|
+
|
|
110
|
+
expect(result.winner).toBe('Python');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('vote result includes question', async () => {
|
|
114
|
+
const a = createMockAgent('a', ['X']);
|
|
115
|
+
const pattern = new VotingPattern([a]);
|
|
116
|
+
const result = await pattern.vote('Q?', ['X', 'Y']);
|
|
117
|
+
expect(result.question).toBe('Q?');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ─── PipelinePattern ────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe('PipelinePattern', () => {
|
|
124
|
+
it('3-stage pipeline processes sequentially', async () => {
|
|
125
|
+
const s1 = createMockAgent('stage1', (input) => `[s1:${input}]`);
|
|
126
|
+
const s2 = createMockAgent('stage2', (input) => `[s2:${input}]`);
|
|
127
|
+
const s3 = createMockAgent('stage3', (input) => `[s3:${input}]`);
|
|
128
|
+
|
|
129
|
+
const pattern = new PipelinePattern([
|
|
130
|
+
{ agent: s1 },
|
|
131
|
+
{ agent: s2 },
|
|
132
|
+
{ agent: s3 },
|
|
133
|
+
]);
|
|
134
|
+
const result = await pattern.process('hello');
|
|
135
|
+
|
|
136
|
+
expect(result.stages.length).toBe(3);
|
|
137
|
+
expect(result.stages[0].output).toBe('[s1:hello]');
|
|
138
|
+
expect(result.stages[1].input).toBe('[s1:hello]');
|
|
139
|
+
expect(result.finalOutput).toBe('[s3:[s2:[s1:hello]]]');
|
|
140
|
+
expect(result.totalDurationMs).toBeGreaterThanOrEqual(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('pipeline with transform function', async () => {
|
|
144
|
+
const agent = createMockAgent('a', (input) => input.toUpperCase());
|
|
145
|
+
const pattern = new PipelinePattern([
|
|
146
|
+
{ agent, transform: (s) => `prefix:${s}` },
|
|
147
|
+
]);
|
|
148
|
+
const result = await pattern.process('data');
|
|
149
|
+
|
|
150
|
+
expect(result.stages[0].input).toBe('prefix:data');
|
|
151
|
+
expect(result.finalOutput).toBe('PREFIX:DATA');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('single stage pipeline', async () => {
|
|
155
|
+
const agent = createMockAgent('only', ['processed']);
|
|
156
|
+
const pattern = new PipelinePattern([{ agent }]);
|
|
157
|
+
const result = await pattern.process('input');
|
|
158
|
+
|
|
159
|
+
expect(result.stages.length).toBe(1);
|
|
160
|
+
expect(result.finalOutput).toBe('processed');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('pipeline stage records duration', async () => {
|
|
164
|
+
const agent = createMockAgent('slow', ['done']);
|
|
165
|
+
const pattern = new PipelinePattern([{ agent }]);
|
|
166
|
+
const result = await pattern.process('x');
|
|
167
|
+
expect(result.stages[0].durationMs).toBeGreaterThanOrEqual(0);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── HierarchyPattern ───────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe('HierarchyPattern', () => {
|
|
174
|
+
it('leader decomposes, workers execute, leader synthesizes', async () => {
|
|
175
|
+
const leader = createMockAgent('leader', [
|
|
176
|
+
'Research the topic\nWrite the code', // decomposition
|
|
177
|
+
'Combined: research + code done.', // synthesis
|
|
178
|
+
]);
|
|
179
|
+
const w1 = createMockAgent('worker1', ['Research complete']);
|
|
180
|
+
const w2 = createMockAgent('worker2', ['Code written']);
|
|
181
|
+
|
|
182
|
+
const pattern = new HierarchyPattern(leader, [w1, w2]);
|
|
183
|
+
const result = await pattern.execute('Build a feature');
|
|
184
|
+
|
|
185
|
+
expect(result.task).toBe('Build a feature');
|
|
186
|
+
expect(result.subtasks.length).toBe(2);
|
|
187
|
+
expect(result.subtasks[0].agent).toBe('worker1');
|
|
188
|
+
expect(result.subtasks[1].agent).toBe('worker2');
|
|
189
|
+
expect(result.synthesis).toBeTruthy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('single worker gets single subtask', async () => {
|
|
193
|
+
const leader = createMockAgent('boss', ['Do the thing', 'All done']);
|
|
194
|
+
const worker = createMockAgent('w', ['Done']);
|
|
195
|
+
|
|
196
|
+
const pattern = new HierarchyPattern(leader, [worker]);
|
|
197
|
+
const result = await pattern.execute('task');
|
|
198
|
+
|
|
199
|
+
expect(result.subtasks.length).toBe(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('workers execute in parallel', async () => {
|
|
203
|
+
const timestamps: number[] = [];
|
|
204
|
+
const leader = createMockAgent('l', ['A\nB\nC', 'Synthesized']);
|
|
205
|
+
const workers = [1, 2, 3].map(i =>
|
|
206
|
+
createMockAgent(`w${i}`, () => { timestamps.push(Date.now()); return `result${i}`; })
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const pattern = new HierarchyPattern(leader, workers);
|
|
210
|
+
await pattern.execute('parallel task');
|
|
211
|
+
|
|
212
|
+
// All workers should start nearly simultaneously (within 50ms)
|
|
213
|
+
if (timestamps.length >= 2) {
|
|
214
|
+
const spread = Math.max(...timestamps) - Math.min(...timestamps);
|
|
215
|
+
expect(spread).toBeLessThan(200);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ─── SharedContext ───────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
describe('SharedContext', () => {
|
|
223
|
+
it('set and get values', () => {
|
|
224
|
+
const ctx = new SharedContext();
|
|
225
|
+
ctx.set('key', 'value');
|
|
226
|
+
expect(ctx.get('key')).toBe('value');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('get returns undefined for missing key', () => {
|
|
230
|
+
const ctx = new SharedContext();
|
|
231
|
+
expect(ctx.get('nope')).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('getAll returns all entries', () => {
|
|
235
|
+
const ctx = new SharedContext();
|
|
236
|
+
ctx.set('a', 1);
|
|
237
|
+
ctx.set('b', 2);
|
|
238
|
+
expect(ctx.getAll()).toEqual({ a: 1, b: 2 });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('onChange fires on set', () => {
|
|
242
|
+
const ctx = new SharedContext();
|
|
243
|
+
const values: any[] = [];
|
|
244
|
+
ctx.onChange('x', (v) => values.push(v));
|
|
245
|
+
ctx.set('x', 10);
|
|
246
|
+
ctx.set('x', 20);
|
|
247
|
+
expect(values).toEqual([10, 20]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('onChange only fires for subscribed key', () => {
|
|
251
|
+
const ctx = new SharedContext();
|
|
252
|
+
const fired: any[] = [];
|
|
253
|
+
ctx.onChange('a', (v) => fired.push(v));
|
|
254
|
+
ctx.set('b', 1);
|
|
255
|
+
expect(fired.length).toBe(0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('multiple listeners on same key', () => {
|
|
259
|
+
const ctx = new SharedContext();
|
|
260
|
+
const r1: any[] = [];
|
|
261
|
+
const r2: any[] = [];
|
|
262
|
+
ctx.onChange('k', (v) => r1.push(v));
|
|
263
|
+
ctx.onChange('k', (v) => r2.push(v));
|
|
264
|
+
ctx.set('k', 'hello');
|
|
265
|
+
expect(r1).toEqual(['hello']);
|
|
266
|
+
expect(r2).toEqual(['hello']);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('typed get', () => {
|
|
270
|
+
const ctx = new SharedContext();
|
|
271
|
+
ctx.set('num', 42);
|
|
272
|
+
const val = ctx.get<number>('num');
|
|
273
|
+
expect(val).toBe(42);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ─── ConversationProtocol ───────────────────────────────────
|
|
278
|
+
|
|
279
|
+
describe('ConversationProtocol', () => {
|
|
280
|
+
it('roundRobin produces correct number of messages', async () => {
|
|
281
|
+
const agents = [
|
|
282
|
+
createMockAgent('a', ['hello from a']),
|
|
283
|
+
createMockAgent('b', ['hello from b']),
|
|
284
|
+
];
|
|
285
|
+
const proto = new ConversationProtocol();
|
|
286
|
+
const msgs = await proto.roundRobin(agents, 'test topic', 2);
|
|
287
|
+
|
|
288
|
+
expect(msgs.length).toBe(4); // 2 agents × 2 rounds
|
|
289
|
+
expect((msgs[0] as any).agent).toBe('a');
|
|
290
|
+
expect((msgs[1] as any).agent).toBe('b');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('roundRobin single round', async () => {
|
|
294
|
+
const agents = [createMockAgent('x', ['x says'])];
|
|
295
|
+
const proto = new ConversationProtocol();
|
|
296
|
+
const msgs = await proto.roundRobin(agents, 'topic', 1);
|
|
297
|
+
|
|
298
|
+
expect(msgs.length).toBe(1);
|
|
299
|
+
expect(msgs[0].content).toBe('x says');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('moderated discussion has opening, responses, and summary', async () => {
|
|
303
|
+
const mod = createMockAgent('mod', ['Welcome!', 'Great discussion.']);
|
|
304
|
+
const agents = [
|
|
305
|
+
createMockAgent('p1', ['My opinion is X']),
|
|
306
|
+
createMockAgent('p2', ['I think Y']),
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const proto = new ConversationProtocol();
|
|
310
|
+
const msgs = await proto.moderated(agents, mod, 'AI ethics');
|
|
311
|
+
|
|
312
|
+
// 1 opening + 2 responses + 1 summary = 4
|
|
313
|
+
expect(msgs.length).toBe(4);
|
|
314
|
+
expect((msgs[0] as any).agent).toBe('mod');
|
|
315
|
+
expect((msgs[1] as any).agent).toBe('p1');
|
|
316
|
+
expect((msgs[2] as any).agent).toBe('p2');
|
|
317
|
+
expect((msgs[3] as any).agent).toBe('mod');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { AgentDeployer } from '../src/deploy/index';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
describe('AgentDeployer', () => {
|
|
8
|
+
let deployer: AgentDeployer;
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
deployer = new AgentDeployer();
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opc-deploy-test-'));
|
|
14
|
+
// Create a minimal package.json
|
|
15
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({
|
|
16
|
+
name: 'test-agent',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
main: 'dist/index.js',
|
|
19
|
+
scripts: { start: 'node dist/index.js' },
|
|
20
|
+
}));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should generate a valid Dockerfile', async () => {
|
|
28
|
+
const dockerfile = await deployer.generateDockerfile(tmpDir);
|
|
29
|
+
expect(dockerfile).toContain('FROM node:22-slim');
|
|
30
|
+
expect(dockerfile).toContain('WORKDIR /app');
|
|
31
|
+
expect(dockerfile).toContain('npm ci --production');
|
|
32
|
+
expect(dockerfile).toContain('EXPOSE 3000');
|
|
33
|
+
expect(dockerfile).toContain('NODE_ENV=production');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should generate Dockerfile with custom port', async () => {
|
|
37
|
+
const dockerfile = await deployer.generateDockerfile(tmpDir, { port: 8080 });
|
|
38
|
+
expect(dockerfile).toContain('EXPOSE 8080');
|
|
39
|
+
expect(dockerfile).toContain('PORT=8080');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should generate docker-compose.yml', async () => {
|
|
43
|
+
const compose = await deployer.generateCompose(tmpDir);
|
|
44
|
+
expect(compose).toContain('version: "3.8"');
|
|
45
|
+
expect(compose).toContain('build: .');
|
|
46
|
+
expect(compose).toContain('3000:3000');
|
|
47
|
+
expect(compose).toContain('NODE_ENV=production');
|
|
48
|
+
expect(compose).toContain('restart: unless-stopped');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should generate docker-compose.yml with custom options', async () => {
|
|
52
|
+
const compose = await deployer.generateCompose(tmpDir, { port: 8080, replicas: 3 });
|
|
53
|
+
expect(compose).toContain('8080:8080');
|
|
54
|
+
expect(compose).toContain('replicas: 3');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should generate docker-compose.yml with env vars', async () => {
|
|
58
|
+
const compose = await deployer.generateCompose(tmpDir, { env: { API_KEY: 'test123', DEBUG: 'true' } });
|
|
59
|
+
expect(compose).toContain('API_KEY=test123');
|
|
60
|
+
expect(compose).toContain('DEBUG=true');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should generate all deployment files', async () => {
|
|
64
|
+
const result = await deployer.generateFiles(tmpDir);
|
|
65
|
+
expect(result.success).toBe(true);
|
|
66
|
+
expect(result.files).toContain('Dockerfile');
|
|
67
|
+
expect(result.files).toContain('docker-compose.yml');
|
|
68
|
+
expect(result.files).toContain('.dockerignore');
|
|
69
|
+
expect(fs.existsSync(path.join(tmpDir, 'Dockerfile'))).toBe(true);
|
|
70
|
+
expect(fs.existsSync(path.join(tmpDir, 'docker-compose.yml'))).toBe(true);
|
|
71
|
+
expect(fs.existsSync(path.join(tmpDir, '.dockerignore'))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should include node_modules in .dockerignore', async () => {
|
|
75
|
+
await deployer.generateFiles(tmpDir);
|
|
76
|
+
const content = fs.readFileSync(path.join(tmpDir, '.dockerignore'), 'utf-8');
|
|
77
|
+
expect(content).toContain('node_modules');
|
|
78
|
+
expect(content).toContain('.git');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle missing package.json gracefully', async () => {
|
|
82
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opc-empty-'));
|
|
83
|
+
const dockerfile = await deployer.generateDockerfile(emptyDir);
|
|
84
|
+
expect(dockerfile).toContain('FROM node:22-slim');
|
|
85
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should fail railway deploy without CLI installed', async () => {
|
|
89
|
+
const result = await deployer.deployRailway(tmpDir);
|
|
90
|
+
// Railway CLI likely not installed in test env
|
|
91
|
+
expect(result.platform).toBe('railway');
|
|
92
|
+
expect(typeof result.success).toBe('boolean');
|
|
93
|
+
expect(typeof result.message).toBe('string');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should fail fly deploy without CLI installed', async () => {
|
|
97
|
+
const result = await deployer.deployFly(tmpDir);
|
|
98
|
+
expect(result.platform).toBe('fly');
|
|
99
|
+
expect(typeof result.success).toBe('boolean');
|
|
100
|
+
expect(typeof result.message).toBe('string');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('Workflow JSON serialization', () => {
|
|
105
|
+
it('should serialize workflow to valid JSON', () => {
|
|
106
|
+
const workflow = {
|
|
107
|
+
id: 'wf-1',
|
|
108
|
+
name: 'Test Workflow',
|
|
109
|
+
nodes: [
|
|
110
|
+
{ id: 'n1', type: 'input', name: 'Start', x: 0, y: 0, config: {} },
|
|
111
|
+
{ id: 'n2', type: 'agent', name: 'GPT Agent', x: 200, y: 0, config: { systemPrompt: 'You are helpful', model: 'gpt-4o' } },
|
|
112
|
+
{ id: 'n3', type: 'output', name: 'End', x: 400, y: 0, config: {} },
|
|
113
|
+
],
|
|
114
|
+
edges: [
|
|
115
|
+
{ id: 'e1', from: 'n1', to: 'n2', fromPort: 'out', toPort: 'in' },
|
|
116
|
+
{ id: 'e2', from: 'n2', to: 'n3', fromPort: 'out', toPort: 'in' },
|
|
117
|
+
],
|
|
118
|
+
created: '2026-04-18T00:00:00Z',
|
|
119
|
+
updated: '2026-04-18T00:00:00Z',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const json = JSON.stringify(workflow);
|
|
123
|
+
const parsed = JSON.parse(json);
|
|
124
|
+
expect(parsed.nodes).toHaveLength(3);
|
|
125
|
+
expect(parsed.edges).toHaveLength(2);
|
|
126
|
+
expect(parsed.name).toBe('Test Workflow');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should deserialize workflow from JSON', () => {
|
|
130
|
+
const json = '{"name":"W1","nodes":[{"id":"n1","type":"input","name":"In","x":0,"y":0,"config":{}}],"edges":[]}';
|
|
131
|
+
const wf = JSON.parse(json);
|
|
132
|
+
expect(wf.name).toBe('W1');
|
|
133
|
+
expect(wf.nodes[0].type).toBe('input');
|
|
134
|
+
expect(wf.edges).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle all node types', () => {
|
|
138
|
+
const types = ['agent', 'tool', 'condition', 'loop', 'parallel', 'input', 'output'];
|
|
139
|
+
const nodes = types.map((type, i) => ({
|
|
140
|
+
id: `n${i}`, type, name: type, x: i * 200, y: 0, config: {},
|
|
141
|
+
}));
|
|
142
|
+
const json = JSON.stringify({ nodes, edges: [] });
|
|
143
|
+
const parsed = JSON.parse(json);
|
|
144
|
+
expect(parsed.nodes).toHaveLength(7);
|
|
145
|
+
for (let i = 0; i < types.length; i++) {
|
|
146
|
+
expect(parsed.nodes[i].type).toBe(types[i]);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should preserve node config through serialization', () => {
|
|
151
|
+
const node = {
|
|
152
|
+
id: 'n1', type: 'agent', name: 'Test',
|
|
153
|
+
x: 100, y: 200,
|
|
154
|
+
config: { systemPrompt: 'Be helpful', model: 'claude-3' },
|
|
155
|
+
};
|
|
156
|
+
const parsed = JSON.parse(JSON.stringify(node));
|
|
157
|
+
expect(parsed.config.systemPrompt).toBe('Be helpful');
|
|
158
|
+
expect(parsed.config.model).toBe('claude-3');
|
|
159
|
+
expect(parsed.x).toBe(100);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should validate edge references', () => {
|
|
163
|
+
const nodes = [
|
|
164
|
+
{ id: 'n1', type: 'input', name: 'In', x: 0, y: 0, config: {} },
|
|
165
|
+
{ id: 'n2', type: 'output', name: 'Out', x: 200, y: 0, config: {} },
|
|
166
|
+
];
|
|
167
|
+
const edges = [{ id: 'e1', from: 'n1', to: 'n2', fromPort: 'out', toPort: 'in' }];
|
|
168
|
+
const nodeIds = new Set(nodes.map(n => n.id));
|
|
169
|
+
for (const e of edges) {
|
|
170
|
+
expect(nodeIds.has(e.from)).toBe(true);
|
|
171
|
+
expect(nodeIds.has(e.to)).toBe(true);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('Deploy config validation', () => {
|
|
177
|
+
it('should accept valid deploy options', () => {
|
|
178
|
+
const opts = { port: 3000, platform: 'docker' as const, replicas: 1 };
|
|
179
|
+
expect(opts.port).toBeGreaterThan(0);
|
|
180
|
+
expect(opts.port).toBeLessThan(65536);
|
|
181
|
+
expect(['docker', 'railway', 'fly', 'render']).toContain(opts.platform);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should use defaults for missing options', () => {
|
|
185
|
+
const defaults = { port: 3000, platform: 'docker', replicas: 1 };
|
|
186
|
+
const opts = { ...defaults };
|
|
187
|
+
expect(opts.port).toBe(3000);
|
|
188
|
+
expect(opts.replicas).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle env vars in deploy options', () => {
|
|
192
|
+
const opts = { env: { NODE_ENV: 'production', API_KEY: 'secret' } };
|
|
193
|
+
expect(Object.keys(opts.env)).toHaveLength(2);
|
|
194
|
+
expect(opts.env.NODE_ENV).toBe('production');
|
|
195
|
+
});
|
|
196
|
+
});
|