dialectic 0.5.1 → 0.5.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.
@@ -3,13 +3,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const os_1 = __importDefault(require("os"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const index_1 = require("../index");
10
+ const dialectic_core_1 = require("dialectic-core");
11
+ const debate_1 = require("./debate");
12
+ const MOCK_SOLUTION_TEXT = 'Solution text';
6
13
  jest.mock('openai', () => {
7
14
  return {
8
15
  __esModule: true,
9
16
  default: class OpenAIMock {
10
17
  chat = {
11
18
  completions: {
12
- create: async (_) => ({ choices: [{ message: { content: 'Solution text' } }] }),
19
+ create: async (_) => ({ choices: [{ message: { content: MOCK_SOLUTION_TEXT } }] }),
13
20
  },
14
21
  };
15
22
  constructor(_opts) { }
@@ -20,7 +27,11 @@ jest.mock('dialectic-core', () => {
20
27
  const actual = jest.requireActual('dialectic-core');
21
28
  return {
22
29
  ...actual,
23
- loadEnvironmentFile: jest.fn()
30
+ loadEnvironmentFile: jest.fn().mockImplementation(() => {
31
+ return undefined;
32
+ }),
33
+ collectClarifications: jest.fn().mockResolvedValue([]),
34
+ generateDebateReport: jest.fn().mockImplementation(actual.generateDebateReport)
24
35
  };
25
36
  });
26
37
  jest.mock('readline', () => {
@@ -32,7 +43,7 @@ jest.mock('readline', () => {
32
43
  createInterface: () => ({
33
44
  question: (_, cb) => {
34
45
  const ans = currentIndex < mockAnswers.length ? mockAnswers[currentIndex++] : '';
35
- setImmediate(() => cb(String(ans)));
46
+ Promise.resolve().then(() => cb(String(ans)));
36
47
  },
37
48
  close: () => { },
38
49
  })
@@ -43,13 +54,16 @@ jest.mock('readline', () => {
43
54
  }
44
55
  };
45
56
  });
46
- const os_1 = __importDefault(require("os"));
47
- const path_1 = __importDefault(require("path"));
48
- const fs_1 = __importDefault(require("fs"));
49
- const index_1 = require("../index");
50
- const dialectic_core_1 = require("dialectic-core");
51
- const debate_1 = require("./debate");
52
57
  const mockedLoadEnvironmentFile = dialectic_core_1.loadEnvironmentFile;
58
+ const mockedCollectClarifications = dialectic_core_1.collectClarifications;
59
+ const mockedGenerateDebateReport = dialectic_core_1.generateDebateReport;
60
+ function resetLoadEnvironmentFileMock() {
61
+ mockedLoadEnvironmentFile.mockClear();
62
+ mockedLoadEnvironmentFile.mockReturnValue(undefined);
63
+ mockedLoadEnvironmentFile.mockImplementation(() => {
64
+ return undefined;
65
+ });
66
+ }
53
67
  const TEST_CONFIG_FILENAME = 'test-config.json';
54
68
  const TEST_AGENT_ID = 'test-agent';
55
69
  const TEST_AGENT_NAME = 'Test Agent';
@@ -101,8 +115,7 @@ describe('CLI debate command', () => {
101
115
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
102
116
  stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
103
117
  stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
104
- mockedLoadEnvironmentFile.mockClear();
105
- mockedLoadEnvironmentFile.mockReturnValue(undefined);
118
+ resetLoadEnvironmentFileMock();
106
119
  });
107
120
  afterEach(() => {
108
121
  process.env = originalEnv;
@@ -118,8 +131,18 @@ describe('CLI debate command', () => {
118
131
  });
119
132
  it('prints only minimal solution to stdout (non-verbose)', async () => {
120
133
  process.env.OPENAI_API_KEY = 'test';
134
+ const capturedStdout = [];
135
+ const stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
136
+ capturedStdout.push(String(chunk));
137
+ return true;
138
+ });
121
139
  await (0, index_1.runCli)(['debate', 'Design a rate limiting system']);
122
- expect(stdoutSpy).toHaveBeenCalled();
140
+ const stdout = capturedStdout.join('');
141
+ expect(stdout).toContain(MOCK_SOLUTION_TEXT);
142
+ expect(stdout).not.toContain('Running debate (verbose)');
143
+ expect(stdout).not.toContain('Summary (verbose)');
144
+ expect(stdout).not.toMatch(/Round\s+\d+/);
145
+ stdoutWriteSpy.mockRestore();
123
146
  });
124
147
  it('prints verbose header and summary with metadata when --verbose', async () => {
125
148
  process.env.OPENAI_API_KEY = 'test';
@@ -136,7 +159,7 @@ describe('CLI debate command', () => {
136
159
  await (0, index_1.runCli)(['debate', 'Design X', '--rounds', '2', '--verbose']);
137
160
  const stdout = capturedStdout.join('');
138
161
  const stderr = capturedStderr.join('');
139
- expect(stdout).toContain('Solution text');
162
+ expect(stdout).toContain(MOCK_SOLUTION_TEXT);
140
163
  expect(stderr).toContain('Running debate (verbose)');
141
164
  expect(stderr).toContain('Summary (verbose)');
142
165
  expect(stderr).toMatch(/Round\s+1/);
@@ -150,6 +173,136 @@ describe('CLI debate command', () => {
150
173
  .rejects.toHaveProperty('code', dialectic_core_1.EXIT_INVALID_ARGS);
151
174
  expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid arguments: problem is required (provide <problem> or --problemDescription)'));
152
175
  });
176
+ describe('Output results', () => {
177
+ let tmpDir;
178
+ beforeEach(() => {
179
+ process.env.OPENAI_API_KEY = 'test';
180
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'output-test-'));
181
+ });
182
+ afterEach(() => {
183
+ try {
184
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
185
+ }
186
+ catch { }
187
+ });
188
+ it('should write JSON output when output path ends with .json', async () => {
189
+ const outputFile = path_1.default.join(tmpDir, 'result.json');
190
+ await (0, index_1.runCli)(['debate', 'Design a system', '--output', outputFile]);
191
+ expect(fs_1.default.existsSync(outputFile)).toBe(true);
192
+ const content = JSON.parse(fs_1.default.readFileSync(outputFile, 'utf-8'));
193
+ expect(content).toHaveProperty('id');
194
+ expect(content).toHaveProperty('problem');
195
+ expect(content).toHaveProperty('rounds');
196
+ });
197
+ it('should write text output when output path does not end with .json', async () => {
198
+ const outputFile = path_1.default.join(tmpDir, 'result.txt');
199
+ await (0, index_1.runCli)(['debate', 'Design a system', '--output', outputFile]);
200
+ expect(fs_1.default.existsSync(outputFile)).toBe(true);
201
+ const content = fs_1.default.readFileSync(outputFile, 'utf-8');
202
+ expect(content).toContain(MOCK_SOLUTION_TEXT);
203
+ });
204
+ it('should write to stdout when no output path is provided', async () => {
205
+ const capturedStdout = [];
206
+ const stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
207
+ capturedStdout.push(String(chunk));
208
+ return true;
209
+ });
210
+ await (0, index_1.runCli)(['debate', 'Design a system']);
211
+ const stdout = capturedStdout.join('');
212
+ expect(stdout).toContain(MOCK_SOLUTION_TEXT);
213
+ stdoutWriteSpy.mockRestore();
214
+ });
215
+ it('should show verbose summary when no output path and verbose is true', async () => {
216
+ const capturedStderr = [];
217
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
218
+ capturedStderr.push(String(chunk));
219
+ return true;
220
+ });
221
+ await (0, index_1.runCli)(['debate', 'Design a system', '--verbose', '--rounds', '1']);
222
+ const stderr = capturedStderr.join('');
223
+ expect(stderr).toContain('Summary (verbose)');
224
+ stderrWriteSpy.mockRestore();
225
+ });
226
+ it('should not show verbose summary when output path is provided', async () => {
227
+ const outputFile = path_1.default.join(tmpDir, 'result.txt');
228
+ const capturedStderr = [];
229
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
230
+ capturedStderr.push(String(chunk));
231
+ return true;
232
+ });
233
+ await (0, index_1.runCli)(['debate', 'Design a system', '--verbose', '--output', outputFile, '--rounds', '1']);
234
+ const stderr = capturedStderr.join('');
235
+ expect(stderr).not.toContain('Summary (verbose)');
236
+ stderrWriteSpy.mockRestore();
237
+ });
238
+ });
239
+ describe('Problem description resolution', () => {
240
+ let tmpDir;
241
+ beforeEach(() => {
242
+ process.env.OPENAI_API_KEY = 'test';
243
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'problem-test-'));
244
+ });
245
+ afterEach(() => {
246
+ try {
247
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
248
+ }
249
+ catch { }
250
+ });
251
+ it('should use file when both problem string and --problemDescription are provided', async () => {
252
+ const problemFile = path_1.default.join(tmpDir, 'problem.txt');
253
+ fs_1.default.writeFileSync(problemFile, 'Problem from file');
254
+ const stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
255
+ await (0, index_1.runCli)(['debate', 'Problem from string', '--problemDescription', problemFile, '--rounds', '1']);
256
+ expect(stdoutWriteSpy).toHaveBeenCalled();
257
+ stdoutWriteSpy.mockRestore();
258
+ });
259
+ it('should read problem from file when --problemDescription is provided', async () => {
260
+ const problemFile = path_1.default.join(tmpDir, 'problem.txt');
261
+ const problemContent = 'Design a distributed cache system';
262
+ fs_1.default.writeFileSync(problemFile, problemContent);
263
+ await (0, index_1.runCli)(['debate', '--problemDescription', problemFile]);
264
+ expect(stdoutSpy).toHaveBeenCalled();
265
+ });
266
+ it('should error when problem file does not exist', async () => {
267
+ const nonExistentFile = path_1.default.join(tmpDir, 'nonexistent.txt');
268
+ await expect((0, index_1.runCli)(['debate', '--problemDescription', nonExistentFile]))
269
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_INVALID_ARGS);
270
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid arguments: problem description file not found'));
271
+ });
272
+ it('should error when problem file path is a directory', async () => {
273
+ await expect((0, index_1.runCli)(['debate', '--problemDescription', tmpDir]))
274
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_INVALID_ARGS);
275
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid arguments: problem description path is a directory'));
276
+ });
277
+ it('should error when problem file is empty', async () => {
278
+ const emptyFile = path_1.default.join(tmpDir, 'empty.txt');
279
+ fs_1.default.writeFileSync(emptyFile, '');
280
+ await expect((0, index_1.runCli)(['debate', '--problemDescription', emptyFile]))
281
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_INVALID_ARGS);
282
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid arguments: problem description file is empty'));
283
+ });
284
+ it('should error when problem file is whitespace-only', async () => {
285
+ const whitespaceFile = path_1.default.join(tmpDir, 'whitespace.txt');
286
+ fs_1.default.writeFileSync(whitespaceFile, ' \n\t ');
287
+ await expect((0, index_1.runCli)(['debate', '--problemDescription', whitespaceFile]))
288
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_INVALID_ARGS);
289
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid arguments: problem description file is empty'));
290
+ });
291
+ it('should error when problem file read fails', async () => {
292
+ const problemFile = path_1.default.join(tmpDir, 'problem.txt');
293
+ fs_1.default.writeFileSync(problemFile, 'Some content');
294
+ jest.spyOn(fs_1.default.promises, 'readFile').mockRejectedValueOnce(new Error('Permission denied'));
295
+ await expect((0, index_1.runCli)(['debate', '--problemDescription', problemFile]))
296
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_GENERAL_ERROR);
297
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to read problem description file'));
298
+ jest.spyOn(fs_1.default.promises, 'readFile').mockRestore();
299
+ });
300
+ it('should trim whitespace from problem string', async () => {
301
+ const problemWithWhitespace = ' Design a system \n\t ';
302
+ await (0, index_1.runCli)(['debate', problemWithWhitespace]);
303
+ expect(stdoutSpy).toHaveBeenCalled();
304
+ });
305
+ });
153
306
  describe('environment file loading', () => {
154
307
  it('should call loadEnvironmentFile with default parameters', async () => {
155
308
  process.env.OPENAI_API_KEY = 'test';
@@ -207,6 +360,749 @@ describe('Configuration loading', () => {
207
360
  }
208
361
  }
209
362
  });
363
+ it('should use built-in defaults when agents array is empty', async () => {
364
+ let tmpDir;
365
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'config-test-'));
366
+ try {
367
+ const configPath = getTestConfigPath(tmpDir);
368
+ const configContent = {
369
+ agents: [],
370
+ debate: createTestDebateConfig(),
371
+ };
372
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
373
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
374
+ const cfg = await (0, debate_1.loadConfig)(configPath);
375
+ expect(cfg.agents.length).toBeGreaterThan(0);
376
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Config missing agents'));
377
+ consoleErrorSpy.mockRestore();
378
+ }
379
+ finally {
380
+ try {
381
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
382
+ }
383
+ catch { }
384
+ }
385
+ });
386
+ it('should use default judge when judge is missing', async () => {
387
+ let tmpDir;
388
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'config-test-'));
389
+ try {
390
+ const configPath = getTestConfigPath(tmpDir);
391
+ const configContent = {
392
+ agents: [createTestAgentConfig()],
393
+ debate: createTestDebateConfig(),
394
+ };
395
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
396
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
397
+ const cfg = await (0, debate_1.loadConfig)(configPath);
398
+ expect(cfg.judge).toBeDefined();
399
+ expect(cfg.judge.id).toBeDefined();
400
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Config missing judge'));
401
+ consoleErrorSpy.mockRestore();
402
+ }
403
+ finally {
404
+ try {
405
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
406
+ }
407
+ catch { }
408
+ }
409
+ });
410
+ it('should use default debate when debate is missing', async () => {
411
+ let tmpDir;
412
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'config-test-'));
413
+ try {
414
+ const configPath = getTestConfigPath(tmpDir);
415
+ const configContent = {
416
+ agents: [createTestAgentConfig()],
417
+ judge: {
418
+ id: 'test-judge',
419
+ name: 'Test Judge',
420
+ role: 'generalist',
421
+ model: 'gpt-4',
422
+ provider: 'openai',
423
+ temperature: 0.3,
424
+ },
425
+ };
426
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
427
+ const cfg = await (0, debate_1.loadConfig)(configPath);
428
+ expect(cfg.debate).toBeDefined();
429
+ expect(cfg.debate.rounds).toBeDefined();
430
+ }
431
+ finally {
432
+ try {
433
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
434
+ }
435
+ catch { }
436
+ }
437
+ });
438
+ it('should load config successfully when all fields are present', async () => {
439
+ let tmpDir;
440
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'config-test-'));
441
+ try {
442
+ const configPath = getTestConfigPath(tmpDir);
443
+ const configContent = createTestConfigContent();
444
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
445
+ const cfg = await (0, debate_1.loadConfig)(configPath);
446
+ expect(cfg.agents).toBeDefined();
447
+ expect(cfg.agents.length).toBe(1);
448
+ expect(cfg.judge).toBeDefined();
449
+ expect(cfg.debate).toBeDefined();
450
+ }
451
+ finally {
452
+ try {
453
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
454
+ }
455
+ catch { }
456
+ }
457
+ });
458
+ });
459
+ describe('Debate config validation', () => {
460
+ const originalEnv = process.env;
461
+ let stderrWriteSpy;
462
+ beforeEach(() => {
463
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
464
+ stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
465
+ resetLoadEnvironmentFileMock();
466
+ });
467
+ afterEach(() => {
468
+ process.env = originalEnv;
469
+ stderrWriteSpy.mockRestore();
470
+ });
471
+ it('should use options.rounds when provided', async () => {
472
+ const capturedStdout = [];
473
+ const stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
474
+ capturedStdout.push(String(chunk));
475
+ return true;
476
+ });
477
+ await (0, index_1.runCli)(['debate', 'Design a system', '--rounds', '2']);
478
+ expect(stdoutWriteSpy).toHaveBeenCalled();
479
+ stdoutWriteSpy.mockRestore();
480
+ });
481
+ it('should use sysConfig.debate.rounds when options.rounds is not provided', async () => {
482
+ let tmpDir;
483
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'config-test-'));
484
+ try {
485
+ const configPath = getTestConfigPath(tmpDir);
486
+ const configContent = createTestConfigContent(undefined, {
487
+ rounds: 5,
488
+ });
489
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
490
+ const stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
491
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath]);
492
+ expect(stdoutWriteSpy).toHaveBeenCalled();
493
+ stdoutWriteSpy.mockRestore();
494
+ }
495
+ finally {
496
+ try {
497
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
498
+ }
499
+ catch { }
500
+ }
501
+ });
502
+ it('should use DEFAULT_ROUNDS when neither options.rounds nor sysConfig.debate.rounds is provided', async () => {
503
+ const stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
504
+ await (0, index_1.runCli)(['debate', 'Design a system']);
505
+ expect(stdoutWriteSpy).toHaveBeenCalled();
506
+ stdoutWriteSpy.mockRestore();
507
+ });
508
+ it('should error when rounds is 0', async () => {
509
+ await expect((0, index_1.runCli)(['debate', 'Design a system', '--rounds', '0']))
510
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_INVALID_ARGS);
511
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid arguments: --rounds must be >= 1'));
512
+ });
513
+ it('should error when rounds is negative', async () => {
514
+ await expect((0, index_1.runCli)(['debate', 'Design a system', '--rounds', '-1']))
515
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_INVALID_ARGS);
516
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid arguments: --rounds must be >= 1'));
517
+ });
518
+ });
519
+ describe('Agent filtering', () => {
520
+ let tmpDir;
521
+ const originalEnv = process.env;
522
+ let stdoutSpy;
523
+ beforeEach(() => {
524
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
525
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'agent-filter-test-'));
526
+ stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
527
+ resetLoadEnvironmentFileMock();
528
+ });
529
+ afterEach(() => {
530
+ try {
531
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
532
+ }
533
+ catch { }
534
+ process.env = originalEnv;
535
+ stdoutSpy.mockRestore();
536
+ });
537
+ it('should filter agents by role when --agents is provided', async () => {
538
+ const configPath = getTestConfigPath(tmpDir);
539
+ const configContent = {
540
+ agents: [
541
+ createTestAgentConfig({ id: 'arch1', role: 'architect' }),
542
+ createTestAgentConfig({ id: 'perf1', role: 'performance' }),
543
+ createTestAgentConfig({ id: 'sec1', role: 'security' }),
544
+ ],
545
+ debate: createTestDebateConfig(),
546
+ judge: {
547
+ id: 'test-judge',
548
+ name: 'Test Judge',
549
+ role: 'generalist',
550
+ model: 'gpt-4',
551
+ provider: 'openai',
552
+ temperature: 0.3,
553
+ },
554
+ };
555
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
556
+ const capturedStderr = [];
557
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
558
+ capturedStderr.push(String(chunk));
559
+ return true;
560
+ });
561
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--agents', 'architect,performance', '--verbose', '--rounds', '1']);
562
+ const stderr = capturedStderr.join('');
563
+ expect(stderr).toContain('architect');
564
+ expect(stderr).toContain('performance');
565
+ expect(stderr).not.toContain('sec1');
566
+ stderrWriteSpy.mockRestore();
567
+ });
568
+ it('should use all enabled agents when --agents is not provided', async () => {
569
+ const configPath = getTestConfigPath(tmpDir);
570
+ const configContent = {
571
+ agents: [
572
+ createTestAgentConfig({ id: 'arch1', role: 'architect' }),
573
+ createTestAgentConfig({ id: 'perf1', role: 'performance' }),
574
+ ],
575
+ debate: createTestDebateConfig(),
576
+ judge: {
577
+ id: 'test-judge',
578
+ name: 'Test Judge',
579
+ role: 'generalist',
580
+ model: 'gpt-4',
581
+ provider: 'openai',
582
+ temperature: 0.3,
583
+ },
584
+ };
585
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
586
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
587
+ expect(stdoutSpy).toHaveBeenCalled();
588
+ });
589
+ it('should filter out disabled agents', async () => {
590
+ const configPath = getTestConfigPath(tmpDir);
591
+ const configContent = {
592
+ agents: [
593
+ createTestAgentConfig({ id: 'arch1', name: 'Architect Agent', role: 'architect', enabled: true }),
594
+ createTestAgentConfig({ id: 'perf1', name: 'Performance Agent', role: 'performance', enabled: false }),
595
+ ],
596
+ debate: createTestDebateConfig(),
597
+ judge: {
598
+ id: 'test-judge',
599
+ name: 'Test Judge',
600
+ role: 'generalist',
601
+ model: 'gpt-4',
602
+ provider: 'openai',
603
+ temperature: 0.3,
604
+ },
605
+ };
606
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
607
+ const capturedStderr = [];
608
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
609
+ capturedStderr.push(String(chunk));
610
+ return true;
611
+ });
612
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--verbose', '--rounds', '1']);
613
+ const stderr = capturedStderr.join('');
614
+ expect(stderr).toContain('Architect Agent');
615
+ expect(stderr).not.toContain('Performance Agent');
616
+ stderrWriteSpy.mockRestore();
617
+ });
618
+ it('should default to built-in agents when no agents match filter', async () => {
619
+ const configPath = getTestConfigPath(tmpDir);
620
+ const configContent = {
621
+ agents: [
622
+ createTestAgentConfig({ id: 'arch1', role: 'architect' }),
623
+ ],
624
+ debate: createTestDebateConfig(),
625
+ judge: {
626
+ id: 'test-judge',
627
+ name: 'Test Judge',
628
+ role: 'generalist',
629
+ model: 'gpt-4',
630
+ provider: 'openai',
631
+ temperature: 0.3,
632
+ },
633
+ };
634
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
635
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
636
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--agents', 'nonexistent-role', '--rounds', '1']);
637
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('No agents selected; defaulting to architect,performance.'));
638
+ consoleErrorSpy.mockRestore();
639
+ });
640
+ it('should default to built-in agents when filtered result is empty', async () => {
641
+ const configPath = getTestConfigPath(tmpDir);
642
+ const configContent = {
643
+ agents: [
644
+ createTestAgentConfig({ id: 'arch1', role: 'architect', enabled: false }),
645
+ ],
646
+ debate: createTestDebateConfig(),
647
+ judge: {
648
+ id: 'test-judge',
649
+ name: 'Test Judge',
650
+ role: 'generalist',
651
+ model: 'gpt-4',
652
+ provider: 'openai',
653
+ temperature: 0.3,
654
+ },
655
+ };
656
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
657
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
658
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
659
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('No agents selected; defaulting to architect,performance.'));
660
+ consoleErrorSpy.mockRestore();
661
+ });
662
+ });
663
+ describe('Prompt resolution branches', () => {
664
+ let tmpDir;
665
+ const originalEnv = process.env;
666
+ let stdoutSpy;
667
+ beforeEach(() => {
668
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
669
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'prompt-test-'));
670
+ stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
671
+ resetLoadEnvironmentFileMock();
672
+ });
673
+ afterEach(() => {
674
+ try {
675
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
676
+ }
677
+ catch { }
678
+ process.env = originalEnv;
679
+ stdoutSpy.mockRestore();
680
+ });
681
+ it('should use file prompt when systemPromptPath is provided', async () => {
682
+ const promptFile = path_1.default.join(tmpDir, 'system-prompt.txt');
683
+ fs_1.default.writeFileSync(promptFile, 'Custom system prompt');
684
+ const configPath = getTestConfigPath(tmpDir);
685
+ const configContent = {
686
+ agents: [
687
+ createTestAgentConfig({ systemPromptPath: promptFile }),
688
+ ],
689
+ debate: createTestDebateConfig(),
690
+ judge: {
691
+ id: 'test-judge',
692
+ name: 'Test Judge',
693
+ role: 'generalist',
694
+ model: 'gpt-4',
695
+ provider: 'openai',
696
+ temperature: 0.3,
697
+ },
698
+ };
699
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
700
+ const capturedStderr = [];
701
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
702
+ capturedStderr.push(String(chunk));
703
+ return true;
704
+ });
705
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--verbose', '--rounds', '1']);
706
+ const stderr = capturedStderr.join('');
707
+ expect(stderr).toMatch(/System prompt: (file|.*\.txt)/);
708
+ stderrWriteSpy.mockRestore();
709
+ });
710
+ it('should use built-in prompt when systemPromptPath is not provided', async () => {
711
+ const configPath = getTestConfigPath(tmpDir);
712
+ const configContent = createTestConfigContent();
713
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
714
+ const capturedStderr = [];
715
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
716
+ capturedStderr.push(String(chunk));
717
+ return true;
718
+ });
719
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--verbose', '--rounds', '1']);
720
+ const stderr = capturedStderr.join('');
721
+ expect(stderr).toContain('built-in default');
722
+ stderrWriteSpy.mockRestore();
723
+ });
724
+ it('should use file summary prompt when summaryPromptPath is provided', async () => {
725
+ const summaryPromptFile = path_1.default.join(tmpDir, 'summary-prompt.txt');
726
+ fs_1.default.writeFileSync(summaryPromptFile, 'Custom summary prompt');
727
+ const configPath = getTestConfigPath(tmpDir);
728
+ const configContent = {
729
+ agents: [
730
+ createTestAgentConfig({ summaryPromptPath: summaryPromptFile }),
731
+ ],
732
+ debate: createTestDebateConfig(),
733
+ judge: {
734
+ id: 'test-judge',
735
+ name: 'Test Judge',
736
+ role: 'generalist',
737
+ model: 'gpt-4',
738
+ provider: 'openai',
739
+ temperature: 0.3,
740
+ },
741
+ };
742
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
743
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
744
+ expect(stdoutSpy).toHaveBeenCalled();
745
+ });
746
+ it('should use built-in summary prompt when summaryPromptPath is not provided', async () => {
747
+ const configPath = getTestConfigPath(tmpDir);
748
+ const configContent = createTestConfigContent();
749
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
750
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
751
+ expect(stdoutSpy).toHaveBeenCalled();
752
+ });
753
+ it('should include absPath in metadata when prompt file is used', async () => {
754
+ const promptFile = path_1.default.join(tmpDir, 'system-prompt.txt');
755
+ fs_1.default.writeFileSync(promptFile, 'Custom system prompt');
756
+ const configPath = getTestConfigPath(tmpDir);
757
+ const configContent = {
758
+ agents: [
759
+ createTestAgentConfig({ systemPromptPath: promptFile }),
760
+ ],
761
+ debate: createTestDebateConfig(),
762
+ judge: {
763
+ id: 'test-judge',
764
+ name: 'Test Judge',
765
+ role: 'generalist',
766
+ model: 'gpt-4',
767
+ provider: 'openai',
768
+ temperature: 0.3,
769
+ systemPromptPath: promptFile,
770
+ },
771
+ };
772
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
773
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1', '--output', path_1.default.join(tmpDir, 'result.json')]);
774
+ const resultFile = path_1.default.join(tmpDir, 'result.json');
775
+ if (fs_1.default.existsSync(resultFile)) {
776
+ const debateState = JSON.parse(fs_1.default.readFileSync(resultFile, 'utf-8'));
777
+ expect(debateState).toBeDefined();
778
+ }
779
+ });
780
+ it('should use configDir when provided', async () => {
781
+ const configPath = getTestConfigPath(tmpDir);
782
+ const promptFile = 'system-prompt.txt';
783
+ const fullPromptPath = path_1.default.join(tmpDir, promptFile);
784
+ fs_1.default.writeFileSync(fullPromptPath, 'Custom system prompt');
785
+ const configContent = {
786
+ agents: [
787
+ createTestAgentConfig({ systemPromptPath: promptFile }),
788
+ ],
789
+ debate: createTestDebateConfig(),
790
+ judge: {
791
+ id: 'test-judge',
792
+ name: 'Test Judge',
793
+ role: 'generalist',
794
+ model: 'gpt-4',
795
+ provider: 'openai',
796
+ temperature: 0.3,
797
+ },
798
+ };
799
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
800
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
801
+ expect(stdoutSpy).toHaveBeenCalled();
802
+ });
803
+ it('should use process.cwd() when configDir is not provided', async () => {
804
+ await (0, index_1.runCli)(['debate', 'Design a system', '--rounds', '1']);
805
+ expect(stdoutSpy).toHaveBeenCalled();
806
+ });
807
+ });
808
+ describe('Tracing context', () => {
809
+ let tmpDir;
810
+ const originalEnv = process.env;
811
+ let stdoutSpy;
812
+ let consoleErrorSpy;
813
+ beforeEach(() => {
814
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
815
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'tracing-test-'));
816
+ stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
817
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
818
+ resetLoadEnvironmentFileMock();
819
+ });
820
+ afterEach(() => {
821
+ try {
822
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
823
+ }
824
+ catch { }
825
+ process.env = originalEnv;
826
+ stdoutSpy.mockRestore();
827
+ consoleErrorSpy.mockRestore();
828
+ });
829
+ it('should return undefined when trace is not LANGFUSE', async () => {
830
+ const configPath = getTestConfigPath(tmpDir);
831
+ const configContent = createTestConfigContent(undefined, {
832
+ trace: 'none',
833
+ });
834
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
835
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
836
+ expect(stdoutSpy).toHaveBeenCalled();
837
+ expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining('Langfuse tracing enabled'));
838
+ });
839
+ it('should initialize tracing when trace is LANGFUSE and config is valid', async () => {
840
+ process.env.LANGFUSE_SECRET_KEY = 'test-secret-key';
841
+ process.env.LANGFUSE_PUBLIC_KEY = 'test-public-key';
842
+ const configPath = getTestConfigPath(tmpDir);
843
+ const configContent = createTestConfigContent(undefined, {
844
+ trace: 'langfuse',
845
+ });
846
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
847
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
848
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Langfuse tracing enabled'));
849
+ });
850
+ it('should handle tracing initialization errors gracefully', async () => {
851
+ delete process.env.LANGFUSE_SECRET_KEY;
852
+ delete process.env.LANGFUSE_PUBLIC_KEY;
853
+ const configPath = getTestConfigPath(tmpDir);
854
+ const configContent = createTestConfigContent(undefined, {
855
+ trace: 'langfuse',
856
+ });
857
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
858
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
859
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Langfuse tracing initialization failed'));
860
+ expect(stdoutSpy).toHaveBeenCalled();
861
+ });
862
+ it('should include problemFileName in metadata when provided', async () => {
863
+ process.env.LANGFUSE_SECRET_KEY = 'test-secret-key';
864
+ process.env.LANGFUSE_PUBLIC_KEY = 'test-public-key';
865
+ const problemFile = path_1.default.join(tmpDir, 'problem.txt');
866
+ fs_1.default.writeFileSync(problemFile, 'Design a system');
867
+ const configPath = getTestConfigPath(tmpDir);
868
+ const configContent = createTestConfigContent(undefined, {
869
+ trace: 'langfuse',
870
+ });
871
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
872
+ await (0, index_1.runCli)(['debate', '--problemDescription', problemFile, '--config', configPath, '--rounds', '1']);
873
+ expect(stdoutSpy).toHaveBeenCalled();
874
+ });
875
+ it('should not include problemFileName in metadata when not provided', async () => {
876
+ process.env.LANGFUSE_SECRET_KEY = 'test-secret-key';
877
+ process.env.LANGFUSE_PUBLIC_KEY = 'test-public-key';
878
+ const configPath = getTestConfigPath(tmpDir);
879
+ const configContent = createTestConfigContent(undefined, {
880
+ trace: 'langfuse',
881
+ });
882
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
883
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
884
+ expect(stdoutSpy).toHaveBeenCalled();
885
+ });
886
+ it('should include contextFileName in metadata when provided', async () => {
887
+ process.env.LANGFUSE_SECRET_KEY = 'test-secret-key';
888
+ process.env.LANGFUSE_PUBLIC_KEY = 'test-public-key';
889
+ const contextFile = path_1.default.join(tmpDir, 'context.txt');
890
+ fs_1.default.writeFileSync(contextFile, 'Additional context');
891
+ const configPath = getTestConfigPath(tmpDir);
892
+ const configContent = createTestConfigContent(undefined, {
893
+ trace: 'langfuse',
894
+ });
895
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
896
+ await (0, index_1.runCli)(['debate', 'Design a system', '--context', contextFile, '--config', configPath, '--rounds', '1']);
897
+ expect(stdoutSpy).toHaveBeenCalled();
898
+ });
899
+ it('should include judgeConfig in metadata when judge exists', async () => {
900
+ process.env.LANGFUSE_SECRET_KEY = 'test-secret-key';
901
+ process.env.LANGFUSE_PUBLIC_KEY = 'test-public-key';
902
+ const configPath = getTestConfigPath(tmpDir);
903
+ const configContent = createTestConfigContent(undefined, {
904
+ trace: 'langfuse',
905
+ });
906
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
907
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--rounds', '1']);
908
+ expect(stdoutSpy).toHaveBeenCalled();
909
+ });
910
+ });
911
+ describe('Round summary output', () => {
912
+ const originalEnv = process.env;
913
+ beforeEach(() => {
914
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
915
+ resetLoadEnvironmentFileMock();
916
+ });
917
+ afterEach(() => {
918
+ process.env = originalEnv;
919
+ });
920
+ it('should output summaries when round has summaries', async () => {
921
+ const capturedStderr = [];
922
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
923
+ capturedStderr.push(String(chunk));
924
+ return true;
925
+ });
926
+ await (0, index_1.runCli)(['debate', 'Design a system', '--verbose', '--rounds', '1']);
927
+ const stderr = capturedStderr.join('');
928
+ expect(stderr).toMatch(/Round\s+\d+/);
929
+ stderrWriteSpy.mockRestore();
930
+ });
931
+ it('should output contributions when round has contributions', async () => {
932
+ const capturedStderr = [];
933
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
934
+ capturedStderr.push(String(chunk));
935
+ return true;
936
+ });
937
+ await (0, index_1.runCli)(['debate', 'Design a system', '--verbose', '--rounds', '1']);
938
+ const stderr = capturedStderr.join('');
939
+ expect(stderr.length).toBeGreaterThan(0);
940
+ stderrWriteSpy.mockRestore();
941
+ });
942
+ it('should handle contributions with metadata', async () => {
943
+ const capturedStderr = [];
944
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
945
+ capturedStderr.push(String(chunk));
946
+ return true;
947
+ });
948
+ await (0, index_1.runCli)(['debate', 'Design a system', '--verbose', '--rounds', '1']);
949
+ const stderr = capturedStderr.join('');
950
+ expect(stderr).toMatch(/latency=|tokens=/);
951
+ stderrWriteSpy.mockRestore();
952
+ });
953
+ });
954
+ describe('Verbose header branches', () => {
955
+ let tmpDir;
956
+ const originalEnv = process.env;
957
+ beforeEach(() => {
958
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
959
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'verbose-header-test-'));
960
+ resetLoadEnvironmentFileMock();
961
+ });
962
+ afterEach(() => {
963
+ try {
964
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
965
+ }
966
+ catch { }
967
+ process.env = originalEnv;
968
+ });
969
+ it('should show file prompt source in verbose header when prompt is from file', async () => {
970
+ const promptFile = path_1.default.join(tmpDir, 'system-prompt.txt');
971
+ fs_1.default.writeFileSync(promptFile, 'Custom system prompt');
972
+ const configPath = getTestConfigPath(tmpDir);
973
+ const configContent = {
974
+ agents: [
975
+ createTestAgentConfig({ systemPromptPath: promptFile }),
976
+ ],
977
+ debate: createTestDebateConfig(),
978
+ judge: {
979
+ id: 'test-judge',
980
+ name: 'Test Judge',
981
+ role: 'generalist',
982
+ model: 'gpt-4',
983
+ provider: 'openai',
984
+ temperature: 0.3,
985
+ systemPromptPath: promptFile,
986
+ },
987
+ };
988
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
989
+ const capturedStderr = [];
990
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
991
+ capturedStderr.push(String(chunk));
992
+ return true;
993
+ });
994
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--verbose', '--rounds', '1']);
995
+ const stderr = capturedStderr.join('');
996
+ expect(stderr).toMatch(/System prompt: (file|.*system-prompt\.txt)/);
997
+ stderrWriteSpy.mockRestore();
998
+ });
999
+ it('should show built-in prompt source in verbose header when prompt is built-in', async () => {
1000
+ const configPath = getTestConfigPath(tmpDir);
1001
+ const configContent = createTestConfigContent();
1002
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
1003
+ const capturedStderr = [];
1004
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
1005
+ capturedStderr.push(String(chunk));
1006
+ return true;
1007
+ });
1008
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--verbose', '--rounds', '1']);
1009
+ const stderr = capturedStderr.join('');
1010
+ expect(stderr).toContain('System prompt: built-in default');
1011
+ stderrWriteSpy.mockRestore();
1012
+ });
1013
+ it('should show prompt path when available', async () => {
1014
+ const promptFile = path_1.default.join(tmpDir, 'system-prompt.txt');
1015
+ fs_1.default.writeFileSync(promptFile, 'Custom system prompt');
1016
+ const configPath = getTestConfigPath(tmpDir);
1017
+ const configContent = {
1018
+ agents: [
1019
+ createTestAgentConfig({ systemPromptPath: promptFile }),
1020
+ ],
1021
+ debate: createTestDebateConfig(),
1022
+ judge: {
1023
+ id: 'test-judge',
1024
+ name: 'Test Judge',
1025
+ role: 'generalist',
1026
+ model: 'gpt-4',
1027
+ provider: 'openai',
1028
+ temperature: 0.3,
1029
+ systemPromptPath: promptFile,
1030
+ },
1031
+ };
1032
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
1033
+ const capturedStderr = [];
1034
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
1035
+ capturedStderr.push(String(chunk));
1036
+ return true;
1037
+ });
1038
+ await (0, index_1.runCli)(['debate', 'Design a system', '--config', configPath, '--verbose', '--rounds', '1']);
1039
+ const stderr = capturedStderr.join('');
1040
+ expect(stderr).toMatch(/System prompt: (file|.*\.txt)/);
1041
+ stderrWriteSpy.mockRestore();
1042
+ });
1043
+ it('should not show verbose header when verbose is false', async () => {
1044
+ const capturedStderr = [];
1045
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
1046
+ capturedStderr.push(String(chunk));
1047
+ return true;
1048
+ });
1049
+ await (0, index_1.runCli)(['debate', 'Design a system', '--rounds', '1']);
1050
+ const stderr = capturedStderr.join('');
1051
+ expect(stderr).not.toContain('Running debate (verbose)');
1052
+ expect(stderr).not.toContain('Active Agents:');
1053
+ stderrWriteSpy.mockRestore();
1054
+ });
1055
+ });
1056
+ describe('Error handling', () => {
1057
+ const originalEnv = process.env;
1058
+ let stderrWriteSpy;
1059
+ let orchestratorSpy;
1060
+ beforeEach(() => {
1061
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
1062
+ stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
1063
+ mockedLoadEnvironmentFile.mockImplementation(() => undefined);
1064
+ });
1065
+ afterEach(() => {
1066
+ process.env = originalEnv;
1067
+ stderrWriteSpy.mockRestore();
1068
+ if (orchestratorSpy) {
1069
+ orchestratorSpy.mockRestore();
1070
+ orchestratorSpy = undefined;
1071
+ }
1072
+ });
1073
+ it('should use error code when error has code property', async () => {
1074
+ orchestratorSpy = jest.spyOn(require('dialectic-core'), 'DebateOrchestrator').mockImplementation(function () {
1075
+ throw Object.assign(new Error('Test error'), { code: dialectic_core_1.EXIT_INVALID_ARGS });
1076
+ });
1077
+ await expect((0, index_1.runCli)(['debate', 'Design a system', '--rounds', '1']))
1078
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_INVALID_ARGS);
1079
+ });
1080
+ it('should use EXIT_GENERAL_ERROR when error has no code property', async () => {
1081
+ orchestratorSpy = jest.spyOn(require('dialectic-core'), 'DebateOrchestrator').mockImplementation(function () {
1082
+ throw new Error('Test error without code');
1083
+ });
1084
+ await expect((0, index_1.runCli)(['debate', 'Design a system', '--rounds', '1']))
1085
+ .rejects.toHaveProperty('code', dialectic_core_1.EXIT_GENERAL_ERROR);
1086
+ });
1087
+ it('should use error message when available', async () => {
1088
+ const errorMessage = 'Custom error message';
1089
+ orchestratorSpy = jest.spyOn(require('dialectic-core'), 'DebateOrchestrator').mockImplementation(function () {
1090
+ throw Object.assign(new Error(errorMessage), { code: dialectic_core_1.EXIT_GENERAL_ERROR });
1091
+ });
1092
+ await expect((0, index_1.runCli)(['debate', 'Design a system', '--rounds', '1']))
1093
+ .rejects.toThrow(errorMessage);
1094
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining(errorMessage));
1095
+ });
1096
+ it('should use "Unknown error" when error has no message', async () => {
1097
+ const errorWithoutMessage = {};
1098
+ errorWithoutMessage.code = dialectic_core_1.EXIT_GENERAL_ERROR;
1099
+ orchestratorSpy = jest.spyOn(require('dialectic-core'), 'DebateOrchestrator').mockImplementation(function () {
1100
+ throw errorWithoutMessage;
1101
+ });
1102
+ await expect((0, index_1.runCli)(['debate', 'Design a system', '--rounds', '1']))
1103
+ .rejects.toThrow('Unknown error');
1104
+ expect(stderrWriteSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown error'));
1105
+ });
210
1106
  });
211
1107
  describe('Summarization configuration loading', () => {
212
1108
  let tmpDir;
@@ -319,15 +1215,16 @@ describe('CLI clarifications phase', () => {
319
1215
  process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
320
1216
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
321
1217
  stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
322
- mockedLoadEnvironmentFile.mockClear();
323
- mockedLoadEnvironmentFile.mockReturnValue(undefined);
1218
+ resetLoadEnvironmentFileMock();
1219
+ mockedCollectClarifications.mockClear();
1220
+ mockedCollectClarifications.mockResolvedValue([]);
1221
+ mockReadlineWithAnswers([]);
324
1222
  });
325
1223
  afterEach(() => {
326
1224
  process.env = originalEnv;
327
1225
  consoleErrorSpy.mockRestore();
328
1226
  stdoutSpy.mockRestore();
329
1227
  jest.restoreAllMocks();
330
- jest.resetModules();
331
1228
  });
332
1229
  function mockReadlineWithAnswers(answers) {
333
1230
  const readlineMock = require('readline');
@@ -337,15 +1234,27 @@ describe('CLI clarifications phase', () => {
337
1234
  }
338
1235
  it('runs clarifications when --clarify and collects answers (including NA)', async () => {
339
1236
  mockReadlineWithAnswers(['My answer', '']);
340
- const spy = jest.spyOn(dialectic_core_1.RoleBasedAgent.prototype, 'askClarifyingQuestions')
341
- .mockResolvedValueOnce({ questions: [{ id: 'q1', text: 'What is the SLA?' }] })
342
- .mockResolvedValueOnce({ questions: [{ id: 'q1', text: 'Any data retention rules?' }] });
1237
+ mockedCollectClarifications.mockResolvedValueOnce([
1238
+ {
1239
+ agentId: 'agent-architect',
1240
+ agentName: 'System Architect',
1241
+ role: 'architect',
1242
+ items: [{ id: 'q1', question: 'What is the SLA?', answer: '' }]
1243
+ },
1244
+ {
1245
+ agentId: 'agent-performance',
1246
+ agentName: 'Performance Specialist',
1247
+ role: 'performance',
1248
+ items: [{ id: 'q2', question: 'Any data retention rules?', answer: '' }]
1249
+ }
1250
+ ]);
343
1251
  const tmpReport = path_1.default.join(os_1.default.tmpdir(), `clarify-report-${Date.now()}.md`);
344
- await (0, index_1.runCli)(['debate', 'Design Y', '--clarify', '--report', tmpReport]);
345
- expect(spy).toHaveBeenCalled();
1252
+ await (0, index_1.runCli)(['debate', 'Design Y', '--clarify', '--report', tmpReport, '--rounds', '1']);
1253
+ expect(mockedCollectClarifications).toHaveBeenCalled();
346
1254
  const content = fs_1.default.readFileSync(tmpReport, 'utf-8');
347
1255
  expect(content).toContain('## Clarifications');
348
1256
  expect(content).toContain('Question (q1):');
1257
+ expect(content).toContain('Question (q2):');
349
1258
  expect(content).toContain('My answer');
350
1259
  expect(content).toContain('\n```text\nNA\n```');
351
1260
  });
@@ -356,14 +1265,221 @@ describe('CLI clarifications phase', () => {
356
1265
  expect(spy).not.toHaveBeenCalled();
357
1266
  });
358
1267
  it('truncates questions per agent and warns', async () => {
359
- const many = Array.from({ length: 7 }, (_, i) => ({ id: `q${i + 1}`, text: `Q${i + 1}` }));
360
- const spy = jest.spyOn(dialectic_core_1.RoleBasedAgent.prototype, 'askClarifyingQuestions')
361
- .mockResolvedValue({ questions: many });
1268
+ const truncatedQuestions = Array.from({ length: 5 }, (_, i) => ({
1269
+ id: `q${i + 1}`,
1270
+ question: `Q${i + 1}`,
1271
+ answer: ''
1272
+ }));
1273
+ mockedCollectClarifications.mockImplementationOnce((_problem, _agents, _maxPerAgent, warn) => {
1274
+ warn('Agent Test Agent returned 7 questions; limited to 5.');
1275
+ return Promise.resolve([{
1276
+ agentId: 'agent-architect',
1277
+ agentName: 'Test Agent',
1278
+ role: 'architect',
1279
+ items: truncatedQuestions
1280
+ }]);
1281
+ });
362
1282
  mockReadlineWithAnswers(new Array(10).fill('A'));
363
1283
  await (0, index_1.runCli)(['debate', 'Design W', '--clarify']);
364
- expect(spy).toHaveBeenCalled();
1284
+ expect(mockedCollectClarifications).toHaveBeenCalled();
365
1285
  const stderr = (consoleErrorSpy.mock.calls.map(args => String(args[0])).join(''));
366
1286
  expect(stderr).toMatch(/limited to 5/);
1287
+ mockedCollectClarifications.mockClear();
1288
+ mockedCollectClarifications.mockResolvedValue([]);
1289
+ });
1290
+ it('should skip groups with empty items array', async () => {
1291
+ mockedCollectClarifications.mockResolvedValueOnce([
1292
+ {
1293
+ agentId: 'agent1',
1294
+ agentName: 'Agent 1',
1295
+ role: 'architect',
1296
+ items: [],
1297
+ },
1298
+ {
1299
+ agentId: 'agent2',
1300
+ agentName: 'Agent 2',
1301
+ role: 'performance',
1302
+ items: [{ id: 'q1', question: 'Question 1', answer: '' }],
1303
+ },
1304
+ ]);
1305
+ mockReadlineWithAnswers(['Answer 1']);
1306
+ const tmpReport = path_1.default.join(os_1.default.tmpdir(), `clarify-report-${Date.now()}.md`);
1307
+ await (0, index_1.runCli)(['debate', 'Design Y', '--clarify', '--report', tmpReport, '--rounds', '1']);
1308
+ const content = fs_1.default.readFileSync(tmpReport, 'utf-8');
1309
+ expect(content).toContain('Question (q1)');
1310
+ expect(content).toContain('Answer 1');
1311
+ mockedCollectClarifications.mockClear();
1312
+ mockedCollectClarifications.mockResolvedValue([]);
1313
+ });
1314
+ it('should set answer to NA when user input is empty', async () => {
1315
+ mockReadlineWithAnswers(['']);
1316
+ mockedCollectClarifications.mockResolvedValueOnce([
1317
+ {
1318
+ agentId: 'agent-architect',
1319
+ agentName: 'System Architect',
1320
+ role: 'architect',
1321
+ items: [{ id: 'q1', question: 'What is the requirement?', answer: '' }]
1322
+ }
1323
+ ]);
1324
+ const tmpReport = path_1.default.join(os_1.default.tmpdir(), `clarify-report-${Date.now()}.md`);
1325
+ await (0, index_1.runCli)(['debate', 'Design Y', '--clarify', '--report', tmpReport, '--rounds', '1']);
1326
+ const content = fs_1.default.readFileSync(tmpReport, 'utf-8');
1327
+ expect(content).toContain('\n```text\nNA\n```');
1328
+ mockedCollectClarifications.mockClear();
1329
+ mockedCollectClarifications.mockResolvedValue([]);
1330
+ });
1331
+ it('should set answer to user input when provided', async () => {
1332
+ const userAnswer = 'The requirement is X';
1333
+ mockReadlineWithAnswers([userAnswer]);
1334
+ mockedCollectClarifications.mockResolvedValueOnce([
1335
+ {
1336
+ agentId: 'agent-architect',
1337
+ agentName: 'System Architect',
1338
+ role: 'architect',
1339
+ items: [{ id: 'q1', question: 'What is the requirement?', answer: '' }]
1340
+ }
1341
+ ]);
1342
+ const tmpReport = path_1.default.join(os_1.default.tmpdir(), `clarify-report-${Date.now()}.md`);
1343
+ await (0, index_1.runCli)(['debate', 'Design Y', '--clarify', '--report', tmpReport, '--rounds', '1']);
1344
+ const content = fs_1.default.readFileSync(tmpReport, 'utf-8');
1345
+ expect(content).toContain(userAnswer);
1346
+ mockedCollectClarifications.mockClear();
1347
+ mockedCollectClarifications.mockResolvedValue([]);
1348
+ });
1349
+ describe('Report generation', () => {
1350
+ let tmpDir;
1351
+ const originalEnv = process.env;
1352
+ beforeEach(() => {
1353
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
1354
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'report-test-'));
1355
+ });
1356
+ afterEach(() => {
1357
+ try {
1358
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
1359
+ }
1360
+ catch { }
1361
+ process.env = originalEnv;
1362
+ });
1363
+ it('should append .md extension when report path does not end with .md', async () => {
1364
+ const reportPath = path_1.default.join(tmpDir, 'report');
1365
+ await (0, index_1.runCli)(['debate', 'Design a system', '--report', reportPath, '--rounds', '1']);
1366
+ const expectedPath = reportPath + '.md';
1367
+ expect(fs_1.default.existsSync(expectedPath)).toBe(true);
1368
+ const content = fs_1.default.readFileSync(expectedPath, 'utf-8');
1369
+ expect(content).toContain('## Problem Description');
1370
+ });
1371
+ it('should not append .md extension when report path already ends with .md', async () => {
1372
+ const reportPath = path_1.default.join(tmpDir, 'report.md');
1373
+ await (0, index_1.runCli)(['debate', 'Design a system', '--report', reportPath, '--rounds', '1']);
1374
+ expect(fs_1.default.existsSync(reportPath)).toBe(true);
1375
+ const content = fs_1.default.readFileSync(reportPath, 'utf-8');
1376
+ expect(content).toContain('## Problem Description');
1377
+ });
1378
+ it('should handle report generation errors gracefully', async () => {
1379
+ const reportPath = path_1.default.join(tmpDir, 'report.md');
1380
+ mockedGenerateDebateReport.mockImplementationOnce(() => {
1381
+ throw new Error('Report generation failed');
1382
+ });
1383
+ await (0, index_1.runCli)(['debate', 'Design a system', '--report', reportPath, '--rounds', '1']);
1384
+ const errorCalls = consoleErrorSpy.mock.calls.map(args => String(args[0])).join('');
1385
+ expect(errorCalls).toMatch(/Failed to generate report/);
1386
+ mockedGenerateDebateReport.mockImplementation(jest.requireActual('dialectic-core').generateDebateReport);
1387
+ });
1388
+ });
1389
+ describe('Agent logger', () => {
1390
+ const originalEnv = process.env;
1391
+ beforeEach(() => {
1392
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
1393
+ });
1394
+ afterEach(() => {
1395
+ process.env = originalEnv;
1396
+ });
1397
+ it('should log when onlyVerbose is false', async () => {
1398
+ const capturedStdout = [];
1399
+ const stdoutWriteSpy = jest.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
1400
+ capturedStdout.push(String(chunk));
1401
+ return true;
1402
+ });
1403
+ await (0, index_1.runCli)(['debate', 'Design a system', '--rounds', '1']);
1404
+ const stdout = capturedStdout.join('');
1405
+ expect(stdout.length).toBeGreaterThan(0);
1406
+ stdoutWriteSpy.mockRestore();
1407
+ });
1408
+ it('should log when onlyVerbose is true and verbose is true', async () => {
1409
+ const capturedStderr = [];
1410
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
1411
+ capturedStderr.push(String(chunk));
1412
+ return true;
1413
+ });
1414
+ await (0, index_1.runCli)(['debate', 'Design a system', '--verbose', '--rounds', '1']);
1415
+ const stderr = capturedStderr.join('');
1416
+ expect(stderr).toContain('Running debate (verbose)');
1417
+ stderrWriteSpy.mockRestore();
1418
+ });
1419
+ it('should not log when onlyVerbose is true and verbose is false', async () => {
1420
+ const capturedStderr = [];
1421
+ const stderrWriteSpy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
1422
+ capturedStderr.push(String(chunk));
1423
+ return true;
1424
+ });
1425
+ await (0, index_1.runCli)(['debate', 'Design a system', '--rounds', '1']);
1426
+ const stderr = capturedStderr.join('');
1427
+ expect(stderr).not.toContain('Running debate (verbose)');
1428
+ stderrWriteSpy.mockRestore();
1429
+ });
1430
+ });
1431
+ describe('Context file handling', () => {
1432
+ let tmpDir;
1433
+ const originalEnv = process.env;
1434
+ beforeEach(() => {
1435
+ process.env = { ...originalEnv, OPENAI_API_KEY: 'test' };
1436
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'context-test-'));
1437
+ });
1438
+ afterEach(() => {
1439
+ try {
1440
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
1441
+ }
1442
+ catch { }
1443
+ process.env = originalEnv;
1444
+ });
1445
+ it('should return undefined when context file does not exist', async () => {
1446
+ const nonExistentFile = path_1.default.join(tmpDir, 'nonexistent.txt');
1447
+ await (0, index_1.runCli)(['debate', 'Design a system', '--context', nonExistentFile]);
1448
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Context file not found'));
1449
+ });
1450
+ it('should return undefined when context path is a directory', async () => {
1451
+ await (0, index_1.runCli)(['debate', 'Design a system', '--context', tmpDir]);
1452
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Context path is a directory'));
1453
+ });
1454
+ it('should return undefined when context file is empty', async () => {
1455
+ const emptyFile = path_1.default.join(tmpDir, 'empty.txt');
1456
+ fs_1.default.writeFileSync(emptyFile, '');
1457
+ await (0, index_1.runCli)(['debate', 'Design a system', '--context', emptyFile]);
1458
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Context file is empty'));
1459
+ });
1460
+ it('should truncate context file exceeding MAX_CONTEXT_LENGTH', async () => {
1461
+ const longFile = path_1.default.join(tmpDir, 'long.txt');
1462
+ const longContent = 'x'.repeat(6000);
1463
+ fs_1.default.writeFileSync(longFile, longContent);
1464
+ await (0, index_1.runCli)(['debate', 'Design a system', '--context', longFile]);
1465
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Context file exceeds 5000 characters'));
1466
+ });
1467
+ it('should return undefined when context file read fails', async () => {
1468
+ const contextFile = path_1.default.join(tmpDir, 'context.txt');
1469
+ fs_1.default.writeFileSync(contextFile, 'Some context');
1470
+ jest.spyOn(fs_1.default.promises, 'readFile').mockRejectedValueOnce(new Error('Permission denied'));
1471
+ await (0, index_1.runCli)(['debate', 'Design a system', '--context', contextFile]);
1472
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to read context file'));
1473
+ jest.spyOn(fs_1.default.promises, 'readFile').mockRestore();
1474
+ });
1475
+ it('should return context content when file is valid', async () => {
1476
+ const contextFile = path_1.default.join(tmpDir, 'context.txt');
1477
+ const contextContent = 'Additional context information';
1478
+ fs_1.default.writeFileSync(contextFile, contextContent);
1479
+ await (0, index_1.runCli)(['debate', 'Design a system', '--context', contextFile]);
1480
+ expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining('Context file not found'));
1481
+ expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining('Context file is empty'));
1482
+ });
367
1483
  });
368
1484
  });
369
1485
  //# sourceMappingURL=debate.spec.js.map