fcis 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.
Files changed (151) hide show
  1. package/.plans/001-fcis-analyzer.md +832 -0
  2. package/.plans/002-fcis-analyzer-improvements.md +205 -0
  3. package/README.md +272 -0
  4. package/TECHNICAL.md +386 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +1836 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/index.d.ts +709 -0
  9. package/dist/index.js +1845 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +47 -0
  12. package/pnpm-workspace.yaml +0 -0
  13. package/src/analyzer.ts +266 -0
  14. package/src/classification/classifier.ts +156 -0
  15. package/src/classification/derive-status.ts +171 -0
  16. package/src/classification/quality-scorer.ts +481 -0
  17. package/src/cli.ts +286 -0
  18. package/src/detection/detect-markers.ts +480 -0
  19. package/src/detection/markers.ts +332 -0
  20. package/src/extraction/extract-functions.ts +570 -0
  21. package/src/extraction/extractor.ts +188 -0
  22. package/src/index.ts +111 -0
  23. package/src/reporting/report-console.ts +416 -0
  24. package/src/reporting/report-json.ts +232 -0
  25. package/src/scoring/scorer.ts +504 -0
  26. package/src/types.ts +248 -0
  27. package/tests/classifier.test.ts +480 -0
  28. package/tests/derive-status.test.ts +464 -0
  29. package/tests/detect-markers.test.ts +639 -0
  30. package/tests/extractor.test.ts +155 -0
  31. package/tests/integration.test.ts +706 -0
  32. package/tests/quality-scorer.test.ts +650 -0
  33. package/tests/scorer.test.ts +768 -0
  34. package/tsconfig.json +34 -0
  35. package/tsup.config.ts +17 -0
  36. package/vendor/ts-morph/.editorconfig +10 -0
  37. package/vendor/ts-morph/.gitattributes +11 -0
  38. package/vendor/ts-morph/.github/CODE_OF_CONDUCT.md +77 -0
  39. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  40. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/custom.md +4 -0
  41. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
  42. package/vendor/ts-morph/.github/workflows/ci.yml +50 -0
  43. package/vendor/ts-morph/.github/workflows/publish.yml +53 -0
  44. package/vendor/ts-morph/.vscode/settings.json +10 -0
  45. package/vendor/ts-morph/CONTRIBUTING.md +23 -0
  46. package/vendor/ts-morph/DEVELOPMENT.md +32 -0
  47. package/vendor/ts-morph/LICENSE +21 -0
  48. package/vendor/ts-morph/deno.json +8 -0
  49. package/vendor/ts-morph/deno.lock +1233 -0
  50. package/vendor/ts-morph/docs/CNAME +1 -0
  51. package/vendor/ts-morph/docs/Gemfile +2 -0
  52. package/vendor/ts-morph/docs/_config.yml +5 -0
  53. package/vendor/ts-morph/docs/_layouts/default.html +159 -0
  54. package/vendor/ts-morph/docs/_script-templates/main.ts +116 -0
  55. package/vendor/ts-morph/docs/assets/css/style.scss +212 -0
  56. package/vendor/ts-morph/docs/details/ambient.md +38 -0
  57. package/vendor/ts-morph/docs/details/async.md +31 -0
  58. package/vendor/ts-morph/docs/details/classes.md +314 -0
  59. package/vendor/ts-morph/docs/details/comment-ranges.md +7 -0
  60. package/vendor/ts-morph/docs/details/comments.md +122 -0
  61. package/vendor/ts-morph/docs/details/decorators.md +119 -0
  62. package/vendor/ts-morph/docs/details/documentation.md +73 -0
  63. package/vendor/ts-morph/docs/details/enums.md +117 -0
  64. package/vendor/ts-morph/docs/details/exports.md +308 -0
  65. package/vendor/ts-morph/docs/details/expressions.md +46 -0
  66. package/vendor/ts-morph/docs/details/functions.md +150 -0
  67. package/vendor/ts-morph/docs/details/generators.md +27 -0
  68. package/vendor/ts-morph/docs/details/identifiers.md +79 -0
  69. package/vendor/ts-morph/docs/details/imports.md +191 -0
  70. package/vendor/ts-morph/docs/details/index.md +52 -0
  71. package/vendor/ts-morph/docs/details/initializers.md +40 -0
  72. package/vendor/ts-morph/docs/details/interfaces.md +218 -0
  73. package/vendor/ts-morph/docs/details/literals.md +20 -0
  74. package/vendor/ts-morph/docs/details/modifiers.md +38 -0
  75. package/vendor/ts-morph/docs/details/modules.md +113 -0
  76. package/vendor/ts-morph/docs/details/namespaces.md +7 -0
  77. package/vendor/ts-morph/docs/details/object-literal-expressions.md +106 -0
  78. package/vendor/ts-morph/docs/details/parameters.md +64 -0
  79. package/vendor/ts-morph/docs/details/signatures.md +41 -0
  80. package/vendor/ts-morph/docs/details/source-files.md +292 -0
  81. package/vendor/ts-morph/docs/details/type-aliases.md +34 -0
  82. package/vendor/ts-morph/docs/details/type-parameters.md +72 -0
  83. package/vendor/ts-morph/docs/details/types.md +254 -0
  84. package/vendor/ts-morph/docs/details/variables.md +110 -0
  85. package/vendor/ts-morph/docs/emitting.md +151 -0
  86. package/vendor/ts-morph/docs/index.md +25 -0
  87. package/vendor/ts-morph/docs/manipulation/code-writer.md +20 -0
  88. package/vendor/ts-morph/docs/manipulation/formatting.md +76 -0
  89. package/vendor/ts-morph/docs/manipulation/index.md +136 -0
  90. package/vendor/ts-morph/docs/manipulation/order.md +14 -0
  91. package/vendor/ts-morph/docs/manipulation/performance.md +222 -0
  92. package/vendor/ts-morph/docs/manipulation/removing.md +31 -0
  93. package/vendor/ts-morph/docs/manipulation/renaming.md +106 -0
  94. package/vendor/ts-morph/docs/manipulation/settings.md +76 -0
  95. package/vendor/ts-morph/docs/manipulation/structures.md +117 -0
  96. package/vendor/ts-morph/docs/manipulation/transforms.md +84 -0
  97. package/vendor/ts-morph/docs/metrics/performance.json +4 -0
  98. package/vendor/ts-morph/docs/navigation/ambient-modules.md +22 -0
  99. package/vendor/ts-morph/docs/navigation/compiler-nodes.md +82 -0
  100. package/vendor/ts-morph/docs/navigation/directories.md +287 -0
  101. package/vendor/ts-morph/docs/navigation/example.md +50 -0
  102. package/vendor/ts-morph/docs/navigation/finding-references.md +53 -0
  103. package/vendor/ts-morph/docs/navigation/getting-source-files.md +59 -0
  104. package/vendor/ts-morph/docs/navigation/images/getChildrenVsForEachChild.gif +0 -0
  105. package/vendor/ts-morph/docs/navigation/index.md +94 -0
  106. package/vendor/ts-morph/docs/navigation/language-service.md +23 -0
  107. package/vendor/ts-morph/docs/navigation/program.md +25 -0
  108. package/vendor/ts-morph/docs/navigation/type-checker.md +33 -0
  109. package/vendor/ts-morph/docs/setup/adding-source-files.md +145 -0
  110. package/vendor/ts-morph/docs/setup/ast-viewers.md +46 -0
  111. package/vendor/ts-morph/docs/setup/diagnostics.md +109 -0
  112. package/vendor/ts-morph/docs/setup/file-system.md +106 -0
  113. package/vendor/ts-morph/docs/setup/images/atom-ast.png +0 -0
  114. package/vendor/ts-morph/docs/setup/images/atom-ast_small.png +0 -0
  115. package/vendor/ts-morph/docs/setup/images/atom-command-palette.png +0 -0
  116. package/vendor/ts-morph/docs/setup/images/atom-file.png +0 -0
  117. package/vendor/ts-morph/docs/setup/images/ts-ast-viewer.png +0 -0
  118. package/vendor/ts-morph/docs/setup/index.md +94 -0
  119. package/vendor/ts-morph/docs/utilities.md +55 -0
  120. package/vendor/ts-morph/dprint.json +23 -0
  121. package/vendor/ts-morph/package.json +30 -0
  122. package/vendor/ts-morph/packages/bootstrap/LICENSE +21 -0
  123. package/vendor/ts-morph/packages/bootstrap/lib/ts-morph-bootstrap.d.ts +397 -0
  124. package/vendor/ts-morph/packages/bootstrap/package.json +46 -0
  125. package/vendor/ts-morph/packages/bootstrap/readme.md +200 -0
  126. package/vendor/ts-morph/packages/common/LICENSE +21 -0
  127. package/vendor/ts-morph/packages/common/lib/ts-morph-common.d.ts +1082 -0
  128. package/vendor/ts-morph/packages/common/lib/typescript.d.ts +11439 -0
  129. package/vendor/ts-morph/packages/common/package.json +65 -0
  130. package/vendor/ts-morph/packages/common/readme.md +5 -0
  131. package/vendor/ts-morph/packages/scripts/changeTypeScriptVersion.ts +28 -0
  132. package/vendor/ts-morph/packages/scripts/createDeclarationProject.ts +47 -0
  133. package/vendor/ts-morph/packages/scripts/deps.ts +2 -0
  134. package/vendor/ts-morph/packages/scripts/execScript.ts +31 -0
  135. package/vendor/ts-morph/packages/scripts/folders.ts +11 -0
  136. package/vendor/ts-morph/packages/scripts/getDevCompilerVersions.ts +19 -0
  137. package/vendor/ts-morph/packages/scripts/mod.ts +7 -0
  138. package/vendor/ts-morph/packages/scripts/utils/Memoize.ts +36 -0
  139. package/vendor/ts-morph/packages/scripts/utils/forEachTypeText.ts +23 -0
  140. package/vendor/ts-morph/packages/scripts/utils/makeConstructorsPrivate.ts +26 -0
  141. package/vendor/ts-morph/packages/scripts/utils/mod.ts +4 -0
  142. package/vendor/ts-morph/packages/scripts/utils/printDiagnostics.ts +10 -0
  143. package/vendor/ts-morph/packages/ts-morph/LICENSE +21 -0
  144. package/vendor/ts-morph/packages/ts-morph/lib/ts-morph.d.ts +11198 -0
  145. package/vendor/ts-morph/packages/ts-morph/package.json +78 -0
  146. package/vendor/ts-morph/packages/ts-morph/readme.md +111 -0
  147. package/vendor/ts-morph/readme.md +14 -0
  148. package/vendor/ts-morph/rfcs/README.md +13 -0
  149. package/vendor/ts-morph/rfcs/RFC-0001 - Inserting Into Statements Handling Comments.md +181 -0
  150. package/vendor/ts-morph/tsconfig.common.json +17 -0
  151. package/vitest.config.ts +16 -0
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Status Derivation Tests
3
+ *
4
+ * Tests for the pure core status derivation logic.
5
+ * These tests verify that status is correctly derived from
6
+ * classification and quality score.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest'
10
+
11
+ import {
12
+ deriveStatus,
13
+ getStatusDescription,
14
+ getStatusEmoji,
15
+ getStatusColor,
16
+ needsAttention,
17
+ isOk,
18
+ sortByStatusPriority,
19
+ validateThresholds,
20
+ } from '../src/classification/derive-status.js'
21
+ import type {
22
+ ClassifiedFunction,
23
+ ExtractedFunction,
24
+ QualityThresholds,
25
+ } from '../src/types.js'
26
+ import { DEFAULT_QUALITY_THRESHOLDS } from '../src/types.js'
27
+
28
+ /**
29
+ * Helper to create a minimal extracted function for testing
30
+ */
31
+ function createTestFunction(
32
+ overrides: Partial<ExtractedFunction> = {},
33
+ ): ExtractedFunction {
34
+ return {
35
+ name: 'testFunction',
36
+ filePath: '/test/file.ts',
37
+ startLine: 1,
38
+ endLine: 10,
39
+ isAsync: false,
40
+ isExported: false,
41
+ bodyLineCount: 10,
42
+ statementCount: 5,
43
+ hasConditionals: false,
44
+ parentContext: null,
45
+ callSites: [],
46
+ hasAwait: false,
47
+ propertyAccessChains: [],
48
+ kind: 'function',
49
+ ...overrides,
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Helper to create a classified function
55
+ */
56
+ function createClassifiedFunction(
57
+ overrides: Partial<ClassifiedFunction>,
58
+ ): ClassifiedFunction {
59
+ return {
60
+ ...createTestFunction(),
61
+ markers: [],
62
+ classification: 'pure',
63
+ qualityScore: null,
64
+ status: 'ok',
65
+ ...overrides,
66
+ }
67
+ }
68
+
69
+ describe('deriveStatus', () => {
70
+ describe('pure functions', () => {
71
+ it('should return "ok" for pure functions', () => {
72
+ const status = deriveStatus('pure', null)
73
+ expect(status).toBe('ok')
74
+ })
75
+
76
+ it('should return "ok" for pure functions regardless of thresholds', () => {
77
+ const customThresholds: QualityThresholds = {
78
+ okThreshold: 90,
79
+ reviewThreshold: 80,
80
+ }
81
+
82
+ const status = deriveStatus('pure', null, customThresholds)
83
+ expect(status).toBe('ok')
84
+ })
85
+ })
86
+
87
+ describe('impure functions with default thresholds', () => {
88
+ it('should return "ok" for quality score >= 70', () => {
89
+ expect(deriveStatus('impure', 70)).toBe('ok')
90
+ expect(deriveStatus('impure', 75)).toBe('ok')
91
+ expect(deriveStatus('impure', 100)).toBe('ok')
92
+ })
93
+
94
+ it('should return "review" for quality score 40-69', () => {
95
+ expect(deriveStatus('impure', 40)).toBe('review')
96
+ expect(deriveStatus('impure', 50)).toBe('review')
97
+ expect(deriveStatus('impure', 69)).toBe('review')
98
+ })
99
+
100
+ it('should return "refactor" for quality score < 40', () => {
101
+ expect(deriveStatus('impure', 0)).toBe('refactor')
102
+ expect(deriveStatus('impure', 20)).toBe('refactor')
103
+ expect(deriveStatus('impure', 39)).toBe('refactor')
104
+ })
105
+
106
+ it('should handle boundary cases correctly', () => {
107
+ expect(deriveStatus('impure', 70)).toBe('ok')
108
+ expect(deriveStatus('impure', 69)).toBe('review')
109
+ expect(deriveStatus('impure', 40)).toBe('review')
110
+ expect(deriveStatus('impure', 39)).toBe('refactor')
111
+ })
112
+ })
113
+
114
+ describe('impure functions with custom thresholds', () => {
115
+ it('should respect custom okThreshold', () => {
116
+ const customThresholds: QualityThresholds = {
117
+ okThreshold: 80,
118
+ reviewThreshold: 50,
119
+ }
120
+
121
+ expect(deriveStatus('impure', 80, customThresholds)).toBe('ok')
122
+ expect(deriveStatus('impure', 79, customThresholds)).toBe('review')
123
+ })
124
+
125
+ it('should respect custom reviewThreshold', () => {
126
+ const customThresholds: QualityThresholds = {
127
+ okThreshold: 80,
128
+ reviewThreshold: 50,
129
+ }
130
+
131
+ expect(deriveStatus('impure', 50, customThresholds)).toBe('review')
132
+ expect(deriveStatus('impure', 49, customThresholds)).toBe('refactor')
133
+ })
134
+
135
+ it('should handle stricter thresholds', () => {
136
+ const strictThresholds: QualityThresholds = {
137
+ okThreshold: 90,
138
+ reviewThreshold: 70,
139
+ }
140
+
141
+ expect(deriveStatus('impure', 90, strictThresholds)).toBe('ok')
142
+ expect(deriveStatus('impure', 89, strictThresholds)).toBe('review')
143
+ expect(deriveStatus('impure', 70, strictThresholds)).toBe('review')
144
+ expect(deriveStatus('impure', 69, strictThresholds)).toBe('refactor')
145
+ })
146
+
147
+ it('should handle more lenient thresholds', () => {
148
+ const lenientThresholds: QualityThresholds = {
149
+ okThreshold: 50,
150
+ reviewThreshold: 20,
151
+ }
152
+
153
+ expect(deriveStatus('impure', 50, lenientThresholds)).toBe('ok')
154
+ expect(deriveStatus('impure', 49, lenientThresholds)).toBe('review')
155
+ expect(deriveStatus('impure', 20, lenientThresholds)).toBe('review')
156
+ expect(deriveStatus('impure', 19, lenientThresholds)).toBe('refactor')
157
+ })
158
+ })
159
+
160
+ describe('edge cases', () => {
161
+ it('should return "review" for impure function with null quality score', () => {
162
+ // This is a defensive case that shouldn't normally happen
163
+ const status = deriveStatus('impure', null)
164
+ expect(status).toBe('review')
165
+ })
166
+
167
+ it('should handle quality score of exactly 0', () => {
168
+ const status = deriveStatus('impure', 0)
169
+ expect(status).toBe('refactor')
170
+ })
171
+
172
+ it('should handle quality score of exactly 100', () => {
173
+ const status = deriveStatus('impure', 100)
174
+ expect(status).toBe('ok')
175
+ })
176
+ })
177
+ })
178
+
179
+ describe('getStatusDescription', () => {
180
+ it('should return appropriate description for "ok"', () => {
181
+ const description = getStatusDescription('ok')
182
+ expect(description).toContain('No action')
183
+ })
184
+
185
+ it('should return appropriate description for "review"', () => {
186
+ const description = getStatusDescription('review')
187
+ expect(description.toLowerCase()).toContain('improving')
188
+ })
189
+
190
+ it('should return appropriate description for "refactor"', () => {
191
+ const description = getStatusDescription('refactor')
192
+ expect(description.toLowerCase()).toContain('cleanup')
193
+ })
194
+ })
195
+
196
+ describe('getStatusEmoji', () => {
197
+ it('should return check mark for "ok"', () => {
198
+ const emoji = getStatusEmoji('ok')
199
+ expect(emoji).toBe('✓')
200
+ })
201
+
202
+ it('should return half-filled circle for "review"', () => {
203
+ const emoji = getStatusEmoji('review')
204
+ expect(emoji).toBe('◐')
205
+ })
206
+
207
+ it('should return X mark for "refactor"', () => {
208
+ const emoji = getStatusEmoji('refactor')
209
+ expect(emoji).toBe('✗')
210
+ })
211
+ })
212
+
213
+ describe('getStatusColor', () => {
214
+ it('should return green for "ok"', () => {
215
+ const color = getStatusColor('ok')
216
+ expect(color).toBe('green')
217
+ })
218
+
219
+ it('should return yellow for "review"', () => {
220
+ const color = getStatusColor('review')
221
+ expect(color).toBe('yellow')
222
+ })
223
+
224
+ it('should return red for "refactor"', () => {
225
+ const color = getStatusColor('refactor')
226
+ expect(color).toBe('red')
227
+ })
228
+ })
229
+
230
+ describe('needsAttention', () => {
231
+ it('should return false for functions with status "ok"', () => {
232
+ const fn = createClassifiedFunction({ status: 'ok' })
233
+ expect(needsAttention(fn)).toBe(false)
234
+ })
235
+
236
+ it('should return true for functions with status "review"', () => {
237
+ const fn = createClassifiedFunction({ status: 'review' })
238
+ expect(needsAttention(fn)).toBe(true)
239
+ })
240
+
241
+ it('should return true for functions with status "refactor"', () => {
242
+ const fn = createClassifiedFunction({ status: 'refactor' })
243
+ expect(needsAttention(fn)).toBe(true)
244
+ })
245
+ })
246
+
247
+ describe('isOk', () => {
248
+ it('should return true for functions with status "ok"', () => {
249
+ const fn = createClassifiedFunction({ status: 'ok' })
250
+ expect(isOk(fn)).toBe(true)
251
+ })
252
+
253
+ it('should return false for functions with status "review"', () => {
254
+ const fn = createClassifiedFunction({ status: 'review' })
255
+ expect(isOk(fn)).toBe(false)
256
+ })
257
+
258
+ it('should return false for functions with status "refactor"', () => {
259
+ const fn = createClassifiedFunction({ status: 'refactor' })
260
+ expect(isOk(fn)).toBe(false)
261
+ })
262
+ })
263
+
264
+ describe('sortByStatusPriority', () => {
265
+ it('should sort refactor first, then review, then ok', () => {
266
+ const okFn = createClassifiedFunction({ name: 'okFn', status: 'ok' })
267
+ const reviewFn = createClassifiedFunction({
268
+ name: 'reviewFn',
269
+ status: 'review',
270
+ })
271
+ const refactorFn = createClassifiedFunction({
272
+ name: 'refactorFn',
273
+ status: 'refactor',
274
+ })
275
+
276
+ const sorted = sortByStatusPriority([okFn, reviewFn, refactorFn])
277
+
278
+ expect(sorted[0]?.status).toBe('refactor')
279
+ expect(sorted[1]?.status).toBe('review')
280
+ expect(sorted[2]?.status).toBe('ok')
281
+ })
282
+
283
+ it('should sort by quality score within same status (lower first)', () => {
284
+ const impure1 = createClassifiedFunction({
285
+ name: 'impure1',
286
+ status: 'refactor',
287
+ classification: 'impure',
288
+ qualityScore: 30,
289
+ })
290
+ const impure2 = createClassifiedFunction({
291
+ name: 'impure2',
292
+ status: 'refactor',
293
+ classification: 'impure',
294
+ qualityScore: 20,
295
+ })
296
+ const impure3 = createClassifiedFunction({
297
+ name: 'impure3',
298
+ status: 'refactor',
299
+ classification: 'impure',
300
+ qualityScore: 35,
301
+ })
302
+
303
+ const sorted = sortByStatusPriority([impure1, impure2, impure3])
304
+
305
+ expect(sorted[0]?.qualityScore).toBe(20)
306
+ expect(sorted[1]?.qualityScore).toBe(30)
307
+ expect(sorted[2]?.qualityScore).toBe(35)
308
+ })
309
+
310
+ it('should sort by body line count within same status and null quality scores', () => {
311
+ const pure1 = createClassifiedFunction({
312
+ name: 'pure1',
313
+ status: 'ok',
314
+ bodyLineCount: 20,
315
+ })
316
+ const pure2 = createClassifiedFunction({
317
+ name: 'pure2',
318
+ status: 'ok',
319
+ bodyLineCount: 50,
320
+ })
321
+ const pure3 = createClassifiedFunction({
322
+ name: 'pure3',
323
+ status: 'ok',
324
+ bodyLineCount: 30,
325
+ })
326
+
327
+ const sorted = sortByStatusPriority([pure1, pure2, pure3])
328
+
329
+ // Larger body line count first (tertiary sort)
330
+ expect(sorted[0]?.bodyLineCount).toBe(50)
331
+ expect(sorted[1]?.bodyLineCount).toBe(30)
332
+ expect(sorted[2]?.bodyLineCount).toBe(20)
333
+ })
334
+
335
+ it('should handle empty array', () => {
336
+ const sorted = sortByStatusPriority([])
337
+ expect(sorted).toEqual([])
338
+ })
339
+
340
+ it('should handle single element array', () => {
341
+ const fn = createClassifiedFunction({ name: 'single' })
342
+ const sorted = sortByStatusPriority([fn])
343
+ expect(sorted).toHaveLength(1)
344
+ expect(sorted[0]?.name).toBe('single')
345
+ })
346
+
347
+ it('should not mutate original array', () => {
348
+ const okFn = createClassifiedFunction({ name: 'okFn', status: 'ok' })
349
+ const refactorFn = createClassifiedFunction({
350
+ name: 'refactorFn',
351
+ status: 'refactor',
352
+ })
353
+
354
+ const original = [okFn, refactorFn]
355
+ sortByStatusPriority(original)
356
+
357
+ expect(original[0]?.name).toBe('okFn')
358
+ expect(original[1]?.name).toBe('refactorFn')
359
+ })
360
+ })
361
+
362
+ describe('validateThresholds', () => {
363
+ it('should validate correct thresholds', () => {
364
+ const result = validateThresholds(DEFAULT_QUALITY_THRESHOLDS)
365
+ expect(result.valid).toBe(true)
366
+ expect(result.errors).toHaveLength(0)
367
+ })
368
+
369
+ it('should reject okThreshold < 0', () => {
370
+ const thresholds: QualityThresholds = {
371
+ okThreshold: -1,
372
+ reviewThreshold: 40,
373
+ }
374
+
375
+ const result = validateThresholds(thresholds)
376
+
377
+ expect(result.valid).toBe(false)
378
+ expect(result.errors.some(e => e.includes('okThreshold'))).toBe(true)
379
+ })
380
+
381
+ it('should reject okThreshold > 100', () => {
382
+ const thresholds: QualityThresholds = {
383
+ okThreshold: 101,
384
+ reviewThreshold: 40,
385
+ }
386
+
387
+ const result = validateThresholds(thresholds)
388
+
389
+ expect(result.valid).toBe(false)
390
+ expect(result.errors.some(e => e.includes('okThreshold'))).toBe(true)
391
+ })
392
+
393
+ it('should reject reviewThreshold < 0', () => {
394
+ const thresholds: QualityThresholds = {
395
+ okThreshold: 70,
396
+ reviewThreshold: -1,
397
+ }
398
+
399
+ const result = validateThresholds(thresholds)
400
+
401
+ expect(result.valid).toBe(false)
402
+ expect(result.errors.some(e => e.includes('reviewThreshold'))).toBe(true)
403
+ })
404
+
405
+ it('should reject reviewThreshold > 100', () => {
406
+ const thresholds: QualityThresholds = {
407
+ okThreshold: 70,
408
+ reviewThreshold: 101,
409
+ }
410
+
411
+ const result = validateThresholds(thresholds)
412
+
413
+ expect(result.valid).toBe(false)
414
+ expect(result.errors.some(e => e.includes('reviewThreshold'))).toBe(true)
415
+ })
416
+
417
+ it('should reject reviewThreshold >= okThreshold', () => {
418
+ const thresholds: QualityThresholds = {
419
+ okThreshold: 70,
420
+ reviewThreshold: 70,
421
+ }
422
+
423
+ const result = validateThresholds(thresholds)
424
+
425
+ expect(result.valid).toBe(false)
426
+ expect(result.errors.some(e => e.includes('less than'))).toBe(true)
427
+ })
428
+
429
+ it('should reject reviewThreshold > okThreshold', () => {
430
+ const thresholds: QualityThresholds = {
431
+ okThreshold: 50,
432
+ reviewThreshold: 70,
433
+ }
434
+
435
+ const result = validateThresholds(thresholds)
436
+
437
+ expect(result.valid).toBe(false)
438
+ expect(result.errors.some(e => e.includes('less than'))).toBe(true)
439
+ })
440
+
441
+ it('should collect multiple errors', () => {
442
+ const thresholds: QualityThresholds = {
443
+ okThreshold: 150,
444
+ reviewThreshold: 200,
445
+ }
446
+
447
+ const result = validateThresholds(thresholds)
448
+
449
+ expect(result.valid).toBe(false)
450
+ expect(result.errors.length).toBeGreaterThanOrEqual(2)
451
+ })
452
+
453
+ it('should accept valid edge case thresholds', () => {
454
+ const thresholds: QualityThresholds = {
455
+ okThreshold: 100,
456
+ reviewThreshold: 0,
457
+ }
458
+
459
+ const result = validateThresholds(thresholds)
460
+
461
+ expect(result.valid).toBe(true)
462
+ expect(result.errors).toHaveLength(0)
463
+ })
464
+ })