agent-quality-kit 0.10.1 → 0.11.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 (50) hide show
  1. package/README.md +8 -3
  2. package/README.ru.md +8 -3
  3. package/kit/gates/_native.sh +4 -0
  4. package/kit/gates/_skip.sh +7 -0
  5. package/kit/gates/api-contract-has-arbiter/check.sh +7 -0
  6. package/kit/gates/color-from-token/check.sh +7 -0
  7. package/kit/gates/commit-explains-itself/check.sh +8 -2
  8. package/kit/gates/complexity-limit/check.sh +22 -4
  9. package/kit/gates/complexity-limit/gate.yml +2 -2
  10. package/kit/gates/complexity-limit/red/deep.js +15 -0
  11. package/kit/gates/duplicate-code/check.sh +7 -0
  12. package/kit/gates/duplicate-code/gate.yml +5 -1
  13. package/kit/gates/file-size-limit/check.sh +7 -0
  14. package/kit/gates/file-size-limit/red/big.js +600 -0
  15. package/kit/gates/gate-not-weakened/check.sh +7 -0
  16. package/kit/gates/gate-not-weakened/green/suppress.js +4 -0
  17. package/kit/gates/gate-not-weakened/red/suppress.js +5 -0
  18. package/kit/gates/mcp-server-resolves/check.sh +7 -0
  19. package/kit/gates/no-phantom-package/check.sh +7 -0
  20. package/kit/gates/no-print-in-prod/gate.yml +3 -3
  21. package/kit/gates/personal-config-not-shared/check.sh +7 -0
  22. package/kit/gates/secrets-not-in-code/check.sh +7 -0
  23. package/kit/gates/secrets-not-in-code/green/config.js +4 -0
  24. package/kit/gates/secrets-not-in-code/red/leak.js +6 -0
  25. package/kit/gates/swallowed-error/gate.yml +3 -3
  26. package/kit/gates/test-has-assertion/check.sh +7 -0
  27. package/kit/gates/test-has-assertion/green/checkout.test.js +5 -0
  28. package/kit/gates/test-has-assertion/red/checkout.test.js +8 -0
  29. package/kit/gates/test-not-adjusted/check.sh +8 -2
  30. package/kit/gates/todo-without-task/check.sh +7 -0
  31. package/kit/gates/todo-without-task/gate.yml +2 -2
  32. package/kit/gates/todo-without-task/green/app.js +2 -0
  33. package/kit/gates/todo-without-task/red/later.js +4 -0
  34. package/llms.txt +4 -2
  35. package/package.json +1 -1
  36. package/tool/commands/badge.mjs +1 -1
  37. package/tool/commands/doctor.mjs +18 -148
  38. package/tool/commands/probe.mjs +197 -41
  39. package/tool/commands/report.mjs +1 -1
  40. package/tool/i18n/en-gates.mjs +19 -5
  41. package/tool/i18n/en.mjs +3 -0
  42. package/tool/i18n/ru-gates.mjs +20 -6
  43. package/tool/i18n/ru.mjs +3 -0
  44. package/tool/lib/history.mjs +79 -8
  45. package/tool/lib/run.mjs +165 -0
  46. package/tool/selfcheck/gates.sh +32 -4
  47. package/tool/selfcheck/smoke/_fixture.mjs +24 -2
  48. package/tool/selfcheck/smoke/fail-closed.test.mjs +112 -0
  49. package/tool/selfcheck/smoke/own-samples.test.mjs +131 -0
  50. package/tool/selfcheck/units-probe.mjs +282 -22
@@ -2,8 +2,8 @@
2
2
  // «функция работает» от «функция написана».
3
3
  import test from "node:test";
4
4
  import assert from "node:assert/strict";
5
- import { isFix, fixHotspots, probeVerdict } from "../lib/history.mjs";
6
- import { scanningGates, isCode } from "../commands/probe.mjs";
5
+ import { isFix, fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
6
+ import { probeableGates, gatesState, extAlternatives, planProbeGates, isCode } from "../commands/probe.mjs";
7
7
 
8
8
  // Признак починки берётся из ТЕМЫ коммита, а не из тела: тема — единственное, что пишут все,
9
9
  // и единственное, что видно в `git log --oneline`. Три написания, потому что репозитории
@@ -61,33 +61,25 @@ test("пустая история — пустой рейтинг, а не па
61
61
  assert.deepEqual(fixHotspots("", { isCode: () => true }), []);
62
62
  });
63
63
 
64
- // Три состояния, и сливать их нельзя. «Не смогли проверить» — не «прикрыто»: ровно та
65
- // подмена, против которой написан весь комплект.
66
- test("вердикт пробы различает поймано, не поймано и нечем проверить", () => {
67
- assert.equal(probeVerdict([{ code: 1 }, { code: 0 }]), "caught");
68
- assert.equal(probeVerdict([{ code: 0 }, { code: 0 }]), "blind");
69
- assert.equal(probeVerdict([{ code: 2 }, { code: 0 }]), "unknown");
70
- // Поймавший гейт сильнее непроверенного: класс закрыт, даже если рядом чего-то не хватает.
71
- assert.equal(probeVerdict([{ code: 2 }, { code: 1 }]), "caught");
72
- assert.equal(probeVerdict([]), "unknown");
73
- });
74
64
 
75
- // Пробовать можно только те гейты, которым есть куда подставить каталог: рецепт каталога
76
- // кончается каталогом проверки, команда, написанная руками, чем угодно. То же правило, по
77
- // которому `prove` объявляет запись недоказуемой, а не сломанной.
78
- test("пробуются только гейты, кончающиеся каталогом проверки", () => {
65
+ // Пробуется ЛЮБАЯ непустая команда: образец подсаживается в копию проекта, а не в аргумент
66
+ // команды, поэтому форма команды больше ничего не решает. До 2026-09-10 здесь стояло обратное
67
+ // правило «кончается каталогом», и из-за него проба не запускалась у шести чужих
68
+ // репозиториев из семи.
69
+ test("пробуется любая непустая команда, форма больше не решает", () => {
79
70
  const man = { gates: {
80
- ok: "bash gates/x/check.sh .",
81
- slash: "bash gates/y/check.sh ./",
82
- handmade: "eslint . --max-warnings 0",
71
+ dir: "bash gates/x/check.sh .",
72
+ npm: "npm test",
73
+ glob: "eslint lib/**/*.js",
83
74
  empty: "",
75
+ blank: " ",
84
76
  } };
85
- assert.deepEqual(scanningGates(man).map(([n]) => n), ["ok", "slash"]);
77
+ assert.deepEqual(probeableGates(man).map(([n]) => n), ["dir", "npm", "glob"]);
86
78
  });
87
79
 
88
80
  test("манифест без гейтов не роняет разбор", () => {
89
- assert.deepEqual(scanningGates(null), []);
90
- assert.deepEqual(scanningGates({ gates: [] }), []);
81
+ assert.deepEqual(probeableGates(null), []);
82
+ assert.deepEqual(probeableGates({ gates: [] }), []);
91
83
  });
92
84
 
93
85
  // Образцы каталога исключены по той же причине, по какой их исключает каждая сканирующая
@@ -98,3 +90,271 @@ test("горячим считается код, но не документ и н
98
90
  assert.equal(isCode("README.md"), false);
99
91
  assert.equal(isCode("gates/secrets-not-in-code/red/a.py"), false);
100
92
  });
93
+
94
+ // ─────────────────────────────────────────────────────────────────────────────
95
+ // Итог пробы. Написано ДО кода 2026-09-10, после замера на чужеподобном проекте.
96
+ //
97
+ // ЗАЧЕМ. Итог считался одним числом: `blind ? «есть дыры» : «поймано всё»`. Исходов у
98
+ // пробы три (`probeVerdict`: caught · blind · unknown), а ветки две — и `unknown`
99
+ // молча падал в «поймано всё». Замер: проба, где ЕДИНСТВЕННАЯ запись вернула
100
+ // «нечем проверить — инструмент не установлен», напечатала «в пробованных местах
101
+ // каждый применимый класс кем-то ловится». Ошибка запуска выдана за чистоту — ровно
102
+ // тот класс, ради которого написан весь стандарт, в нашей же главной команде.
103
+ test("итог пробы: ничего не запустилось — это НЕ чистота", () => {
104
+ assert.equal(probeSummary({ caught: 0, blind: 0, unknown: 4 }), "nothing-ran");
105
+ });
106
+
107
+ test("итог пробы: поймано и при этом что-то не проверили — это НЕ чистота", () => {
108
+ assert.equal(probeSummary({ caught: 3, blind: 0, unknown: 2 }), "partial");
109
+ });
110
+
111
+ // Исходы перечислены исчерпывающе: у каждого сочетания есть имя, и ни одно не
112
+ // сваливается в соседнее по умолчанию.
113
+ test("итог пробы: остальные сочетания названы каждое своим именем", () => {
114
+ assert.equal(probeSummary({ caught: 3, blind: 0, unknown: 0 }), "clean");
115
+ assert.equal(probeSummary({ caught: 0, blind: 2, unknown: 0 }), "blind");
116
+ assert.equal(probeSummary({ caught: 3, blind: 2, unknown: 1 }), "blind");
117
+ assert.equal(probeSummary({ caught: 0, blind: 0, unknown: 0 }), "nothing-probed");
118
+ });
119
+
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+ // Отчего проба не состоялась. Написано ДО кода 2026-09-10.
122
+ //
123
+ // ЗАЧЕМ. `scanningGates` намеренно берёт только гейты, чья команда кончается каталогом:
124
+ // подставить образец больше некуда. Фильтр верен, сломан ОТВЕТ. При двух объявленных
125
+ // гейтах проба говорила «гейтов не объявлено — нечем пробовать, сначала: aqk add» и
126
+ // отправляла человека заводить то, что у него уже есть.
127
+ //
128
+
129
+ test("гейтов нет вовсе — и это другое состояние", () => {
130
+ assert.deepEqual(gatesState({ gates: {} }), { state: "none", declared: 0, probeable: 0 });
131
+ assert.deepEqual(gatesState(null), { state: "none", declared: 0, probeable: 0 });
132
+ });
133
+
134
+ // Пустая команда не считается объявленным гейтом: объявление без команды ничего не
135
+ // защищает, и его же отклоняет `gates-are-runnable`.
136
+ test("пустая команда гейтом не считается", () => {
137
+ assert.deepEqual(gatesState({ gates: { a: "", b: " " } }), { state: "none", declared: 0, probeable: 0 });
138
+ });
139
+
140
+ test("объявленные гейты пригодны все — состояний осталось два", () => {
141
+ const man = { gates: { scan: "bash gates/x/check.sh .", test: "npm test" } };
142
+ assert.deepEqual(gatesState(man), { state: "ok", declared: 2, probeable: 2 });
143
+ });
144
+
145
+ // Файлы, которым проба НЕ ДЕЛАЛАСЬ ВОВСЕ, тоже обязаны попадать в итог. Написано ДО кода
146
+ // 2026-09-10, после прогона на самом комплекте: из пяти горячих файлов два (`.mjs`) не
147
+ // пробовались никак — красного образца такого расширения в каталоге нет ни одного, — а итог
148
+ // сказал «каждый применимый класс кем-то ловится». Оговорка «в пробованных местах» верна
149
+ // буквально и обманывает по смыслу: она молча сужает утверждение до мест, где проба удалась,
150
+ // и никогда не говорит, сколько мест пропущено. Ровно та тишина, которая читается как чисто.
151
+ test("итог пробы: непробованные файлы не дают права говорить «чисто»", () => {
152
+ assert.equal(probeSummary({ caught: 3, blind: 0, unknown: 0, unprobed: 2 }), "partial");
153
+ assert.equal(probeSummary({ caught: 0, blind: 0, unknown: 0, unprobed: 2 }), "nothing-probed");
154
+ });
155
+
156
+ test("итог пробы: «чисто» — только когда пропущенных нет", () => {
157
+ assert.equal(probeSummary({ caught: 3, blind: 0, unknown: 0, unprobed: 0 }), "clean");
158
+ });
159
+
160
+ // ─────────────────────────────────────────────────────────────────────────────
161
+ // Парный вердикт пробы: гейт прогоняется ДВАЖДЫ — по чистой песочнице и по ней же с
162
+ // подсаженным образцом. Написано ДО кода 2026-09-10.
163
+ //
164
+ // ЗАЧЕМ. Проба умела подставлять образец только гейтам, чья команда кончается каталогом.
165
+ // Замер на семи склонированных чужих репозиториях: у шести из семи (chalk, execa, axios,
166
+ // requests, click, dependency-cruiser) команды — `xo`, `eslint lib/**/*.js`, `mocha --require…`,
167
+ // `pytest`, — и проба не запускалась вовсе. То есть на настоящих проектах она давала ноль
168
+ // сведений.
169
+ //
170
+ // Способ взят не из головы: так работает мутационное тестирование двадцать лет. Stryker
171
+ // копирует проект во временный каталог, симлинкует `node_modules` и гоняет там РОДНУЮ команду
172
+ // тестов; PIT и mutmut делают то же. Прогон по чистой копии («dry run») там обязателен —
173
+ // без него нельзя отличить «поймал подсадку» от «был красным и до неё».
174
+ function pairs(before, after) {
175
+ return { before: before.map(([name, code]) => ({ name, code })),
176
+ after: after.map(([name, code]) => ({ name, code })) };
177
+ }
178
+
179
+ test("парный вердикт: был зелёным, с подсадкой покраснел — поймано", () => {
180
+ const { before, after } = pairs([["lint", 0], ["test", 0]], [["lint", 0], ["test", 1]]);
181
+ assert.equal(probeVerdictPaired(before, after).verdict, "caught");
182
+ });
183
+
184
+ test("парный вердикт: был зелёным и остался — не поймано никем", () => {
185
+ const { before, after } = pairs([["lint", 0], ["test", 0]], [["lint", 0], ["test", 0]]);
186
+ assert.equal(probeVerdictPaired(before, after).verdict, "blind");
187
+ });
188
+
189
+ // Гейт, красный ЕЩЁ ДО подсадки, о подсадке не говорит ничего: его краснота объясняется
190
+ // состоянием проекта. Считать её поимкой значит выдавать чужой долг за свою заслугу.
191
+ test("парный вердикт: гейт был красным до подсадки — судить по нему нельзя", () => {
192
+ const { before, after } = pairs([["lint", 1]], [["lint", 1]]);
193
+ const r = probeVerdictPaired(before, after);
194
+ assert.equal(r.verdict, "unknown");
195
+ assert.equal(r.alreadyRed, 1);
196
+ assert.equal(r.usable, 0);
197
+ });
198
+
199
+ // Сбой ЗАПУСКА (код не 0 и не 1) исключает ОДИН гейт, а не весь вердикт: если сосед
200
+ // отработал и поймал, знание получено.
201
+ test("парный вердикт: сбой одного гейта не отменяет поимку другим", () => {
202
+ const { before, after } = pairs([["broken", 127], ["test", 0]], [["broken", 127], ["test", 1]]);
203
+ const r = probeVerdictPaired(before, after);
204
+ assert.equal(r.verdict, "caught");
205
+ assert.equal(r.failed, 1);
206
+ assert.equal(r.usable, 1);
207
+ });
208
+
209
+ test("парный вердикт: судить не по чему — все гейты либо сломаны, либо уже красные", () => {
210
+ const { before, after } = pairs([["a", 127], ["b", 1]], [["a", 127], ["b", 1]]);
211
+ const r = probeVerdictPaired(before, after);
212
+ assert.equal(r.verdict, "unknown");
213
+ assert.equal(r.failed, 1);
214
+ assert.equal(r.alreadyRed, 1);
215
+ });
216
+
217
+ // Позеленение от подсадки — тоже не поимка, а признак, что гейт смотрит не туда.
218
+ test("парный вердикт: гейт позеленел от подсадки — это не поимка", () => {
219
+ const { before, after } = pairs([["odd", 1]], [["odd", 0]]);
220
+ assert.equal(probeVerdictPaired(before, after).verdict, "unknown");
221
+ });
222
+
223
+ test("парный вердикт: гейтов нет вовсе", () => {
224
+ assert.equal(probeVerdictPaired([], []).verdict, "unknown");
225
+ });
226
+
227
+ // ─────────────────────────────────────────────────────────────────────────────
228
+ // Семьи расширений. Написано ДО кода 2026-09-10.
229
+ //
230
+ // ЗАЧЕМ. Образец подбирается по ТОЧНОМУ совпадению расширения, и это правило верное: питоновский
231
+ // образец в проекте на TypeScript не проверит ничего, а покажет «не прикрыто» — ложная тревога
232
+ // того же класса, что молчащий гейт, только наоборот.
233
+ //
234
+ // Но `.js` и `.mjs` — это один язык и одно содержимое, а не два. Прогон пробы на САМОМ комплекте
235
+ // 2026-09-10: два горячих файла из пяти — `tool/i18n/en.mjs` и `ru.mjs`, — и обоим ответили «в
236
+ // каталоге нет красного образца под .mjs». Комплект целиком написан в этом расширении, то есть
237
+ // проба слепа к собственному коду. Заводить второй набор файлов ради той же строчки кода —
238
+ // дублирование, которое разойдётся через месяц.
239
+ //
240
+ // Семьи узкие намеренно. `.jsx`/`.tsx` сюда не входят: у них своя разметка, и образец без неё
241
+ // проверит не то, ради чего запись существует.
242
+ test("семья расширений: .mjs и .cjs берут образец .js, и наоборот", () => {
243
+ assert.deepEqual(extAlternatives(".mjs"), [".mjs", ".js", ".cjs"]);
244
+ assert.deepEqual(extAlternatives(".js"), [".js", ".mjs", ".cjs"]);
245
+ });
246
+
247
+ test("семья расширений: .ts со своими, и точное совпадение всегда первое", () => {
248
+ assert.equal(extAlternatives(".mts")[0], ".mts");
249
+ assert.ok(extAlternatives(".mts").includes(".ts"));
250
+ assert.ok(!extAlternatives(".ts").includes(".js"));
251
+ });
252
+
253
+ // Расширение вне семьи остаётся один на один с собой: это и есть запрет подсовывать
254
+ // питоновский образец в чужой язык.
255
+ test("семья расширений: одиночка не получает чужих родственников", () => {
256
+ assert.deepEqual(extAlternatives(".py"), [".py"]);
257
+ assert.deepEqual(extAlternatives(".rs"), [".rs"]);
258
+ assert.deepEqual(extAlternatives(".tsx"), [".tsx"]);
259
+ });
260
+
261
+ // ─────────────────────────────────────────────────────────────────────────────
262
+ // План пробы: каким гейтом пробовать, в каком порядке и каким — не пробовать. Написано ДО кода.
263
+ //
264
+ // ЗАЧЕМ. Цена пробы = (файлы × записи) × сумма длительностей ВСЕХ гейтов. На самом комплекте
265
+ // после перехода на песочницу это стало больше десяти минут и упёрлось в таймаут: среди 30
266
+ // гейтов есть `smoke` на 58 секунд, и он гонялся заново на каждую подсадку. Команда, которая
267
+ // идёт четверть часа, не запускается никем — то есть чинить надо здесь, а не «когда-нибудь».
268
+ //
269
+ // Длительности берутся из СУХОГО ПРОГОНА, который и так обязателен, — лишних запусков нет.
270
+ // Порядок: от быстрых к медленным, чтобы ранний выход на первом красном срабатывал чаще.
271
+ // Слишком медленные исключаются, но НЕ молча: их имена обязаны попасть в вывод, иначе
272
+ // «никто не ловит» будет означать «никто из тех, кого мы решили спросить».
273
+ test("план пробы: гейты идут от быстрых к медленным", () => {
274
+ const before = [
275
+ { name: "smoke", code: 0, ms: 58000 },
276
+ { name: "lint", code: 0, ms: 300 },
277
+ { name: "types", code: 0, ms: 4000 },
278
+ ];
279
+ const plan = planProbeGates(before, { slowMs: 20000 });
280
+ assert.deepEqual(plan.use.map((g) => g.name), ["lint", "types"]);
281
+ });
282
+
283
+ test("план пробы: слишком медленный назван поимённо, а не выброшен молча", () => {
284
+ const before = [{ name: "smoke", code: 0, ms: 58000 }, { name: "lint", code: 0, ms: 300 }];
285
+ const plan = planProbeGates(before, { slowMs: 20000 });
286
+ assert.deepEqual(plan.tooSlow.map((g) => g.name), ["smoke"]);
287
+ });
288
+
289
+ // Уже красный и сломанный не попадают в план вообще: судить по ним нельзя, а гонять их на
290
+ // каждую подсадку — платить за то, что всё равно не будет засчитано.
291
+ test("план пробы: уже красные и несработавшие в план не берутся", () => {
292
+ const before = [
293
+ { name: "red", code: 1, ms: 100 },
294
+ { name: "broken", code: 127, ms: 50 },
295
+ { name: "ok", code: 0, ms: 100 },
296
+ ];
297
+ const plan = planProbeGates(before, { slowMs: 20000 });
298
+ assert.deepEqual(plan.use.map((g) => g.name), ["ok"]);
299
+ assert.deepEqual(plan.tooSlow, []);
300
+ });
301
+
302
+ // Все годные оказались медленными — пробовать нечем, и это НЕ «никто не ловит».
303
+ test("план пробы: все годные медленные — пробовать нечем", () => {
304
+ const plan = planProbeGates([{ name: "smoke", code: 0, ms: 58000 }], { slowMs: 20000 });
305
+ assert.deepEqual(plan.use, []);
306
+ assert.deepEqual(plan.tooSlow.map((g) => g.name), ["smoke"]);
307
+ });
308
+
309
+ // ─────────────────────────────────────────────────────────────────────────────
310
+ // Счёт непокрытого: КЛАССЫ, а не проба×файл. Написано ДО кода 2026-09-10.
311
+ //
312
+ // ЗАЧЕМ. Замер на десяти живых репозиториях: у requests, click, flask и httpx проба сказала
313
+ // «непокрытых классов: 18». Различных классов там ШЕСТЬ — они просто повторены по трём горячим
314
+ // файлам. Втрое завышенное число, и завышаем его мы сами, ровно тем приёмом, который ловим у
315
+ // других: считаем события, а называем их сущностями.
316
+ //
317
+ // Списки у всех четырёх проектов совпали побайтово — то есть ответ говорит не про репозиторий,
318
+ // а про связку `ruff + pytest`. Это законный ответ («ваш инструментарий не покрывает вот эти
319
+ // шесть классов»), но продавать его как восемнадцать находок ИМЕННО У ВАС нельзя.
320
+ //
321
+ // Число проб остаётся видно отдельно: «шесть классов на трёх файлах» и «шесть классов на
322
+ // одном» — разные факты, и сливать их тоже нельзя.
323
+ test("счёт непокрытого: шесть классов на трёх файлах — это шесть, а не восемнадцать", () => {
324
+ const recs = [];
325
+ for (const f of ["a.py", "b.py", "c.py"]) {
326
+ for (const e of ["secrets", "dead-code", "print", "swallowed", "todo", "suppress"]) {
327
+ recs.push({ entry: e, file: f, verdict: "blind" });
328
+ }
329
+ }
330
+ const r = countProbe(recs);
331
+ assert.equal(r.blindClasses, 6);
332
+ assert.equal(r.probes, 18);
333
+ });
334
+
335
+ // Класс, слепой ХОТЬ ГДЕ-ТО, — дыра. Пойманный в одном файле и слепой в другом остаётся дырой:
336
+ // «где-то ловится» не защищает то место, где не ловится.
337
+ test("счёт непокрытого: слепой хоть где-то считается непокрытым", () => {
338
+ const r = countProbe([
339
+ { entry: "secrets", file: "a.py", verdict: "caught" },
340
+ { entry: "secrets", file: "b.py", verdict: "blind" },
341
+ { entry: "todo", file: "a.py", verdict: "caught" },
342
+ ]);
343
+ assert.equal(r.blindClasses, 1);
344
+ assert.equal(r.caughtClasses, 2);
345
+ });
346
+
347
+ test("счёт непокрытого: исходы разложены по своим корзинам", () => {
348
+ const r = countProbe([
349
+ { entry: "a", file: "f", verdict: "caught" },
350
+ { entry: "b", file: "f", verdict: "unknown" },
351
+ { entry: "c", file: "f", verdict: "blind" },
352
+ ]);
353
+ assert.deepEqual(
354
+ { b: r.blindClasses, c: r.caughtClasses, u: r.unknownClasses, p: r.probes },
355
+ { b: 1, c: 1, u: 1, p: 3 });
356
+ });
357
+
358
+ test("счёт непокрытого: пусто не роняет", () => {
359
+ assert.deepEqual(countProbe([]), { blindClasses: 0, caughtClasses: 0, unknownClasses: 0, probes: 0 });
360
+ });