@application-kit/testing 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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @application-kit/testing
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fb35f7c: Поддержка vitest 5 (peer `^4.1.0 || ^5.0.0`): фабрики кладут кеш модулей ключом `fsModuleCache` верхнего уровня на
8
+ vitest 5 и `experimental.fsModuleCache` на vitest 4, без deprecation в каждом прогоне. Новый
9
+ `defineApplicationMutationConfig(unitConfig)` собирает конфиг vitest для Stryker: кеш модулей выключен, на vitest 5
10
+ файлы спек изолированы — без этого vitest-runner Stryker собирает покрытие по тестам только с первого файла спек
11
+ воркера. Переход приложения на vitest 5 — с этой функцией в `vitest.stryker.config.mts`: ручное
12
+ `experimental.fsModuleCache: false` поверх фабрики там не действует.
13
+
14
+ ### Patch Changes
15
+
16
+ - aa9114b: `defineStrykerLayers` перед прогоном выбрасывает из incremental-отчёта группы мутанты со статусом NoCoverage. Stryker
17
+ переиспользует их, не сверяя мутант с `ignoreStatic`, и статус, записанный прогоном с потерянным покрытием (vitest 5
18
+ без изоляции спек), переживал починку раннера: статические мутанты оставались NoCoverage и держали score ниже порога.
19
+ Заново такой мутант классифицируется по покрытию dry run, тесты на него не тратятся.
20
+
3
21
  ## 0.6.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -84,13 +84,18 @@ export default defineApplicationEndToEndConfig({
84
84
  `server.deps.inline` (`inlineDependencies`) — у приложения. Фабрики решают сами:
85
85
 
86
86
  - оба профиля — `globals`, `isolate: false`, `testTimeout` 20 с (таймаут ловит зависший промис, а не медленный раннер),
87
- кеш трансформов `.vitest-module-cache` (путь — контракт кеша джоб `ts:test:*` шаблона CI), покрытие v8 с отчётом
87
+ кеш трансформов `.vitest-module-cache` (путь — контракт кеша джоб `ts:test:*` шаблона CI; ключ — `fsModuleCache`
88
+ верхнего уровня на vitest 5 и `experimental.fsModuleCache` на vitest 4), покрытие v8 с отчётом
88
89
  `text` и `skipFull`; `@application-kit/graphql` идёт через Vite, а не нативно через Node — иначе ошибка из пакета и код
89
90
  приложения видят разные копии `graphql`, и `instanceof GraphQLError` ложен;
90
91
  - unit — проекты `server` (`node`, спеки `specs` без `excludeSpecs` и `clientSpecs`) и `client` (`jsdom`, `clientSpecs`),
91
92
  ячейки шарда CI и `ProjectShardSequencer` (ниже);
92
93
  - e2e — окружение `node` и алиас `graphql` на CJS-вход `node_modules/graphql/index.js` под `root`: Vite для SSR берёт
93
- ESM-сборку `graphql`, внешний `mercurius` — CJS, и без алиаса схема и валидация запроса живут в разных realm.
94
+ ESM-сборку `graphql`, внешний `mercurius` — CJS, и без алиаса схема и валидация запроса живут в разных realm;
95
+ - мутации — `defineApplicationMutationConfig(unitConfig)`: unit-конфиг для `vitestConfigFile` Stryker без кеша модулей
96
+ (под Stryker он отдаёт тестам трансформ без активного мутанта), на vitest 5 — ещё и с `isolate: true`: без изоляции
97
+ vitest-runner Stryker собирает покрытие по тестам только с первого файла спек воркера. Ручное
98
+ `experimental.fsModuleCache: false` поверх фабрики на vitest 5 не действует — верхний ключ фабрики главнее.
94
99
 
95
100
  ## Вход `/vitest/fast-bcrypt`: дешёвый bcrypt
96
101
 
@@ -136,6 +141,9 @@ export default defineStrykerConfig({
136
141
 
137
142
  // vitest.config.mts
138
143
  coverage: { exclude: [...CODE_WITHOUT_LOGIC, '**/*.html'], include: ['src'], thresholds: { lines: 95 } }
144
+
145
+ // vitest.stryker.config.mts — defineApplicationMutationConfig из входа /vitest
146
+ export default defineApplicationMutationConfig(unitConfig)
139
147
  ```
140
148
 
141
149
  `defineStrykerLayers` режет прогон на группы каталогов: ячейка CI мутирует одну группу со своим порогом `break` (общий
@@ -15,6 +15,7 @@ const MATRIX_PATTERN = (variable) => {
15
15
  };
16
16
  const sorted = (values) => [...values].toSorted((left, right) => left.localeCompare(right));
17
17
  const isUnderDirectory = (file, directory) => file.startsWith(`${directory}/`);
18
+ const isUncovered = (mutant) => mutant.status === 'NoCoverage';
18
19
  const rootsOf = (options) => [
19
20
  ...options.scope.directories,
20
21
  ...options.scope.parents.flatMap((parent) => (0, node_fs_1.readdirSync)(node_path_1.default.join(options.root, parent), { withFileTypes: true })
@@ -53,15 +54,20 @@ const pruneIncrementalReport = (incrementalFile, isInScope, configName) => {
53
54
  return;
54
55
  }
55
56
  const report = JSON.parse((0, node_fs_1.readFileSync)(incrementalFile, 'utf8'));
56
- const files = report.files ?? {};
57
- const foreign = Object.keys(files).filter((file) => !isInScope(file));
58
- if (foreign.length === 0) {
57
+ const files = Object.entries(report.files ?? {});
58
+ const own = files.filter(([file]) => isInScope(file));
59
+ const uncovered = own.flatMap(([, data]) => data.mutants).filter((mutant) => isUncovered(mutant));
60
+ if (own.length === files.length && uncovered.length === 0) {
59
61
  return;
60
62
  }
61
- report.files = Object.fromEntries(Object.entries(files).filter(([file]) => isInScope(file)));
63
+ report.files = Object.fromEntries(own.map(([file, data]) => [
64
+ file,
65
+ { ...data, mutants: data.mutants.filter((mutant) => !isUncovered(mutant)) },
66
+ ]));
62
67
  (0, node_fs_1.writeFileSync)(`${incrementalFile}.tmp`, JSON.stringify(report));
63
68
  (0, node_fs_1.renameSync)(`${incrementalFile}.tmp`, incrementalFile);
64
- console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${foreign.length}`);
69
+ console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${files.length - own.length}, `
70
+ + `мутантов NoCoverage: ${uncovered.length}`);
65
71
  };
66
72
  const defineStrykerLayers = (options) => {
67
73
  const variable = options.layerVariable ?? DEFAULT_LAYER_VARIABLE;
@@ -2,3 +2,4 @@ import type { ViteUserConfig } from 'vitest/config';
2
2
  import type { ApplicationTestConfigOptionsInterface, ApplicationUnitConfigOptionsInterface } from './application-vitest-config-options.interface.js';
3
3
  export declare const defineApplicationUnitConfig: (options: ApplicationUnitConfigOptionsInterface) => ViteUserConfig;
4
4
  export declare const defineApplicationEndToEndConfig: (options: ApplicationTestConfigOptionsInterface) => ViteUserConfig;
5
+ export declare const defineApplicationMutationConfig: (unitConfig: ViteUserConfig) => ViteUserConfig;
@@ -3,13 +3,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.defineApplicationEndToEndConfig = exports.defineApplicationUnitConfig = void 0;
6
+ exports.defineApplicationMutationConfig = exports.defineApplicationEndToEndConfig = exports.defineApplicationUnitConfig = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const node_process_1 = __importDefault(require("node:process"));
9
9
  const vite_1 = require("vite");
10
+ const node_1 = require("vitest/node");
10
11
  const project_shard_sequencer_js_1 = require("./project-shard-sequencer.js");
11
12
  const PACKAGES_THROUGH_VITE = ['@application-kit/graphql'];
12
13
  const TEST_TIMEOUT = 20_000;
14
+ const MODULE_CACHE_PATH = '.vitest-module-cache';
15
+ const IS_VITEST_5 = Number(node_1.version.split('.', 1)[0]) >= 5;
16
+ const moduleCacheOf = (isEnabled) => {
17
+ const cache = { fsModuleCache: isEnabled, fsModuleCachePath: MODULE_CACHE_PATH };
18
+ return IS_VITEST_5 ? cache : { experimental: cache };
19
+ };
13
20
  const cellOf = (argv) => {
14
21
  const shard = (0, project_shard_sequencer_js_1.parseShard)(argv);
15
22
  if (shard === undefined) {
@@ -28,7 +35,7 @@ const coverageOf = (scope) => ({
28
35
  thresholds: scope.thresholds,
29
36
  });
30
37
  const sharedTestOptions = (options) => ({
31
- experimental: { fsModuleCache: true, fsModuleCachePath: '.vitest-module-cache' },
38
+ ...moduleCacheOf(true),
32
39
  globals: true,
33
40
  isolate: false,
34
41
  server: { deps: { inline: [...PACKAGES_THROUGH_VITE, ...(options.inlineDependencies ?? [])] } },
@@ -85,3 +92,7 @@ const defineApplicationEndToEndConfig = (options) => {
85
92
  });
86
93
  };
87
94
  exports.defineApplicationEndToEndConfig = defineApplicationEndToEndConfig;
95
+ const defineApplicationMutationConfig = (unitConfig) => (0, vite_1.mergeConfig)(unitConfig, {
96
+ test: { ...moduleCacheOf(false), ...(IS_VITEST_5 && { isolate: true }) },
97
+ });
98
+ exports.defineApplicationMutationConfig = defineApplicationMutationConfig;
@@ -9,6 +9,7 @@ const MATRIX_PATTERN = (variable) => {
9
9
  };
10
10
  const sorted = (values) => [...values].toSorted((left, right) => left.localeCompare(right));
11
11
  const isUnderDirectory = (file, directory) => file.startsWith(`${directory}/`);
12
+ const isUncovered = (mutant) => mutant.status === 'NoCoverage';
12
13
  const rootsOf = (options) => [
13
14
  ...options.scope.directories,
14
15
  ...options.scope.parents.flatMap((parent) => readdirSync(path.join(options.root, parent), { withFileTypes: true })
@@ -47,15 +48,20 @@ const pruneIncrementalReport = (incrementalFile, isInScope, configName) => {
47
48
  return;
48
49
  }
49
50
  const report = JSON.parse(readFileSync(incrementalFile, 'utf8'));
50
- const files = report.files ?? {};
51
- const foreign = Object.keys(files).filter((file) => !isInScope(file));
52
- if (foreign.length === 0) {
51
+ const files = Object.entries(report.files ?? {});
52
+ const own = files.filter(([file]) => isInScope(file));
53
+ const uncovered = own.flatMap(([, data]) => data.mutants).filter((mutant) => isUncovered(mutant));
54
+ if (own.length === files.length && uncovered.length === 0) {
53
55
  return;
54
56
  }
55
- report.files = Object.fromEntries(Object.entries(files).filter(([file]) => isInScope(file)));
57
+ report.files = Object.fromEntries(own.map(([file, data]) => [
58
+ file,
59
+ { ...data, mutants: data.mutants.filter((mutant) => !isUncovered(mutant)) },
60
+ ]));
56
61
  writeFileSync(`${incrementalFile}.tmp`, JSON.stringify(report));
57
62
  renameSync(`${incrementalFile}.tmp`, incrementalFile);
58
- console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${foreign.length}`);
63
+ console.error(`${configName}: из ${incrementalFile} убрано файлов вне области группы: ${files.length - own.length}, `
64
+ + `мутантов NoCoverage: ${uncovered.length}`);
59
65
  };
60
66
  export const defineStrykerLayers = (options) => {
61
67
  const variable = options.layerVariable ?? DEFAULT_LAYER_VARIABLE;
@@ -2,3 +2,4 @@ import type { ViteUserConfig } from 'vitest/config';
2
2
  import type { ApplicationTestConfigOptionsInterface, ApplicationUnitConfigOptionsInterface } from './application-vitest-config-options.interface.js';
3
3
  export declare const defineApplicationUnitConfig: (options: ApplicationUnitConfigOptionsInterface) => ViteUserConfig;
4
4
  export declare const defineApplicationEndToEndConfig: (options: ApplicationTestConfigOptionsInterface) => ViteUserConfig;
5
+ export declare const defineApplicationMutationConfig: (unitConfig: ViteUserConfig) => ViteUserConfig;
@@ -1,9 +1,16 @@
1
1
  import path from 'node:path';
2
2
  import process from 'node:process';
3
3
  import { mergeConfig } from 'vite';
4
+ import { version } from 'vitest/node';
4
5
  import { parseShard, ProjectShardSequencer } from './project-shard-sequencer.js';
5
6
  const PACKAGES_THROUGH_VITE = ['@application-kit/graphql'];
6
7
  const TEST_TIMEOUT = 20_000;
8
+ const MODULE_CACHE_PATH = '.vitest-module-cache';
9
+ const IS_VITEST_5 = Number(version.split('.', 1)[0]) >= 5;
10
+ const moduleCacheOf = (isEnabled) => {
11
+ const cache = { fsModuleCache: isEnabled, fsModuleCachePath: MODULE_CACHE_PATH };
12
+ return IS_VITEST_5 ? cache : { experimental: cache };
13
+ };
7
14
  const cellOf = (argv) => {
8
15
  const shard = parseShard(argv);
9
16
  if (shard === undefined) {
@@ -22,7 +29,7 @@ const coverageOf = (scope) => ({
22
29
  thresholds: scope.thresholds,
23
30
  });
24
31
  const sharedTestOptions = (options) => ({
25
- experimental: { fsModuleCache: true, fsModuleCachePath: '.vitest-module-cache' },
32
+ ...moduleCacheOf(true),
26
33
  globals: true,
27
34
  isolate: false,
28
35
  server: { deps: { inline: [...PACKAGES_THROUGH_VITE, ...(options.inlineDependencies ?? [])] } },
@@ -77,3 +84,6 @@ export const defineApplicationEndToEndConfig = (options) => {
77
84
  },
78
85
  });
79
86
  };
87
+ export const defineApplicationMutationConfig = (unitConfig) => mergeConfig(unitConfig, {
88
+ test: { ...moduleCacheOf(false), ...(IS_VITEST_5 && { isolate: true }) },
89
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@application-kit/testing",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Тестовая инфраструктура NestJS-приложений: шины CQRS, логгер, фейковый microservice, сборка web-приложения, Postgres в testcontainers, запуск e2e-приложения, изоляция окружения прогона, дешёвый bcrypt для unit-прогона, раскладка ячеек CI по проектам vitest и фабрики конфигов vitest",
5
5
  "keywords": [
6
6
  "cqrs",
@@ -120,7 +120,7 @@
120
120
  "testcontainers": "^12.1.0",
121
121
  "typeorm": "^1.0.0",
122
122
  "vite": "^8.0.0",
123
- "vitest": "^4.1.0"
123
+ "vitest": "^4.1.0 || ^5.0.0"
124
124
  },
125
125
  "peerDependenciesMeta": {
126
126
  "bcrypt": {