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.
- package/dist/commands/debate.spec.js +1141 -25
- package/dist/commands/debate.spec.js.map +1 -1
- package/dist/commands/eval-requirements.js +1 -1
- package/dist/commands/eval-requirements.js.map +1 -1
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +18 -6
- package/dist/commands/eval.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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(
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
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(
|
|
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
|