@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.
- package/README.md +929 -0
- package/dashboards/dotnet/Program.cs +5 -5
- package/dashboards/express/app.js +5 -5
- package/dashboards/go/main.go +8 -8
- package/dashboards/html/index.html +41 -18
- package/dashboards/java/AgenticMailDashboard.java +5 -5
- package/dashboards/php/index.php +8 -8
- package/dashboards/python/app.py +8 -8
- package/dashboards/ruby/app.rb +7 -7
- package/dashboards/shared-styles.css +57 -0
- package/dist/chunk-BE7MXVLA.js +757 -0
- package/dist/chunk-BS2WCSHO.js +48 -0
- package/dist/chunk-FL3VQBGL.js +757 -0
- package/dist/chunk-GXIEEA2T.js +48 -0
- package/dist/chunk-JLSQOQ5L.js +255 -0
- package/dist/chunk-TVF23PUW.js +338 -0
- package/dist/cli.js +305 -140
- package/dist/dashboard/index.html +833 -510
- package/dist/factory-HINWFYZ3.js +9 -0
- package/dist/factory-V37IG5AT.js +9 -0
- package/dist/index.js +18 -12
- package/dist/managed-RZITNPXG.js +14 -0
- package/dist/server-32YYCI3A.js +8 -0
- package/dist/server-H3C6WUOS.js +8 -0
- package/dist/sqlite-VLKVAJA4.js +442 -0
- package/package.json +18 -2
- package/src/cli.ts +15 -251
- package/src/dashboard/index.html +833 -510
- package/src/db/sqlite.ts +4 -1
- package/src/server.ts +1 -1
- package/src/setup/company.ts +64 -0
- package/src/setup/database.ts +119 -0
- package/src/setup/deployment.ts +50 -0
- package/src/setup/domain.ts +46 -0
- package/src/setup/index.ts +82 -0
- package/src/setup/provision.ts +226 -0
- package/test-integration.mjs +383 -0
- package/agenticmail-enterprise.db +0 -0
|
@@ -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
|