@7n/rules-lang-js 0.6.0 → 0.7.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 (71) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -1
  3. package/rules/test/docs/index.md +11 -0
  4. package/rules/test/lib/collect-test-file-offenders.mjs +49 -0
  5. package/rules/test/lib/docs/collect-test-file-offenders.md +29 -0
  6. package/rules/test/lib/docs/index.md +9 -0
  7. package/rules/test/location/concern.json +8 -0
  8. package/rules/test/location/docs/index.md +11 -0
  9. package/rules/test/location/docs/main.md +36 -0
  10. package/rules/test/location/location.mdc +52 -0
  11. package/rules/test/location/main.mjs +70 -0
  12. package/rules/test/main.json +1 -0
  13. package/rules/test/main.mdc +26 -0
  14. package/rules/test/no-bun-test-import/concern.json +7 -0
  15. package/rules/test/no-bun-test-import/docs/fix-no-bun-test-import.md +30 -0
  16. package/rules/test/no-bun-test-import/docs/index.md +10 -0
  17. package/rules/test/no-bun-test-import/docs/main.md +35 -0
  18. package/rules/test/no-bun-test-import/fix-no-bun-test-import.mjs +51 -0
  19. package/rules/test/no-bun-test-import/main.mjs +105 -0
  20. package/rules/test/no-bun-test-import/no-bun-test-import.mdc +9 -0
  21. package/rules/test/no-console-store-restore/concern.json +7 -0
  22. package/rules/test/no-console-store-restore/docs/index.md +11 -0
  23. package/rules/test/no-console-store-restore/docs/main.md +44 -0
  24. package/rules/test/no-console-store-restore/main.mjs +55 -0
  25. package/rules/test/no-console-store-restore/no-console-store-restore.mdc +11 -0
  26. package/rules/test/no-process-chdir/concern.json +7 -0
  27. package/rules/test/no-process-chdir/docs/index.md +11 -0
  28. package/rules/test/no-process-chdir/docs/main.md +32 -0
  29. package/rules/test/no-process-chdir/main.mjs +40 -0
  30. package/rules/test/no-process-chdir/no-process-chdir.mdc +15 -0
  31. package/rules/test/no-relative-fs-path/concern.json +7 -0
  32. package/rules/test/no-relative-fs-path/docs/index.md +11 -0
  33. package/rules/test/no-relative-fs-path/docs/main.md +34 -0
  34. package/rules/test/no-relative-fs-path/main.mjs +239 -0
  35. package/rules/test/no-relative-fs-path/no-relative-fs-path.mdc +22 -0
  36. package/rules/test/package_json/concern.json +11 -0
  37. package/rules/test/package_json/package_json.mdc +18 -0
  38. package/rules/test/package_json/package_json.rego +25 -0
  39. package/rules/test/package_json/template/package.json.contains.json +6 -0
  40. package/rules/test/sandbox-aware-test/concern.json +7 -0
  41. package/rules/test/sandbox-aware-test/docs/index.md +11 -0
  42. package/rules/test/sandbox-aware-test/docs/main.md +55 -0
  43. package/rules/test/sandbox-aware-test/main.mjs +89 -0
  44. package/rules/test/sandbox-aware-test/sandbox-aware-test.mdc +28 -0
  45. package/rules/test/stryker_config/concern.json +8 -0
  46. package/rules/test/stryker_config/data/stryker_config/docs/index.md +13 -0
  47. package/rules/test/stryker_config/data/stryker_config/docs/stryker-vue-macros-ignorer.md +31 -0
  48. package/rules/test/stryker_config/data/stryker_config/docs/stryker.config.baseline.md +31 -0
  49. package/rules/test/stryker_config/data/stryker_config/docs/stryker.config.vue.baseline.md +35 -0
  50. package/rules/test/stryker_config/data/stryker_config/stryker-vue-macros-ignorer.mjs +48 -0
  51. package/rules/test/stryker_config/data/stryker_config/stryker.config.baseline.mjs +18 -0
  52. package/rules/test/stryker_config/data/stryker_config/stryker.config.vue.baseline.mjs +23 -0
  53. package/rules/test/stryker_config/data/vitest_config/docs/index.md +11 -0
  54. package/rules/test/stryker_config/data/vitest_config/docs/vitest.config.baseline.md +35 -0
  55. package/rules/test/stryker_config/data/vitest_config/vitest.config.baseline.js +22 -0
  56. package/rules/test/stryker_config/docs/fix-stryker_config.md +41 -0
  57. package/rules/test/stryker_config/docs/index.md +12 -0
  58. package/rules/test/stryker_config/docs/main.md +38 -0
  59. package/rules/test/stryker_config/fix-stryker_config.mjs +77 -0
  60. package/rules/test/stryker_config/main.mjs +504 -0
  61. package/rules/test/stryker_config/stryker_config.mdc +26 -0
  62. package/rules/test/vitest-api-conventions/concern.json +7 -0
  63. package/rules/test/vitest-api-conventions/docs/index.md +9 -0
  64. package/rules/test/vitest-api-conventions/docs/main.md +40 -0
  65. package/rules/test/vitest-api-conventions/main.mjs +153 -0
  66. package/rules/test/vitest-api-conventions/vitest-api-conventions.mdc +129 -0
  67. package/rules/test/vitest-config-pool-forks/concern.json +7 -0
  68. package/rules/test/vitest-config-pool-forks/docs/index.md +11 -0
  69. package/rules/test/vitest-config-pool-forks/docs/main.md +42 -0
  70. package/rules/test/vitest-config-pool-forks/main.mjs +41 -0
  71. package/rules/test/vitest-config-pool-forks/vitest-config-pool-forks.mdc +34 -0
@@ -0,0 +1,40 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/test/vitest-api-conventions/main.mjs
5
+ docgen:
6
+ crc: 638031f8
7
+ ---
8
+
9
+ ## Огляд
10
+
11
+ Детектор concern-а `vitest-api-conventions`: перевіряє, що жоден тестовий файл проєкту
12
+ не викликає `expect(...).toBe(...)` з об'єктним чи масивним літералом як першим аргументом.
13
+ Така перевірка завжди хибна незалежно від вмісту (reference equality на новоствореному
14
+ значенні) — канонічна заміна для об'єктів/масивів — `toEqual` (deep equality).
15
+
16
+ ## Поведінка
17
+
18
+ Обходить дерево проєкту (пропускаючи `node_modules` і шляхи з cursor-ignore) і збирає всі
19
+ файли з іменем `*.test.mjs`/`*.test.js`. У кожному файлі шукає виклики `.toBe(`, чий перший
20
+ аргумент — саме об'єктний (`{...}`) чи масивний (`[...]`) літерал: сканує парність дужок
21
+ з урахуванням рядкових і template-літералів усередині, щоб не збитись на дужки в рядках.
22
+ Виклик рахується порушенням лише якщо одразу після закриваючої дужки літерала (з пропуском
23
+ пробілів) іде дужка, що закриває сам виклик, `)` — тобто ланцюжок на кшталт
24
+ `.toBe([...].join('\n'))` не матчиться, бо результат виклику — рядок-примітив, а не
25
+ посилання на масив/обʼєкт.
26
+
27
+ Для кожного знайденого випадку формує порушення з файлом (відносний шлях від cwd) і
28
+ номером рядка.
29
+
30
+ ## Публічний API
31
+
32
+ `lint(ctx)` — читає всі тестові файли проєкту (`ctx.cwd`), перевіряє їх на заборонений
33
+ патерн `toBe` з об'єктним/масивним літералом і повертає список порушень
34
+ (`{ file, message, reason }` на кожен знайдений виклик), або порожній результат, якщо
35
+ жоден тестовий файл не порушує конвенцію (test.mdc, vitest-api-conventions).
36
+
37
+ ## Гарантії поведінки
38
+
39
+ - Read-only: не виконує операцій запису (ФС/БД) — лише читає файли й повертає порушення.
40
+ - Сканує лише `*.test.mjs`/`*.test.js`; інші файли (в т.ч. `node_modules`) ігноруються.
@@ -0,0 +1,153 @@
1
+ /** @see ./docs/main.md */
2
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
3
+ import { collectTestFileOffenders } from '../lib/collect-test-file-offenders.mjs'
4
+
5
+ /**
6
+ * `expect(x).toBe({…})` / `expect(x).toBe([…])` — `toBe` — це `Object.is`
7
+ * (reference equality); нове об'єктне/масивне літеральне значення завжди має інше
8
+ * посилання, тож перевірка **завжди false**, незалежно від вмісту. Канон — `toEqual`
9
+ * (deep equality) для об'єктів і масивів (vitest-api-conventions.mdc, п.4).
10
+ * Знаходить лише `.toBe(` де аргумент — САМЕ літерал (нічого не приєднано після
11
+ * закриваючої дужки, окрім опційних пробілів і `)`) — `.toBe([...].join('\n'))`
12
+ * не матчиться: результат `.join()` — рядок-примітив, а не масив-посилання.
13
+ */
14
+ const TO_BE_CALL_RE = /\.toBe\(/gu
15
+ /** Whitespace-символ (пробіл/таб/перенос) — для `skipWhitespace`, module scope (oxlint prefer-static-regex). */
16
+ const WS_RE = /\s/u
17
+ /** Рядкові/template-лапки, що відкривають літерал усередині `findMatchingBracketEnd`. */
18
+ const QUOTE_CHARS = new Set(['"', "'", '`'])
19
+ /** Мапа закриваючої дужки на парну дужку, яка її відкриває — для перевірки балансу стеку. */
20
+ const CLOSE_TO_OPEN = { '}': '{', ']': '[' }
21
+
22
+ /**
23
+ * Пропускає пробіли/переноси рядків, повертає індекс першого не-whitespace символу.
24
+ * @param {string} body вміст файлу
25
+ * @param {number} from стартовий індекс
26
+ * @returns {number} індекс першого значущого символу (може дорівнювати `body.length`)
27
+ */
28
+ function skipWhitespace(body, from) {
29
+ let i = from
30
+ while (i < body.length && WS_RE.test(body[i])) i++
31
+ return i
32
+ }
33
+
34
+ /**
35
+ * Просуває сканер на один крок усередині рядкового/template-літералу: обробляє
36
+ * екранування (`\x`) і повідомляє, чи саме на цьому символі рядок закрився.
37
+ * Виокремлено з `findMatchingBracketEnd`, аби тримати cognitive complexity
38
+ * головного циклу під порогом лінтера.
39
+ * @param {string} body вміст файлу
40
+ * @param {number} i індекс поточного символу (усередині рядка)
41
+ * @param {string} quote символ лапки, що відкриває поточний рядок
42
+ * @returns {{next: number, closed: boolean}} нова позиція сканера і прапорець закриття
43
+ */
44
+ function stepInsideQuote(body, i, quote) {
45
+ const ch = body[i]
46
+ if (ch === '\\') return { next: i + 2, closed: false }
47
+ if (ch === quote) return { next: i + 1, closed: true }
48
+ return { next: i + 1, closed: false }
49
+ }
50
+
51
+ /**
52
+ * Знаходить індекс символу, що йде одразу за парною дужкою, яка закриває
53
+ * дужку на позиції `openIndex` (`{` чи `[`). Ігнорує дужки всередині
54
+ * рядкових/template-літералів, аби не збитись на `{ a: '}' }` тощо.
55
+ * @param {string} body вміст файлу
56
+ * @param {number} openIndex індекс відкриваючої дужки
57
+ * @returns {number|null} індекс одразу після закриваючої дужки, або `null` якщо не збалансовано
58
+ */
59
+ function findMatchingBracketEnd(body, openIndex) {
60
+ const stack = [body[openIndex]]
61
+ let i = openIndex + 1
62
+ let quote = null
63
+
64
+ while (i < body.length) {
65
+ if (quote) {
66
+ const step = stepInsideQuote(body, i, quote)
67
+ if (step.closed) quote = null
68
+ i = step.next
69
+ continue
70
+ }
71
+
72
+ const ch = body[i]
73
+
74
+ if (QUOTE_CHARS.has(ch)) {
75
+ quote = ch
76
+ i++
77
+ continue
78
+ }
79
+
80
+ if (ch === '{' || ch === '[') {
81
+ stack.push(ch)
82
+ i++
83
+ continue
84
+ }
85
+
86
+ if (ch in CLOSE_TO_OPEN) {
87
+ if (stack.at(-1) !== CLOSE_TO_OPEN[ch]) return null // незбалансовано — здаємось
88
+ stack.pop()
89
+ if (stack.length === 0) return i + 1
90
+ i++
91
+ continue
92
+ }
93
+
94
+ i++
95
+ }
96
+
97
+ return null
98
+ }
99
+
100
+ /**
101
+ * Знаходить усі виклики `.toBe(` де перший аргумент — САМЕ об'єктний/масивний
102
+ * літерал (не результат ланцюжка викликів на ньому).
103
+ * @param {string} body вміст файлу
104
+ * @returns {Array<{line: number}>} знайдені порушення
105
+ */
106
+ function findOffenders(body) {
107
+ const offenders = []
108
+ for (const m of body.matchAll(TO_BE_CALL_RE)) {
109
+ const argStart = skipWhitespace(body, m.index + m[0].length)
110
+ const ch = body[argStart]
111
+ if (ch !== '{' && ch !== '[') continue
112
+
113
+ const afterLiteral = findMatchingBracketEnd(body, argStart)
114
+ if (afterLiteral === null) continue
115
+
116
+ const afterWs = skipWhitespace(body, afterLiteral)
117
+ if (body[afterWs] !== ')') continue // приєднано щось після літерала (напр. .join(...)) — не порушення
118
+
119
+ const line = body.slice(0, m.index).split('\n').length
120
+ offenders.push({ line })
121
+ }
122
+ return offenders
123
+ }
124
+
125
+ /**
126
+ * Detector: жоден `*.test.{mjs,js}` не викликає `expect(...).toBe(...)` з
127
+ * об'єктним/масивним літералом — `toBe` (Object.is) на новоствореному
128
+ * об'єкті/масиві завжди false; канон — `toEqual` (vitest-api-conventions.mdc, п.4).
129
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
130
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат перевірки з порушеннями.
131
+ */
132
+ export async function lint(ctx) {
133
+ const reporter = createViolationReporter(ctx)
134
+ const { pass, fail } = reporter
135
+
136
+ const cwd = ctx.cwd
137
+ const { testFiles, offenders } = await collectTestFileOffenders(cwd, findOffenders)
138
+
139
+ if (offenders.length === 0) {
140
+ pass(`Жоден з ${testFiles.length} тестових файлів не викликає toBe(...) з об'єктним/масивним літералом (test.mdc)`)
141
+ return reporter.result()
142
+ }
143
+
144
+ for (const { file, line } of offenders) {
145
+ fail(
146
+ `${file}:${line}: expect(...).toBe(...) з об'єктним/масивним літералом завжди false ` +
147
+ `(Object.is на новому посиланні) — використовуй toEqual (test.mdc, vitest-api-conventions)`,
148
+ { file }
149
+ )
150
+ }
151
+
152
+ return reporter.result()
153
+ }
@@ -0,0 +1,129 @@
1
+ ## Vitest API — обов'язкові конвенції (для генерації тестів LLM)
2
+
3
+ Цей розділ адресований генераторам тестів (LLM, local і cloud). Порушення будь-якого пункту — рівнозначне синтаксичній помилці: тест не запуститься або дасть хибно-позитивний результат.
4
+
5
+ Канон: тести виконуються через **vitest** під Bun-рушієм (`bun run --bun vitest run`, див. `test.package_json`), а не через нативний `bun test` — `import ... from 'bun:test'` заборонено (`no-bun-test-import`). API нижче — саме vitest, не bun:test і не jest.
6
+
7
+ ### 1. Модульна система — лише ESM
8
+
9
+ `*.test.mjs` завжди ESM; `require()` у `.mjs` кидає `ERR_REQUIRE_ESM`.
10
+
11
+ ✓ `import { foo } from '../foo.mjs'`
12
+ ✗ `const { foo } = require('../foo.mjs')`
13
+
14
+ ### 2. `vi.*` замість `jest.*`
15
+
16
+ Проект використовує Vitest, не Jest. Будь-який `jest.fn()`, `jest.mock()`, `jest.spyOn()` — помилка компіляції або runtime-помилка.
17
+
18
+ ✓ `vi.fn()`, `vi.mock(...)`, `vi.spyOn(...)`
19
+ ✗ `jest.fn()`, `jest.mock(...)`, `jest.spyOn(...)`
20
+
21
+ ### 3. Кастинг mock-функцій — лише `vi.mocked()`
22
+
23
+ ✓ `vi.mocked(fetch).mockResolvedValue(res)`
24
+ ✗ `(fetch as vi.Mock).mockResolvedValue(res)` — TypeScript у `.mjs` не компілюється
25
+
26
+ ### 4. `toBe` vs `toEqual`
27
+
28
+ `toBe` (Object.is) — для примітивів (`string`, `number`, `boolean`, `null`, `undefined`).
29
+ `toEqual` (deep equality) — для об'єктів і масивів.
30
+
31
+ `toBe({…})` для об'єктів **завжди false** (різні посилання) — використовуй `toEqual`.
32
+
33
+ ✓ `expect(count).toBe(3)`
34
+ ✓ `expect(result).toEqual({ ok: true })`
35
+ ✗ `expect(result).toBe({ ok: true })`
36
+
37
+ **Перевірка** (`rules/test/vitest-api-conventions/main.mjs`): сканує `**/*.test.{mjs,js}`, шукає `expect(...).toBe(...)` з об'єктним/масивним літералом як першим аргументом — таке порівняння завжди `false` через reference equality, незалежно від вмісту.
38
+
39
+ ### 5. Моки для Date / часу
40
+
41
+ `vi.spyOn(Date).mockReturnValue(…)` **не працює** з `new Date()` (TypeError: Date is not a constructor при `new`).
42
+
43
+
44
+
45
+ ```js
46
+ vi.useFakeTimers()
47
+ vi.setSystemTime(new Date('2024-01-01'))
48
+ afterEach(() => vi.useRealTimers())
49
+ ```
50
+
51
+ ✗ `vi.spyOn(global, 'Date').mockReturnValue(new Date('2024-01-01'))`
52
+
53
+ ### 6. Моки для `process.env`
54
+
55
+ `process.env.X = '…'` забруднює глобальний стан; `vi.spyOn(process, 'env')` не дає ізоляції між тестами.
56
+
57
+
58
+
59
+ ```js
60
+ vi.stubEnv('KEY', 'val')
61
+ afterEach(() => vi.unstubAllEnvs())
62
+ ```
63
+
64
+ ✗ `process.env.KEY = 'val'` (без відновлення)
65
+
66
+ ### 7. Модулі з top-level side-effect
67
+
68
+ Якщо source-модуль читає `process.env` або викликає функцію на рівні модуля — env/моки мають бути встановлені **до** `import`; сам import — dynamic.
69
+
70
+
71
+
72
+ ```js
73
+ vi.stubEnv('KEY', 'val')
74
+ const { fn } = await import('../index.js')
75
+ ```
76
+
77
+ ✗ статичний `import { fn } from '../index.js'` коли source викликає функцію на рівні модуля
78
+
79
+ ### 8. Cleanup у `afterEach` — обов'язковий
80
+
81
+ Після кожного тест-файлу з mock-ами — відновлення стану:
82
+
83
+ ```js
84
+ afterEach(() => {
85
+ vi.restoreAllMocks()
86
+ vi.unstubAllEnvs()
87
+ vi.useRealTimers() // лише якщо vi.useFakeTimers() використовувався
88
+ })
89
+ ```
90
+
91
+ Пропуск `afterEach` — витік стану між тестами в одному файлі.
92
+
93
+ ### 9. FormData у Node.js
94
+
95
+ `formData.get('field')` повертає `Blob | string | null`. Для file-полів — `toBeInstanceOf(Blob)`, НЕ `toBe('filename')`.
96
+
97
+ ✓ `expect(formData.get('file')).toBeInstanceOf(Blob)`
98
+ ✗ `expect(formData.get('file')).toBe('photo.jpg')`
99
+
100
+ ---
101
+
102
+ ### Канонічний шаблон тест-файлу
103
+
104
+ ```js
105
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
106
+
107
+ import { myFn } from '../my-module.mjs'
108
+
109
+ vi.mock('../dep.mjs', () => ({ dep: vi.fn() }))
110
+
111
+ beforeEach(() => {
112
+ vi.stubEnv('API_KEY', 'test-key')
113
+ vi.useFakeTimers()
114
+ vi.setSystemTime(new Date('2024-06-01'))
115
+ })
116
+
117
+ afterEach(() => {
118
+ vi.restoreAllMocks()
119
+ vi.unstubAllEnvs()
120
+ vi.useRealTimers()
121
+ })
122
+
123
+ describe('myFn', () => {
124
+ it('повертає очікуване значення', async () => {
125
+ const result = await myFn()
126
+ expect(result).toEqual({ ok: true })
127
+ })
128
+ })
129
+ ```
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "lint": {
4
+ "scope": "full",
5
+ "glob": ["vitest.config.mjs", "vitest.config.js"]
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/test/vitest-config-pool-forks
4
+ resource: plugins/lang-js/rules/test/vitest-config-pool-forks/
5
+ ---
6
+
7
+ # npm/rules/test/vitest-config-pool-forks
8
+
9
+ | Файл | Тип |
10
+ | ------------------- | --------- |
11
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,42 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/test/vitest-config-pool-forks/main.mjs
5
+ docgen:
6
+ crc: 3d6d8777
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.98
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Огляд:
16
+ Перевіряє конфігураційний файл Vitest для валідації наявності ізоляції тестів через режим `forks`. Це забезпечує відсутність гонок даних при паралельному виконанні тестів.
17
+
18
+ Поведінка:
19
+
20
+ 1. Викликається публічна функція `main` для аналізу конфігураційного файлу Vitest.
21
+ 2. Перевірка пропущена, якщо файли `vitest.config.mjs` або `vitest.config.js` відсутні.
22
+ 3. При знаходженні файлу його вміст зчитується.
23
+ 4. Перевірка успішна, якщо вміст файлу містить шаблон `pool: 'forks'` (test.mdc).
24
+ 5. Перевірка провалена, якщо вміст файлу не містить шаблон `pool: 'forks'` (test.mdc), оскільки це обов'язковий захист від гонок даних у паралельних тестах.
25
+
26
+ ## Поведінка
27
+
28
+ Поведінка:
29
+
30
+ 1. Викликається `main` для перевірки конфігураційного файлу Vitest.
31
+ 2. Якщо файл `vitest.config.mjs` або `vitest.config.js` відсутній, перевірка пропущена.
32
+ 3. Якщо конфігураційний файл знайдено, його вміст читається.
33
+ 4. Якщо вміст файлу містить шаблон `pool: 'forks'`, перевірка успішна (test.mdc).
34
+ 5. Якщо вміст файлу не містить шаблон `pool: 'forks'`, перевірка провалена (test.mdc), оскільки це необхідна оборонна міра проти гонки даних у паралельних файлах тестів.
35
+
36
+ ## Публічний API
37
+
38
+ main — Вимагає налаштування `vitest.config` на використання ізольованих процесів (`pool: 'forks'`).
39
+
40
+ ## Гарантії поведінки
41
+
42
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,41 @@
1
+ /** @see ./docs/vitest-config-pool-forks.md */
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+
6
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
7
+
8
+ /** Subтring-pattern: `pool: 'forks'` або `pool: "forks"` (з опційним whitespace). */
9
+ const POOL_FORKS_RE = /pool\s*:\s*['"]forks['"]/u
10
+
11
+ // Канонічна назва — `.mjs` (нові файли, js.mdc), але legacy `.js` лишається
12
+ // валідним. Перший знайдений виграє: `.mjs` пріоритетніший.
13
+ const VITEST_CONFIG_NAMES = ['vitest.config.mjs', 'vitest.config.js']
14
+
15
+ /**
16
+ * Перевіряє, що `vitest.config.{mjs,js}` (якщо існує) містить `pool: 'forks'`.
17
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінт-прогону.
18
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат перевірки наявності `pool: 'forks'`.
19
+ */
20
+ export async function lint(ctx) {
21
+ const reporter = createViolationReporter(ctx)
22
+ const { pass, fail } = reporter
23
+
24
+ const cwdParam = ctx.cwd
25
+ const configName = VITEST_CONFIG_NAMES.find(name => existsSync(join(cwdParam, name)))
26
+ if (!configName) {
27
+ pass('vitest.config.mjs/.js відсутній — pool-перевірку пропущено')
28
+ return reporter.result()
29
+ }
30
+
31
+ const body = await readFile(join(cwdParam, configName), 'utf8')
32
+ if (POOL_FORKS_RE.test(body)) {
33
+ pass(`${configName} містить pool: 'forks' (test.mdc)`)
34
+ } else {
35
+ fail(
36
+ `${configName} має містити pool: 'forks' — defense-in-depth для race у process.cwd() між паралельними test files (test.mdc)`
37
+ )
38
+ }
39
+
40
+ return reporter.result()
41
+ }
@@ -0,0 +1,34 @@
1
+ ## Vitest: `pool: 'forks'` як захист від race у `process.cwd()`
2
+
3
+ **`vitest-config-pool-forks`** (`rules/test/js/vitest-config-pool-forks.mjs`) — substring-перевірка `pool: 'forks'` у `vitest.config.mjs` (або legacy `vitest.config.js`; `.mjs` пріоритетніший).
4
+
5
+ **Чому:** Defense-in-depth проти race у `process.cwd()` між паралельними test файлами. Навіть якщо хтось пропустить заборону `process.chdir`, fork-ізоляція не дасть race у production tree — кожен test-файл отримує власний процес зі своїм `cwd`.
6
+
7
+ Якщо `vitest.config.mjs` / `vitest.config.js` відсутній — перевірка пропускається (pass без помилки).
8
+
9
+ **Canonical `vitest.config.mjs`** (для довідки — `pool: 'forks'` + `include` + `coverage`) — у `rules/test/js/data/vitest_config/vitest.config.baseline.js`:
10
+
11
+ - `environment: 'node'`
12
+ - `coverage.provider: 'v8'` з lcov+text-summary репортами
13
+ - `include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}']` — підхоплює тести у `tests/`-піддиректоріях і top-level integration suites у `<root>/tests/`
14
+
15
+ У `package.json#scripts` має бути `"test": "bun run --bun vitest run"` (substring `vitest` + `--bun`; допустимі локальні розширення-обгортки) та опційно `"test:watch": "vitest"`.
16
+
17
+ **Frontend-варіант (Vue/Vite + happy-dom):**
18
+
19
+ Для проєктів зі своїм `vite.config.js` `vitest.config.mjs` має повторно використовувати vite-плагіни та aliases і перемкнути `environment` на `'happy-dom'` (або `'jsdom'`):
20
+
21
+ ```js
22
+ import { defineConfig, mergeConfig } from 'vitest/config'
23
+ import viteConfig from './vite.config.js'
24
+
25
+ export default mergeConfig(viteConfig, defineConfig({
26
+ test: {
27
+ include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}'],
28
+ environment: 'happy-dom',
29
+ coverage: { provider: 'v8', reporter: ['lcov', 'text-summary'] }
30
+ }
31
+ }))
32
+ ```
33
+
34
+ Концерн ставить node-варіант baseline; перехід на frontend — ручна модифікація, після якої концерн уже не перетирає (idempotent).