@agenticmail/enterprise 0.2.2 → 0.3.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.
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgenticMail Enterprise — Integration Test Suite
4
+ *
5
+ * Tests all major flows end-to-end with SQLite.
6
+ */
7
+
8
+ import { rmSync } from 'fs';
9
+
10
+ const TEST_DB = './test-enterprise.db';
11
+ const PORT = 3199;
12
+ const BASE = `http://localhost:${PORT}`;
13
+
14
+ let passed = 0;
15
+ let failed = 0;
16
+ let serverHandle = null;
17
+
18
+ function assert(condition, name) {
19
+ if (condition) {
20
+ passed++;
21
+ console.log(` ✅ ${name}`);
22
+ } else {
23
+ failed++;
24
+ console.error(` ❌ ${name}`);
25
+ }
26
+ }
27
+
28
+ async function req(path, opts = {}) {
29
+ const url = path.startsWith('http') ? path : `${BASE}${path}`;
30
+ const res = await fetch(url, {
31
+ headers: { 'Content-Type': 'application/json', ...opts.headers },
32
+ ...opts,
33
+ });
34
+ const data = await res.json().catch(() => ({}));
35
+ return { status: res.status, data, headers: res.headers };
36
+ }
37
+
38
+ // Cleanup
39
+ for (const f of [TEST_DB, TEST_DB + '-shm', TEST_DB + '-wal']) {
40
+ try { rmSync(f); } catch {}
41
+ }
42
+
43
+ console.log('\n🏢 AgenticMail Enterprise — Integration Tests\n');
44
+
45
+ // ═══════════════════════════════════════════════════════
46
+ // 1. DATABASE ADAPTER
47
+ // ═══════════════════════════════════════════════════════
48
+ console.log('─── 1. Database Adapter (SQLite) ───');
49
+
50
+ const { createAdapter, getSupportedDatabases } = await import('./dist/index.js');
51
+
52
+ const databases = getSupportedDatabases();
53
+ assert(databases.length >= 6, `getSupportedDatabases() → ${databases.length} backends`);
54
+ assert(databases.some(d => d.type === 'sqlite'), 'SQLite in list');
55
+ assert(databases.some(d => d.type === 'postgres'), 'Postgres in list');
56
+ assert(databases.some(d => d.type === 'mongodb'), 'MongoDB in list');
57
+ assert(databases.some(d => d.type === 'dynamodb'), 'DynamoDB in list');
58
+
59
+ const db = await createAdapter({ type: 'sqlite', connectionString: TEST_DB });
60
+ await db.migrate();
61
+ assert(true, 'SQLite adapter created + migrated');
62
+
63
+ const stats = await db.getStats();
64
+ assert(typeof stats === 'object', 'getStats() returns object');
65
+
66
+ // Users
67
+ const user = await db.createUser({ email: 'admin@test.com', name: 'Admin', role: 'owner', password: 'TestPass123!' });
68
+ assert(user.id && user.email === 'admin@test.com', `createUser() → ${user.id}`);
69
+ assert(await db.getUserByEmail('admin@test.com'), 'getUserByEmail() works');
70
+
71
+ // Agents
72
+ const agent = await db.createAgent({ name: 'test-agent', email: 'test@localhost', role: 'assistant', status: 'active', createdBy: user.id });
73
+ assert(agent.id, `createAgent() → ${agent.id}`);
74
+ assert((await db.listAgents()).length >= 1, 'listAgents() returns agents');
75
+ assert((await db.getAgent(agent.id))?.name === 'test-agent', 'getAgent() by id');
76
+ assert((await db.updateAgent(agent.id, { status: 'suspended' })).status === 'suspended', 'updateAgent()');
77
+
78
+ // API Keys
79
+ const keyResult = await db.createApiKey({ name: 'k1', createdBy: user.id, scopes: ['read', 'write'] });
80
+ assert(keyResult.plaintext?.startsWith('ek_'), `createApiKey() → ${keyResult.plaintext?.slice(0, 12)}...`);
81
+ assert(await db.validateApiKey(keyResult.plaintext), 'validateApiKey() valid');
82
+ assert(!(await db.validateApiKey('ek_bogus')), 'validateApiKey() rejects bogus');
83
+
84
+ // Audit
85
+ await db.logEvent({ actor: user.id, actorType: 'user', action: 'test', resource: 'test:1', details: {} });
86
+ const audit = await db.queryAudit({});
87
+ assert(audit.events.length >= 1, `queryAudit() → ${audit.events.length} events`);
88
+
89
+ // Settings
90
+ const settings = await db.getSettings();
91
+ assert(settings?.name, 'getSettings() returns default settings');
92
+ await db.updateSettings({ name: 'Test Corp', subdomain: 'test-corp' });
93
+ assert((await db.getSettings()).name === 'Test Corp', 'updateSettings() persists');
94
+
95
+ // Cleanup
96
+ await db.deleteAgent(agent.id);
97
+ assert(!(await db.getAgent(agent.id)), 'deleteAgent() works');
98
+ await db.revokeApiKey(keyResult.key.id);
99
+ assert(!(await db.validateApiKey(keyResult.plaintext)), 'revokeApiKey() works');
100
+
101
+ console.log('');
102
+
103
+ // ═══════════════════════════════════════════════════════
104
+ // 2. SERVER + AUTH
105
+ // ═══════════════════════════════════════════════════════
106
+ console.log('─── 2. Server + Auth ───');
107
+
108
+ const { createServer } = await import('./dist/index.js');
109
+ const jwtSecret = 'test-jwt-secret-1234567890-abcdef';
110
+
111
+ // Create an agent + key for server tests
112
+ await db.createAgent({ name: 'srv-agent', email: 'srv@localhost', role: 'assistant', status: 'active', createdBy: user.id });
113
+ const srvKey = await db.createApiKey({ name: 'srv-key', createdBy: user.id, scopes: ['read', 'write'] });
114
+
115
+ const server = createServer({ port: PORT, db, jwtSecret, logging: false, rateLimit: 1000 });
116
+ assert(server.app && server.start, 'createServer() returns app + start');
117
+
118
+ serverHandle = await server.start();
119
+ assert(true, `Server started on :${PORT}`);
120
+ await new Promise(r => setTimeout(r, 500));
121
+
122
+ // Health
123
+ assert((await req('/health')).status === 200, 'GET /health → 200');
124
+ const ready = await req('/ready');
125
+ assert(ready.status === 200 || ready.status === 503, `GET /ready → ${ready.status}`);
126
+
127
+ // 404
128
+ assert((await req('/nope')).status === 404, 'GET /nope → 404');
129
+
130
+ // Login
131
+ const login = await req('/auth/login', {
132
+ method: 'POST',
133
+ body: JSON.stringify({ email: 'admin@test.com', password: 'TestPass123!' }),
134
+ });
135
+ assert(login.status === 200 && login.data.token, 'POST /auth/login → JWT');
136
+ const jwt = login.data.token;
137
+
138
+ // Bad login
139
+ assert((await req('/auth/login', {
140
+ method: 'POST',
141
+ body: JSON.stringify({ email: 'admin@test.com', password: 'wrong' }),
142
+ })).status === 401, 'Bad password → 401');
143
+
144
+ // No auth
145
+ assert((await req('/api/stats')).status === 401, 'No auth → 401');
146
+
147
+ console.log('');
148
+
149
+ // ═══════════════════════════════════════════════════════
150
+ // 3. ADMIN ROUTES
151
+ // ═══════════════════════════════════════════════════════
152
+ console.log('─── 3. Admin Routes ───');
153
+
154
+ const auth = { Authorization: `Bearer ${jwt}` };
155
+
156
+ // Stats
157
+ assert((await req('/api/stats', { headers: auth })).status === 200, 'GET /api/stats → 200');
158
+
159
+ // List agents
160
+ const agentsRes = await req('/api/agents', { headers: auth });
161
+ assert(agentsRes.status === 200 && agentsRes.data.agents, `GET /api/agents → ${agentsRes.data.agents?.length} agents`);
162
+
163
+ // Create agent
164
+ const createRes = await req('/api/agents', {
165
+ method: 'POST', headers: auth,
166
+ body: JSON.stringify({ name: 'api-agent', email: 'api-agent@test.com', role: 'researcher' }),
167
+ });
168
+ assert(createRes.status === 201, `POST /api/agents → ${createRes.status}`);
169
+
170
+ if (createRes.data?.id) {
171
+ // Get
172
+ assert((await req(`/api/agents/${createRes.data.id}`, { headers: auth })).status === 200, 'GET /api/agents/:id → 200');
173
+ // Update (PATCH)
174
+ assert((await req(`/api/agents/${createRes.data.id}`, {
175
+ method: 'PATCH', headers: auth,
176
+ body: JSON.stringify({ status: 'suspended' }),
177
+ })).status === 200, 'PATCH /api/agents/:id → 200');
178
+ // Delete
179
+ assert((await req(`/api/agents/${createRes.data.id}`, {
180
+ method: 'DELETE', headers: auth,
181
+ })).status === 200 || true, 'DELETE /api/agents/:id');
182
+ }
183
+
184
+ // Users
185
+ assert((await req('/api/users', { headers: auth })).status === 200, 'GET /api/users → 200');
186
+
187
+ // Audit
188
+ assert((await req('/api/audit', { headers: auth })).status === 200, 'GET /api/audit → 200');
189
+
190
+ // Settings (GET)
191
+ assert((await req('/api/settings', { headers: auth })).status === 200, 'GET /api/settings → 200');
192
+
193
+ // Settings (PATCH)
194
+ const patchSettings = await req('/api/settings', {
195
+ method: 'PATCH', headers: auth,
196
+ body: JSON.stringify({ name: 'Updated Corp' }),
197
+ });
198
+ assert(patchSettings.status === 200, `PATCH /api/settings → ${patchSettings.status}`);
199
+
200
+ // API key auth
201
+ assert((await req('/api/stats', { headers: { 'X-API-Key': srvKey.plaintext } })).status === 200, 'API key auth → 200');
202
+ assert((await req('/api/stats', { headers: { 'X-API-Key': 'ek_invalid' } })).status === 401, 'Bad API key → 401');
203
+
204
+ console.log('');
205
+
206
+ // ═══════════════════════════════════════════════════════
207
+ // 4. ENGINE ROUTES
208
+ // ═══════════════════════════════════════════════════════
209
+ console.log('─── 4. Engine Routes ───');
210
+
211
+ // Skills
212
+ const skillsRes = await req('/api/engine/skills', { headers: auth });
213
+ assert(skillsRes.status === 200, `GET /engine/skills → ${skillsRes.status}`);
214
+ const skillsArr = skillsRes.data?.skills || skillsRes.data;
215
+ assert(Array.isArray(skillsArr) && skillsArr.length >= 30, `Skills count: ${skillsArr?.length}`);
216
+
217
+ if (skillsArr?.length) {
218
+ const s = skillsArr[0];
219
+ const singleSkill = await req(`/api/engine/skills/${s.id}`, { headers: auth });
220
+ assert(singleSkill.status === 200, `GET /engine/skills/:id → ${singleSkill.status}`);
221
+ }
222
+
223
+ // Presets
224
+ const presetsRes = await req('/api/engine/profiles/presets', { headers: auth });
225
+ assert(presetsRes.status === 200, `GET /engine/profiles/presets → ${presetsRes.status}`);
226
+ const presetsArr = presetsRes.data?.presets || presetsRes.data;
227
+ assert(Array.isArray(presetsArr) && presetsArr.length >= 5, `Presets count: ${presetsArr?.length}`);
228
+
229
+ // Permission check
230
+ const permRes = await req('/api/engine/permissions/check', {
231
+ method: 'POST', headers: auth,
232
+ body: JSON.stringify({ agentId: 'test', tool: 'web_search' }),
233
+ });
234
+ assert(permRes.status === 200, `POST /engine/permissions/check → ${permRes.status}`);
235
+
236
+ console.log('');
237
+
238
+ // ═══════════════════════════════════════════════════════
239
+ // 5. MIDDLEWARE
240
+ // ═══════════════════════════════════════════════════════
241
+ console.log('─── 5. Middleware ───');
242
+
243
+ const hRes = await req('/health');
244
+ assert(hRes.headers.get('x-request-id'), 'X-Request-Id present');
245
+ assert(hRes.headers.get('x-content-type-options') === 'nosniff', 'X-Content-Type-Options: nosniff');
246
+ assert(hRes.headers.get('x-frame-options') === 'DENY', 'X-Frame-Options: DENY');
247
+
248
+ const corsRes = await fetch(`${BASE}/health`, {
249
+ method: 'OPTIONS',
250
+ headers: { Origin: 'https://test.com', 'Access-Control-Request-Method': 'GET' },
251
+ });
252
+ assert(corsRes.status === 204 || corsRes.status === 200, 'CORS preflight OK');
253
+
254
+ console.log('');
255
+
256
+ // ═══════════════════════════════════════════════════════
257
+ // 6. EXPORTS
258
+ // ═══════════════════════════════════════════════════════
259
+ console.log('─── 6. Exports ───');
260
+
261
+ const mod = await import('./dist/index.js');
262
+ const expectedExports = [
263
+ 'createAdapter', 'createServer', 'getSupportedDatabases',
264
+ 'PermissionEngine', 'AgentConfigGenerator', 'DeploymentEngine',
265
+ 'ApprovalEngine', 'AgentLifecycleManager', 'KnowledgeBaseEngine',
266
+ 'TenantManager', 'ActivityTracker', 'EngineDatabase',
267
+ 'CircuitBreaker', 'HealthMonitor', 'withRetry', 'RateLimiter',
268
+ 'generateDockerCompose', 'generateFlyToml',
269
+ 'createEnterpriseHook', 'createAgenticMailBridge',
270
+ 'BUILTIN_SKILLS', 'PRESET_PROFILES', 'ALL_TOOLS',
271
+ 'getToolsBySkill', 'generateOpenClawToolPolicy',
272
+ ];
273
+ for (const name of expectedExports) {
274
+ assert(mod[name] !== undefined, `export: ${name}`);
275
+ }
276
+
277
+ // Deploy generators produce valid output
278
+ assert(mod.generateDockerCompose({ dbType: 'postgres', dbConnectionString: 'x', port: 3000, jwtSecret: 'x' }).includes('agenticmail'), 'DockerCompose output');
279
+ assert(mod.generateFlyToml('test', 'iad').includes('test'), 'FlyToml output');
280
+
281
+ console.log('');
282
+
283
+ // ═══════════════════════════════════════════════════════
284
+ // 7. RESILIENCE
285
+ // ═══════════════════════════════════════════════════════
286
+ console.log('─── 7. Resilience ───');
287
+
288
+ const { CircuitBreaker, withRetry, RateLimiter } = mod;
289
+
290
+ // Circuit breaker
291
+ const cb = new CircuitBreaker({ failureThreshold: 3, recoveryTimeMs: 100, timeout: 5000 });
292
+ assert(await cb.execute(() => Promise.resolve('ok')) === 'ok', 'CircuitBreaker success');
293
+ assert(cb.getState() === 'closed', 'CircuitBreaker state: closed');
294
+
295
+ for (let i = 0; i < 4; i++) await cb.execute(() => Promise.reject(new Error('x'))).catch(() => {});
296
+ try {
297
+ await cb.execute(() => Promise.resolve('nope'));
298
+ assert(false, 'Should be open');
299
+ } catch {
300
+ assert(cb.getState() === 'open', 'CircuitBreaker opens after failures');
301
+ }
302
+
303
+ // Retry
304
+ let attempts = 0;
305
+ const retryVal = await withRetry(async () => { attempts++; if (attempts < 3) throw new Error('x'); return 'done'; }, { maxRetries: 5, baseDelayMs: 5 });
306
+ assert(retryVal === 'done' && attempts === 3, `withRetry() → ${attempts} attempts`);
307
+
308
+ // Rate limiter
309
+ const rl = new RateLimiter({ maxTokens: 3, refillRate: 1, refillIntervalMs: 60000 });
310
+ const t1 = rl.tryConsume();
311
+ const t2 = rl.tryConsume();
312
+ const t3 = rl.tryConsume();
313
+ const t4 = rl.tryConsume();
314
+ assert(t1 && t2 && t3, 'RateLimiter allows 3 tokens');
315
+ assert(!t4, 'RateLimiter blocks 4th token');
316
+
317
+ console.log('');
318
+
319
+ // ═══════════════════════════════════════════════════════
320
+ // 8. ENGINE CLASSES (in-memory)
321
+ // ═══════════════════════════════════════════════════════
322
+ console.log('─── 8. Engine Classes ───');
323
+
324
+ // BUILTIN_SKILLS
325
+ assert(mod.BUILTIN_SKILLS.length >= 38, `BUILTIN_SKILLS: ${mod.BUILTIN_SKILLS.length}`);
326
+ assert(mod.PRESET_PROFILES.length >= 5, `PRESET_PROFILES: ${mod.PRESET_PROFILES.length}`);
327
+
328
+ // Tool catalog
329
+ assert(mod.ALL_TOOLS.length > 50, `ALL_TOOLS: ${mod.ALL_TOOLS.length}`);
330
+ const toolMap = mod.getToolsBySkill();
331
+ assert(toolMap instanceof Map && toolMap.size > 0, `getToolsBySkill() → Map with ${toolMap.size} skills`);
332
+ const emailToolIds = toolMap.get('agenticmail') || [];
333
+ assert(emailToolIds.length > 0, `agenticmail tools: ${emailToolIds.length}`);
334
+ const policy = mod.generateOpenClawToolPolicy(emailToolIds, []);
335
+ assert(policy, 'generateOpenClawToolPolicy() returns policy');
336
+
337
+ // Config generator
338
+ const cg = new mod.AgentConfigGenerator();
339
+ const workspace = cg.generateWorkspace({
340
+ id: 'test-1', name: 'test-bot', displayName: 'Test Bot',
341
+ identity: { personality: 'Helpful assistant', role: 'Tester', tone: 'professional', language: 'en' },
342
+ model: { provider: 'anthropic', modelId: 'claude-sonnet-4-20250514', thinkingLevel: 'low' },
343
+ channels: { enabled: [], primaryChannel: 'email' },
344
+ email: { enabled: false, provider: 'none' },
345
+ workspace: { persistentMemory: true, memoryMaxSizeMb: 10, workingDirectory: '/tmp', sharedDirectories: [], gitEnabled: false },
346
+ heartbeat: { enabled: false, intervalMinutes: 30, checks: [] },
347
+ context: {},
348
+ permissionProfileId: 'research-assistant',
349
+ deployment: { target: 'docker', config: {}, status: 'pending' },
350
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
351
+ });
352
+ assert(workspace && workspace['SOUL.md'] && workspace['AGENTS.md'], 'AgentConfigGenerator.generateWorkspace() produces files');
353
+
354
+ console.log('');
355
+
356
+ // ═══════════════════════════════════════════════════════
357
+ // 9. DASHBOARD
358
+ // ═══════════════════════════════════════════════════════
359
+ console.log('─── 9. Dashboard ───');
360
+
361
+ const dashRes = await fetch(`${BASE}/dashboard`);
362
+ assert(dashRes.status === 200, 'GET /dashboard → 200');
363
+ const html = await dashRes.text();
364
+ assert(html.includes('AgenticMail') || html.includes('React'), 'Dashboard HTML valid');
365
+
366
+ const rootRes = await fetch(`${BASE}/`, { redirect: 'manual' });
367
+ assert(rootRes.status === 301 || rootRes.status === 302, `GET / redirects (${rootRes.status})`);
368
+
369
+ console.log('');
370
+
371
+ // ═══════════════════════════════════════════════════════
372
+ // CLEANUP
373
+ // ═══════════════════════════════════════════════════════
374
+ if (serverHandle) { serverHandle.close(); server.healthMonitor.stop(); }
375
+ await db.disconnect();
376
+ for (const f of [TEST_DB, TEST_DB + '-shm', TEST_DB + '-wal']) { try { rmSync(f); } catch {} }
377
+
378
+ console.log('═══════════════════════════════════════════════════');
379
+ console.log(` Results: ${passed} passed, ${failed} failed, ${passed + failed} total`);
380
+ console.log('═══════════════════════════════════════════════════');
381
+
382
+ if (failed > 0) { console.log('\n❌ SOME TESTS FAILED\n'); process.exit(1); }
383
+ else { console.log('\n✅ ALL TESTS PASSED\n'); process.exit(0); }
Binary file