dialectic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/commands/setup-test.mdc +175 -0
- package/.cursor/rules/basic-code-cleanup.mdc +1110 -0
- package/.cursor/rules/riper5.mdc +96 -0
- package/.env.example +6 -0
- package/AGENTS.md +1052 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/WARP.md +113 -0
- package/dialectic-1.0.0.tgz +0 -0
- package/dialectic.js +10 -0
- package/docs/commands.md +375 -0
- package/docs/configuration.md +882 -0
- package/docs/context_summarization.md +1023 -0
- package/docs/debate_flow.md +1127 -0
- package/docs/eval_flow.md +795 -0
- package/docs/evaluator.md +141 -0
- package/examples/debate-config-openrouter.json +48 -0
- package/examples/debate_config1.json +48 -0
- package/examples/eval/eval1/eval_config1.json +13 -0
- package/examples/eval/eval1/result1.json +62 -0
- package/examples/eval/eval1/result2.json +97 -0
- package/examples/eval_summary_format.md +11 -0
- package/examples/example3/debate-config.json +64 -0
- package/examples/example3/eval_config2.json +25 -0
- package/examples/example3/problem.md +17 -0
- package/examples/example3/rounds_test/eval_run.sh +16 -0
- package/examples/example3/rounds_test/run_test.sh +16 -0
- package/examples/kata1/architect-only-solution_2-rounds.json +121 -0
- package/examples/kata1/architect-perf-solution_2-rounds.json +234 -0
- package/examples/kata1/debate-config-kata1.json +54 -0
- package/examples/kata1/eval_architect-only_2-rounds.json +97 -0
- package/examples/kata1/eval_architect-perf_2-rounds.json +97 -0
- package/examples/kata1/kata1-report.md +12224 -0
- package/examples/kata1/kata1-report_temps-01_01_01_07.md +2451 -0
- package/examples/kata1/kata1.md +5 -0
- package/examples/kata1/meta.txt +1 -0
- package/examples/kata2/debate-config.json +54 -0
- package/examples/kata2/eval_config1.json +21 -0
- package/examples/kata2/eval_config2.json +25 -0
- package/examples/kata2/kata2.md +5 -0
- package/examples/kata2/only_architect/debate-config.json +45 -0
- package/examples/kata2/only_architect/eval_run.sh +11 -0
- package/examples/kata2/only_architect/run_test.sh +5 -0
- package/examples/kata2/rounds_test/eval_run.sh +11 -0
- package/examples/kata2/rounds_test/run_test.sh +5 -0
- package/examples/kata2/summary_length_test/eval_run.sh +11 -0
- package/examples/kata2/summary_length_test/eval_run_w_clarify.sh +7 -0
- package/examples/kata2/summary_length_test/run_test.sh +5 -0
- package/examples/task-queue/debate-config.json +76 -0
- package/examples/task-queue/debate_report.md +566 -0
- package/examples/task-queue/task-queue-system.md +25 -0
- package/jest.config.ts +13 -0
- package/multi_agent_debate_spec.md +2980 -0
- package/package.json +38 -0
- package/sanity-check-problem.txt +9 -0
- package/src/agents/prompts/architect-prompts.ts +203 -0
- package/src/agents/prompts/generalist-prompts.ts +157 -0
- package/src/agents/prompts/index.ts +41 -0
- package/src/agents/prompts/judge-prompts.ts +19 -0
- package/src/agents/prompts/kiss-prompts.ts +230 -0
- package/src/agents/prompts/performance-prompts.ts +142 -0
- package/src/agents/prompts/prompt-types.ts +68 -0
- package/src/agents/prompts/security-prompts.ts +149 -0
- package/src/agents/prompts/shared.ts +144 -0
- package/src/agents/prompts/testing-prompts.ts +149 -0
- package/src/agents/role-based-agent.ts +386 -0
- package/src/cli/commands/debate.ts +761 -0
- package/src/cli/commands/eval.ts +475 -0
- package/src/cli/commands/report.ts +265 -0
- package/src/cli/index.ts +79 -0
- package/src/core/agent.ts +198 -0
- package/src/core/clarifications.ts +34 -0
- package/src/core/judge.ts +257 -0
- package/src/core/orchestrator.ts +432 -0
- package/src/core/state-manager.ts +322 -0
- package/src/eval/evaluator-agent.ts +130 -0
- package/src/eval/prompts/system.md +41 -0
- package/src/eval/prompts/user.md +64 -0
- package/src/providers/llm-provider.ts +25 -0
- package/src/providers/openai-provider.ts +84 -0
- package/src/providers/openrouter-provider.ts +122 -0
- package/src/providers/provider-factory.ts +64 -0
- package/src/types/agent.types.ts +141 -0
- package/src/types/config.types.ts +47 -0
- package/src/types/debate.types.ts +237 -0
- package/src/types/eval.types.ts +85 -0
- package/src/utils/common.ts +104 -0
- package/src/utils/context-formatter.ts +102 -0
- package/src/utils/context-summarizer.ts +143 -0
- package/src/utils/env-loader.ts +46 -0
- package/src/utils/exit-codes.ts +5 -0
- package/src/utils/id.ts +11 -0
- package/src/utils/logger.ts +48 -0
- package/src/utils/paths.ts +10 -0
- package/src/utils/progress-ui.ts +313 -0
- package/src/utils/prompt-loader.ts +79 -0
- package/src/utils/report-generator.ts +301 -0
- package/tests/clarifications.spec.ts +128 -0
- package/tests/cli.debate.spec.ts +144 -0
- package/tests/config-loading.spec.ts +206 -0
- package/tests/context-summarizer.spec.ts +131 -0
- package/tests/debate-config-custom.json +38 -0
- package/tests/env-loader.spec.ts +149 -0
- package/tests/eval.command.spec.ts +1191 -0
- package/tests/logger.spec.ts +19 -0
- package/tests/openai-provider.spec.ts +26 -0
- package/tests/openrouter-provider.spec.ts +279 -0
- package/tests/orchestrator-summary.spec.ts +386 -0
- package/tests/orchestrator.spec.ts +207 -0
- package/tests/prompt-loader.spec.ts +52 -0
- package/tests/prompts/architect.md +16 -0
- package/tests/provider-factory.spec.ts +150 -0
- package/tests/report.command.spec.ts +546 -0
- package/tests/role-based-agent-summary.spec.ts +476 -0
- package/tests/security-agent.spec.ts +221 -0
- package/tests/shared-prompts.spec.ts +318 -0
- package/tests/state-manager.spec.ts +251 -0
- package/tests/summary-prompts.spec.ts +153 -0
- package/tsconfig.json +49 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { runCli } from '../src/cli/index';
|
|
5
|
+
import { EXIT_INVALID_ARGS } from '../src/utils/exit-codes';
|
|
6
|
+
import { DebateState } from '../src/types/debate.types';
|
|
7
|
+
import { DEBATE_STATUS } from '../src/types/debate.types';
|
|
8
|
+
|
|
9
|
+
// Mock env-loader
|
|
10
|
+
jest.mock('../src/utils/env-loader', () => ({
|
|
11
|
+
loadEnvironmentFile: jest.fn()
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import { loadEnvironmentFile } from '../src/utils/env-loader';
|
|
15
|
+
|
|
16
|
+
const mockedLoadEnvironmentFile = loadEnvironmentFile as jest.MockedFunction<typeof loadEnvironmentFile>;
|
|
17
|
+
|
|
18
|
+
describe('CLI report command', () => {
|
|
19
|
+
const originalEnv = process.env;
|
|
20
|
+
let stderrSpy: jest.SpyInstance;
|
|
21
|
+
let stdoutSpy: jest.SpyInstance;
|
|
22
|
+
let exitSpy: jest.SpyInstance;
|
|
23
|
+
let tmpDir: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
process.env = { ...originalEnv };
|
|
27
|
+
stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
28
|
+
stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
29
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
|
|
30
|
+
throw new Error(`process.exit: ${code}`);
|
|
31
|
+
}) as any);
|
|
32
|
+
mockedLoadEnvironmentFile.mockClear();
|
|
33
|
+
|
|
34
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'report-test-'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
process.env = originalEnv;
|
|
39
|
+
stderrSpy.mockRestore();
|
|
40
|
+
stdoutSpy.mockRestore();
|
|
41
|
+
exitSpy.mockRestore();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
45
|
+
} catch {}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a minimal valid debate state for testing.
|
|
50
|
+
*/
|
|
51
|
+
function createMinimalDebateState(): DebateState {
|
|
52
|
+
return {
|
|
53
|
+
id: 'deb-test-123',
|
|
54
|
+
problem: 'Test problem description',
|
|
55
|
+
status: DEBATE_STATUS.COMPLETED,
|
|
56
|
+
currentRound: 1,
|
|
57
|
+
rounds: [
|
|
58
|
+
{
|
|
59
|
+
roundNumber: 1,
|
|
60
|
+
contributions: [
|
|
61
|
+
{
|
|
62
|
+
agentId: 'agent-architect',
|
|
63
|
+
agentRole: 'architect',
|
|
64
|
+
type: 'proposal',
|
|
65
|
+
content: 'Test proposal content',
|
|
66
|
+
metadata: {}
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
timestamp: new Date()
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
createdAt: new Date(),
|
|
73
|
+
updatedAt: new Date(),
|
|
74
|
+
finalSolution: {
|
|
75
|
+
description: 'Test solution',
|
|
76
|
+
tradeoffs: [],
|
|
77
|
+
recommendations: [],
|
|
78
|
+
confidence: 80,
|
|
79
|
+
synthesizedBy: 'judge-main'
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Creates a minimal valid config file for testing.
|
|
86
|
+
*/
|
|
87
|
+
function createMinimalConfig(): any {
|
|
88
|
+
return {
|
|
89
|
+
agents: [
|
|
90
|
+
{
|
|
91
|
+
id: 'agent-architect',
|
|
92
|
+
name: 'System Architect',
|
|
93
|
+
role: 'architect',
|
|
94
|
+
model: 'gpt-4',
|
|
95
|
+
provider: 'openai',
|
|
96
|
+
temperature: 0.5
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
judge: {
|
|
100
|
+
id: 'judge-main',
|
|
101
|
+
name: 'Technical Judge',
|
|
102
|
+
role: 'generalist',
|
|
103
|
+
model: 'gpt-4',
|
|
104
|
+
provider: 'openai',
|
|
105
|
+
temperature: 0.3
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe('Required flag validation', () => {
|
|
111
|
+
it('should reject when --debate flag is missing', async () => {
|
|
112
|
+
await expect(runCli(['report']))
|
|
113
|
+
.rejects.toThrow();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('File existence validation', () => {
|
|
118
|
+
it('should exit with invalid args when debate file does not exist', async () => {
|
|
119
|
+
await expect(runCli(['report', '--debate', 'nonexistent.json']))
|
|
120
|
+
.rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should exit with invalid args when debate path is a directory', async () => {
|
|
124
|
+
const dirPath = path.join(tmpDir, 'dir');
|
|
125
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
126
|
+
|
|
127
|
+
await expect(runCli(['report', '--debate', dirPath]))
|
|
128
|
+
.rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('JSON format validation', () => {
|
|
133
|
+
it('should exit with invalid args when debate file contains invalid JSON', async () => {
|
|
134
|
+
const debatePath = path.join(tmpDir, 'invalid.json');
|
|
135
|
+
fs.writeFileSync(debatePath, 'not valid json {');
|
|
136
|
+
|
|
137
|
+
await expect(runCli(['report', '--debate', debatePath]))
|
|
138
|
+
.rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should exit with invalid args when debate JSON is missing required fields', async () => {
|
|
142
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
143
|
+
fs.writeFileSync(debatePath, JSON.stringify({ id: 'test' })); // Missing problem, status, rounds
|
|
144
|
+
|
|
145
|
+
await expect(runCli(['report', '--debate', debatePath]))
|
|
146
|
+
.rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should exit with invalid args when debate JSON has invalid id field', async () => {
|
|
150
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
151
|
+
const invalidState = createMinimalDebateState();
|
|
152
|
+
invalidState.id = undefined as any;
|
|
153
|
+
fs.writeFileSync(debatePath, JSON.stringify(invalidState));
|
|
154
|
+
|
|
155
|
+
await expect(runCli(['report', '--debate', debatePath]))
|
|
156
|
+
.rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should exit with invalid args when debate JSON has invalid problem field', async () => {
|
|
160
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
161
|
+
const invalidState = createMinimalDebateState();
|
|
162
|
+
invalidState.problem = undefined as any;
|
|
163
|
+
fs.writeFileSync(debatePath, JSON.stringify(invalidState));
|
|
164
|
+
|
|
165
|
+
await expect(runCli(['report', '--debate', debatePath]))
|
|
166
|
+
.rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should exit with invalid args when debate JSON has invalid rounds field', async () => {
|
|
170
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
171
|
+
const invalidState = createMinimalDebateState();
|
|
172
|
+
invalidState.rounds = undefined as any;
|
|
173
|
+
fs.writeFileSync(debatePath, JSON.stringify(invalidState));
|
|
174
|
+
|
|
175
|
+
await expect(runCli(['report', '--debate', debatePath]))
|
|
176
|
+
.rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('Report generation', () => {
|
|
181
|
+
it('should generate report without config file when --config not provided', async () => {
|
|
182
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
183
|
+
const debateState = createMinimalDebateState();
|
|
184
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
185
|
+
|
|
186
|
+
// Ensure no config file exists
|
|
187
|
+
const defaultConfigPath = path.join(process.cwd(), 'debate-config.json');
|
|
188
|
+
const configExists = fs.existsSync(defaultConfigPath);
|
|
189
|
+
let configBackup: string | undefined;
|
|
190
|
+
|
|
191
|
+
if (configExists) {
|
|
192
|
+
configBackup = fs.readFileSync(defaultConfigPath, 'utf-8');
|
|
193
|
+
fs.unlinkSync(defaultConfigPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await runCli(['report', '--debate', debatePath]);
|
|
198
|
+
|
|
199
|
+
// Should write to stdout
|
|
200
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
201
|
+
const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
|
|
202
|
+
const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
|
|
203
|
+
|
|
204
|
+
// Should contain report content
|
|
205
|
+
expect(stdoutContent).toContain('# Debate:');
|
|
206
|
+
expect(stdoutContent).toContain('Test problem description');
|
|
207
|
+
expect(stdoutContent).toContain('## Problem Description');
|
|
208
|
+
expect(stdoutContent).toContain('## Agents');
|
|
209
|
+
expect(stdoutContent).toContain('## Judge');
|
|
210
|
+
expect(stdoutContent).toContain('## Rounds');
|
|
211
|
+
|
|
212
|
+
// Should contain minimal configs (agent ID as name, N/A for model/provider)
|
|
213
|
+
expect(stdoutContent).toContain('agent-architect');
|
|
214
|
+
expect(stdoutContent).toContain('N/A');
|
|
215
|
+
} finally {
|
|
216
|
+
// Restore config file if it existed
|
|
217
|
+
if (configExists && configBackup) {
|
|
218
|
+
fs.writeFileSync(defaultConfigPath, configBackup, 'utf-8');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should generate report with config file when --config provided', async () => {
|
|
224
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
225
|
+
const configPath = path.join(tmpDir, 'custom-config.json');
|
|
226
|
+
const debateState = createMinimalDebateState();
|
|
227
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
228
|
+
|
|
229
|
+
const config = createMinimalConfig();
|
|
230
|
+
fs.writeFileSync(configPath, JSON.stringify(config));
|
|
231
|
+
|
|
232
|
+
await runCli(['report', '--debate', debatePath, '--config', configPath]);
|
|
233
|
+
|
|
234
|
+
// Should write to stdout
|
|
235
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
236
|
+
const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
|
|
237
|
+
const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
|
|
238
|
+
|
|
239
|
+
// Should contain report content
|
|
240
|
+
expect(stdoutContent).toContain('# Debate:');
|
|
241
|
+
expect(stdoutContent).toContain('Test problem description');
|
|
242
|
+
expect(stdoutContent).toContain('## Problem Description');
|
|
243
|
+
expect(stdoutContent).toContain('## Agents');
|
|
244
|
+
expect(stdoutContent).toContain('## Judge');
|
|
245
|
+
expect(stdoutContent).toContain('## Rounds');
|
|
246
|
+
|
|
247
|
+
// Should contain config values (not N/A)
|
|
248
|
+
expect(stdoutContent).toContain('System Architect');
|
|
249
|
+
expect(stdoutContent).toContain('gpt-4');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should generate report and write to stdout when no output path provided', async () => {
|
|
253
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
254
|
+
const debateState = createMinimalDebateState();
|
|
255
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
256
|
+
|
|
257
|
+
// Don't create config file - should work without it
|
|
258
|
+
await runCli(['report', '--debate', debatePath]);
|
|
259
|
+
|
|
260
|
+
// Should write to stdout
|
|
261
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
262
|
+
const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
|
|
263
|
+
const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
|
|
264
|
+
|
|
265
|
+
// Should contain report content
|
|
266
|
+
expect(stdoutContent).toContain('# Debate:');
|
|
267
|
+
expect(stdoutContent).toContain('Test problem description');
|
|
268
|
+
expect(stdoutContent).toContain('## Problem Description');
|
|
269
|
+
expect(stdoutContent).toContain('## Agents');
|
|
270
|
+
expect(stdoutContent).toContain('## Judge');
|
|
271
|
+
expect(stdoutContent).toContain('## Rounds');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should generate report and write to file when output path provided', async () => {
|
|
275
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
276
|
+
const outputPath = path.join(tmpDir, 'report.md');
|
|
277
|
+
const debateState = createMinimalDebateState();
|
|
278
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
279
|
+
|
|
280
|
+
// Don't create config file - should work without it
|
|
281
|
+
await runCli(['report', '--debate', debatePath, '--output', outputPath]);
|
|
282
|
+
|
|
283
|
+
// Should create output file
|
|
284
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
285
|
+
|
|
286
|
+
// Should contain report content
|
|
287
|
+
const reportContent = fs.readFileSync(outputPath, 'utf-8');
|
|
288
|
+
expect(reportContent).toContain('# Debate:');
|
|
289
|
+
expect(reportContent).toContain('Test problem description');
|
|
290
|
+
expect(reportContent).toContain('## Problem Description');
|
|
291
|
+
expect(reportContent).toContain('## Agents');
|
|
292
|
+
expect(reportContent).toContain('## Judge');
|
|
293
|
+
expect(reportContent).toContain('## Rounds');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should create parent directories when output path is provided', async () => {
|
|
297
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
298
|
+
const outputPath = path.join(tmpDir, 'nested', 'dir', 'report.md');
|
|
299
|
+
const debateState = createMinimalDebateState();
|
|
300
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
301
|
+
|
|
302
|
+
// Don't create config file - should work without it
|
|
303
|
+
await runCli(['report', '--debate', debatePath, '--output', outputPath]);
|
|
304
|
+
|
|
305
|
+
// Should create nested directories and file
|
|
306
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
307
|
+
|
|
308
|
+
// Should contain report content
|
|
309
|
+
const reportContent = fs.readFileSync(outputPath, 'utf-8');
|
|
310
|
+
expect(reportContent).toContain('# Debate:');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should overwrite existing file when output path exists', async () => {
|
|
314
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
315
|
+
const outputPath = path.join(tmpDir, 'report.md');
|
|
316
|
+
const debateState = createMinimalDebateState();
|
|
317
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
318
|
+
|
|
319
|
+
// Create existing output file
|
|
320
|
+
fs.writeFileSync(outputPath, 'old content');
|
|
321
|
+
|
|
322
|
+
// Don't create config file - should work without it
|
|
323
|
+
await runCli(['report', '--debate', debatePath, '--output', outputPath]);
|
|
324
|
+
|
|
325
|
+
// Should overwrite with new content
|
|
326
|
+
const reportContent = fs.readFileSync(outputPath, 'utf-8');
|
|
327
|
+
expect(reportContent).not.toContain('old content');
|
|
328
|
+
expect(reportContent).toContain('# Debate:');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should use custom config file when --config provided', async () => {
|
|
332
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
333
|
+
const configPath = path.join(tmpDir, 'custom-config.json');
|
|
334
|
+
const debateState = createMinimalDebateState();
|
|
335
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
336
|
+
|
|
337
|
+
const config = createMinimalConfig();
|
|
338
|
+
fs.writeFileSync(configPath, JSON.stringify(config));
|
|
339
|
+
|
|
340
|
+
await runCli(['report', '--debate', debatePath, '--config', configPath]);
|
|
341
|
+
|
|
342
|
+
// Should succeed and write to stdout
|
|
343
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should include verbose metadata when --verbose flag is provided', async () => {
|
|
347
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
348
|
+
const debateState = createMinimalDebateState();
|
|
349
|
+
// Add metadata to contribution
|
|
350
|
+
if (debateState.rounds[0] && debateState.rounds[0].contributions[0]) {
|
|
351
|
+
debateState.rounds[0].contributions[0].metadata = {
|
|
352
|
+
latencyMs: 1234,
|
|
353
|
+
tokensUsed: 567,
|
|
354
|
+
model: 'gpt-4'
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
358
|
+
|
|
359
|
+
// Don't create config file - should work without it
|
|
360
|
+
await runCli(['report', '--debate', debatePath, '--verbose']);
|
|
361
|
+
|
|
362
|
+
// Should write to stdout
|
|
363
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
364
|
+
const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
|
|
365
|
+
const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
|
|
366
|
+
|
|
367
|
+
// Should contain verbose metadata
|
|
368
|
+
expect(stdoutContent).toContain('latency=');
|
|
369
|
+
expect(stdoutContent).toContain('tokens=');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should match agent configs by ID from debate state when --config provided', async () => {
|
|
373
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
374
|
+
const configPath = path.join(tmpDir, 'custom-config.json');
|
|
375
|
+
const debateState = createMinimalDebateState();
|
|
376
|
+
// Add another agent to debate
|
|
377
|
+
if (debateState.rounds[0]) {
|
|
378
|
+
debateState.rounds[0].contributions.push({
|
|
379
|
+
agentId: 'agent-performance',
|
|
380
|
+
agentRole: 'performance',
|
|
381
|
+
type: 'proposal',
|
|
382
|
+
content: 'Performance proposal',
|
|
383
|
+
metadata: {}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
387
|
+
|
|
388
|
+
// Create config with multiple agents
|
|
389
|
+
const config = createMinimalConfig();
|
|
390
|
+
config.agents.push({
|
|
391
|
+
id: 'agent-performance',
|
|
392
|
+
name: 'Performance Engineer',
|
|
393
|
+
role: 'performance',
|
|
394
|
+
model: 'gpt-4',
|
|
395
|
+
provider: 'openai',
|
|
396
|
+
temperature: 0.6
|
|
397
|
+
});
|
|
398
|
+
fs.writeFileSync(configPath, JSON.stringify(config));
|
|
399
|
+
|
|
400
|
+
await runCli(['report', '--debate', debatePath, '--config', configPath]);
|
|
401
|
+
|
|
402
|
+
// Should write to stdout
|
|
403
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
404
|
+
const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
|
|
405
|
+
const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
|
|
406
|
+
|
|
407
|
+
// Should contain both agents from config
|
|
408
|
+
expect(stdoutContent).toContain('System Architect');
|
|
409
|
+
expect(stdoutContent).toContain('Performance Engineer');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should create minimal configs from debate state when --config not provided', async () => {
|
|
413
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
414
|
+
const debateState = createMinimalDebateState();
|
|
415
|
+
// Add another agent to debate
|
|
416
|
+
if (debateState.rounds[0]) {
|
|
417
|
+
debateState.rounds[0].contributions.push({
|
|
418
|
+
agentId: 'agent-performance',
|
|
419
|
+
agentRole: 'performance',
|
|
420
|
+
type: 'proposal',
|
|
421
|
+
content: 'Performance proposal',
|
|
422
|
+
metadata: {}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
426
|
+
|
|
427
|
+
// Don't create config file - should work without it
|
|
428
|
+
await runCli(['report', '--debate', debatePath]);
|
|
429
|
+
|
|
430
|
+
// Should write to stdout
|
|
431
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
432
|
+
const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
|
|
433
|
+
const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
|
|
434
|
+
|
|
435
|
+
// Should contain both agents (as IDs since no config)
|
|
436
|
+
expect(stdoutContent).toContain('agent-architect');
|
|
437
|
+
expect(stdoutContent).toContain('agent-performance');
|
|
438
|
+
// Should contain N/A values for minimal configs
|
|
439
|
+
expect(stdoutContent).toContain('N/A');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should handle clarifications in debate state', async () => {
|
|
443
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
444
|
+
const debateState = createMinimalDebateState();
|
|
445
|
+
debateState.clarifications = [
|
|
446
|
+
{
|
|
447
|
+
agentId: 'agent-architect',
|
|
448
|
+
agentName: 'System Architect',
|
|
449
|
+
role: 'architect',
|
|
450
|
+
items: [
|
|
451
|
+
{
|
|
452
|
+
id: 'q1',
|
|
453
|
+
question: 'What is the expected scale?',
|
|
454
|
+
answer: '1000 users'
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
}
|
|
458
|
+
];
|
|
459
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
460
|
+
|
|
461
|
+
// Don't create config file - should work without it
|
|
462
|
+
await runCli(['report', '--debate', debatePath]);
|
|
463
|
+
|
|
464
|
+
// Should write to stdout
|
|
465
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
466
|
+
const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
|
|
467
|
+
const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
|
|
468
|
+
|
|
469
|
+
// Should contain clarifications section
|
|
470
|
+
expect(stdoutContent).toContain('## Clarifications');
|
|
471
|
+
expect(stdoutContent).toContain('What is the expected scale?');
|
|
472
|
+
expect(stdoutContent).toContain('1000 users');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should handle Date objects in debate state (revive from strings)', async () => {
|
|
476
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
477
|
+
const debateState = createMinimalDebateState();
|
|
478
|
+
// Serialize dates as strings (as they would be in JSON)
|
|
479
|
+
const serializedState = JSON.parse(JSON.stringify(debateState));
|
|
480
|
+
fs.writeFileSync(debatePath, JSON.stringify(serializedState));
|
|
481
|
+
|
|
482
|
+
// Don't create config file - should work without it
|
|
483
|
+
await runCli(['report', '--debate', debatePath]);
|
|
484
|
+
|
|
485
|
+
// Should succeed (dates are revived)
|
|
486
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('Error handling', () => {
|
|
491
|
+
it('should work when --config provided and config is missing judge (uses defaults)', async () => {
|
|
492
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
493
|
+
const configPath = path.join(tmpDir, 'config-no-judge.json');
|
|
494
|
+
const debateState = createMinimalDebateState();
|
|
495
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
496
|
+
|
|
497
|
+
// Create config without judge (loadConfig will add default judge)
|
|
498
|
+
const config = {
|
|
499
|
+
agents: createMinimalConfig().agents
|
|
500
|
+
// No judge - loadConfig will add default
|
|
501
|
+
};
|
|
502
|
+
fs.writeFileSync(configPath, JSON.stringify(config));
|
|
503
|
+
|
|
504
|
+
await runCli(['report', '--debate', debatePath, '--config', configPath]);
|
|
505
|
+
|
|
506
|
+
// Should succeed (loadConfig adds default judge)
|
|
507
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should work without config file when --config not provided', async () => {
|
|
511
|
+
const debatePath = path.join(tmpDir, 'debate.json');
|
|
512
|
+
const debateState = createMinimalDebateState();
|
|
513
|
+
fs.writeFileSync(debatePath, JSON.stringify(debateState));
|
|
514
|
+
|
|
515
|
+
// Ensure no config file exists
|
|
516
|
+
const defaultConfigPath = path.join(process.cwd(), 'debate-config.json');
|
|
517
|
+
const configExists = fs.existsSync(defaultConfigPath);
|
|
518
|
+
let configBackup: string | undefined;
|
|
519
|
+
|
|
520
|
+
if (configExists) {
|
|
521
|
+
configBackup = fs.readFileSync(defaultConfigPath, 'utf-8');
|
|
522
|
+
fs.unlinkSync(defaultConfigPath);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
await runCli(['report', '--debate', debatePath]);
|
|
527
|
+
|
|
528
|
+
// Should succeed without config (creates minimal configs from debate state)
|
|
529
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
530
|
+
const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
|
|
531
|
+
const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
|
|
532
|
+
|
|
533
|
+
// Should contain report with minimal configs
|
|
534
|
+
expect(stdoutContent).toContain('# Debate:');
|
|
535
|
+
expect(stdoutContent).toContain('## Agents');
|
|
536
|
+
expect(stdoutContent).toContain('agent-architect');
|
|
537
|
+
} finally {
|
|
538
|
+
// Restore config file if it existed
|
|
539
|
+
if (configExists && configBackup) {
|
|
540
|
+
fs.writeFileSync(defaultConfigPath, configBackup, 'utf-8');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|