@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.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/rules/test/docs/index.md +11 -0
- package/rules/test/lib/collect-test-file-offenders.mjs +49 -0
- package/rules/test/lib/docs/collect-test-file-offenders.md +29 -0
- package/rules/test/lib/docs/index.md +9 -0
- package/rules/test/location/concern.json +8 -0
- package/rules/test/location/docs/index.md +11 -0
- package/rules/test/location/docs/main.md +36 -0
- package/rules/test/location/location.mdc +52 -0
- package/rules/test/location/main.mjs +70 -0
- package/rules/test/main.json +1 -0
- package/rules/test/main.mdc +26 -0
- package/rules/test/no-bun-test-import/concern.json +7 -0
- package/rules/test/no-bun-test-import/docs/fix-no-bun-test-import.md +30 -0
- package/rules/test/no-bun-test-import/docs/index.md +10 -0
- package/rules/test/no-bun-test-import/docs/main.md +35 -0
- package/rules/test/no-bun-test-import/fix-no-bun-test-import.mjs +51 -0
- package/rules/test/no-bun-test-import/main.mjs +105 -0
- package/rules/test/no-bun-test-import/no-bun-test-import.mdc +9 -0
- package/rules/test/no-console-store-restore/concern.json +7 -0
- package/rules/test/no-console-store-restore/docs/index.md +11 -0
- package/rules/test/no-console-store-restore/docs/main.md +44 -0
- package/rules/test/no-console-store-restore/main.mjs +55 -0
- package/rules/test/no-console-store-restore/no-console-store-restore.mdc +11 -0
- package/rules/test/no-process-chdir/concern.json +7 -0
- package/rules/test/no-process-chdir/docs/index.md +11 -0
- package/rules/test/no-process-chdir/docs/main.md +32 -0
- package/rules/test/no-process-chdir/main.mjs +40 -0
- package/rules/test/no-process-chdir/no-process-chdir.mdc +15 -0
- package/rules/test/no-relative-fs-path/concern.json +7 -0
- package/rules/test/no-relative-fs-path/docs/index.md +11 -0
- package/rules/test/no-relative-fs-path/docs/main.md +34 -0
- package/rules/test/no-relative-fs-path/main.mjs +239 -0
- package/rules/test/no-relative-fs-path/no-relative-fs-path.mdc +22 -0
- package/rules/test/package_json/concern.json +11 -0
- package/rules/test/package_json/package_json.mdc +18 -0
- package/rules/test/package_json/package_json.rego +25 -0
- package/rules/test/package_json/template/package.json.contains.json +6 -0
- package/rules/test/sandbox-aware-test/concern.json +7 -0
- package/rules/test/sandbox-aware-test/docs/index.md +11 -0
- package/rules/test/sandbox-aware-test/docs/main.md +55 -0
- package/rules/test/sandbox-aware-test/main.mjs +89 -0
- package/rules/test/sandbox-aware-test/sandbox-aware-test.mdc +28 -0
- package/rules/test/stryker_config/concern.json +8 -0
- package/rules/test/stryker_config/data/stryker_config/docs/index.md +13 -0
- package/rules/test/stryker_config/data/stryker_config/docs/stryker-vue-macros-ignorer.md +31 -0
- package/rules/test/stryker_config/data/stryker_config/docs/stryker.config.baseline.md +31 -0
- package/rules/test/stryker_config/data/stryker_config/docs/stryker.config.vue.baseline.md +35 -0
- package/rules/test/stryker_config/data/stryker_config/stryker-vue-macros-ignorer.mjs +48 -0
- package/rules/test/stryker_config/data/stryker_config/stryker.config.baseline.mjs +18 -0
- package/rules/test/stryker_config/data/stryker_config/stryker.config.vue.baseline.mjs +23 -0
- package/rules/test/stryker_config/data/vitest_config/docs/index.md +11 -0
- package/rules/test/stryker_config/data/vitest_config/docs/vitest.config.baseline.md +35 -0
- package/rules/test/stryker_config/data/vitest_config/vitest.config.baseline.js +22 -0
- package/rules/test/stryker_config/docs/fix-stryker_config.md +41 -0
- package/rules/test/stryker_config/docs/index.md +12 -0
- package/rules/test/stryker_config/docs/main.md +38 -0
- package/rules/test/stryker_config/fix-stryker_config.mjs +77 -0
- package/rules/test/stryker_config/main.mjs +504 -0
- package/rules/test/stryker_config/stryker_config.mdc +26 -0
- package/rules/test/vitest-api-conventions/concern.json +7 -0
- package/rules/test/vitest-api-conventions/docs/index.md +9 -0
- package/rules/test/vitest-api-conventions/docs/main.md +40 -0
- package/rules/test/vitest-api-conventions/main.mjs +153 -0
- package/rules/test/vitest-api-conventions/vitest-api-conventions.mdc +129 -0
- package/rules/test/vitest-config-pool-forks/concern.json +7 -0
- package/rules/test/vitest-config-pool-forks/docs/index.md +11 -0
- package/rules/test/vitest-config-pool-forks/docs/main.md +42 -0
- package/rules/test/vitest-config-pool-forks/main.mjs +41 -0
- 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,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).
|