@tjamescouch/agentchat 0.1.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/.claude/settings.local.json +12 -0
- package/.github/workflows/fly-deploy.yml +18 -0
- package/Dockerfile +12 -0
- package/README.md +296 -0
- package/ROADMAP.md +88 -0
- package/SPEC.md +279 -0
- package/bin/agentchat.js +702 -0
- package/fly.toml +21 -0
- package/lib/client.js +362 -0
- package/lib/deploy/akash.js +811 -0
- package/lib/deploy/config.js +128 -0
- package/lib/deploy/index.js +149 -0
- package/lib/identity.js +166 -0
- package/lib/protocol.js +236 -0
- package/lib/server.js +526 -0
- package/package.json +44 -0
- package/quick-test.sh +45 -0
- package/test/integration.test.js +536 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentChat Integration Tests
|
|
3
|
+
* Run with: node --test test/integration.test.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { test, describe, before, after } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { AgentChatServer } from '../lib/server.js';
|
|
12
|
+
import { AgentChatClient } from '../lib/client.js';
|
|
13
|
+
import { Identity, isValidPubkey, pubkeyToAgentId } from '../lib/identity.js';
|
|
14
|
+
import { validateConfig, DEFAULT_CONFIG } from '../lib/deploy/config.js';
|
|
15
|
+
import { deployToDocker, generateDockerfile } from '../lib/deploy/index.js';
|
|
16
|
+
|
|
17
|
+
describe('AgentChat', () => {
|
|
18
|
+
let server;
|
|
19
|
+
const PORT = 16667; // Use non-standard port for testing
|
|
20
|
+
const SERVER_URL = `ws://localhost:${PORT}`;
|
|
21
|
+
|
|
22
|
+
before(() => {
|
|
23
|
+
server = new AgentChatServer({ port: PORT });
|
|
24
|
+
server.start();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
after(() => {
|
|
28
|
+
server.stop();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('client can connect and identify', async () => {
|
|
32
|
+
const client = new AgentChatClient({
|
|
33
|
+
server: SERVER_URL,
|
|
34
|
+
name: 'test-agent'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await client.connect();
|
|
38
|
+
|
|
39
|
+
assert.ok(client.connected);
|
|
40
|
+
assert.ok(client.agentId);
|
|
41
|
+
assert.ok(client.agentId.startsWith('@'));
|
|
42
|
+
|
|
43
|
+
client.disconnect();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('client can join channel', async () => {
|
|
47
|
+
const client = new AgentChatClient({
|
|
48
|
+
server: SERVER_URL,
|
|
49
|
+
name: 'test-agent'
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await client.connect();
|
|
53
|
+
const result = await client.join('#general');
|
|
54
|
+
|
|
55
|
+
assert.equal(result.channel, '#general');
|
|
56
|
+
assert.ok(Array.isArray(result.agents));
|
|
57
|
+
|
|
58
|
+
client.disconnect();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('two clients can communicate', async () => {
|
|
62
|
+
const client1 = new AgentChatClient({
|
|
63
|
+
server: SERVER_URL,
|
|
64
|
+
name: 'agent-1'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const client2 = new AgentChatClient({
|
|
68
|
+
server: SERVER_URL,
|
|
69
|
+
name: 'agent-2'
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await client1.connect();
|
|
73
|
+
await client2.connect();
|
|
74
|
+
|
|
75
|
+
await client1.join('#general');
|
|
76
|
+
await client2.join('#general');
|
|
77
|
+
|
|
78
|
+
// Set up listener
|
|
79
|
+
const received = new Promise((resolve) => {
|
|
80
|
+
client2.on('message', (msg) => {
|
|
81
|
+
if (msg.content === 'hello from agent-1') {
|
|
82
|
+
resolve(msg);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Send message
|
|
88
|
+
await client1.send('#general', 'hello from agent-1');
|
|
89
|
+
|
|
90
|
+
// Wait for message
|
|
91
|
+
const msg = await received;
|
|
92
|
+
|
|
93
|
+
assert.equal(msg.from, client1.agentId);
|
|
94
|
+
assert.equal(msg.to, '#general');
|
|
95
|
+
assert.equal(msg.content, 'hello from agent-1');
|
|
96
|
+
|
|
97
|
+
client1.disconnect();
|
|
98
|
+
client2.disconnect();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('direct messages work', async () => {
|
|
102
|
+
const client1 = new AgentChatClient({
|
|
103
|
+
server: SERVER_URL,
|
|
104
|
+
name: 'agent-1'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const client2 = new AgentChatClient({
|
|
108
|
+
server: SERVER_URL,
|
|
109
|
+
name: 'agent-2'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await client1.connect();
|
|
113
|
+
await client2.connect();
|
|
114
|
+
|
|
115
|
+
// Set up listener
|
|
116
|
+
const received = new Promise((resolve) => {
|
|
117
|
+
client2.on('message', (msg) => {
|
|
118
|
+
if (msg.content === 'private hello') {
|
|
119
|
+
resolve(msg);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Send DM
|
|
125
|
+
await client1.dm(client2.agentId, 'private hello');
|
|
126
|
+
|
|
127
|
+
// Wait for message
|
|
128
|
+
const msg = await received;
|
|
129
|
+
|
|
130
|
+
assert.equal(msg.from, client1.agentId);
|
|
131
|
+
assert.equal(msg.to, client2.agentId);
|
|
132
|
+
assert.equal(msg.content, 'private hello');
|
|
133
|
+
|
|
134
|
+
client1.disconnect();
|
|
135
|
+
client2.disconnect();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('can list channels', async () => {
|
|
139
|
+
const client = new AgentChatClient({
|
|
140
|
+
server: SERVER_URL,
|
|
141
|
+
name: 'test-agent'
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await client.connect();
|
|
145
|
+
const channels = await client.listChannels();
|
|
146
|
+
|
|
147
|
+
assert.ok(Array.isArray(channels));
|
|
148
|
+
assert.ok(channels.some(ch => ch.name === '#general'));
|
|
149
|
+
assert.ok(channels.some(ch => ch.name === '#agents'));
|
|
150
|
+
|
|
151
|
+
client.disconnect();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('can create private channel', async () => {
|
|
155
|
+
const client = new AgentChatClient({
|
|
156
|
+
server: SERVER_URL,
|
|
157
|
+
name: 'test-agent'
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await client.connect();
|
|
161
|
+
|
|
162
|
+
const channelName = `#private-${Date.now()}`;
|
|
163
|
+
await client.createChannel(channelName, true);
|
|
164
|
+
|
|
165
|
+
assert.ok(client.channels.has(channelName));
|
|
166
|
+
|
|
167
|
+
client.disconnect();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('Identity', () => {
|
|
172
|
+
const testDir = path.join(os.tmpdir(), `agentchat-test-${Date.now()}`);
|
|
173
|
+
const testIdentityPath = path.join(testDir, 'identity.json');
|
|
174
|
+
|
|
175
|
+
after(async () => {
|
|
176
|
+
// Cleanup
|
|
177
|
+
try {
|
|
178
|
+
await fs.rm(testDir, { recursive: true });
|
|
179
|
+
} catch {}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('can generate identity', () => {
|
|
183
|
+
const identity = Identity.generate('test-agent');
|
|
184
|
+
|
|
185
|
+
assert.equal(identity.name, 'test-agent');
|
|
186
|
+
assert.ok(identity.pubkey);
|
|
187
|
+
assert.ok(identity.privkey);
|
|
188
|
+
assert.ok(identity.created);
|
|
189
|
+
assert.ok(isValidPubkey(identity.pubkey));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('can save and load identity', async () => {
|
|
193
|
+
const identity = Identity.generate('test-agent');
|
|
194
|
+
await identity.save(testIdentityPath);
|
|
195
|
+
|
|
196
|
+
const loaded = await Identity.load(testIdentityPath);
|
|
197
|
+
|
|
198
|
+
assert.equal(loaded.name, identity.name);
|
|
199
|
+
assert.equal(loaded.pubkey, identity.pubkey);
|
|
200
|
+
assert.equal(loaded.privkey, identity.privkey);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('can sign and verify', () => {
|
|
204
|
+
const identity = Identity.generate('test-agent');
|
|
205
|
+
const message = 'hello world';
|
|
206
|
+
|
|
207
|
+
const signature = identity.sign(message);
|
|
208
|
+
assert.ok(signature);
|
|
209
|
+
|
|
210
|
+
const verified = Identity.verify(message, signature, identity.pubkey);
|
|
211
|
+
assert.ok(verified);
|
|
212
|
+
|
|
213
|
+
// Verify fails with wrong message
|
|
214
|
+
const wrongVerify = Identity.verify('wrong message', signature, identity.pubkey);
|
|
215
|
+
assert.ok(!wrongVerify);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('fingerprint is consistent', () => {
|
|
219
|
+
const identity = Identity.generate('test-agent');
|
|
220
|
+
const fp1 = identity.getFingerprint();
|
|
221
|
+
const fp2 = identity.getFingerprint();
|
|
222
|
+
|
|
223
|
+
assert.equal(fp1, fp2);
|
|
224
|
+
assert.equal(fp1.length, 16);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('pubkeyToAgentId generates stable IDs', () => {
|
|
228
|
+
const identity = Identity.generate('test-agent');
|
|
229
|
+
const id1 = pubkeyToAgentId(identity.pubkey);
|
|
230
|
+
const id2 = pubkeyToAgentId(identity.pubkey);
|
|
231
|
+
|
|
232
|
+
assert.equal(id1, id2);
|
|
233
|
+
assert.equal(id1.length, 8);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('export excludes private key', () => {
|
|
237
|
+
const identity = Identity.generate('test-agent');
|
|
238
|
+
const exported = identity.export();
|
|
239
|
+
|
|
240
|
+
assert.equal(exported.name, identity.name);
|
|
241
|
+
assert.equal(exported.pubkey, identity.pubkey);
|
|
242
|
+
assert.equal(exported.privkey, undefined);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('AgentChat with Identity', () => {
|
|
247
|
+
let server;
|
|
248
|
+
const PORT = 16668; // Different port for identity tests
|
|
249
|
+
const SERVER_URL = `ws://localhost:${PORT}`;
|
|
250
|
+
const testDir = path.join(os.tmpdir(), `agentchat-identity-test-${Date.now()}`);
|
|
251
|
+
|
|
252
|
+
before(async () => {
|
|
253
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
254
|
+
server = new AgentChatServer({ port: PORT });
|
|
255
|
+
server.start();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
after(async () => {
|
|
259
|
+
server.stop();
|
|
260
|
+
try {
|
|
261
|
+
await fs.rm(testDir, { recursive: true });
|
|
262
|
+
} catch {}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('client with identity gets stable ID', async () => {
|
|
266
|
+
// Create identity file
|
|
267
|
+
const identity = Identity.generate('persistent-agent');
|
|
268
|
+
const identityPath = path.join(testDir, 'test-identity.json');
|
|
269
|
+
await identity.save(identityPath);
|
|
270
|
+
|
|
271
|
+
// First connection
|
|
272
|
+
const client1 = new AgentChatClient({
|
|
273
|
+
server: SERVER_URL,
|
|
274
|
+
identity: identityPath
|
|
275
|
+
});
|
|
276
|
+
await client1.connect();
|
|
277
|
+
const id1 = client1.agentId;
|
|
278
|
+
client1.disconnect();
|
|
279
|
+
|
|
280
|
+
// Wait a bit
|
|
281
|
+
await new Promise(r => setTimeout(r, 100));
|
|
282
|
+
|
|
283
|
+
// Second connection with same identity
|
|
284
|
+
const client2 = new AgentChatClient({
|
|
285
|
+
server: SERVER_URL,
|
|
286
|
+
identity: identityPath
|
|
287
|
+
});
|
|
288
|
+
await client2.connect();
|
|
289
|
+
const id2 = client2.agentId;
|
|
290
|
+
client2.disconnect();
|
|
291
|
+
|
|
292
|
+
// Should have same ID
|
|
293
|
+
assert.equal(id1, id2);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('ephemeral clients get different IDs', async () => {
|
|
297
|
+
const client1 = new AgentChatClient({
|
|
298
|
+
server: SERVER_URL,
|
|
299
|
+
name: 'ephemeral-1'
|
|
300
|
+
});
|
|
301
|
+
await client1.connect();
|
|
302
|
+
const id1 = client1.agentId;
|
|
303
|
+
client1.disconnect();
|
|
304
|
+
|
|
305
|
+
await new Promise(r => setTimeout(r, 100));
|
|
306
|
+
|
|
307
|
+
const client2 = new AgentChatClient({
|
|
308
|
+
server: SERVER_URL,
|
|
309
|
+
name: 'ephemeral-2'
|
|
310
|
+
});
|
|
311
|
+
await client2.connect();
|
|
312
|
+
const id2 = client2.agentId;
|
|
313
|
+
client2.disconnect();
|
|
314
|
+
|
|
315
|
+
// Different IDs (overwhelming probability)
|
|
316
|
+
assert.notEqual(id1, id2);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('signed messages include signature', async () => {
|
|
320
|
+
const identity = Identity.generate('signing-agent');
|
|
321
|
+
const identityPath = path.join(testDir, 'signing-identity.json');
|
|
322
|
+
await identity.save(identityPath);
|
|
323
|
+
|
|
324
|
+
const sender = new AgentChatClient({
|
|
325
|
+
server: SERVER_URL,
|
|
326
|
+
identity: identityPath
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const receiver = new AgentChatClient({
|
|
330
|
+
server: SERVER_URL,
|
|
331
|
+
name: 'receiver'
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await sender.connect();
|
|
335
|
+
await receiver.connect();
|
|
336
|
+
await sender.join('#general');
|
|
337
|
+
await receiver.join('#general');
|
|
338
|
+
|
|
339
|
+
const received = new Promise((resolve) => {
|
|
340
|
+
receiver.on('message', (msg) => {
|
|
341
|
+
if (msg.content === 'signed hello') {
|
|
342
|
+
resolve(msg);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await sender.send('#general', 'signed hello');
|
|
348
|
+
|
|
349
|
+
const msg = await received;
|
|
350
|
+
|
|
351
|
+
assert.equal(msg.content, 'signed hello');
|
|
352
|
+
assert.ok(msg.sig, 'Message should have signature');
|
|
353
|
+
|
|
354
|
+
sender.disconnect();
|
|
355
|
+
receiver.disconnect();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('duplicate identity connection is rejected', async () => {
|
|
359
|
+
const identity = Identity.generate('unique-agent');
|
|
360
|
+
const identityPath = path.join(testDir, 'unique-identity.json');
|
|
361
|
+
await identity.save(identityPath);
|
|
362
|
+
|
|
363
|
+
// First connection
|
|
364
|
+
const client1 = new AgentChatClient({
|
|
365
|
+
server: SERVER_URL,
|
|
366
|
+
identity: identityPath
|
|
367
|
+
});
|
|
368
|
+
await client1.connect();
|
|
369
|
+
|
|
370
|
+
// Second connection with same identity should fail
|
|
371
|
+
const client2 = new AgentChatClient({
|
|
372
|
+
server: SERVER_URL,
|
|
373
|
+
identity: identityPath
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
let errorReceived = false;
|
|
377
|
+
client2.on('error', () => {
|
|
378
|
+
errorReceived = true;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
await client2.connect();
|
|
383
|
+
} catch {
|
|
384
|
+
errorReceived = true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Cleanup
|
|
388
|
+
client1.disconnect();
|
|
389
|
+
if (client2.ws) client2.disconnect();
|
|
390
|
+
|
|
391
|
+
// Note: The error handling may vary, but duplicate should not succeed silently
|
|
392
|
+
// This test ensures we handle the case
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('Deploy Configuration', () => {
|
|
397
|
+
test('validates correct config', () => {
|
|
398
|
+
const config = validateConfig({
|
|
399
|
+
provider: 'docker',
|
|
400
|
+
port: 8080,
|
|
401
|
+
name: 'test-server'
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
assert.equal(config.provider, 'docker');
|
|
405
|
+
assert.equal(config.port, 8080);
|
|
406
|
+
assert.equal(config.name, 'test-server');
|
|
407
|
+
assert.equal(config.host, DEFAULT_CONFIG.host);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('rejects invalid provider', () => {
|
|
411
|
+
assert.throws(() => {
|
|
412
|
+
validateConfig({ provider: 'invalid' });
|
|
413
|
+
}, /Invalid provider/);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('rejects invalid port', () => {
|
|
417
|
+
assert.throws(() => {
|
|
418
|
+
validateConfig({ port: 99999 });
|
|
419
|
+
}, /Invalid port/);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('rejects invalid name', () => {
|
|
423
|
+
assert.throws(() => {
|
|
424
|
+
validateConfig({ name: 'invalid name with spaces' });
|
|
425
|
+
}, /Invalid name/);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('validates TLS config requires both cert and key', () => {
|
|
429
|
+
assert.throws(() => {
|
|
430
|
+
validateConfig({ tls: { cert: './cert.pem' } });
|
|
431
|
+
}, /TLS config must include key path/);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('accepts valid TLS config', () => {
|
|
435
|
+
const config = validateConfig({
|
|
436
|
+
tls: { cert: './cert.pem', key: './key.pem' }
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
assert.deepEqual(config.tls, { cert: './cert.pem', key: './key.pem' });
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('Docker Compose Generation', () => {
|
|
444
|
+
test('generates basic docker-compose', async () => {
|
|
445
|
+
const compose = await deployToDocker({
|
|
446
|
+
port: 6667,
|
|
447
|
+
name: 'agentchat'
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
assert.ok(compose.includes('version:'));
|
|
451
|
+
assert.ok(compose.includes('agentchat:'));
|
|
452
|
+
assert.ok(compose.includes('6667:6667'));
|
|
453
|
+
assert.ok(compose.includes('restart: unless-stopped'));
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('generates docker-compose with health check', async () => {
|
|
457
|
+
const compose = await deployToDocker({
|
|
458
|
+
healthCheck: true
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
assert.ok(compose.includes('healthcheck:'));
|
|
462
|
+
assert.ok(compose.includes('interval:'));
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('generates docker-compose without health check', async () => {
|
|
466
|
+
const compose = await deployToDocker({
|
|
467
|
+
healthCheck: false
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
assert.ok(!compose.includes('healthcheck:'));
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('generates docker-compose with volumes', async () => {
|
|
474
|
+
const compose = await deployToDocker({
|
|
475
|
+
volumes: true
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
assert.ok(compose.includes('agentchat-data:/app/data'));
|
|
479
|
+
assert.ok(compose.includes('volumes:'));
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('generates docker-compose with TLS mounts', async () => {
|
|
483
|
+
const compose = await deployToDocker({
|
|
484
|
+
tls: { cert: './cert.pem', key: './key.pem' }
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
assert.ok(compose.includes('./cert.pem:/app/certs/cert.pem'));
|
|
488
|
+
assert.ok(compose.includes('./key.pem:/app/certs/key.pem'));
|
|
489
|
+
assert.ok(compose.includes('TLS_CERT=/app/certs/cert.pem'));
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test('generates docker-compose with network', async () => {
|
|
493
|
+
const compose = await deployToDocker({
|
|
494
|
+
network: 'my-network'
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
assert.ok(compose.includes('my-network'));
|
|
498
|
+
assert.ok(compose.includes('driver: bridge'));
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('generates Dockerfile', async () => {
|
|
502
|
+
const dockerfile = await generateDockerfile();
|
|
503
|
+
|
|
504
|
+
assert.ok(dockerfile.includes('FROM node:18-alpine'));
|
|
505
|
+
assert.ok(dockerfile.includes('npm ci --production'));
|
|
506
|
+
assert.ok(dockerfile.includes('HEALTHCHECK'));
|
|
507
|
+
assert.ok(dockerfile.includes('ENV PORT=6667'));
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('TLS Server', () => {
|
|
512
|
+
// TLS tests require generating certificates which is complex
|
|
513
|
+
// These are basic structural tests
|
|
514
|
+
|
|
515
|
+
test('server accepts TLS options', () => {
|
|
516
|
+
const server = new AgentChatServer({
|
|
517
|
+
port: 16670,
|
|
518
|
+
cert: 'nonexistent.pem',
|
|
519
|
+
key: 'nonexistent.pem'
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
assert.equal(server.tlsCert, 'nonexistent.pem');
|
|
523
|
+
assert.equal(server.tlsKey, 'nonexistent.pem');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('server without TLS options uses plain WebSocket', () => {
|
|
527
|
+
const server = new AgentChatServer({
|
|
528
|
+
port: 16671
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
assert.equal(server.tlsCert, null);
|
|
532
|
+
assert.equal(server.tlsKey, null);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
console.log('Running tests...');
|