agent-quality-kit 0.5.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/README.md +53 -2
- package/README.ru.md +35 -1
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +65 -0
- package/kit/gates/README.md +60 -0
- package/kit/gates/ci-actually-fails/README.md +54 -0
- package/kit/gates/ci-actually-fails/check.sh +116 -0
- package/kit/gates/ci-actually-fails/gate.yml +14 -0
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +30 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
- package/kit/gates/color-from-token/check.sh +13 -1
- package/kit/gates/commit-explains-itself/README.md +13 -3
- package/kit/gates/commit-explains-itself/check.sh +8 -4
- package/kit/gates/complexity-limit/README.md +5 -0
- package/kit/gates/complexity-limit/check.sh +21 -2
- package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
- package/kit/gates/deps-are-pinned/README.md +14 -1
- package/kit/gates/deps-are-pinned/check.sh +6 -1
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
- package/kit/gates/duplicate-code/README.md +11 -2
- package/kit/gates/duplicate-code/check.sh +31 -4
- package/kit/gates/duplicate-code/gate.yml +8 -0
- package/kit/gates/duplicate-code/green/imports_a.go +20 -0
- package/kit/gates/duplicate-code/green/imports_b.go +19 -0
- package/kit/gates/entry-links-exist/README.md +5 -0
- package/kit/gates/entry-links-exist/check.sh +6 -0
- package/kit/gates/entry-links-exist/green/AGENTS.md +3 -0
- package/kit/gates/file-size-limit/README.md +9 -2
- package/kit/gates/file-size-limit/check.sh +13 -1
- package/kit/gates/gate-not-weakened/README.md +54 -0
- package/kit/gates/gate-not-weakened/check.sh +84 -0
- package/kit/gates/gate-not-weakened/gate.yml +15 -0
- package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
- package/kit/gates/gate-not-weakened/green/payments.py +6 -0
- package/kit/gates/gate-not-weakened/green/release.sh +2 -0
- package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
- package/kit/gates/gate-not-weakened/red/payments.py +6 -0
- package/kit/gates/gate-not-weakened/red/release.sh +2 -0
- package/kit/gates/hook-actually-fires/README.md +74 -0
- package/kit/gates/hook-actually-fires/check.sh +183 -0
- package/kit/gates/hook-actually-fires/gate.yml +15 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
- package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
- package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
- package/kit/gates/no-phantom-package/README.md +84 -0
- package/kit/gates/no-phantom-package/check.sh +161 -0
- package/kit/gates/no-phantom-package/gate.yml +20 -0
- package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
- package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
- package/kit/gates/no-print-in-prod/README.md +33 -39
- package/kit/gates/no-print-in-prod/gate.yml +14 -6
- package/kit/gates/personal-config-not-shared/README.md +66 -0
- package/kit/gates/personal-config-not-shared/check.sh +103 -0
- package/kit/gates/personal-config-not-shared/gate.yml +16 -0
- package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
- package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
- package/kit/gates/promise-has-gate/README.md +50 -0
- package/kit/gates/promise-has-gate/check.sh +88 -0
- package/kit/gates/promise-has-gate/gate.yml +14 -0
- package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
- package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
- package/kit/gates/secrets-not-in-code/check.sh +13 -1
- package/kit/gates/swallowed-error/README.md +36 -18
- package/kit/gates/swallowed-error/gate.yml +13 -3
- package/kit/gates/test-has-assertion/README.md +47 -0
- package/kit/gates/test-has-assertion/check.sh +206 -0
- package/kit/gates/test-has-assertion/gate.yml +15 -0
- package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
- package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
- package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
- package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
- package/kit/gates/test-not-adjusted/README.md +79 -0
- package/kit/gates/test-not-adjusted/check.sh +136 -0
- package/kit/gates/test-not-adjusted/gate.yml +19 -0
- package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
- package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
- package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
- package/kit/gates/todo-without-task/README.md +6 -0
- package/kit/gates/todo-without-task/check.sh +13 -1
- package/kit/ratchet/ratchet.sh +70 -2
- package/kit/rules/general.md +23 -0
- package/kit/rules-en/general.md +82 -0
- package/kit/rules-en/security.md +33 -0
- package/kit/rules-en/testing.md +48 -0
- package/llms.txt +2 -1
- package/package.json +4 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/doctor.mjs +90 -10
- package/tool/commands/gates.mjs +19 -5
- package/tool/commands/project.mjs +15 -2
- package/tool/commands/prove.mjs +67 -0
- package/tool/commands/report.mjs +4 -1
- package/tool/i18n/en-docs.mjs +70 -0
- package/tool/i18n/en.mjs +66 -54
- package/tool/i18n/ru-docs.mjs +70 -0
- package/tool/i18n/ru.mjs +66 -54
- package/tool/i18n/templates-en.mjs +9 -9
- package/tool/i18n/templates-ru.mjs +9 -9
- package/tool/lib/core.mjs +7 -1
- package/tool/lib/manifest.mjs +72 -5
- package/tool/lib/prove.mjs +160 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +131 -0
- package/tool/lib/templates.mjs +2 -0
- package/tool/program.mjs +6 -0
- package/tool/selfcheck/gates.sh +86 -3
- package/tool/selfcheck/lifecycle.mjs +29 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +329 -36
- package/tool/selfcheck/units-level.mjs +60 -0
- package/tool/selfcheck/units.mjs +196 -1
- package/kit/gates/no-print-in-prod/check.sh +0 -38
- package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
- package/kit/gates/no-print-in-prod/green/main.go +0 -8
- package/kit/gates/no-print-in-prod/green/main.rs +0 -4
- package/kit/gates/no-print-in-prod/red/main.go +0 -8
- package/kit/gates/no-print-in-prod/red/main.rs +0 -4
- package/kit/gates/swallowed-error/check.sh +0 -54
- package/kit/gates/swallowed-error/green/run.js +0 -8
- package/kit/gates/swallowed-error/red/run.js +0 -3
- /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
- /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Обещание в точке входа подкреплено командой с кодом возврата — или честно помечено как
|
|
3
|
+
# исполняемое человеком.
|
|
4
|
+
#
|
|
5
|
+
# ЗАЧЕМ. Это центральный вопрос всего комплекта. `AGENTS.md` — обычный текст: «никогда не
|
|
6
|
+
# коммитим секреты» и «мы очень стараемся» выглядят одинаково и стоят одинаково, пока никто не
|
|
7
|
+
# спросил, чем первое отличается от второго. Осенью 2026 появился целый класс линтеров обвеса
|
|
8
|
+
# (agnix — 455 правил, agents-lint), и все они проверяют ДОКУМЕНТ: формат, живые ли ссылки,
|
|
9
|
+
# существуют ли скрипты. Ни один не спрашивает, ИСПОЛНИМО ли обещание.
|
|
10
|
+
#
|
|
11
|
+
# ПОЧЕМУ ЯВНАЯ ПОМЕТКА, А НЕ УГАДЫВАНИЕ. Сопоставлять обещание с гейтом по совпадению слов —
|
|
12
|
+
# значит выдавать вердикт по догадке. Пометка ставится один раз и делает документ честнее:
|
|
13
|
+
# у каждого правила видно, кто его сторожит — машина или человек.
|
|
14
|
+
#
|
|
15
|
+
# - **Секреты не в коде.** <!-- aqk: secrets-not-in-code -->
|
|
16
|
+
# - **План до кода.** <!-- aqk: человек -->
|
|
17
|
+
DIR="${1:-.}"
|
|
18
|
+
MAN="$DIR/.aqk.yml"
|
|
19
|
+
[ -f "$MAN" ] || { echo "нет .aqk.yml — проверять нечего"; exit 0; }
|
|
20
|
+
MANTEXT="$(tr -d '\r' < "$MAN")"
|
|
21
|
+
|
|
22
|
+
ENTRIES=$(printf '%s\n' "$MANTEXT" | awk '
|
|
23
|
+
/^entry:/ { if ($0 ~ /\[/) { s=$0; sub(/^entry:[[:space:]]*\[/,"",s); sub(/\].*$/,"",s);
|
|
24
|
+
gsub(/[[:space:]]/,"",s); n=split(s,a,","); for(i=1;i<=n;i++) print a[i]; next }
|
|
25
|
+
g=1; next }
|
|
26
|
+
/^[A-Za-z]/ { g=0 }
|
|
27
|
+
g && /^[[:space:]]*-[[:space:]]/ { s=$0; sub(/^[[:space:]]*-[[:space:]]*/,"",s);
|
|
28
|
+
gsub(/^["\x27]|["\x27]$/,"",s); print s }')
|
|
29
|
+
[ -z "$ENTRIES" ] && { echo "точка входа не объявлена — проверять нечего"; exit 0; }
|
|
30
|
+
|
|
31
|
+
# Объявленные гейты: их имена и есть словарь допустимых пометок.
|
|
32
|
+
GATES=$(printf '%s\n' "$MANTEXT" | awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0}
|
|
33
|
+
g && /^[[:space:]]+[A-Za-z0-9_-]+:/ { sub(/^[[:space:]]*/,""); sub(/:.*$/,""); print }')
|
|
34
|
+
|
|
35
|
+
OUT=""
|
|
36
|
+
for E in $ENTRIES; do
|
|
37
|
+
F="$DIR/$E"
|
|
38
|
+
[ -f "$F" ] || continue
|
|
39
|
+
R=$(tr -d '\r' < "$F" | awk -v file="$E" -v gates="$GATES" '
|
|
40
|
+
# Внутри блока кода правил не бывает — там примеры.
|
|
41
|
+
/^[[:space:]]*```/ { code = !code; next }
|
|
42
|
+
code { next }
|
|
43
|
+
{
|
|
44
|
+
# Обещание — пункт списка, который либо выделен жирным (обычная форма правила), либо
|
|
45
|
+
# содержит слово долженствования или запрета. Пункт-перечисление файлов обещанием не
|
|
46
|
+
# считается: он ничего не утверждает о поведении.
|
|
47
|
+
# Только маркированный пункт. Нумерованный список — это шаги или пояснения, а не свод
|
|
48
|
+
# правил: «три вещи, которые надо понимать» в нашем же шаблоне выглядели обещаниями и
|
|
49
|
+
# требовали сторожа для утверждения о том, как устроен инструмент.
|
|
50
|
+
if ($0 !~ /^[[:space:]]*[-*][[:space:]]+/) next
|
|
51
|
+
body = $0; sub(/^[[:space:]]*[-*][[:space:]]+/, "", body)
|
|
52
|
+
isRule = (body ~ /^\*\*/) ||
|
|
53
|
+
(body ~ /никогда|всегда|обязан|обязательн|запрещ|нельзя|не должн|каждый|каждая|каждое/) ||
|
|
54
|
+
(body ~ /(^|[^a-z])(must|never|always|required|forbidden|shall)([^a-z]|$)/)
|
|
55
|
+
# Пункт-команда — не обещание. «обязательный минимум проекта прогоном: `aqk doctor`»
|
|
56
|
+
# содержит слово долженствования, но ничего не обещает: это строка справочника.
|
|
57
|
+
if (body ~ /`(node|bash|sh|npx|npm|aqk|make|python|go|cargo) /) next
|
|
58
|
+
if (!isRule) next
|
|
59
|
+
|
|
60
|
+
# Пометка: <!-- aqk: имя-гейта --> или <!-- aqk: человек -->
|
|
61
|
+
# Берём всё до закрывающей скобки комментария и уже потом обрезаем. Класс символов
|
|
62
|
+
# с исключением «-» съедал имя гейта на первом же дефисе: «deps-are-pinned» читалось
|
|
63
|
+
# как «deps», и верная пометка объявлялась ведущей в никуда.
|
|
64
|
+
if (match($0, /<!--[[:space:]]*aqk:[^>]*-->/)) {
|
|
65
|
+
tag = substr($0, RSTART, RLENGTH)
|
|
66
|
+
sub(/^<!--[[:space:]]*aqk:[[:space:]]*/, "", tag)
|
|
67
|
+
sub(/[[:space:]]*-->$/, "", tag)
|
|
68
|
+
sub(/[[:space:]]+$/, "", tag)
|
|
69
|
+
if (tag == "человек" || tag == "human") next
|
|
70
|
+
n = split(gates, G, "\n")
|
|
71
|
+
for (i = 1; i <= n; i++) if (G[i] != "" && G[i] == tag) next
|
|
72
|
+
printf "%s:%d: пометка ведёт в никуда — гейта «%s» нет в манифесте\n", file, FNR, tag
|
|
73
|
+
next
|
|
74
|
+
}
|
|
75
|
+
t = body; sub(/[[:space:]]+$/, "", t)
|
|
76
|
+
printf "%s:%d: обещание ничем не подкреплено: %s\n", file, FNR, substr(t, 1, 90)
|
|
77
|
+
}' 2>/dev/null)
|
|
78
|
+
[ -z "$R" ] || OUT="$OUT$R
|
|
79
|
+
"
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
LEFT="$(printf '%s' "$OUT" | grep -v '^$')"
|
|
83
|
+
[ -z "$LEFT" ] && exit 0
|
|
84
|
+
printf '%s\n' "$LEFT"
|
|
85
|
+
echo " почини: у каждого правила должен быть виден сторож."
|
|
86
|
+
echo " машина: <!-- aqk: имя-гейта --> человек: <!-- aqk: человек -->"
|
|
87
|
+
echo " правило, за которым не следит никто, через месяц отличается от лозунга только длиной."
|
|
88
|
+
exit 1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
intent: у каждого правила в точке входа виден сторож — команда или названный человек
|
|
2
|
+
intent_en: every rule in the entry point names its enforcer — a command or, honestly, a human
|
|
3
|
+
|
|
4
|
+
# Там, где уже есть манифест и объявленные гейты: без них сопоставлять нечего.
|
|
5
|
+
trigger:
|
|
6
|
+
has_gates: true
|
|
7
|
+
|
|
8
|
+
recipes:
|
|
9
|
+
any: bash {gate}/check.sh {dir}
|
|
10
|
+
|
|
11
|
+
proof: incidents/README.md, 2026-09-06 «тринадцать собственных правил без сторожа» — прогон по
|
|
12
|
+
своему же AGENTS.md показал, что ни одно из тринадцати железных правил не называет, кто за
|
|
13
|
+
ним следит; после разметки одиннадцать оказались исполняемыми человеком, и это стало видно
|
|
14
|
+
впервые
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Как мы работаем
|
|
2
|
+
|
|
3
|
+
## Правила
|
|
4
|
+
|
|
5
|
+
- **Секреты никогда не попадают в код.** Ключи живут в окружении. <!-- aqk: secrets-not-in-code -->
|
|
6
|
+
- **Каждая функция короче пятидесяти строк.** <!-- aqk: человек -->
|
|
7
|
+
- **Обзор кода делает второй человек.** Автор не вливает сам. <!-- aqk: человек -->
|
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
# Секрет, попавший в историю, из неё уже не убрать: ветку перепишут, а копии останутся у всех,
|
|
3
3
|
# кто её тянул. Поэтому проверка стоит до коммита, а не «иногда руками».
|
|
4
4
|
DIR="${1:-.}"
|
|
5
|
-
|
|
5
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
6
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
7
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
8
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
9
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
10
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
11
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
12
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
13
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
14
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
15
|
+
exit 2
|
|
16
|
+
fi
|
|
17
|
+
. "$SKIP_LIB"
|
|
6
18
|
# Красный образец — намеренно сломанный код в репозитории. Сканирующий гейт обязан его
|
|
7
19
|
# пропускать, иначе будет вечно краснеть на том, что сам же и положил. Исключение снимается,
|
|
8
20
|
# когда проверяют сам образец: тогда каталог red и есть цель проверки.
|
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
# Ошибка не глушится молча
|
|
2
2
|
|
|
3
|
-
**Намерение.**
|
|
3
|
+
**Намерение.** Перехваченная и выброшенная ошибка — это отказ, о котором никто не узнал.
|
|
4
|
+
Система продолжает работать «как будто всё хорошо», а причина всплывает через недели и в другом
|
|
5
|
+
месте. Ошибка либо обработана и записана, либо проброшена дальше.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Запись в журнале: `incidents/README.md`, 2026-08-25.
|
|
7
|
+
**Своей проверки здесь больше нет — так решил замер.** Переносимая версия искала перехват по
|
|
8
|
+
тексту. Прогон по пяти чужим репозиториям (`httpx`, `fastapi`, `zod`, `cobra`, `ripgrep`) дал
|
|
9
|
+
46 находок, и настоящим оказался ровно один класс — пустые `catch (_) {}` в `zod`. Остальное:
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
```python
|
|
12
|
+
try:
|
|
13
|
+
if sniffio.current_async_library() == "trio":
|
|
14
|
+
return True
|
|
15
|
+
except ImportError: # библиотеки нет — значит не trio. Это не глушение, это ветвление.
|
|
16
|
+
pass
|
|
17
|
+
```
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
```rust
|
|
20
|
+
let _ = self.wtr.write(b"\n")?; // «?» пробрасывает ошибку. Отброшено только число байт.
|
|
21
|
+
```
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
Питоновский `except <узкое исключение>: pass` — законный приём: ловят не ошибку, а её отсутствие.
|
|
24
|
+
Растовый `let _ = …?` вообще ничего не глушит. Отличить это от настоящего глушения можно только
|
|
25
|
+
разбором кода — а разбор уже написан.
|
|
18
26
|
|
|
19
|
-
**Готовый аналог
|
|
20
|
-
покрывают разные оттенки: `BLE` — ловля голого исключения, `TRY400` — запись в лог без трейса,
|
|
21
|
-
`SIM105` — перехват ради тишины. В TypeScript — `no-empty` с запретом пустого перехвата.
|
|
22
|
-
Измерено на живом проекте: `TRY400` нашёл 47 мест, которые самописный разбор искал час.
|
|
23
|
-
Рецепты под эти стеки берут готовое; своя проверка — запасная.
|
|
27
|
+
**Готовый аналог — это и есть он сам.**
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
| Стек | Чем |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Python | `ruff --select BLE,TRY400,SIM105` |
|
|
32
|
+
| JavaScript, TypeScript | `eslint no-empty` с `allowEmptyCatch: false` |
|
|
33
|
+
| Go | [`errcheck`](https://github.com/kisielk/errcheck) — канонический инструмент под этот предмет |
|
|
34
|
+
|
|
35
|
+
**Чего НЕ ловит.** Перехват, который что-то делает, но делает не то: `except: return None`,
|
|
36
|
+
`catch { return [] }`. Формально обработка есть, по сути ошибка потеряна так же. Это видно
|
|
37
|
+
только глазами и на ревью. Для Rust рецепта нет: у `clippy` есть близкие правила
|
|
38
|
+
(`let_underscore_must_use`), но мы их не замеряли, а рецепт без замера — та же догадка,
|
|
39
|
+
из-за которой эта запись и лишилась своей проверки.
|
|
40
|
+
|
|
41
|
+
**Чем это оплачено.** Проект на Rust, Java, Ruby, PHP по этому пункту не получает ничего.
|
|
42
|
+
|
|
43
|
+
**Образцы.** `red/loader.py` — `except Exception: pass`. `green/loader.py` — узкое исключение,
|
|
44
|
+
запись в журнал и проброс. Проверяются рецептом `python` (`samples_for`).
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
intent: ошибка не глушится молча — она обработана и записана либо проброшена
|
|
2
2
|
intent_en: an error is not silently swallowed — it is handled and logged, or re-raised
|
|
3
3
|
|
|
4
|
+
# Только языки, под которые есть готовый разбор кода. Переносимой проверки здесь больше нет:
|
|
5
|
+
# отличить «перехват ради тишины» от законного `except ImportError: pass` можно разбором,
|
|
6
|
+
# а не поиском по тексту.
|
|
4
7
|
trigger:
|
|
5
|
-
|
|
8
|
+
langs: python, javascript, typescript, go
|
|
6
9
|
|
|
7
10
|
recipes:
|
|
8
|
-
any: bash {gate}/check.sh {dir}
|
|
9
11
|
# BLE — ловля голого исключения, TRY400 — запись без трейса, SIM105 — перехват ради тишины.
|
|
10
12
|
python: ruff check --select BLE,TRY400,SIM105 {dir}
|
|
11
13
|
typescript: eslint --rule '{"no-empty":["error",{"allowEmptyCatch":false}]}' {dir}
|
|
14
|
+
javascript: eslint --rule '{"no-empty":["error",{"allowEmptyCatch":false}]}' {dir}
|
|
15
|
+
# errcheck — канонический инструмент Go под этот же предмет: ошибка присвоена и не проверена.
|
|
16
|
+
go: errcheck -blank {dir}/...
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
samples_for: python
|
|
19
|
+
|
|
20
|
+
proof: incidents/README.md, 2026-09-07 «замер по пяти стекам вынес приговор пяти записям» —
|
|
21
|
+
переносимая версия дала 46 находок на пяти чужих репозиториях, настоящими оказались только
|
|
22
|
+
пустые `catch {}` в zod; весь питоновский класс `except ImportError: pass` и растовый
|
|
23
|
+
`let _ = write(..)?` были ложными
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Тест умеет провалиться
|
|
2
|
+
|
|
3
|
+
**Намерение.** «Почини тесты» агент решает буквально: тело теста пустеет, утверждение
|
|
4
|
+
заменяется тавтологией, тест помечается пропуском. Все три способа дают ровно то, что просили —
|
|
5
|
+
зелёный прогон, — и ни один не виден в сводке. Покрытие при этом растёт.
|
|
6
|
+
|
|
7
|
+
**Какой отказ это поймало.** Замер по девятнадцати чужим репозиториям (15 инструментов и 4
|
|
8
|
+
прикладных, около 25 000 файлов): 8 находок. Шесть — выключенные тесты без единого слова
|
|
9
|
+
о причине в `swr`, библиотеке с тридцатью тысячами звёзд; два — в `kodus-ai`. Запись в журнале:
|
|
10
|
+
`incidents/README.md`, 2026-09-06.
|
|
11
|
+
|
|
12
|
+
**Что именно проверяется.** Три случая, в которых тест не может провалиться ни при каких данных:
|
|
13
|
+
|
|
14
|
+
| Красное | Почему |
|
|
15
|
+
|---|---|
|
|
16
|
+
| тело теста пустое (`pass`, `...`, только докстрока, пустые фигурные скобки) | выполнять нечего |
|
|
17
|
+
| утверждение-тавтология (`assert True`, `expect(true).toBe(true)`, `assert 1 == 1`) | истинно всегда |
|
|
18
|
+
| пропуск без причины (`@pytest.mark.skip`, `it.skip(…)`, `xit(…)`, `@Ignore`) | тест выключен, и неизвестно, до каких пор |
|
|
19
|
+
|
|
20
|
+
Причина у пропуска засчитывается в трёх формах: `reason=` в той же строке, `reason=` в
|
|
21
|
+
продолжении многострочного декоратора и объясняющий комментарий над тестом. Это не придирка к
|
|
22
|
+
форме: комментарий сверху — самая частая запись причины, и на замере все находки такого вида
|
|
23
|
+
оказались законными.
|
|
24
|
+
|
|
25
|
+
**Готовый аналог.** Есть, и частями:
|
|
26
|
+
[`flake8-pytest-style`](https://github.com/m-burst/flake8-pytest-style) и
|
|
27
|
+
[`pylint`](https://pylint.readthedocs.io/) ловят часть питоновских случаев;
|
|
28
|
+
[`eslint-plugin-jest`](https://github.com/jest-community/eslint-plugin-jest) — правила
|
|
29
|
+
`expect-expect`, `no-disabled-tests`, `valid-expect` — заметно точнее нас на JavaScript, потому
|
|
30
|
+
что разбирает синтаксис. Если проект на одном языке — ставь их. Эта запись нужна там, где
|
|
31
|
+
языков несколько или где ставить нечего.
|
|
32
|
+
|
|
33
|
+
**Чего НЕ ловит.**
|
|
34
|
+
|
|
35
|
+
- **Тест без утверждения, но с телом.** Вызвали код, проверили, что не упал, — законный стиль:
|
|
36
|
+
утверждение там в самом отсутствии исключения. Первая версия красила такие тесты и дала
|
|
37
|
+
186 находок в двух зрелых проектах — все до одной ложные. Правило снято намеренно.
|
|
38
|
+
- **Тест, утверждающий не то.** `assert response.status_code` вместо `== 200` — проверка
|
|
39
|
+
отвечает на вопрос «может ли тест провалиться», а не «то ли он проверяет».
|
|
40
|
+
- **Языки, кроме python, javascript и typescript, — только частично.** Пустое тело в Go или Ruby
|
|
41
|
+
не разбирается; тавтологии и пропуски ловятся во всех перечисленных расширениях.
|
|
42
|
+
- **Голый `t.Skip()` в Go.** На замере 9 находок из 13 оказались условным пропуском по
|
|
43
|
+
переменной окружения — это управление прогоном, а не выключенный тест. Правило снято.
|
|
44
|
+
- **Скобки внутри строковых литералов считаются наравне с настоящими.** Границы тела теста
|
|
45
|
+
определяются балансом скобок; точный ответ требует разбора языка, а его у переносимой
|
|
46
|
+
проверки нет. Строки внутри незакрытых шаблонных литералов пропускаются целиком — иначе
|
|
47
|
+
тесты, лежащие в фикстурах как текст, читались бы как настоящие.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Тест, который не может провалиться: прогон зелёный, покрытие выросло, проверено ничего.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ ИМЕННО ЭТО. «Почини тесты» агент решает буквально: тело теста пустеет, утверждение
|
|
5
|
+
# заменяется тавтологией, тест помечается пропуском. Все три способа дают ровно то, что просили
|
|
6
|
+
# — зелёный прогон, — и ни один не виден в сводке. Это тот же класс, что молчащий гейт: сигнал
|
|
7
|
+
# исчез, а уверенность осталась.
|
|
8
|
+
#
|
|
9
|
+
# ГРАНИЦА НАЙДЕНА ЗАМЕРОМ, А НЕ ВЫДУМАНА. Первая версия красила «тест без утверждения» и дала
|
|
10
|
+
# 186 находок в двух зрелых проектах — все до одной оказались тестами вида «вызвали и проверили,
|
|
11
|
+
# что не упало». Это законный и распространённый стиль: утверждение там в самом отсутствии
|
|
12
|
+
# исключения. Гейт, спорящий о стиле, выключают целиком, поэтому здесь остались только
|
|
13
|
+
# безусловные случаи — те, где тест не может провалиться ни при каких данных.
|
|
14
|
+
DIR="${1:-.}"
|
|
15
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
16
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
17
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
18
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
19
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
20
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
21
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
22
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
23
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
24
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
. "$SKIP_LIB"
|
|
28
|
+
|
|
29
|
+
# Только файлы тестов. Слово «assert» в обычном коде — не тест, а проверка входа.
|
|
30
|
+
FILES=$(find "$DIR" $(skip_find) -type f \( \
|
|
31
|
+
-name 'test_*.py' -o -name '*_test.py' -o -name 'tests.py' \
|
|
32
|
+
-o -name '*.test.js' -o -name '*.test.jsx' -o -name '*.test.ts' -o -name '*.test.tsx' \
|
|
33
|
+
-o -name '*.test.mjs' -o -name '*.spec.js' -o -name '*.spec.jsx' -o -name '*.spec.ts' \
|
|
34
|
+
-o -name '*.spec.tsx' -o -name '*.spec.mjs' \
|
|
35
|
+
-o -name '*_test.go' -o -name '*_test.rb' -o -name '*_spec.rb' \) -print 2>/dev/null)
|
|
36
|
+
[ -z "$FILES" ] && { echo "файлов тестов не нашлось — эта проверка не про тебя"; exit 0; }
|
|
37
|
+
|
|
38
|
+
# Сгенерированный файл правят не руками: тест в нём написал инструмент. Признак проверяется
|
|
39
|
+
# внутри awk по первым строкам, а не циклом с `head` на файл.
|
|
40
|
+
#
|
|
41
|
+
# ПОЧЕМУ БЕЗ ЦИКЛА. Накопление списка строкой («KEEP="$KEEP $F"») — квадрат по длине: оболочка
|
|
42
|
+
# копирует растущую строку на каждой итерации. На соседней проверке это дало 44 секунды вместо
|
|
43
|
+
# одной на том же репозитории.
|
|
44
|
+
|
|
45
|
+
# Тройная одинарная кавычка нужна разбору докстрок, но написать её внутри awk-программы нельзя:
|
|
46
|
+
# она закроет кавычки самой программы. Передаём переменной.
|
|
47
|
+
Q3="'''"
|
|
48
|
+
|
|
49
|
+
# ОДИН вызов awk на все файлы, а не по вызову на файл: запуск процесса стоит дороже разбора.
|
|
50
|
+
# На репозитории в 7128 файлов проверка занимала 49 секунд, и почти всё это были запуски.
|
|
51
|
+
# shellcheck disable=SC2086
|
|
52
|
+
OUT=$(awk -v q3="$Q3" '
|
|
53
|
+
# --- что считается безусловно сломанным ------------------------------------
|
|
54
|
+
function tautology(l) {
|
|
55
|
+
return l ~ /assert[[:space:]]+(True|true|1)[[:space:]]*($|#)/ ||
|
|
56
|
+
l ~ /assert(True|Equal)\((True|true|1)([[:space:]]*,[[:space:]]*(True|true|1))?\)/ ||
|
|
57
|
+
l ~ /expect\((true|1)\)\.(toBe|toEqual)\((true|1)\)/ ||
|
|
58
|
+
l ~ /assert[[:space:]]+[0-9]+[[:space:]]*==[[:space:]]*[0-9]+/
|
|
59
|
+
}
|
|
60
|
+
# Пропуск без причины. С причиной — законный приём: тест ждёт починки, и это записано.
|
|
61
|
+
# Голый `t.Skip()` из Go сюда НЕ входит: на замере 9 находок из 13 оказались условным
|
|
62
|
+
# пропуском по переменной окружения — это управление прогоном, а не выключенный тест.
|
|
63
|
+
function bareSkip(l) {
|
|
64
|
+
return (l ~ /@(pytest\.mark\.)?skip([[:space:]]*$|\()/ && !reasonNear(l)) ||
|
|
65
|
+
(l ~ /(^|[^a-zA-Z])(x?it|x?describe|test)\.skip\(/ && l !~ /--|TODO|[Ii]ssue|#[0-9]/) ||
|
|
66
|
+
(l ~ /(^|[^a-zA-Z])(xit|xdescribe)\(/) ||
|
|
67
|
+
(l ~ /@Ignore([[:space:]]*$|\(\))/)
|
|
68
|
+
}
|
|
69
|
+
# Строка, которая ничего не делает: заглушка, докстрока, комментарий, хвост из скобок.
|
|
70
|
+
# Сравнением строк, а не регулярным выражением: тройная одинарная кавычка внутри
|
|
71
|
+
# awk-программы закрыла бы кавычки оболочки.
|
|
72
|
+
function isNoop(b, t) {
|
|
73
|
+
t = b; sub(/^[[:space:]]+/, "", t); sub(/[[:space:]]+$/, "", t)
|
|
74
|
+
if (t == "" || t == "pass" || t == "..." || t == "return") return 1
|
|
75
|
+
if (substr(t, 1, 1) == "#") return 1
|
|
76
|
+
if (substr(t, 1, 3) == "\"\"\"" || substr(t, 1, 3) == q3) return 1
|
|
77
|
+
if (substr(t, 1, 2) == "//" || substr(t, 1, 2) == "/*" || substr(t, 1, 1) == "*") return 1
|
|
78
|
+
if (t ~ /^[])};,[:space:]]+$/) return 1
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
function trim(x) { sub(/^[[:space:]]+/, "", x); return substr(x, 1, 80) }
|
|
82
|
+
# Причина, записанная комментарием НАД пропуском, — нормальная форма, и она встречается чаще
|
|
83
|
+
# инлайновой: на замере все 4 оставшиеся находки оказались именно такими, с подробным
|
|
84
|
+
# объяснением в трёх строках выше. Требование писать причину в той же строке — придирка к
|
|
85
|
+
# форме, а придирки к форме и есть то, из-за чего гейты выключают.
|
|
86
|
+
# Причина у пропуска может стоять на следующей строке: многострочный декоратор
|
|
87
|
+
# «@pytest.mark.skip(\n reason="…"\n)» — обычная форма, когда причина длинная.
|
|
88
|
+
function reasonNear(l, k) {
|
|
89
|
+
if (l ~ /reason[[:space:]]*=/) return 1
|
|
90
|
+
if (l !~ /\($/ && l !~ /\([[:space:]]*$/) return 0
|
|
91
|
+
for (k = cursor + 1; k <= n && k <= cursor + 4; k++) {
|
|
92
|
+
if (line[k] ~ /reason[[:space:]]*=/) return 1
|
|
93
|
+
if (line[k] ~ /^[[:space:]]*\)/) return 0
|
|
94
|
+
}
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
function explainedAbove(i, k, t, w) {
|
|
98
|
+
for (k = i - 1; k >= 1 && k >= i - 4; k--) {
|
|
99
|
+
t = line[k]; sub(/^[[:space:]]+/, "", t)
|
|
100
|
+
if (t == "") continue
|
|
101
|
+
if (substr(t, 1, 2) != "//" && substr(t, 1, 1) != "#" && substr(t, 1, 1) != "*") return 0
|
|
102
|
+
w = split(t, _unused, /[[:space:]]+/)
|
|
103
|
+
if (w >= 4) return 1
|
|
104
|
+
}
|
|
105
|
+
return 0
|
|
106
|
+
}
|
|
107
|
+
# Баланс круглых скобок со строки a по строку b, через префиксные суммы. Пересчёт от a при
|
|
108
|
+
# каждом обращении давал кубическую сложность и не укладывался в две минуты на большом
|
|
109
|
+
# репозитории. Приближение: скобки внутри строковых литералов считаются наравне с
|
|
110
|
+
# настоящими — точный ответ требует разбора языка, а его у переносимой проверки нет.
|
|
111
|
+
function bal(a, b) { return pre[b] - pre[a - 1] }
|
|
112
|
+
function balBrace(a, b) { return prb[b] - prb[a - 1] }
|
|
113
|
+
|
|
114
|
+
# Границы файла: awk без gawk не знает ENDFILE, поэтому предыдущий файл разбирается на первой
|
|
115
|
+
# строке следующего, а последний — в END. Буферы обнуляются вместе с именем: общий буфер
|
|
116
|
+
# склеил бы скобки соседних файлов и сдвинул все границы.
|
|
117
|
+
FNR == 1 { if (n > 0) report(); n = 0; delete line; delete pre; delete prb; delete tck; cur = FILENAME; skipFile = 0 }
|
|
118
|
+
FNR <= 5 && /@[Gg]enerated|[Dd]o not edit|DO NOT EDIT|[Aa]utogenerated|[Aa]uto-generated|[Gg]enerated by|сгенерирован/ { skipFile = 1 }
|
|
119
|
+
{
|
|
120
|
+
n++; line[n] = $0
|
|
121
|
+
t = $0; o = gsub(/\(/, "(", t); t = $0; c = gsub(/\)/, ")", t)
|
|
122
|
+
pre[n] = pre[n - 1] + o - c
|
|
123
|
+
t = $0; o = gsub(/\{/, "{", t); t = $0; c = gsub(/\}/, "}", t)
|
|
124
|
+
prb[n] = prb[n - 1] + o - c
|
|
125
|
+
# Чётность обратных кавычек. Строка внутри незакрытого шаблонного литерала — это ДАННЫЕ,
|
|
126
|
+
# а не код: проекты, которые проверяют чужой код, держат в таких литералах целые файлы с
|
|
127
|
+
# тестами внутри. Три находки из четырёх на замере пришли ровно оттуда.
|
|
128
|
+
t = $0; tck[n] = tck[n - 1] + gsub(/`/, "`", t)
|
|
129
|
+
}
|
|
130
|
+
END { if (n > 0) report() }
|
|
131
|
+
|
|
132
|
+
function report( i, j, l, t0, e, ind, start, found, hasBody, b, bi) {
|
|
133
|
+
if (skipFile) return
|
|
134
|
+
for (i = 1; i <= n; i++) {
|
|
135
|
+
l = line[i]; cursor = i
|
|
136
|
+
if (l ~ /^[[:space:]]*(#|\/\/|\*)/) continue
|
|
137
|
+
if (tautology(l) && !explainedAbove(i)) {
|
|
138
|
+
printf "%s:%d: утверждение-тавтология: %s\n", cur, i, trim(l); continue
|
|
139
|
+
}
|
|
140
|
+
if (bareSkip(l) && !explainedAbove(i)) {
|
|
141
|
+
printf "%s:%d: тест пропущен без названной причины: %s\n", cur, i, trim(l); continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# --- python: тело ограничено отступом -----------------------------------
|
|
145
|
+
if (l ~ /^[[:space:]]*(async[[:space:]]+)?def[[:space:]]+test[_A-Za-z0-9]*[[:space:]]*\(/) {
|
|
146
|
+
# Тело в той же строке — это не тест, а вложенная функция-фикстура: «def test_function():
|
|
147
|
+
# ...» внутри настоящего теста. Настоящий тест так не пишут. Найдено замером на
|
|
148
|
+
# sentry-python, где такая фикстура объявлялась пустым тестом.
|
|
149
|
+
if (bal(i, i) <= 0 && l !~ /:[[:space:]]*(#.*)?$/) continue
|
|
150
|
+
match(l, /^[[:space:]]*/); ind = RLENGTH; found = 0
|
|
151
|
+
# Подпись бывает на несколько строк, и её закрывающая «):» стоит на отступе самого def.
|
|
152
|
+
# Пока тело искали прямо со следующей строки, эта скобка обрывала поиск: тест объявляли
|
|
153
|
+
# пустым, ни разу не заглянув внутрь. 142 ложных в одном pre-commit.
|
|
154
|
+
start = i
|
|
155
|
+
for (j = i; j <= n; j++) { start = j; if (bal(i, j) <= 0 && line[j] ~ /:[[:space:]]*(#.*)?$/) break }
|
|
156
|
+
|
|
157
|
+
for (j = start + 1; j <= n; j++) {
|
|
158
|
+
b = line[j]
|
|
159
|
+
if (b ~ /^[[:space:]]*$/) continue
|
|
160
|
+
match(b, /^[[:space:]]*/); bi = RLENGTH
|
|
161
|
+
if (bi <= ind) break
|
|
162
|
+
if (!isNoop(b)) { found = 1; break }
|
|
163
|
+
}
|
|
164
|
+
if (!found) printf "%s:%d: тело теста пустое — он не может провалиться: %s\n", cur, i, trim(l)
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# --- javascript и typescript: тело ограничено круглой скобкой вызова ----
|
|
169
|
+
# ВЫЗОВ В НАЧАЛЕ СТРОКИ, а не слово «test» где угодно. Пока проверялось вхождение,
|
|
170
|
+
# красным становилось всё подряд: объявление обычной функции с таким именем, тот же текст
|
|
171
|
+
# внутри строковой фикстуры с дифом, он же аргументом в toContain. 51 ложная из 60.
|
|
172
|
+
if (tck[i - 1] % 2 == 1) continue
|
|
173
|
+
t0 = l; sub(/^[[:space:]]+/, "", t0)
|
|
174
|
+
if (t0 ~ /^(await[[:space:]]+)?(it|test)(\.[A-Za-z]+)?[[:space:]]*\(/) {
|
|
175
|
+
# Границей служит КРУГЛАЯ скобка вызова, а не фигурная скобка тела: многострочный вызов
|
|
176
|
+
# с объектом настроек между именем и телом обрывался на этом объекте — скобка
|
|
177
|
+
# открылась и закрылась, тело сочли пустым. 1460 ложных из 1843.
|
|
178
|
+
# Конец вызова — когда закрылись И круглые, И фигурные скобки. По одним круглым
|
|
179
|
+
# строка `for (const p of ["a)b"]) {` закрывала вызов раньше времени: скобка внутри
|
|
180
|
+
# строкового литерала считается наравне с настоящей. Фигурная скобка тела там ещё
|
|
181
|
+
# открыта, и это отличает настоящий конец от мнимого.
|
|
182
|
+
e = i
|
|
183
|
+
for (j = i; j <= n; j++) { e = j; if (bal(i, j) <= 0 && balBrace(i, j) <= 0) break }
|
|
184
|
+
hasBody = 0
|
|
185
|
+
for (j = i; j <= e; j++) if (line[j] ~ /=>|function/) hasBody = 1
|
|
186
|
+
if (!hasBody) continue
|
|
187
|
+
found = 0
|
|
188
|
+
# Однострочная запись: тело живёт на той же строке («it("…", () => expectPass(…));»).
|
|
189
|
+
# Пока сканировали только следующие строки, такой тест объявлялся пустым.
|
|
190
|
+
if (e == i && t0 ~ /=>[^)]*[A-Za-z]/) continue
|
|
191
|
+
# Включая строку e: у стрелочной функции без фигурных скобок тело живёт ровно на ней
|
|
192
|
+
# («it("…", () =>\n expectPass(…));»). Пока сканировали до e-1, такие тесты объявлялись
|
|
193
|
+
# пустыми — 15 ложных из 21 на замере.
|
|
194
|
+
for (j = i + 1; j <= e; j++) if (!isNoop(line[j])) { found = 1; break }
|
|
195
|
+
if (!found) printf "%s:%d: тело теста пустое — он не может провалиться: %s\n", cur, i, trim(l)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
' $FILES 2>/dev/null)
|
|
200
|
+
|
|
201
|
+
LEFT="$(printf '%s' "$OUT" | grep -v '^$' | own_samples_filter "$DIR")"
|
|
202
|
+
[ -z "$LEFT" ] && exit 0
|
|
203
|
+
printf '%s\n' "$LEFT"
|
|
204
|
+
echo " почини: тест обязан уметь провалиться. Пропуск — с названной причиной («reason=…», ссылка на задачу)."
|
|
205
|
+
echo " зелёный прогон без проверки — это уверенность без основания, худший из возможных исходов."
|
|
206
|
+
exit 1
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
intent: тест умеет провалиться — тело не пустое, утверждение не тавтология, пропуск с причиной
|
|
2
|
+
intent_en: a test can actually fail — no empty body, no tautology, no skip without a reason
|
|
3
|
+
|
|
4
|
+
# Там, где есть тесты. Триггер по наличию, а не по языку: разбор покрывает python, javascript
|
|
5
|
+
# и typescript, а пропуски и тавтологии ловятся во всех перечисленных расширениях.
|
|
6
|
+
trigger:
|
|
7
|
+
has_tests: true
|
|
8
|
+
|
|
9
|
+
recipes:
|
|
10
|
+
any: bash {gate}/check.sh {dir}
|
|
11
|
+
|
|
12
|
+
proof: incidents/README.md, 2026-09-06 «граница проверки найдена замером» — прогон по
|
|
13
|
+
девятнадцати чужим репозиториям (около 25 000 файлов) даёт 8 находок: шесть выключенных
|
|
14
|
+
тестов без причины в `swr` (30 000 звёзд) и два в `kodus-ai`. Первая версия давала 1843
|
|
15
|
+
находки, из них 1843 ложных
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_total_is_summed():
|
|
5
|
+
order = {"items": [{"price": "1.50"}, {"price": "2.50"}]}
|
|
6
|
+
assert total(order) == 4
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Тест, который проверяет «не падает», — законный стиль: утверждение здесь в самом отсутствии
|
|
10
|
+
# исключения. Эта запись про выключенный тест, а не про то, как его следует писать.
|
|
11
|
+
def test_import_does_not_explode():
|
|
12
|
+
build_report({"items": []})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.skip(reason="ждёт песочницы платёжного шлюза, задача PAY-214")
|
|
16
|
+
def test_refund_is_reversible():
|
|
17
|
+
assert refund(pay(100)) == 0
|