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,207 @@
|
|
|
1
|
+
import { DebateOrchestrator } from '../src/core/orchestrator';
|
|
2
|
+
import { DebateConfig, DebateRound, DebateState, Solution } from '../src/types/debate.types';
|
|
3
|
+
import { Agent } from '../src/core/agent';
|
|
4
|
+
|
|
5
|
+
function createMockAgent(id: string, role: any): Agent {
|
|
6
|
+
return {
|
|
7
|
+
config: { id, role, model: 'gpt-4' },
|
|
8
|
+
propose: async () => ({ content: `${role} proposal`, metadata: {} }),
|
|
9
|
+
critique: async () => ({ content: `${role} critique`, metadata: {} }),
|
|
10
|
+
refine: async () => ({ content: `${role} refined`, metadata: {} }),
|
|
11
|
+
shouldSummarize: () => false,
|
|
12
|
+
prepareContext: async (context: any) => ({ context }),
|
|
13
|
+
} as any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const mockJudge = {
|
|
17
|
+
synthesize: async (_problem: string, _rounds: DebateRound[]) => ({
|
|
18
|
+
description: 'final',
|
|
19
|
+
tradeoffs: [],
|
|
20
|
+
recommendations: [],
|
|
21
|
+
confidence: 80,
|
|
22
|
+
synthesizedBy: 'judge',
|
|
23
|
+
} as Solution),
|
|
24
|
+
prepareContext: async (_rounds: DebateRound[]) => ({ context: { problem: '', history: [] } }),
|
|
25
|
+
} as any;
|
|
26
|
+
|
|
27
|
+
function createMockStateManager() {
|
|
28
|
+
const state: DebateState = {
|
|
29
|
+
id: 'deb-test',
|
|
30
|
+
problem: '',
|
|
31
|
+
status: 'running',
|
|
32
|
+
currentRound: 0,
|
|
33
|
+
rounds: [],
|
|
34
|
+
createdAt: new Date(),
|
|
35
|
+
updatedAt: new Date(),
|
|
36
|
+
} as any;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
createDebate: async (problem: string) => ({ ...state, problem }),
|
|
40
|
+
beginRound: async (_id: string) => {
|
|
41
|
+
const round = { roundNumber: state.rounds.length + 1, contributions: [], timestamp: new Date() } as DebateRound;
|
|
42
|
+
state.rounds.push(round);
|
|
43
|
+
state.currentRound = round.roundNumber;
|
|
44
|
+
state.updatedAt = new Date();
|
|
45
|
+
return round;
|
|
46
|
+
},
|
|
47
|
+
addContribution: async (_id: string, contrib: any) => {
|
|
48
|
+
const round = state.rounds[state.currentRound - 1];
|
|
49
|
+
if (!round) throw new Error('No active round');
|
|
50
|
+
round.contributions.push(contrib);
|
|
51
|
+
state.updatedAt = new Date();
|
|
52
|
+
},
|
|
53
|
+
completeDebate: async (_id: string, solution: Solution) => {
|
|
54
|
+
state.status = 'completed';
|
|
55
|
+
(state as any).finalSolution = solution;
|
|
56
|
+
state.updatedAt = new Date();
|
|
57
|
+
},
|
|
58
|
+
getState: () => state,
|
|
59
|
+
} as any;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('DebateOrchestrator (Flow 1)', () => {
|
|
63
|
+
it('runs the correct phases for rounds=3 and calls judge synthesis', async () => {
|
|
64
|
+
const agents = [createMockAgent('a1', 'architect'), createMockAgent('a2', 'performance')];
|
|
65
|
+
const sm = createMockStateManager();
|
|
66
|
+
const cfg: DebateConfig = {
|
|
67
|
+
rounds: 3,
|
|
68
|
+
terminationCondition: { type: 'fixed' },
|
|
69
|
+
synthesisMethod: 'judge',
|
|
70
|
+
includeFullHistory: true,
|
|
71
|
+
timeoutPerRound: 300000,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const orchestrator = new DebateOrchestrator(agents as any, mockJudge, sm as any, cfg);
|
|
75
|
+
await expect(orchestrator.runDebate('Design a caching system')).resolves.toBeDefined();
|
|
76
|
+
|
|
77
|
+
const state = (sm as any).getState();
|
|
78
|
+
expect(state.rounds.length).toBe(3);
|
|
79
|
+
// Each round should include refinement contributions
|
|
80
|
+
state.rounds.forEach((round: DebateRound) => {
|
|
81
|
+
const hasRefinement = round.contributions.some((c: any) => c.type === 'refinement');
|
|
82
|
+
expect(hasRefinement).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('with rounds=1 runs all phases and synthesizes', async () => {
|
|
87
|
+
const agents = [createMockAgent('a1', 'architect'), createMockAgent('a2', 'performance')];
|
|
88
|
+
const sm = createMockStateManager();
|
|
89
|
+
const cfg: DebateConfig = {
|
|
90
|
+
rounds: 1,
|
|
91
|
+
terminationCondition: { type: 'fixed' },
|
|
92
|
+
synthesisMethod: 'judge',
|
|
93
|
+
includeFullHistory: true,
|
|
94
|
+
timeoutPerRound: 300000,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const orchestrator = new DebateOrchestrator(agents as any, mockJudge, sm as any, cfg);
|
|
98
|
+
await expect(orchestrator.runDebate('Design a rate limiting system')).resolves.toBeDefined();
|
|
99
|
+
|
|
100
|
+
const state = (sm as any).getState();
|
|
101
|
+
expect(state.rounds.length).toBe(1);
|
|
102
|
+
const hasProposal = state.rounds[0].contributions.some((c: any) => c.type === 'proposal');
|
|
103
|
+
const hasCritique = state.rounds[0].contributions.some((c: any) => c.type === 'critique');
|
|
104
|
+
const hasRefinement = state.rounds[0].contributions.some((c: any) => c.type === 'refinement');
|
|
105
|
+
expect(hasProposal && hasCritique && hasRefinement).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('round 2 proposals are sourced from round 1 refinements with zeroed metadata', async () => {
|
|
109
|
+
const agents = [createMockAgent('a1', 'architect'), createMockAgent('a2', 'performance')];
|
|
110
|
+
const sm = createMockStateManager();
|
|
111
|
+
const cfg: DebateConfig = {
|
|
112
|
+
rounds: 2,
|
|
113
|
+
terminationCondition: { type: 'fixed' },
|
|
114
|
+
synthesisMethod: 'judge',
|
|
115
|
+
includeFullHistory: true,
|
|
116
|
+
timeoutPerRound: 300000,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const orchestrator = new DebateOrchestrator(agents as any, mockJudge, sm as any, cfg);
|
|
120
|
+
await orchestrator.runDebate('Design X');
|
|
121
|
+
|
|
122
|
+
const state = (sm as any).getState();
|
|
123
|
+
expect(state.rounds.length).toBe(2);
|
|
124
|
+
|
|
125
|
+
const r1 = state.rounds[0];
|
|
126
|
+
const r2 = state.rounds[1];
|
|
127
|
+
|
|
128
|
+
const r1RefByAgent: Record<string, string> = {};
|
|
129
|
+
r1.contributions.filter((c: any) => c.type === 'refinement').forEach((c: any) => {
|
|
130
|
+
r1RefByAgent[c.agentId] = c.content;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const r2Props = r2.contributions.filter((c: any) => c.type === 'proposal');
|
|
134
|
+
expect(r2Props.length).toBe(agents.length);
|
|
135
|
+
|
|
136
|
+
// Proposals in round 2 must equal refinements from round 1 per agent, with tokens/latency zero
|
|
137
|
+
for (const p of r2Props) {
|
|
138
|
+
expect(p.content).toBe(r1RefByAgent[p.agentId]);
|
|
139
|
+
expect(p.metadata?.tokensUsed ?? 0).toBe(0);
|
|
140
|
+
expect(p.metadata?.latencyMs ?? 0).toBe(0);
|
|
141
|
+
expect(typeof p.metadata?.model).toBe('string');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('falls back to LLM proposal and warns when prior refinement is missing', async () => {
|
|
146
|
+
const agents = [createMockAgent('a1', 'architect'), createMockAgent('a2', 'performance')];
|
|
147
|
+
|
|
148
|
+
// Custom SM that drops refinement for agent a2 in round 1
|
|
149
|
+
const state: any = {
|
|
150
|
+
id: 'deb-test', problem: '', status: 'running', currentRound: 0, rounds: [], createdAt: new Date(), updatedAt: new Date(),
|
|
151
|
+
};
|
|
152
|
+
const sm = {
|
|
153
|
+
createDebate: async (problem: string) => ({ ...state, problem }),
|
|
154
|
+
beginRound: async (_id: string) => {
|
|
155
|
+
const round = { roundNumber: state.rounds.length + 1, contributions: [], timestamp: new Date() } as DebateRound;
|
|
156
|
+
state.rounds.push(round);
|
|
157
|
+
state.currentRound = round.roundNumber;
|
|
158
|
+
state.updatedAt = new Date();
|
|
159
|
+
return round;
|
|
160
|
+
},
|
|
161
|
+
addContribution: async (_id: string, contrib: any) => {
|
|
162
|
+
const round = state.rounds[state.currentRound - 1];
|
|
163
|
+
if (!round) throw new Error('No active round');
|
|
164
|
+
// Drop refinement for a2 in round 1 only
|
|
165
|
+
if (round.roundNumber === 1 && contrib.type === 'refinement' && contrib.agentId === 'a2') {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
round.contributions.push(contrib);
|
|
169
|
+
state.updatedAt = new Date();
|
|
170
|
+
},
|
|
171
|
+
completeDebate: async (_id: string, solution: Solution) => { state.status = 'completed'; (state as any).finalSolution = solution; state.updatedAt = new Date(); },
|
|
172
|
+
getState: () => state,
|
|
173
|
+
} as any;
|
|
174
|
+
|
|
175
|
+
// Spy on stderr warnings
|
|
176
|
+
const cli = await import('../src/cli/index');
|
|
177
|
+
const warnSpy = jest.spyOn(cli, 'writeStderr').mockImplementation(() => {});
|
|
178
|
+
|
|
179
|
+
const cfg: DebateConfig = {
|
|
180
|
+
rounds: 2,
|
|
181
|
+
terminationCondition: { type: 'fixed' },
|
|
182
|
+
synthesisMethod: 'judge',
|
|
183
|
+
includeFullHistory: true,
|
|
184
|
+
timeoutPerRound: 300000,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const orchestrator = new DebateOrchestrator(agents as any, mockJudge, sm as any, cfg);
|
|
188
|
+
await orchestrator.runDebate('Design Y');
|
|
189
|
+
|
|
190
|
+
const r1 = state.rounds[0];
|
|
191
|
+
const r2 = state.rounds[1];
|
|
192
|
+
|
|
193
|
+
// a1 should be carried over; a2 should fall back to LLM proposal content
|
|
194
|
+
const r1RefA1 = r1.contributions.find((c: any) => c.type === 'refinement' && c.agentId === 'a1')?.content;
|
|
195
|
+
const r2PropA1 = r2.contributions.find((c: any) => c.type === 'proposal' && c.agentId === 'a1')?.content;
|
|
196
|
+
expect(r2PropA1).toBe(r1RefA1);
|
|
197
|
+
|
|
198
|
+
const r2PropA2 = r2.contributions.find((c: any) => c.type === 'proposal' && c.agentId === 'a2')?.content;
|
|
199
|
+
expect(r2PropA2).toBe('performance proposal');
|
|
200
|
+
|
|
201
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
202
|
+
const calls = warnSpy.mock.calls.flat().join(' ');
|
|
203
|
+
expect(calls).toMatch(/Missing previous refinement/);
|
|
204
|
+
|
|
205
|
+
warnSpy.mockRestore();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { resolvePrompt } from '../src/utils/prompt-loader';
|
|
5
|
+
|
|
6
|
+
describe('resolvePrompt', () => {
|
|
7
|
+
let tmpDir: string;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prompt-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns built-in when no path provided', () => {
|
|
16
|
+
const res = resolvePrompt({ label: 'Agent A', configDir: tmpDir, defaultText: 'DEFAULT' });
|
|
17
|
+
expect(res.source).toBe('built-in');
|
|
18
|
+
expect(res.text).toBe('DEFAULT');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('resolves relative path and reads entire file', () => {
|
|
22
|
+
const p = path.join(tmpDir, 'a.md');
|
|
23
|
+
fs.writeFileSync(p, '# Title\nHello world');
|
|
24
|
+
const res = resolvePrompt({ label: 'Agent B', configDir: tmpDir, promptPath: 'a.md', defaultText: 'DEFAULT' });
|
|
25
|
+
expect(res.source).toBe('file');
|
|
26
|
+
expect(res.absPath).toBe(p);
|
|
27
|
+
expect(res.text).toContain('Hello world');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('falls back on missing file with warning', () => {
|
|
31
|
+
const res = resolvePrompt({ label: 'Agent C', configDir: tmpDir, promptPath: 'missing.md', defaultText: 'DEFAULT' });
|
|
32
|
+
expect(res.source).toBe('built-in');
|
|
33
|
+
expect(res.text).toBe('DEFAULT');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('falls back on empty file', () => {
|
|
37
|
+
const p = path.join(tmpDir, 'empty.md');
|
|
38
|
+
fs.writeFileSync(p, ' \n\t');
|
|
39
|
+
const res = resolvePrompt({ label: 'Agent D', configDir: tmpDir, promptPath: 'empty.md', defaultText: 'DEFAULT' });
|
|
40
|
+
expect(res.source).toBe('built-in');
|
|
41
|
+
expect(res.text).toBe('DEFAULT');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('accepts absolute path', () => {
|
|
45
|
+
const p = path.join(tmpDir, 'abs.md');
|
|
46
|
+
fs.writeFileSync(p, 'ABC');
|
|
47
|
+
const res = resolvePrompt({ label: 'Agent E', configDir: tmpDir, promptPath: p, defaultText: 'DEFAULT' });
|
|
48
|
+
expect(res.source).toBe('file');
|
|
49
|
+
expect(res.absPath).toBe(p);
|
|
50
|
+
expect(res.text).toBe('ABC');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Architect System Prompt (Custom)
|
|
2
|
+
|
|
3
|
+
You are a senior software architect specializing in pragmatic, production-ready designs.
|
|
4
|
+
|
|
5
|
+
When proposing solutions:
|
|
6
|
+
- Prefer simplicity and clear boundaries over gratuitous microservices
|
|
7
|
+
- Identify critical components, data flows, and failure modes
|
|
8
|
+
- Provide trade-offs and a short migration/rollout plan
|
|
9
|
+
|
|
10
|
+
When critiquing:
|
|
11
|
+
- Prioritize correctness, operability, and cost of ownership
|
|
12
|
+
- Flag risky assumptions and unclear responsibilities
|
|
13
|
+
|
|
14
|
+
When refining:
|
|
15
|
+
- Address the most impactful risks first
|
|
16
|
+
- Simplify where possible without losing essential capabilities
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createProvider } from '../src/providers/provider-factory';
|
|
2
|
+
import { OpenAIProvider } from '../src/providers/openai-provider';
|
|
3
|
+
import { OpenRouterProvider } from '../src/providers/openrouter-provider';
|
|
4
|
+
import { EXIT_CONFIG_ERROR } from '../src/utils/exit-codes';
|
|
5
|
+
|
|
6
|
+
// Mock the provider classes
|
|
7
|
+
jest.mock('../src/providers/openai-provider');
|
|
8
|
+
jest.mock('../src/providers/openrouter-provider');
|
|
9
|
+
|
|
10
|
+
describe('Provider Factory', () => {
|
|
11
|
+
const originalEnv = process.env;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
process.env = { ...originalEnv };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
process.env = originalEnv;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('createProvider', () => {
|
|
23
|
+
it('should create OpenAI provider with valid API key', () => {
|
|
24
|
+
process.env.OPENAI_API_KEY = 'test-openai-key';
|
|
25
|
+
|
|
26
|
+
const provider = createProvider('openai');
|
|
27
|
+
|
|
28
|
+
expect(OpenAIProvider).toHaveBeenCalledWith('test-openai-key');
|
|
29
|
+
expect(provider).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should create OpenRouter provider with valid API key', () => {
|
|
33
|
+
process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
|
|
34
|
+
|
|
35
|
+
const provider = createProvider('openrouter');
|
|
36
|
+
|
|
37
|
+
expect(OpenRouterProvider).toHaveBeenCalledWith('test-openrouter-key');
|
|
38
|
+
expect(provider).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw error when OpenAI API key is missing', () => {
|
|
42
|
+
delete process.env.OPENAI_API_KEY;
|
|
43
|
+
|
|
44
|
+
expect(() => createProvider('openai')).toThrow();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
createProvider('openai');
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
expect(error.message).toBe('OPENAI_API_KEY is not set');
|
|
50
|
+
expect(error.code).toBe(EXIT_CONFIG_ERROR);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should throw error when OpenRouter API key is missing', () => {
|
|
55
|
+
delete process.env.OPENROUTER_API_KEY;
|
|
56
|
+
|
|
57
|
+
expect(() => createProvider('openrouter')).toThrow();
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
createProvider('openrouter');
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
expect(error.message).toBe('OPENROUTER_API_KEY is not set');
|
|
63
|
+
expect(error.code).toBe(EXIT_CONFIG_ERROR);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should throw error for unsupported provider type', () => {
|
|
68
|
+
expect(() => createProvider('unsupported')).toThrow();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
createProvider('unsupported');
|
|
72
|
+
} catch (error: any) {
|
|
73
|
+
expect(error.message).toBe('Unsupported provider type: unsupported. Supported types are: openai, openrouter');
|
|
74
|
+
expect(error.code).toBe(EXIT_CONFIG_ERROR);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should throw error for empty provider type', () => {
|
|
79
|
+
expect(() => createProvider('')).toThrow();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
createProvider('');
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
expect(error.message).toBe('Unsupported provider type: . Supported types are: openai, openrouter');
|
|
85
|
+
expect(error.code).toBe(EXIT_CONFIG_ERROR);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle undefined provider type', () => {
|
|
90
|
+
expect(() => createProvider(undefined as any)).toThrow();
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
createProvider(undefined as any);
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
expect(error.message).toBe('Unsupported provider type: undefined. Supported types are: openai, openrouter');
|
|
96
|
+
expect(error.code).toBe(EXIT_CONFIG_ERROR);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle case sensitivity correctly', () => {
|
|
101
|
+
process.env.OPENAI_API_KEY = 'test-openai-key';
|
|
102
|
+
|
|
103
|
+
// Should work with exact case
|
|
104
|
+
expect(() => createProvider('openai')).not.toThrow();
|
|
105
|
+
|
|
106
|
+
// Should fail with different case
|
|
107
|
+
expect(() => createProvider('OpenAI')).toThrow();
|
|
108
|
+
expect(() => createProvider('OPENAI')).toThrow();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle whitespace in provider type', () => {
|
|
112
|
+
process.env.OPENAI_API_KEY = 'test-openai-key';
|
|
113
|
+
|
|
114
|
+
expect(() => createProvider(' openai ')).toThrow();
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
createProvider(' openai ');
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
expect(error.message).toBe('Unsupported provider type: openai . Supported types are: openai, openrouter');
|
|
120
|
+
expect(error.code).toBe(EXIT_CONFIG_ERROR);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should work with empty string API keys', () => {
|
|
125
|
+
process.env.OPENAI_API_KEY = '';
|
|
126
|
+
|
|
127
|
+
expect(() => createProvider('openai')).toThrow();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
createProvider('openai');
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
expect(error.message).toBe('OPENAI_API_KEY is not set');
|
|
133
|
+
expect(error.code).toBe(EXIT_CONFIG_ERROR);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should work with whitespace-only API keys', () => {
|
|
138
|
+
process.env.OPENAI_API_KEY = ' ';
|
|
139
|
+
|
|
140
|
+
expect(() => createProvider('openai')).toThrow();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
createProvider('openai');
|
|
144
|
+
} catch (error: any) {
|
|
145
|
+
expect(error.message).toBe('OPENAI_API_KEY is not set');
|
|
146
|
+
expect(error.code).toBe(EXIT_CONFIG_ERROR);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|