coderev-cli 1.0.26 → 1.3.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.
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Tests for issue-validator.js
3
+ *
4
+ * Covers:
5
+ * - parseIssueRef (GitHub/GitLab URLs, shorthands)
6
+ * - extractIssueKeywords
7
+ * - validateIssueAgainstDiff
8
+ * - findRelatedIssues
9
+ * - formatIssueReport
10
+ * - validateIssue (integration)
11
+ */
12
+
13
+ const { describe, it } = require('node:test');
14
+ const assert = require('node:assert/strict');
15
+
16
+ describe('issue-validator.js', () => {
17
+ const {
18
+ parseIssueRef,
19
+ extractIssueKeywords,
20
+ validateIssueAgainstDiff,
21
+ findRelatedIssues,
22
+ generateIssueReport,
23
+ formatIssueReport,
24
+ parseCommitLog,
25
+ } = require('./issue-validator');
26
+
27
+ describe('parseIssueRef', () => {
28
+ it('should parse full GitHub issue URL', () => {
29
+ const ref = parseIssueRef('https://github.com/facebook/react/issues/42');
30
+ assert.equal(ref.platform, 'github');
31
+ assert.equal(ref.owner, 'facebook');
32
+ assert.equal(ref.repo, 'react');
33
+ assert.equal(ref.issueNumber, 42);
34
+ });
35
+
36
+ it('should parse GitHub shorthand owner/repo#42', () => {
37
+ const ref = parseIssueRef('facebook/react#42');
38
+ assert.equal(ref.platform, 'github');
39
+ assert.equal(ref.owner, 'facebook');
40
+ assert.equal(ref.repo, 'react');
41
+ assert.equal(ref.issueNumber, 42);
42
+ });
43
+
44
+ it('should parse bare issue number #42', () => {
45
+ const ref = parseIssueRef('#42');
46
+ assert.equal(ref.platform, 'github');
47
+ assert.equal(ref.issueNumber, 42);
48
+ assert.equal(ref.owner, null);
49
+ });
50
+
51
+ it('should parse bare number 42', () => {
52
+ const ref = parseIssueRef('42');
53
+ assert.equal(ref.platform, 'github');
54
+ assert.equal(ref.issueNumber, 42);
55
+ });
56
+
57
+ it('should parse GitLab issue URL', () => {
58
+ const ref = parseIssueRef('https://gitlab.com/gitlab-org/gitlab/-/issues/999');
59
+ assert.equal(ref.platform, 'gitlab');
60
+ assert.equal(ref.owner, 'gitlab-org');
61
+ assert.equal(ref.repo, 'gitlab');
62
+ assert.equal(ref.issueNumber, 999);
63
+ });
64
+
65
+ it('should parse GitLab shorthand owner/repo!42', () => {
66
+ const ref = parseIssueRef('gitlab-org/gitlab!42');
67
+ assert.equal(ref.platform, 'gitlab');
68
+ assert.equal(ref.owner, 'gitlab-org');
69
+ assert.equal(ref.repo, 'gitlab');
70
+ assert.equal(ref.issueNumber, 42);
71
+ });
72
+
73
+ it('should return null for unparseable input', () => {
74
+ assert.equal(parseIssueRef('not-an-issue'), null);
75
+ assert.equal(parseIssueRef(''), null);
76
+ assert.equal(parseIssueRef('https://example.com'), null);
77
+ });
78
+
79
+ it('should parse GitHub URL with .git in repo name', () => {
80
+ const ref = parseIssueRef('https://github.com/owner/repo.git/issues/42');
81
+ assert.equal(ref.repo, 'repo');
82
+ });
83
+
84
+ it('should parse GitHub shorthand with hyphen in name', () => {
85
+ const ref = parseIssueRef('my-org/my-repo#123');
86
+ assert.equal(ref.owner, 'my-org');
87
+ assert.equal(ref.repo, 'my-repo');
88
+ assert.equal(ref.issueNumber, 123);
89
+ });
90
+
91
+ it('should store original URL in ref', () => {
92
+ const url = 'https://github.com/owner/repo/issues/42';
93
+ const ref = parseIssueRef(url);
94
+ assert.equal(ref.url, url);
95
+ });
96
+ });
97
+
98
+ describe('extractIssueKeywords', () => {
99
+ it('should extract file paths from issue body', () => {
100
+ const issue = {
101
+ title: 'Fix login bug',
102
+ body: 'The `src/auth/login.js` file has a typo. Also check `src/utils/helper.ts`.',
103
+ };
104
+ const keywords = extractIssueKeywords(issue);
105
+ assert.ok(keywords.includes('src/auth/login.js'));
106
+ assert.ok(keywords.includes('src/utils/helper.ts'));
107
+ });
108
+
109
+ it('should extract function names', () => {
110
+ const issue = {
111
+ title: 'Refactor handleLogin',
112
+ body: 'Need to fix authenticate() and validateInput() functions.',
113
+ };
114
+ const keywords = extractIssueKeywords(issue);
115
+ // Function names are lowercased: authenticate, validateinput, handlelogin
116
+ assert.ok(keywords.some(k => k.includes('authenticate')));
117
+ assert.ok(keywords.some(k => k.includes('validateinput')));
118
+ });
119
+
120
+ it('should extract technical keywords', () => {
121
+ const issue = {
122
+ title: 'Add rate limiting to API',
123
+ body: 'We need rate limiting on the auth endpoint to prevent abuse.',
124
+ };
125
+ const keywords = extractIssueKeywords(issue);
126
+ assert.ok(keywords.includes('auth'));
127
+ assert.ok(keywords.includes('api'));
128
+ assert.ok(keywords.includes('endpoint'));
129
+ });
130
+
131
+ it('should extract keywords from title and body combined', () => {
132
+ const issue = {
133
+ title: 'Fix database connection pool',
134
+ body: 'The database connections are not being released properly. Need to add cache layer.',
135
+ };
136
+ const keywords = extractIssueKeywords(issue);
137
+ assert.ok(keywords.includes('database'));
138
+ assert.ok(keywords.includes('cache'));
139
+ });
140
+
141
+ it('should handle empty body', () => {
142
+ const issue = { title: 'Update README', body: '' };
143
+ const keywords = extractIssueKeywords(issue);
144
+ // README is not a tech term, so keywords should be minimal
145
+ assert.ok(Array.isArray(keywords));
146
+ });
147
+
148
+ it('should handle emojis and special chars', () => {
149
+ const issue = {
150
+ title: '🐛 Fix login crash',
151
+ body: 'The login page crashes. Need to fix the authentication flow.',
152
+ };
153
+ const keywords = extractIssueKeywords(issue);
154
+ assert.ok(keywords.includes('auth'));
155
+ });
156
+ });
157
+
158
+ describe('validateIssueAgainstDiff', () => {
159
+ it('should detect fully-addressed when keywords match diff', () => {
160
+ const issue = {
161
+ title: 'Fix login handler',
162
+ body: 'The `src/auth/login.js` needs to handle error cases and auth properly.',
163
+ };
164
+ const diff = `
165
+ diff --git a/src/auth/login.js b/src/auth/login.js
166
+ + function login(req, res) {
167
+ + try {
168
+ + authenticate(req.body);
169
+ + } catch (err) {
170
+ + handleError(err);
171
+ + }
172
+ + }
173
+ + function handleError(err) {
174
+ + res.status(500).json({ error: err.message });
175
+ + }
176
+ `;
177
+ const result = validateIssueAgainstDiff(issue, diff);
178
+ // Should be fully-addressed since all keywords are in diff
179
+ assert.ok(result.overallRelevance >= 65);
180
+ assert.ok(result.matchedKeywords.length > 0);
181
+ });
182
+
183
+ it('should detect partially-addressed when some keywords match', () => {
184
+ const issue = {
185
+ title: 'Add login, signup, and password reset pages',
186
+ body: 'We need `signup.js`, `login.js`, and `reset-password.js`.',
187
+ };
188
+ const diff = `
189
+ diff --git a/src/auth/login.js b/src/auth/login.js
190
+ + export function login() {}
191
+ `;
192
+ const result = validateIssueAgainstDiff(issue, diff);
193
+ // Only login.js matched — should be partial
194
+ assert.ok(result.overallRelevance < 70);
195
+ assert.ok(result.unmatchedKeywords.length > 0);
196
+ });
197
+
198
+ it('should detect unaddressed when no keywords match', () => {
199
+ const issue = {
200
+ title: 'Fix payment gateway integration',
201
+ body: 'The Stripe payment integration is broken. Need to fix billing service.',
202
+ };
203
+ const diff = `
204
+ diff --git a/src/homepage.js b/src/homepage.js
205
+ + function renderHomepage() {}
206
+ `;
207
+ const result = validateIssueAgainstDiff(issue, diff);
208
+ assert.equal(result.verdict, 'unaddressed');
209
+ assert.ok(result.overallRelevance < 30);
210
+ });
211
+
212
+ it('should return unknown when no keywords extracted', () => {
213
+ const issue = { title: 'Foo', body: 'Bar' };
214
+ const diff = 'some random diff';
215
+ const result = validateIssueAgainstDiff(issue, diff);
216
+ assert.equal(result.verdict, 'unknown');
217
+ });
218
+
219
+ it('should track matched and unmatched keywords', () => {
220
+ const issue = {
221
+ title: 'Fix API handler',
222
+ body: 'The API handler in api.js needs error handling and caching.',
223
+ };
224
+ const diff = `
225
+ diff --git a/src/api.js b/src/api.js
226
+ + export async function handler(req, res) {
227
+ + try {
228
+ + const result = await getData();
229
+ + res.json(result);
230
+ + } catch (err) {
231
+ + // error handling
232
+ + }
233
+ + }
234
+ `;
235
+ const result = validateIssueAgainstDiff(issue, diff);
236
+ assert.ok(result.matchedKeywords.includes('api'));
237
+ assert.ok(result.matchedKeywords.includes('error'));
238
+ assert.ok(result.matchedFiles.includes('api.js'));
239
+ });
240
+
241
+ it('should be case-insensitive for keyword matching', () => {
242
+ const issue = {
243
+ title: 'Fix AUTH bug',
244
+ body: 'Need to fix Authentication.',
245
+ };
246
+ const diff = 'fixed auth issue';
247
+ const result = validateIssueAgainstDiff(issue, diff);
248
+ assert.ok(result.matchedKeywords.includes('auth'));
249
+ });
250
+ });
251
+
252
+ describe('findRelatedIssues', () => {
253
+ it('should find GitHub issue references in commit log', () => {
254
+ const commitLog = `
255
+ abc1234 fix: resolve login timeout issue (fixes #42)
256
+ def5678 refactor: update build config, ref #43
257
+ ghi9012 docs: closes #44 and resolves #45
258
+ `;
259
+ const related = findRelatedIssues('', commitLog);
260
+ assert.ok(related.includes('#42'));
261
+ assert.ok(related.includes('#43'));
262
+ assert.ok(related.includes('#44'));
263
+ assert.ok(related.includes('#45'));
264
+ });
265
+
266
+ it('should find issue references in diff', () => {
267
+ const diff = 'Fixes #100 and relates to #101. Also related to #102.';
268
+ const related = findRelatedIssues(diff, '');
269
+ assert.ok(related.includes('#100'));
270
+ assert.ok(related.includes('#101'));
271
+ assert.ok(related.includes('#102'));
272
+ });
273
+
274
+ it('should find references in both commit log and diff', () => {
275
+ const commitLog = 'fixes #1';
276
+ const diff = 'closes #2';
277
+ const related = findRelatedIssues(diff, commitLog);
278
+ assert.ok(related.includes('#1'));
279
+ assert.ok(related.includes('#2'));
280
+ });
281
+
282
+ it('should handle GitLab style references', () => {
283
+ const commitLog = 'closes !123 and fixes !456';
284
+ const related = findRelatedIssues('', commitLog);
285
+ assert.ok(related.includes('!123'));
286
+ assert.ok(related.includes('!456'));
287
+ });
288
+
289
+ it('should deduplicate references', () => {
290
+ const text = 'fixes #42 and fixes #42 and closes #42';
291
+ const related = findRelatedIssues(text, '');
292
+ const count42 = related.filter(r => r === '#42').length;
293
+ assert.equal(count42, 1);
294
+ });
295
+
296
+ it('should return empty array when no references found', () => {
297
+ assert.deepEqual(findRelatedIssues('', 'No references here'), []);
298
+ });
299
+
300
+ it('should handle various verb forms', () => {
301
+ const text = 'fix #1 fixed #2 fixes #3 close #4 closed #5 closes #6 resolve #7 resolved #8 resolves #9';
302
+ const related = findRelatedIssues(text, '');
303
+ ['#1', '#2', '#3', '#4', '#5', '#6', '#7', '#8', '#9'].forEach(r => {
304
+ assert.ok(related.includes(r), `Expected ${r} to be found`);
305
+ });
306
+ });
307
+
308
+ it('should match "see #123" and "related to #456" patterns', () => {
309
+ const text = 'see #123, related to #456, reference #789';
310
+ const related = findRelatedIssues(text, '');
311
+ assert.ok(related.includes('#123'));
312
+ assert.ok(related.includes('#456'));
313
+ assert.ok(related.includes('#789'));
314
+ });
315
+ });
316
+
317
+ describe('generateIssueReport', () => {
318
+ it('should generate a structured report', () => {
319
+ const issue = {
320
+ number: 42,
321
+ title: 'Fix login bug',
322
+ url: 'https://github.com/owner/repo/issues/42',
323
+ state: 'open',
324
+ labels: ['bug', 'high-priority'],
325
+ assignees: ['alice'],
326
+ html_url: 'https://github.com/owner/repo/issues/42',
327
+ };
328
+ const validation = {
329
+ verdict: 'fully-addressed',
330
+ overallRelevance: 85,
331
+ details: 'PR touches 3 mentioned files',
332
+ matchedFiles: ['login.js'],
333
+ matchedKeywords: ['auth', 'login'],
334
+ unmatchedKeywords: [],
335
+ };
336
+ const reviewResult = { score: 90, issues: [] };
337
+ const report = generateIssueReport(issue, validation, ['#43', '#44'], reviewResult);
338
+
339
+ assert.equal(report.issue.number, 42);
340
+ assert.equal(report.verdict, 'fully-addressed');
341
+ assert.equal(report.validation.overallRelevance, 85);
342
+ assert.deepEqual(report.relatedIssues, ['#43', '#44']);
343
+ assert.equal(report.combinedScore, 90);
344
+ });
345
+ });
346
+
347
+ describe('formatIssueReport', () => {
348
+ it('should return a non-empty string', () => {
349
+ const issue = {
350
+ number: 1,
351
+ title: 'Test issue',
352
+ url: 'https://github.com/test/test/issues/1',
353
+ state: 'open',
354
+ labels: [],
355
+ assignees: [],
356
+ };
357
+ const validation = {
358
+ verdict: 'fully-addressed',
359
+ overallRelevance: 100,
360
+ details: 'All keywords matched',
361
+ matchedFiles: ['test.js'],
362
+ matchedKeywords: ['test'],
363
+ unmatchedKeywords: [],
364
+ };
365
+ const report = generateIssueReport(issue, validation, [], null);
366
+ const formatted = formatIssueReport(report);
367
+ assert.ok(typeof formatted === 'string');
368
+ assert.ok(formatted.length > 0);
369
+ assert.ok(formatted.includes('Issue Validation'));
370
+ assert.ok(formatted.includes('fully-addressed'));
371
+ });
372
+
373
+ it('should show unmatched keywords when present', () => {
374
+ const issue = {
375
+ number: 2,
376
+ title: 'Partial fix',
377
+ url: 'https://github.com/test/test/issues/2',
378
+ state: 'open',
379
+ labels: [],
380
+ assignees: [],
381
+ };
382
+ const validation = {
383
+ verdict: 'partially-addressed',
384
+ overallRelevance: 40,
385
+ details: 'Partially addressed',
386
+ matchedFiles: [],
387
+ matchedKeywords: ['api'],
388
+ unmatchedKeywords: ['database', 'cache'],
389
+ };
390
+ const report = generateIssueReport(issue, validation, [], null);
391
+ const formatted = formatIssueReport(report);
392
+ assert.ok(formatted.includes('partially-addressed'));
393
+ assert.ok(formatted.includes('database'));
394
+ assert.ok(formatted.includes('cache'));
395
+ });
396
+ });
397
+
398
+ describe('parseCommitLog', () => {
399
+ it('should return empty string for non-git directory', () => {
400
+ const log = parseCommitLog('/tmp/nonexistent-repo');
401
+ assert.equal(log, '');
402
+ });
403
+ });
404
+ });
package/src/models.js CHANGED
@@ -152,9 +152,68 @@ function getTemplate(name) {
152
152
  return BUILTIN_TEMPLATES[name] || null;
153
153
  }
154
154
 
155
+ /**
156
+ * Priority order for auto-detection:
157
+ * recommended tier first, then standard tier
158
+ */
159
+ const AUTO_DETECT_PRIORITY = [
160
+ 'deepseek',
161
+ 'qwen-coder',
162
+ 'qwen',
163
+ 'openai',
164
+ 'claude',
165
+ 'gemini',
166
+ 'zhipu',
167
+ 'moonshot',
168
+ 'codestral',
169
+ ];
170
+
171
+ /**
172
+ * Auto-detect the best available AI provider by scanning environment
173
+ * variables for known API keys.
174
+ *
175
+ * Scans all built-in template apiKeyEnv vars and returns the first
176
+ * matching template in priority order (recommended tier first).
177
+ *
178
+ * @returns {{ template: object, name: string, detected: string[], allDetected: string[] } | null}
179
+ */
180
+ function autoDetectProvider() {
181
+ const detected = [];
182
+
183
+ // Scan all templates for available API keys
184
+ for (const [name, t] of Object.entries(BUILTIN_TEMPLATES)) {
185
+ if (process.env[t.apiKeyEnv]) {
186
+ detected.push(name);
187
+ }
188
+ }
189
+
190
+ if (detected.length === 0) return null;
191
+
192
+ // Pick the highest-priority detected template
193
+ let chosen = null;
194
+ for (const name of AUTO_DETECT_PRIORITY) {
195
+ if (detected.includes(name)) {
196
+ chosen = name;
197
+ break;
198
+ }
199
+ }
200
+
201
+ // Fallback: if default priority list missed something, pick first detected
202
+ if (!chosen) chosen = detected[0];
203
+
204
+ return {
205
+ template: BUILTIN_TEMPLATES[chosen],
206
+ name: chosen,
207
+ chosen,
208
+ allDetected: detected,
209
+ };
210
+ }
211
+
155
212
  module.exports = {
156
213
  BUILTIN_TEMPLATES,
157
214
  resolveTemplate,
158
215
  listTemplates,
159
216
  getTemplate,
217
+ autoDetectProvider,
218
+ AUTO_DETECT_PRIORITY,
160
219
  };
@@ -1,6 +1,6 @@
1
- const { describe, it } = require('node:test');
1
+ const { describe, it, beforeEach } = require('node:test');
2
2
  const assert = require('assert');
3
- const { BUILTIN_TEMPLATES, resolveTemplate, listTemplates, getTemplate } = require('./models');
3
+ const { BUILTIN_TEMPLATES, resolveTemplate, listTemplates, getTemplate, autoDetectProvider, AUTO_DETECT_PRIORITY } = require('./models');
4
4
 
5
5
  describe('models.js', () => {
6
6
  it('should have all 11 built-in templates', () => {
@@ -78,3 +78,140 @@ describe('models.js', () => {
78
78
  assert.strictEqual(t.model, 'deepseek-reasoner');
79
79
  });
80
80
  });
81
+
82
+ describe('autoDetectProvider', () => {
83
+ const savedEnv = {};
84
+
85
+ beforeEach(() => {
86
+ // Save and clear all known API key env vars
87
+ const allKeys = Object.values(BUILTIN_TEMPLATES).map(t => t.apiKeyEnv);
88
+ for (const key of [...new Set(allKeys)]) {
89
+ savedEnv[key] = process.env[key];
90
+ delete process.env[key];
91
+ }
92
+ });
93
+
94
+ // Restore env vars after each test
95
+ const { afterEach } = require('node:test');
96
+ afterEach(() => {
97
+ for (const [key, val] of Object.entries(savedEnv)) {
98
+ if (val !== undefined) {
99
+ process.env[key] = val;
100
+ } else {
101
+ delete process.env[key];
102
+ }
103
+ }
104
+ });
105
+
106
+ it('should return null when no API keys are set', () => {
107
+ const result = autoDetectProvider();
108
+ assert.strictEqual(result, null);
109
+ });
110
+
111
+ it('should detect a single available provider', () => {
112
+ process.env.DEEPSEEK_API_KEY = 'sk-test-deepseek-key';
113
+ const result = autoDetectProvider();
114
+ assert.ok(result);
115
+ assert.strictEqual(result.chosen, 'deepseek');
116
+ assert.ok(result.allDetected.includes('deepseek'));
117
+ assert.ok(result.allDetected.includes('deepseek-r1'));
118
+ assert.strictEqual(result.template.model, 'deepseek-chat');
119
+ });
120
+
121
+ it('should prioritize deepseek over openai when both are available', () => {
122
+ process.env.DEEPSEEK_API_KEY = 'sk-deepseek';
123
+ process.env.OPENAI_API_KEY = 'sk-openai';
124
+ const result = autoDetectProvider();
125
+ assert.ok(result);
126
+ assert.strictEqual(result.chosen, 'deepseek');
127
+ assert.ok(result.allDetected.includes('deepseek'));
128
+ assert.ok(result.allDetected.includes('openai'));
129
+ });
130
+
131
+ it('should detect qwen as fallback when deepseek is not available', () => {
132
+ process.env.DASHSCOPE_API_KEY = 'sk-qwen';
133
+ const result = autoDetectProvider();
134
+ assert.ok(result);
135
+ assert.strictEqual(result.chosen, 'qwen-coder');
136
+ assert.ok(result.allDetected.includes('qwen-coder'));
137
+ assert.ok(result.allDetected.includes('qwen'));
138
+ });
139
+
140
+ it('should detect openai when deepseek and qwen are not available', () => {
141
+ process.env.OPENAI_API_KEY = 'sk-openai-test';
142
+ const result = autoDetectProvider();
143
+ assert.ok(result);
144
+ assert.strictEqual(result.chosen, 'openai');
145
+ assert.ok(result.allDetected.includes('openai'));
146
+ assert.ok(result.allDetected.includes('openai-o3'));
147
+ });
148
+
149
+ it('should detect claude when ANTHROPIC_API_KEY is set', () => {
150
+ process.env.ANTHROPIC_API_KEY = 'sk-ant-test';
151
+ const result = autoDetectProvider();
152
+ assert.ok(result);
153
+ assert.strictEqual(result.chosen, 'claude');
154
+ });
155
+
156
+ it('should detect gemini when GEMINI_API_KEY is set', () => {
157
+ process.env.GEMINI_API_KEY = 'test-gemini-key';
158
+ const result = autoDetectProvider();
159
+ assert.ok(result);
160
+ assert.strictEqual(result.chosen, 'gemini');
161
+ });
162
+
163
+ it('should detect zhipu when ZHIPU_API_KEY is set', () => {
164
+ process.env.ZHIPU_API_KEY = 'test-zhipu-key';
165
+ const result = autoDetectProvider();
166
+ assert.ok(result);
167
+ assert.strictEqual(result.chosen, 'zhipu');
168
+ });
169
+
170
+ it('should detect moonshot when MOONSHOT_API_KEY is set', () => {
171
+ process.env.MOONSHOT_API_KEY = 'test-moonshot-key';
172
+ const result = autoDetectProvider();
173
+ assert.ok(result);
174
+ assert.strictEqual(result.chosen, 'moonshot');
175
+ });
176
+
177
+ it('should detect codestral when MISTRAL_API_KEY is set', () => {
178
+ process.env.MISTRAL_API_KEY = 'test-mistral-key';
179
+ const result = autoDetectProvider();
180
+ assert.ok(result);
181
+ assert.strictEqual(result.chosen, 'codestral');
182
+ });
183
+
184
+ it('should return all detected providers in allDetected', () => {
185
+ process.env.DEEPSEEK_API_KEY = 'sk-ds';
186
+ process.env.OPENAI_API_KEY = 'sk-oa';
187
+ process.env.ANTHROPIC_API_KEY = 'sk-ant';
188
+ const result = autoDetectProvider();
189
+ assert.ok(result);
190
+ assert.strictEqual(result.chosen, 'deepseek');
191
+ assert.ok(result.allDetected.includes('deepseek'));
192
+ assert.ok(result.allDetected.includes('openai'));
193
+ assert.ok(result.allDetected.includes('claude'));
194
+ });
195
+
196
+ it('AUTO_DETECT_PRIORITY should include all standard provider names', () => {
197
+ const allNames = Object.keys(BUILTIN_TEMPLATES);
198
+ const standardProviders = allNames.filter(n => {
199
+ const t = BUILTIN_TEMPLATES[n];
200
+ // Each unique apiKeyEnv should appear at least once
201
+ return true;
202
+ });
203
+ // Priority should cover at least all standard-tier templates
204
+ const standardKeys = [...new Set(
205
+ allNames
206
+ .filter(n => BUILTIN_TEMPLATES[n].tier !== 'reasoning')
207
+ .map(n => BUILTIN_TEMPLATES[n].apiKeyEnv)
208
+ )];
209
+ // Check that priority covers each unique key
210
+ const priorityKeys = [...new Set(
211
+ AUTO_DETECT_PRIORITY.map(n => BUILTIN_TEMPLATES[n]?.apiKeyEnv).filter(Boolean)
212
+ )];
213
+ for (const key of standardKeys) {
214
+ assert.ok(priorityKeys.includes(key), `Priority should include provider with key: ${key}`);
215
+ }
216
+ });
217
+ });