@verdeect/kit 0.0.0-stage → 1.0.0-rc.2
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 +138 -0
- package/README.md +804 -2
- package/alerts/verdeect-kit.rules.yml +133 -0
- package/dist/cli/commands/crypto.d.ts +9 -0
- package/dist/cli/commands/crypto.js +87 -0
- package/dist/cli/commands/db.d.ts +11 -0
- package/dist/cli/commands/db.js +151 -0
- package/dist/cli/commands/openapi.d.ts +9 -0
- package/dist/cli/commands/openapi.js +144 -0
- package/dist/cli/commands/rabbitmq.d.ts +11 -0
- package/dist/cli/commands/rabbitmq.js +124 -0
- package/dist/cli/commands/redis.d.ts +11 -0
- package/dist/cli/commands/redis.js +119 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/config/cli-schemas.d.ts +29 -0
- package/dist/config/cli-schemas.js +15 -0
- package/dist/config/define-env.d.ts +21 -0
- package/dist/config/define-env.js +43 -0
- package/dist/config/fragments.d.ts +105 -0
- package/dist/config/fragments.js +86 -0
- package/dist/config/helpers.d.ts +26 -0
- package/dist/config/helpers.js +62 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/config/kit-config.module.d.ts +20 -0
- package/dist/config/kit-config.module.js +23 -0
- package/dist/config/parse-env.d.ts +14 -0
- package/dist/config/parse-env.js +28 -0
- package/dist/crypto/cipher.d.ts +61 -0
- package/dist/crypto/cipher.js +136 -0
- package/dist/crypto/index.d.ts +2 -0
- package/dist/crypto/index.js +2 -0
- package/dist/database/batches.d.ts +42 -0
- package/dist/database/batches.js +49 -0
- package/dist/database/db.d.ts +93 -0
- package/dist/database/db.js +234 -0
- package/dist/database/if-newer.d.ts +9 -0
- package/dist/database/if-newer.js +15 -0
- package/dist/database/index.d.ts +17 -0
- package/dist/database/index.js +13 -0
- package/dist/database/kit-database.module.d.ts +47 -0
- package/dist/database/kit-database.module.js +181 -0
- package/dist/database/migrations.d.ts +42 -0
- package/dist/database/migrations.js +142 -0
- package/dist/database/pg.d.ts +20 -0
- package/dist/database/pg.js +19 -0
- package/dist/database/prepare-database.d.ts +39 -0
- package/dist/database/prepare-database.js +75 -0
- package/dist/database/run-exclusive.d.ts +19 -0
- package/dist/database/run-exclusive.js +62 -0
- package/dist/database/tenant.d.ts +34 -0
- package/dist/database/tenant.js +28 -0
- package/dist/http/api/api.decorator.d.ts +18 -0
- package/dist/http/api/api.decorator.js +9 -0
- package/dist/http/api/openapi-document.d.ts +40 -0
- package/dist/http/api/openapi-document.js +452 -0
- package/dist/http/bootstrap-service.d.ts +33 -0
- package/dist/http/bootstrap-service.js +107 -0
- package/dist/http/create-http-app.d.ts +38 -0
- package/dist/http/create-http-app.js +137 -0
- package/dist/http/errors.d.ts +118 -0
- package/dist/http/errors.js +94 -0
- package/dist/http/index.d.ts +12 -0
- package/dist/http/index.js +8 -0
- package/dist/http/kit-exception.filter.d.ts +18 -0
- package/dist/http/kit-exception.filter.js +151 -0
- package/dist/http/kit-http-core.module.d.ts +19 -0
- package/dist/http/kit-http-core.module.js +122 -0
- package/dist/http/pagination.d.ts +30 -0
- package/dist/http/pagination.js +73 -0
- package/dist/http/zod-validation.pipe.d.ts +15 -0
- package/dist/http/zod-validation.pipe.js +47 -0
- package/dist/idempotency/idempotency.interceptor.d.ts +55 -0
- package/dist/idempotency/idempotency.interceptor.js +317 -0
- package/dist/idempotency/idempotent.decorator.d.ts +7 -0
- package/dist/idempotency/idempotent.decorator.js +16 -0
- package/dist/idempotency/index.d.ts +7 -0
- package/dist/idempotency/index.js +6 -0
- package/dist/idempotency/kit-idempotency.module.d.ts +47 -0
- package/dist/idempotency/kit-idempotency.module.js +173 -0
- package/dist/idempotency/peer.d.ts +1 -0
- package/dist/idempotency/peer.js +4 -0
- package/dist/idempotency/table.d.ts +203 -0
- package/dist/idempotency/table.js +86 -0
- package/dist/internal/cleanup-result.d.ts +7 -0
- package/dist/internal/cleanup-result.js +12 -0
- package/dist/internal/ids.d.ts +13 -0
- package/dist/internal/ids.js +41 -0
- package/dist/internal/metadata.d.ts +12 -0
- package/dist/internal/metadata.js +3 -0
- package/dist/internal/outbox-policy.d.ts +8 -0
- package/dist/internal/outbox-policy.js +10 -0
- package/dist/internal/peer.d.ts +16 -0
- package/dist/internal/peer.js +56 -0
- package/dist/internal/pg-errors.d.ts +21 -0
- package/dist/internal/pg-errors.js +115 -0
- package/dist/internal/preparsed-env.d.ts +4 -0
- package/dist/internal/preparsed-env.js +14 -0
- package/dist/internal/rabbitmq-definitions.d.ts +85 -0
- package/dist/internal/rabbitmq-definitions.js +73 -0
- package/dist/internal/redis-acl.d.ts +21 -0
- package/dist/internal/redis-acl.js +30 -0
- package/dist/internal/service-name.d.ts +4 -0
- package/dist/internal/service-name.js +10 -0
- package/dist/internal/sql-identifier.d.ts +5 -0
- package/dist/internal/sql-identifier.js +12 -0
- package/dist/internal/tokens.d.ts +10 -0
- package/dist/internal/tokens.js +12 -0
- package/dist/internal/tx-context.d.ts +5 -0
- package/dist/internal/tx-context.js +9 -0
- package/dist/messaging/cleanup.d.ts +49 -0
- package/dist/messaging/cleanup.js +129 -0
- package/dist/messaging/config.d.ts +43 -0
- package/dist/messaging/config.js +8 -0
- package/dist/messaging/define-event.d.ts +29 -0
- package/dist/messaging/define-event.js +38 -0
- package/dist/messaging/envelope.d.ts +66 -0
- package/dist/messaging/envelope.js +49 -0
- package/dist/messaging/errors.d.ts +45 -0
- package/dist/messaging/errors.js +63 -0
- package/dist/messaging/inbox.d.ts +4 -0
- package/dist/messaging/inbox.js +9 -0
- package/dist/messaging/index.d.ts +13 -0
- package/dist/messaging/index.js +10 -0
- package/dist/messaging/kit-messaging.module.d.ts +39 -0
- package/dist/messaging/kit-messaging.module.js +309 -0
- package/dist/messaging/outbox-relay.d.ts +67 -0
- package/dist/messaging/outbox-relay.js +260 -0
- package/dist/messaging/outbox-store.d.ts +75 -0
- package/dist/messaging/outbox-store.js +126 -0
- package/dist/messaging/peer.d.ts +1 -0
- package/dist/messaging/peer.js +5 -0
- package/dist/messaging/rabbit-connection.d.ts +144 -0
- package/dist/messaging/rabbit-connection.js +472 -0
- package/dist/messaging/redelivery.d.ts +6 -0
- package/dist/messaging/redelivery.js +17 -0
- package/dist/messaging/subscriber.d.ts +102 -0
- package/dist/messaging/subscriber.js +412 -0
- package/dist/messaging/tables.d.ts +315 -0
- package/dist/messaging/tables.js +72 -0
- package/dist/messaging/topology.d.ts +56 -0
- package/dist/messaging/topology.js +56 -0
- package/dist/observability/fail-startup.d.ts +14 -0
- package/dist/observability/fail-startup.js +51 -0
- package/dist/observability/health-registry.d.ts +34 -0
- package/dist/observability/health-registry.js +81 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +10 -0
- package/dist/observability/lifecycle.d.ts +52 -0
- package/dist/observability/lifecycle.js +151 -0
- package/dist/observability/logger-options.d.ts +54 -0
- package/dist/observability/logger-options.js +91 -0
- package/dist/observability/metrics-server.d.ts +33 -0
- package/dist/observability/metrics-server.js +108 -0
- package/dist/observability/metrics.d.ts +30 -0
- package/dist/observability/metrics.js +53 -0
- package/dist/observability/redaction.d.ts +23 -0
- package/dist/observability/redaction.js +65 -0
- package/dist/observability/sanitize-error.d.ts +47 -0
- package/dist/observability/sanitize-error.js +194 -0
- package/dist/observability/sentry.d.ts +64 -0
- package/dist/observability/sentry.js +233 -0
- package/dist/redis/channels.d.ts +34 -0
- package/dist/redis/channels.js +128 -0
- package/dist/redis/client.d.ts +42 -0
- package/dist/redis/client.js +85 -0
- package/dist/redis/index.d.ts +7 -0
- package/dist/redis/index.js +7 -0
- package/dist/redis/kit-redis.module.d.ts +18 -0
- package/dist/redis/kit-redis.module.js +131 -0
- package/dist/redis/peer.d.ts +1 -0
- package/dist/redis/peer.js +4 -0
- package/dist/redis/rate-limiter.d.ts +49 -0
- package/dist/redis/rate-limiter.js +139 -0
- package/dist/testing/count-queries.d.ts +30 -0
- package/dist/testing/count-queries.js +58 -0
- package/dist/testing/create-test-app.d.ts +26 -0
- package/dist/testing/create-test-app.js +32 -0
- package/dist/testing/expect-matches-api.d.ts +8 -0
- package/dist/testing/expect-matches-api.js +49 -0
- package/dist/testing/expect-problem.d.ts +24 -0
- package/dist/testing/expect-problem.js +23 -0
- package/dist/testing/images.d.ts +10 -0
- package/dist/testing/images.js +14 -0
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/launch-command.d.ts +52 -0
- package/dist/testing/launch-command.js +510 -0
- package/dist/testing/outbox-coverage.d.ts +26 -0
- package/dist/testing/outbox-coverage.js +98 -0
- package/dist/testing/rabbitmq-admin.d.ts +81 -0
- package/dist/testing/rabbitmq-admin.js +146 -0
- package/dist/testing/redis-worker.d.ts +10 -0
- package/dist/testing/redis-worker.js +15 -0
- package/dist/testing/repo-conventions.d.ts +10 -0
- package/dist/testing/repo-conventions.js +524 -0
- package/dist/testing/repo-files.d.ts +113 -0
- package/dist/testing/repo-files.js +401 -0
- package/dist/testing/scratch-database.d.ts +19 -0
- package/dist/testing/scratch-database.js +75 -0
- package/dist/testing/start-test-stack.d.ts +85 -0
- package/dist/testing/start-test-stack.js +342 -0
- package/dist/testing/tcp-proxy.d.ts +40 -0
- package/dist/testing/tcp-proxy.js +164 -0
- package/dist/testing/tenant-isolation.d.ts +31 -0
- package/dist/testing/tenant-isolation.js +205 -0
- package/dist/testing/test-events.d.ts +37 -0
- package/dist/testing/test-events.js +150 -0
- package/dist/testing/timeout-scale.d.ts +4 -0
- package/dist/testing/timeout-scale.js +17 -0
- package/dist/testing/timing-guard.d.ts +22 -0
- package/dist/testing/timing-guard.js +54 -0
- package/dist/testing/worker-scope.d.ts +31 -0
- package/dist/testing/worker-scope.js +68 -0
- package/package.json +177 -4
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// Чтение файлов репозитория сервиса для assertRepoConventions (prd.md §3.8, AC-1.8): compose, env_file, CI, Dockerfile,
|
|
2
|
+
// package.json, src/redaction.ts. Разбор compose — эталон O test/support/compose-env.spec.ts (yaml, merge, !reset);
|
|
3
|
+
// образы в тексте CI — эталон BFF test/image-tags.spec.ts (тег по regex, адреса вида postgres:5432 — не образ).
|
|
4
|
+
// Всё синхронно; пути в результатах — относительно root, через `/`.
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, posix, relative, sep } from 'node:path';
|
|
7
|
+
import { parse, Scalar, YAMLMap, YAMLSeq } from 'yaml';
|
|
8
|
+
/** Значение `!reset` Compose: ключ сброшен к умолчанию, значение базового файла не наследуется. */
|
|
9
|
+
export const RESET = Symbol('compose !reset');
|
|
10
|
+
const COMPOSE_FILE = /^(docker-)?compose(\.[\w-]+)*\.ya?ml$/;
|
|
11
|
+
const DOCKERFILE = /^Dockerfile(\.[\w-]+)?$/;
|
|
12
|
+
const WORKFLOW_FILE = /\.ya?ml$/;
|
|
13
|
+
/**
|
|
14
|
+
* Предел раскрытий алиасов YAML на файл compose. Умолчание yaml (100) меньше реальных файлов с x-* якорями и `<<:`
|
|
15
|
+
* (verdeect-orchestrator compose.e2e.yml); предел конечный: файл сверх него — ошибка разбора с именем файла, а не
|
|
16
|
+
* зависание на «бомбе алиасов».
|
|
17
|
+
*/
|
|
18
|
+
const MAX_ALIAS_COUNT = 1000;
|
|
19
|
+
/** Метки тегов Compose в результате разбора yaml (строки: yaml оборачивает их в скаляр). */
|
|
20
|
+
const RESET_MARK = '\u0000kit:reset';
|
|
21
|
+
const OVERRIDE_MARK = '\u0000kit:override';
|
|
22
|
+
// Теги Compose: !reset — сброс значения к умолчанию, !override — замена значения базового файла вместо слияния.
|
|
23
|
+
const CUSTOM_TAGS = [
|
|
24
|
+
{ tag: '!reset', resolve: () => RESET_MARK },
|
|
25
|
+
{ tag: '!reset', collection: 'map', nodeClass: YAMLMap, resolve: () => new Scalar(RESET_MARK) },
|
|
26
|
+
{ tag: '!reset', collection: 'seq', nodeClass: YAMLSeq, resolve: () => new Scalar(RESET_MARK) },
|
|
27
|
+
{ tag: '!override', resolve: (value) => value },
|
|
28
|
+
{
|
|
29
|
+
tag: '!override',
|
|
30
|
+
collection: 'map',
|
|
31
|
+
nodeClass: YAMLMap,
|
|
32
|
+
resolve: (map) => {
|
|
33
|
+
if (map instanceof YAMLMap) {
|
|
34
|
+
map.set(OVERRIDE_MARK, true);
|
|
35
|
+
}
|
|
36
|
+
return map;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
tag: '!override',
|
|
41
|
+
collection: 'seq',
|
|
42
|
+
nodeClass: YAMLSeq,
|
|
43
|
+
resolve: (seq) => {
|
|
44
|
+
if (seq instanceof YAMLSeq) {
|
|
45
|
+
seq.add(OVERRIDE_MARK);
|
|
46
|
+
}
|
|
47
|
+
return seq;
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
function toPosix(path) {
|
|
52
|
+
return path.split(sep).join(posix.sep);
|
|
53
|
+
}
|
|
54
|
+
function rel(root, path) {
|
|
55
|
+
return toPosix(relative(root, path));
|
|
56
|
+
}
|
|
57
|
+
function isRecord(value) {
|
|
58
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
function listFiles(dir, pattern) {
|
|
61
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return readdirSync(dir)
|
|
65
|
+
.filter((name) => pattern.test(name) && statSync(join(dir, name)).isFile())
|
|
66
|
+
.sort()
|
|
67
|
+
.map((name) => join(dir, name));
|
|
68
|
+
}
|
|
69
|
+
function listDirs(dir) {
|
|
70
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
return readdirSync(dir)
|
|
74
|
+
.filter((name) => statSync(join(dir, name)).isDirectory())
|
|
75
|
+
.sort()
|
|
76
|
+
.map((name) => join(dir, name));
|
|
77
|
+
}
|
|
78
|
+
function parseFailure(root, file, error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
return new Error(`assertRepoConventions: не удалось разобрать ${rel(root, file)}: ${message}`, {
|
|
81
|
+
cause: error,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** Интерполяции `${VAR:-default}` и `${VAR-default}` → `default`; `${VAR}` без умолчания не меняется. */
|
|
85
|
+
export function expandDefaults(text) {
|
|
86
|
+
return text.replace(/\$\{[A-Za-z_][A-Za-z0-9_]*:?-([^}]*)\}/g, '$1');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Разбор env-файла: `KEY=VALUE`, комментарии `#` и пустые строки пропускаются, `export ` и кавычки снимаются; у
|
|
90
|
+
* значения без кавычек снимается ` #…` (как в Compose).
|
|
91
|
+
*/
|
|
92
|
+
export function parseEnvFile(text) {
|
|
93
|
+
const env = new Map();
|
|
94
|
+
for (const source of text.split(/\r?\n/)) {
|
|
95
|
+
const line = source.trim().replace(/^export\s+/, '');
|
|
96
|
+
if (line === '' || line.startsWith('#')) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const eq = line.indexOf('=');
|
|
100
|
+
if (eq === -1) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const key = line.slice(0, eq).trim();
|
|
104
|
+
const raw = line.slice(eq + 1).trim();
|
|
105
|
+
// Как Compose: значение в кавычках — до парной кавычки (` #…` после неё — комментарий); у значения без
|
|
106
|
+
// кавычек ` #…` — комментарий.
|
|
107
|
+
const quoted = /^(['"])(.*?)\1(?:\s+#.*)?$/.exec(raw);
|
|
108
|
+
env.set(key, quoted === null ? raw.replace(/\s+#.*$/, '') : (quoted[2] ?? ''));
|
|
109
|
+
}
|
|
110
|
+
return env;
|
|
111
|
+
}
|
|
112
|
+
const DOCKER_HUB_PREFIXES = ['docker.io/library/', 'docker.io/'];
|
|
113
|
+
/**
|
|
114
|
+
* Значение `image:` как ссылка на образ: подстановка `${VAR:-default}`, снятие префикса Docker Hub, отдельно
|
|
115
|
+
* дайджест `@sha256:…`; тег — после последнего `:` за последним `/` (порт реестра — не тег), без тега — `latest`.
|
|
116
|
+
*/
|
|
117
|
+
export function parseImageRef(image) {
|
|
118
|
+
let name = expandDefaults(image).trim();
|
|
119
|
+
const at = name.indexOf('@');
|
|
120
|
+
if (at !== -1) {
|
|
121
|
+
name = name.slice(0, at);
|
|
122
|
+
}
|
|
123
|
+
const prefix = DOCKER_HUB_PREFIXES.find((candidate) => name.startsWith(candidate));
|
|
124
|
+
if (prefix !== undefined) {
|
|
125
|
+
name = name.slice(prefix.length);
|
|
126
|
+
}
|
|
127
|
+
const colon = name.indexOf(':', name.lastIndexOf('/') + 1);
|
|
128
|
+
const repository = colon === -1 ? name : name.slice(0, colon);
|
|
129
|
+
const tag = colon === -1 ? 'latest' : name.slice(colon + 1);
|
|
130
|
+
return {
|
|
131
|
+
repository: repository === '' || repository.includes('$') ? null : repository,
|
|
132
|
+
tag,
|
|
133
|
+
tagUnresolved: tag.includes('$'),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Префикс Docker Hub в свободном тексте (`docker.io/library/`, `docker.io/`) — снимается, как в parseImageRef. */
|
|
137
|
+
const DOCKER_HUB_PREFIX_IN_TEXT = /(?<![\w./@-])docker\.io\/(?:library\/)?/g;
|
|
138
|
+
/**
|
|
139
|
+
* Образы репозитория `repository` в свободном тексте (CI): `<repo>:<тег>`; префикс Docker Hub снимается; тег
|
|
140
|
+
* учитывается, только если в нём есть точка или буква.
|
|
141
|
+
*/
|
|
142
|
+
export function imageTags(text, repository) {
|
|
143
|
+
const escaped = repository.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
144
|
+
const pattern = new RegExp(`(?<![\\w./@-])${escaped}:([A-Za-z0-9][\\w.-]*)`, 'g');
|
|
145
|
+
const normalized = expandDefaults(text).replace(DOCKER_HUB_PREFIX_IN_TEXT, '');
|
|
146
|
+
return [...normalized.matchAll(pattern)]
|
|
147
|
+
.map((match) => match[1] ?? '')
|
|
148
|
+
.filter((tag) => /[.A-Za-z]/.test(tag));
|
|
149
|
+
}
|
|
150
|
+
function scalar(value) {
|
|
151
|
+
if (value === null || value === undefined) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return typeof value === 'string' ? value : String(value);
|
|
155
|
+
}
|
|
156
|
+
function environmentOf(value) {
|
|
157
|
+
const env = new Map();
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
for (const item of value) {
|
|
160
|
+
const text = String(item);
|
|
161
|
+
const eq = text.indexOf('=');
|
|
162
|
+
env.set(eq === -1 ? text : text.slice(0, eq), eq === -1 ? null : text.slice(eq + 1));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (isRecord(value)) {
|
|
166
|
+
for (const [key, item] of Object.entries(value)) {
|
|
167
|
+
env.set(key, scalar(item));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return env;
|
|
171
|
+
}
|
|
172
|
+
function envFilePaths(value) {
|
|
173
|
+
const items = Array.isArray(value)
|
|
174
|
+
? value
|
|
175
|
+
: value === undefined || value === null
|
|
176
|
+
? []
|
|
177
|
+
: [value];
|
|
178
|
+
return items.flatMap((item) => {
|
|
179
|
+
if (typeof item === 'string') {
|
|
180
|
+
return [item];
|
|
181
|
+
}
|
|
182
|
+
if (isRecord(item) && typeof item.path === 'string') {
|
|
183
|
+
return [item.path];
|
|
184
|
+
}
|
|
185
|
+
return [];
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function isReset(value) {
|
|
189
|
+
return value === RESET_MARK;
|
|
190
|
+
}
|
|
191
|
+
function commandField(value) {
|
|
192
|
+
if (isReset(value)) {
|
|
193
|
+
return RESET;
|
|
194
|
+
}
|
|
195
|
+
if (typeof value === 'string') {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
return Array.isArray(value) ? value.map(String) : undefined;
|
|
199
|
+
}
|
|
200
|
+
function scalarField(value) {
|
|
201
|
+
if (isReset(value)) {
|
|
202
|
+
return RESET;
|
|
203
|
+
}
|
|
204
|
+
return scalar(value) ?? undefined;
|
|
205
|
+
}
|
|
206
|
+
/** `build:` — строка (каталог контекста) или `{context, dockerfile, target}`; путь Dockerfile — от каталога compose. */
|
|
207
|
+
function buildField(value, root, composeDir) {
|
|
208
|
+
if (isReset(value)) {
|
|
209
|
+
return RESET;
|
|
210
|
+
}
|
|
211
|
+
const spec = typeof value === 'string' ? { context: value } : isRecord(value) ? value : null;
|
|
212
|
+
if (spec === null) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
const context = typeof spec.context === 'string' ? spec.context : '.';
|
|
216
|
+
const dockerfile = typeof spec.dockerfile === 'string' ? spec.dockerfile : 'Dockerfile';
|
|
217
|
+
return {
|
|
218
|
+
dockerfile: rel(root, join(composeDir, context, dockerfile)),
|
|
219
|
+
target: typeof spec.target === 'string' ? spec.target : null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function withoutMarks(value) {
|
|
223
|
+
if (Array.isArray(value)) {
|
|
224
|
+
return value.filter((item) => item !== OVERRIDE_MARK);
|
|
225
|
+
}
|
|
226
|
+
if (isRecord(value)) {
|
|
227
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => key !== OVERRIDE_MARK));
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function hasOverrideMark(value) {
|
|
232
|
+
return Array.isArray(value)
|
|
233
|
+
? value.includes(OVERRIDE_MARK)
|
|
234
|
+
: isRecord(value) && OVERRIDE_MARK in value;
|
|
235
|
+
}
|
|
236
|
+
function readComposeFile(root, path) {
|
|
237
|
+
let document;
|
|
238
|
+
try {
|
|
239
|
+
document = parse(readFileSync(path, 'utf8'), {
|
|
240
|
+
merge: true,
|
|
241
|
+
customTags: CUSTOM_TAGS,
|
|
242
|
+
maxAliasCount: MAX_ALIAS_COUNT,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
throw parseFailure(root, path, error);
|
|
247
|
+
}
|
|
248
|
+
const services = isRecord(document) && isRecord(document.services) ? document.services : {};
|
|
249
|
+
const file = rel(root, path);
|
|
250
|
+
return Object.entries(services).map(([name, raw]) => {
|
|
251
|
+
const service = isRecord(raw) ? raw : {};
|
|
252
|
+
const env = new Map();
|
|
253
|
+
const envSource = new Map();
|
|
254
|
+
if (!isReset(service.env_file)) {
|
|
255
|
+
for (const envFile of envFilePaths(withoutMarks(service.env_file))) {
|
|
256
|
+
const envPath = join(dirname(path), envFile);
|
|
257
|
+
if (!existsSync(envPath)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
for (const [key, value] of parseEnvFile(readFileSync(envPath, 'utf8'))) {
|
|
261
|
+
env.set(key, value);
|
|
262
|
+
envSource.set(key, rel(root, envPath));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!isReset(service.environment)) {
|
|
267
|
+
for (const [key, value] of environmentOf(withoutMarks(service.environment))) {
|
|
268
|
+
env.set(key, value);
|
|
269
|
+
envSource.set(key, file);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const envMode = isReset(service.environment) || isReset(service.env_file)
|
|
273
|
+
? 'reset'
|
|
274
|
+
: hasOverrideMark(service.environment) || hasOverrideMark(service.env_file)
|
|
275
|
+
? 'override'
|
|
276
|
+
: 'merge';
|
|
277
|
+
return {
|
|
278
|
+
file,
|
|
279
|
+
name,
|
|
280
|
+
image: scalar(service.image),
|
|
281
|
+
env,
|
|
282
|
+
envSource,
|
|
283
|
+
envMode,
|
|
284
|
+
stopGracePeriod: scalarField(service.stop_grace_period),
|
|
285
|
+
command: commandField(withoutMarks(service.command)),
|
|
286
|
+
entrypoint: commandField(withoutMarks(service.entrypoint)),
|
|
287
|
+
build: buildField(service.build, root, dirname(path)),
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** Exec-форма инструкции или null: невалидный JSON (`['node', …]`) Docker выполняет как shell-форму. */
|
|
292
|
+
function execForm(body) {
|
|
293
|
+
if (!body.startsWith('[')) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const parsed = JSON.parse(body);
|
|
298
|
+
return Array.isArray(parsed) ? parsed.map(String) : null;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Стадии Dockerfile по правилам Docker: продолжение строки `\` (с пробелами и табуляцией до перевода строки),
|
|
306
|
+
* последние CMD/ENTRYPOINT стадии перекрывают предыдущие, `FROM <стадия>` наследует её ENTRYPOINT и CMD, ENTRYPOINT
|
|
307
|
+
* сбрасывает унаследованный CMD. CMD внутри HEALTHCHECK — не CMD стадии.
|
|
308
|
+
*/
|
|
309
|
+
function readDockerfile(root, path) {
|
|
310
|
+
const text = readFileSync(path, 'utf8').replace(/\\[ \t]*\r?\n/g, ' ');
|
|
311
|
+
const file = rel(root, path);
|
|
312
|
+
const stages = [];
|
|
313
|
+
for (const line of text.split(/\r?\n/)) {
|
|
314
|
+
const from = /^\s*FROM\s+(?:--\S+\s+)*(\S+)(?:\s+AS\s+(\S+))?/i.exec(line);
|
|
315
|
+
if (from !== null) {
|
|
316
|
+
const image = (from[1] ?? '').toLowerCase();
|
|
317
|
+
const parent = stages.find((stage) => stage.name === image);
|
|
318
|
+
stages.push({
|
|
319
|
+
name: from[2]?.toLowerCase() ?? null,
|
|
320
|
+
entrypoint: parent?.entrypoint ?? null,
|
|
321
|
+
cmd: parent?.cmd ?? null,
|
|
322
|
+
cmdInherited: parent?.cmd != null,
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const stage = stages.at(-1);
|
|
327
|
+
const match = /^\s*(CMD|ENTRYPOINT)\s+(.*)$/i.exec(line);
|
|
328
|
+
if (stage === undefined || match === null) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const body = (match[2] ?? '').trim();
|
|
332
|
+
const instruction = { exec: execForm(body), shell: body };
|
|
333
|
+
if ((match[1] ?? '').toUpperCase() === 'CMD') {
|
|
334
|
+
stage.cmd = instruction;
|
|
335
|
+
stage.cmdInherited = false;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
stage.entrypoint = instruction;
|
|
339
|
+
if (stage.cmdInherited) {
|
|
340
|
+
stage.cmd = null;
|
|
341
|
+
stage.cmdInherited = false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return stages.map((stage, index) => ({
|
|
346
|
+
file,
|
|
347
|
+
name: stage.name,
|
|
348
|
+
last: index === stages.length - 1,
|
|
349
|
+
entrypoint: stage.entrypoint,
|
|
350
|
+
cmd: stage.cmd,
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
function readManifest(root, path) {
|
|
354
|
+
let json;
|
|
355
|
+
try {
|
|
356
|
+
json = JSON.parse(readFileSync(path, 'utf8'));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
throw parseFailure(root, path, error);
|
|
360
|
+
}
|
|
361
|
+
const manifest = isRecord(json) ? json : {};
|
|
362
|
+
const dependencies = new Map();
|
|
363
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
364
|
+
const section = manifest[field];
|
|
365
|
+
if (isRecord(section)) {
|
|
366
|
+
for (const [name, version] of Object.entries(section)) {
|
|
367
|
+
dependencies.set(name, String(version));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const scripts = isRecord(manifest.scripts) ? manifest.scripts : {};
|
|
372
|
+
return {
|
|
373
|
+
file: rel(root, path),
|
|
374
|
+
dependencies,
|
|
375
|
+
start: typeof scripts.start === 'string' ? scripts.start : null,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function readText(root, path) {
|
|
379
|
+
return { file: rel(root, path), text: readFileSync(path, 'utf8') };
|
|
380
|
+
}
|
|
381
|
+
function existing(paths) {
|
|
382
|
+
return paths.filter((path) => existsSync(path) && statSync(path).isFile());
|
|
383
|
+
}
|
|
384
|
+
/** Находит и разбирает файлы репозитория сервиса; отсутствующий каталог — пустой список. */
|
|
385
|
+
export function readRepoFiles(root) {
|
|
386
|
+
const apps = listDirs(join(root, 'apps'));
|
|
387
|
+
const packages = listDirs(join(root, 'packages'));
|
|
388
|
+
const ciFiles = [
|
|
389
|
+
...existing([join(root, '.gitlab-ci.yml')]),
|
|
390
|
+
...listFiles(join(root, '.github', 'workflows'), WORKFLOW_FILE),
|
|
391
|
+
];
|
|
392
|
+
return {
|
|
393
|
+
compose: listFiles(root, COMPOSE_FILE).flatMap((path) => readComposeFile(root, path)),
|
|
394
|
+
ci: ciFiles.map((path) => readText(root, path)),
|
|
395
|
+
docker: [root, ...apps]
|
|
396
|
+
.flatMap((dir) => listFiles(dir, DOCKERFILE))
|
|
397
|
+
.flatMap((path) => readDockerfile(root, path)),
|
|
398
|
+
manifests: existing([root, ...apps, ...packages].map((dir) => join(dir, 'package.json'))).map((path) => readManifest(root, path)),
|
|
399
|
+
redaction: existing([root, ...apps].map((dir) => join(dir, 'src', 'redaction.ts'))).map((path) => readText(root, path)),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Пустая мигрированная БД теста (§3.8). */
|
|
2
|
+
export interface KitScratchDatabase {
|
|
3
|
+
/** Имя БД `<svc>_s<8 hex>`. */
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** Роль приложения `<svc>_app`. */
|
|
6
|
+
readonly url: string;
|
|
7
|
+
/** Роль владельца `<svc>_owner`. */
|
|
8
|
+
readonly ownerUrl: string;
|
|
9
|
+
/** Суперпользователь. */
|
|
10
|
+
readonly adminUrl: string;
|
|
11
|
+
/** Удаляет БД (`WITH (FORCE)`); повторный вызов — без ошибки. */
|
|
12
|
+
readonly drop: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Пустая мигрированная БД из шаблона `startTestStack` за доли секунды (AC-4.18); права ролей сервиса выданы. Тест
|
|
16
|
+
* сам вызывает `drop()` в `afterAll`/`afterEach`.
|
|
17
|
+
* @throws {Error} `startTestStack({postgres: true})` не запускался (нет `kitDatabaseTemplate`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function createScratchDatabase(): Promise<KitScratchDatabase>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// createScratchDatabase (prd.md §3.8, AC-4.18, UC-14): CREATE DATABASE … TEMPLATE <svc>_template; адреса — из inject
|
|
2
|
+
// (startTestStack). pg и vitest — loadPeer внутри функции: /testing импортируется без pg.
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { loadPeer } from '../internal/peer.js';
|
|
5
|
+
import { quoteIdentifier } from '../internal/sql-identifier.js';
|
|
6
|
+
/** Шаблон занят другим копированием (`CREATE DATABASE … TEMPLATE`). */
|
|
7
|
+
const OBJECT_IN_USE = '55006';
|
|
8
|
+
const MAX_ATTEMPTS = 5;
|
|
9
|
+
const RETRY_STEP_MS = 50;
|
|
10
|
+
function withDatabase(base, database) {
|
|
11
|
+
const url = new URL(base);
|
|
12
|
+
url.pathname = `/${database}`;
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
async function withClient(pg, url, work) {
|
|
16
|
+
const client = new pg.Client({ connectionString: url });
|
|
17
|
+
client.on('error', () => undefined);
|
|
18
|
+
await client.connect();
|
|
19
|
+
try {
|
|
20
|
+
await work(client);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await client.end();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function createFromTemplate(pg, url, sql) {
|
|
27
|
+
for (let attempt = 1;; attempt += 1) {
|
|
28
|
+
try {
|
|
29
|
+
await withClient(pg, url, async (client) => {
|
|
30
|
+
await client.query(sql);
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== OBJECT_IN_USE || attempt >= MAX_ATTEMPTS) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_STEP_MS * attempt));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function required(value) {
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
throw new Error('createScratchDatabase: startTestStack({postgres: true}) не запускался');
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Пустая мигрированная БД из шаблона `startTestStack` за доли секунды (AC-4.18); права ролей сервиса выданы. Тест
|
|
50
|
+
* сам вызывает `drop()` в `afterAll`/`afterEach`.
|
|
51
|
+
* @throws {Error} `startTestStack({postgres: true})` не запускался (нет `kitDatabaseTemplate`).
|
|
52
|
+
*/
|
|
53
|
+
export async function createScratchDatabase() {
|
|
54
|
+
const { inject } = await loadPeer('vitest', 'testing');
|
|
55
|
+
const template = required(inject('kitDatabaseTemplate'));
|
|
56
|
+
const service = required(inject('kitDatabaseService'));
|
|
57
|
+
const postgresUrl = required(inject('kitPostgresUrl'));
|
|
58
|
+
const appUrl = required(inject('kitDatabaseUrl'));
|
|
59
|
+
const ownerUrl = required(inject('kitDatabaseOwnerUrl'));
|
|
60
|
+
const adminUrl = required(inject('kitDatabaseAdminUrl'));
|
|
61
|
+
const pg = (await loadPeer('pg', 'testing')).default;
|
|
62
|
+
const { grantDatabaseAccess } = await import('../database/prepare-database.js');
|
|
63
|
+
const name = `${service}_s${randomBytes(4).toString('hex')}`;
|
|
64
|
+
await createFromTemplate(pg, postgresUrl, `CREATE DATABASE ${quoteIdentifier(name)} TEMPLATE ${quoteIdentifier(template)}`);
|
|
65
|
+
await withClient(pg, postgresUrl, (client) => grantDatabaseAccess(client, service, name));
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
url: withDatabase(appUrl, name),
|
|
69
|
+
ownerUrl: withDatabase(ownerUrl, name),
|
|
70
|
+
adminUrl: withDatabase(adminUrl, name),
|
|
71
|
+
drop: () => withClient(pg, postgresUrl, async (client) => {
|
|
72
|
+
await client.query(`DROP DATABASE IF EXISTS ${quoteIdentifier(name)} WITH (FORCE)`);
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { TestProject } from 'vitest/node';
|
|
2
|
+
import type { KitEvent } from '../messaging/define-event.js';
|
|
3
|
+
import { type KitRabbitWorker } from './rabbitmq-admin.js';
|
|
4
|
+
/** Параметры startTestStack в E1 (§3.8). */
|
|
5
|
+
export interface TestStackOptions {
|
|
6
|
+
/** Имя экземпляра сервиса (^[a-z][a-z0-9-]{1,31}$); метка verdeect.kit.service контейнеров. */
|
|
7
|
+
readonly service: string;
|
|
8
|
+
/** PostgreSQL TEST_IMAGES.postgres, суперпользователь — kitPostgresUrl; с E4 — prepareDatabase и шаблон БД. */
|
|
9
|
+
readonly postgres?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Папка миграций drizzle-kit относительно `process.cwd()`: применяется к шаблону под ролью `<svc>_owner`, журнал —
|
|
12
|
+
* `<svc>.__migrations`; без `postgres: true` — `TypeError` при вызове `startTestStack`.
|
|
13
|
+
*/
|
|
14
|
+
readonly migrations?: string;
|
|
15
|
+
/** mailpit — kitSmtpUrl, kitMailpitUrl. */
|
|
16
|
+
readonly mailpit?: boolean;
|
|
17
|
+
/** MinIO — kitS3Endpoint, kitS3AccessKey, kitS3SecretKey. */
|
|
18
|
+
readonly minio?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* RabbitMQ TEST_IMAGES.rabbitmq: definitions.json сервиса (как `verdeect-kit rabbitmq definitions`), vhost и
|
|
21
|
+
* пользователь на воркер; адреса — kitRabbitmqUrl, kitRabbitmqManagementUrl, kitRabbitmqWorkers. Teardown удаляет
|
|
22
|
+
* vhost и пользователей с префиксом `<svc>-test-` (AC-5.18).
|
|
23
|
+
*/
|
|
24
|
+
readonly rabbitmq?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Redis TEST_IMAGES.redis с aclfile (тот же генератор, что `verdeect-kit redis acl`): пользователь сервиса
|
|
27
|
+
* `~<svc>:* &<svc>:*` — kitRedisUrl, администратор теста — kitRedisAdminUrl, имя сервиса — kitRedisService. Пользователей
|
|
28
|
+
* на воркер нет: префикс воркера `<svc>:w<N>:` лежит внутри прав сервиса (AC-8.5).
|
|
29
|
+
*/
|
|
30
|
+
readonly redis?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* События, которые публикует сервис: каталог покрытия прогона (`kitOutboxCoverageDir`); при `KIT_OUTBOX_COVERAGE=1`
|
|
33
|
+
* teardown падает со списком событий без успешного `expectOutbox` (GAP-2, AC-5.13).
|
|
34
|
+
*/
|
|
35
|
+
readonly outboxEvents?: readonly KitEvent[];
|
|
36
|
+
}
|
|
37
|
+
declare module 'vitest' {
|
|
38
|
+
interface ProvidedContext {
|
|
39
|
+
/** postgres://postgres:<пароль>@<host>:<port>/postgres — суперпользователь (флаг postgres). */
|
|
40
|
+
readonly kitPostgresUrl?: string;
|
|
41
|
+
/** Роль `<svc>_app` к БД `<svc>` — значение `DATABASE_URL` тестов (флаг postgres). */
|
|
42
|
+
readonly kitDatabaseUrl?: string;
|
|
43
|
+
/** Роль `<svc>_owner` к БД `<svc>` — `DATABASE_MIGRATOR_URL` (флаг postgres). */
|
|
44
|
+
readonly kitDatabaseOwnerUrl?: string;
|
|
45
|
+
/** Суперпользователь к БД `<svc>` (флаг postgres). */
|
|
46
|
+
readonly kitDatabaseAdminUrl?: string;
|
|
47
|
+
/** Имя сервиса: схема и роли БД (флаг postgres). */
|
|
48
|
+
readonly kitDatabaseService?: string;
|
|
49
|
+
/** Шаблонная БД `<svc>_template` для `createScratchDatabase` (флаг postgres). */
|
|
50
|
+
readonly kitDatabaseTemplate?: string;
|
|
51
|
+
/** smtp://<host>:<port 1025> (флаг mailpit). */
|
|
52
|
+
readonly kitSmtpUrl?: string;
|
|
53
|
+
/** http://<host>:<port 8025> — HTTP API mailpit (флаг mailpit). */
|
|
54
|
+
readonly kitMailpitUrl?: string;
|
|
55
|
+
/** http://<host>:<port 9000> (флаг minio). */
|
|
56
|
+
readonly kitS3Endpoint?: string;
|
|
57
|
+
/** Пользователь MinIO (флаг minio). */
|
|
58
|
+
readonly kitS3AccessKey?: string;
|
|
59
|
+
/** Пароль MinIO, случайный на прогон (флаг minio). */
|
|
60
|
+
readonly kitS3SecretKey?: string;
|
|
61
|
+
/** amqp://<администратор>:<пароль>@<host>:<port> — vhost `/`, полные права (флаг rabbitmq). */
|
|
62
|
+
readonly kitRabbitmqUrl?: string;
|
|
63
|
+
/** http://<host>:<port 15672> — management API без учётных данных (флаг rabbitmq). */
|
|
64
|
+
readonly kitRabbitmqManagementUrl?: string;
|
|
65
|
+
/**
|
|
66
|
+
* vhost воркеров: индекс — `VITEST_POOL_ID − 1`; `url` — пользователь воркера с правами M3 сервиса, `adminUrl` —
|
|
67
|
+
* администратор того же vhost (флаг rabbitmq).
|
|
68
|
+
*/
|
|
69
|
+
readonly kitRabbitmqWorkers?: readonly KitRabbitWorker[];
|
|
70
|
+
/** redis://<svc>:<пароль>@<host>:<port> — пользователь сервиса из aclfile, `REDIS_URL` тестов (флаг redis). */
|
|
71
|
+
readonly kitRedisUrl?: string;
|
|
72
|
+
/** redis://kit-test-admin:<пароль>@<host>:<port> — администратор теста: CLIENT LIST, ACL LOG, очистка (флаг redis). */
|
|
73
|
+
readonly kitRedisAdminUrl?: string;
|
|
74
|
+
/** Имя сервиса — пользователь Redis и префикс `workerScope().redisPrefix` (флаг redis). */
|
|
75
|
+
readonly kitRedisService?: string;
|
|
76
|
+
/** Каталог покрытия outbox прогона: файлы воркеров `w<N>.txt` (флаг outboxEvents). */
|
|
77
|
+
readonly kitOutboxCoverageDir?: string;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Число воркеров прогона: `maxWorkers` проектов (число ≥ 1), иначе `os.availableParallelism()`. */
|
|
81
|
+
export declare function maxWorkersOf(configs: readonly {
|
|
82
|
+
readonly maxWorkers?: unknown;
|
|
83
|
+
}[]): number;
|
|
84
|
+
/** globalSetup Vitest: `export default startTestStack({...})`; teardown останавливает контейнеры (§3.8). */
|
|
85
|
+
export declare function startTestStack(options: TestStackOptions): (project: TestProject) => Promise<() => Promise<void>>;
|