agent-quality-kit 0.8.0 → 0.10.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 +154 -12
- package/README.ru.md +185 -27
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/project-baseline.md +14 -0
- package/kit/docs/api-e2e.md +214 -0
- package/kit/docs/ready-made-rules.md +188 -0
- package/kit/gates/README.md +22 -0
- package/kit/gates/api-contract-has-arbiter/README.md +63 -0
- package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
- package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
- package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
- package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
- package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
- package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
- package/kit/gates/ci-actually-fails/check.sh +9 -1
- package/kit/gates/color-from-token/check.sh +5 -1
- package/kit/gates/commit-explains-itself/check.sh +15 -0
- package/kit/gates/complexity-limit/red/deep.go +17 -0
- package/kit/gates/complexity-limit/red/deep.rs +17 -0
- package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
- package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
- package/kit/gates/lesson-has-outcome/check.sh +5 -1
- package/kit/gates/mcp-server-resolves/README.md +62 -0
- package/kit/gates/mcp-server-resolves/check.sh +110 -0
- package/kit/gates/mcp-server-resolves/gate.yml +18 -0
- package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
- package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
- package/kit/gates/protection-not-removed/README.md +67 -0
- package/kit/gates/protection-not-removed/check.sh +92 -0
- package/kit/gates/protection-not-removed/gate.yml +10 -0
- package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
- package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
- package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
- package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
- package/kit/gates/todo-without-task/red/later.go +6 -0
- package/kit/gates/todo-without-task/red/later.rs +4 -0
- package/llms.txt +25 -4
- package/package.json +3 -6
- package/tool/commands/context.mjs +37 -6
- package/tool/commands/doctor.mjs +139 -16
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/project.mjs +18 -2
- package/tool/commands/prove.mjs +1 -0
- package/tool/commands/vitals.mjs +167 -0
- package/tool/i18n/en-docs.mjs +48 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +26 -279
- package/tool/i18n/index.mjs +36 -3
- package/tool/i18n/ru-docs.mjs +48 -0
- package/tool/i18n/ru-gates.mjs +311 -0
- package/tool/i18n/ru.mjs +26 -278
- package/tool/lib/banner.mjs +59 -0
- package/tool/lib/brief.mjs +192 -0
- package/tool/lib/cadence.mjs +57 -0
- package/tool/lib/core.mjs +3 -0
- package/tool/lib/history.mjs +82 -0
- package/tool/lib/manifest.mjs +146 -15
- package/tool/lib/prove.mjs +11 -1
- package/tool/lib/repo.mjs +43 -3
- package/tool/program.mjs +33 -0
- package/tool/selfcheck/smoke/_fixture.mjs +89 -0
- package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
- package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +528 -6
- package/tool/selfcheck/units-banner.mjs +65 -0
- package/tool/selfcheck/units-brief.mjs +97 -0
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-context.mjs +3 -1
- package/tool/selfcheck/units-level.mjs +147 -1
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +164 -0
- package/tool/selfcheck/units-vitals.mjs +81 -0
- package/tool/selfcheck/units.mjs +3 -75
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// tool/selfcheck/smoke/_fixture.mjs — общая оснастка проверок, гоняющих сам инструмент.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. В `smoke.sh` фикстура «временный проект под git» написана сорок раз
|
|
4
|
+
// подряд одними и теми же четырьмя строками. Это не похожие строки, а ОДНО знание в сорока
|
|
5
|
+
// местах: меняется устройство подопытного проекта — правится сорок мест, и они расходятся.
|
|
6
|
+
//
|
|
7
|
+
// ЧТО ЗДЕСЬ ЗАКРЫТО, кроме дублирования (всё — из разборов флейков и из нашего же опыта):
|
|
8
|
+
// · СВОЙ каталог на проверку и уборка после неё — через `t.after`, а не «не забыть rm»;
|
|
9
|
+
// · ТАЙМАУТ на каждый запуск: висящий подпроцесс иначе держит конвейер до его предела;
|
|
10
|
+
// · ГЕРМЕТИЧНОСТЬ: HOME и TMPDIR в песочнице, сеть выключена. Прогон обязан давать один
|
|
11
|
+
// ответ на любой машине и в любой день — иначе зелёное значит «сегодня совпало».
|
|
12
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
19
|
+
const CLI = join(ROOT, "tool", "program.mjs");
|
|
20
|
+
|
|
21
|
+
// Предел на один запуск. Тридцать секунд — с запасом: самый долгий наш прогон в фикстуре
|
|
22
|
+
// (`start` со всеми записями) укладывается в единицы секунд. Предел нужен не ради скорости,
|
|
23
|
+
// а ради того, чтобы висящий процесс называл себя, а не съедал бюджет задания молча.
|
|
24
|
+
const TIMEOUT_MS = 30000;
|
|
25
|
+
|
|
26
|
+
function env(home) {
|
|
27
|
+
return {
|
|
28
|
+
...process.env,
|
|
29
|
+
HOME: home,
|
|
30
|
+
USERPROFILE: home,
|
|
31
|
+
TMPDIR: join(home, "tmp"),
|
|
32
|
+
AQK_LANG: "ru",
|
|
33
|
+
// Сеть в тестах — отдельный класс флейков, и у нас она включалась ТОЛЬКО вне конвейера:
|
|
34
|
+
// локально проверки были сетевыми, в конвейере нет. Две разные среды по построению.
|
|
35
|
+
AQK_UPDATE: "0",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Подопытный проект: свой каталог, свой git, свой дом. Файлы задаются картой «путь → текст»,
|
|
40
|
+
// потому что имя файла в такой проверке — часть утверждения, а не деталь.
|
|
41
|
+
function project(t, files = {}, { git = true } = {}) {
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), "aqk-smoke-"));
|
|
43
|
+
const home = join(dir, ".home");
|
|
44
|
+
mkdirSync(join(home, "tmp"), { recursive: true });
|
|
45
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
46
|
+
for (const [rel, text] of Object.entries(files)) {
|
|
47
|
+
const full = join(dir, rel);
|
|
48
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
49
|
+
writeFileSync(full, text, "utf8");
|
|
50
|
+
}
|
|
51
|
+
const p = { dir, home };
|
|
52
|
+
if (git) {
|
|
53
|
+
run(p, "git", ["init", "-q", "."]);
|
|
54
|
+
run(p, "git", ["config", "user.email", "t@t"]);
|
|
55
|
+
run(p, "git", ["config", "user.name", "t"]);
|
|
56
|
+
}
|
|
57
|
+
return p;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Запуск чего угодно в каталоге проекта. Возвращается И код, И вывод: проверка, смотрящая
|
|
61
|
+
// только на код, не умеет объяснить провал, а смотрящая только на вывод — не умеет заметить,
|
|
62
|
+
// что команда не роняет прогон.
|
|
63
|
+
function run({ dir, home }, cmd, args = []) {
|
|
64
|
+
const r = spawnSync(cmd, args, {
|
|
65
|
+
cwd: dir, encoding: "utf8", timeout: TIMEOUT_MS, env: env(home),
|
|
66
|
+
});
|
|
67
|
+
return { code: r.status, out: `${r.stdout || ""}${r.stderr || ""}` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const aqk = (p, ...args) => run(p, process.execPath, [CLI, ...args]);
|
|
71
|
+
|
|
72
|
+
// ПОЧЕМУ ГЕЙТ ЗОВЁТСЯ ТОЧКОЙ, А НЕ ПУТЁМ, И ПОЧЕМУ ПУТЬ ЧЕРЕЗ КОСУЮ.
|
|
73
|
+
// Node на Windows отдаёт `C:\Users\…\Temp\aqk-smoke-x`, а Git Bash живёт в POSIX-мире и
|
|
74
|
+
// такого пути не знает: проверка искала спецификации в несуществующем каталоге, не находила
|
|
75
|
+
// ничего и выходила с нулём — то есть краснела не на дефекте, а на переносимости. Поймано
|
|
76
|
+
// конвейером на windows-задании в тот же день, когда файл был написан.
|
|
77
|
+
// Лечится двумя приёмами сразу: каталог отдаётся как «.» вместе с cwd (переводить нечего),
|
|
78
|
+
// а путь до самого скрипта — с прямыми косыми, их Git Bash понимает.
|
|
79
|
+
const posix = (p) => String(p).replace(/\\/g, "/");
|
|
80
|
+
// Подкаталог нужен там, где проверяется не сам проект, а его клон: мелкий клон предложения
|
|
81
|
+
// изменений ведёт себя иначе, чем полная история, и это отдельный класс отказов.
|
|
82
|
+
const gate = (p, name, sub = ".") =>
|
|
83
|
+
run(p, "bash", [posix(join(ROOT, "kit", "gates", name, "check.sh")), sub]);
|
|
84
|
+
|
|
85
|
+
// Наружу — только то, что зовут проверки. `ROOT` и `CLI` остаются внутри: экспорт, который
|
|
86
|
+
// никто не берёт, читается как часть договора и мешает менять внутренности — поймал наш же
|
|
87
|
+
// dead-code через минуту после того, как файл был написан. `run` вернулся, когда появилась
|
|
88
|
+
// проверка, которой нужен сырой git: экспорт заводится под потребителя, а не про запас.
|
|
89
|
+
export { project, run, aqk, gate };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Проверки про договор с чужим кодом: кто держит спецификацию API и умеет ли держатель
|
|
2
|
+
// провалиться. Переехали из smoke.sh 2026-09-09 первыми — как доказательство переезда.
|
|
3
|
+
//
|
|
4
|
+
// ЗАМЕР, ИЗ КОТОРОГО ОНИ ВЗЯЛИСЬ. Стенд, где сервер врёт в каждом поле ответа:
|
|
5
|
+
// spectral (spectral:oas) код 0 — нет контакта, описания, тегов
|
|
6
|
+
// schemathesis, умолчания код 1 — три нарушения схемы в ответах
|
|
7
|
+
// schemathesis -c not_a_server_error код 0 — «18 из 18 прошли»
|
|
8
|
+
// oasdiff breaking без --fail-on код 0 — печатает ломающую правку и молчит кодом
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { project, aqk, gate } from "./_fixture.mjs";
|
|
12
|
+
|
|
13
|
+
const SPEC = "openapi: 3.0.3\ninfo: { title: t, version: 1.0.0 }\npaths: {}\n";
|
|
14
|
+
const workflow = (steps) =>
|
|
15
|
+
`name: ci\non: [push]\njobs:\n contract:\n runs-on: ubuntu-latest\n steps:\n${steps}`;
|
|
16
|
+
|
|
17
|
+
// ЛОВУШКА, ИЗ-ЗА КОТОРОЙ ЗАМЕР ЧУТЬ НЕ ОКАЗАЛСЯ ЛОЖНО-ЗЕЛЁНЫМ: шаг с именем «Spec lint»
|
|
18
|
+
// краснеет не потому, что узнали инструмент, а потому что в имени есть слово-примета.
|
|
19
|
+
// Поэтому здесь у шагов НЕТ слов-подсказок — проверяется опознание инструмента.
|
|
20
|
+
test("обезвреженные проверки контракта API опознаются как проверки", (t) => {
|
|
21
|
+
const p = project(t, {
|
|
22
|
+
".github/workflows/api.yml": workflow(
|
|
23
|
+
" - name: соответствие сервера схеме\n" +
|
|
24
|
+
" continue-on-error: true\n" +
|
|
25
|
+
" run: schemathesis run openapi.yaml --url http://localhost:8000\n" +
|
|
26
|
+
" - name: ломающие изменения\n" +
|
|
27
|
+
" continue-on-error: true\n" +
|
|
28
|
+
" run: oasdiff breaking base.yaml openapi.yaml --fail-on ERR\n" +
|
|
29
|
+
" - name: ожидания потребителей\n" +
|
|
30
|
+
" continue-on-error: true\n" +
|
|
31
|
+
" run: pact-broker can-i-deploy --pacticipant web\n"),
|
|
32
|
+
});
|
|
33
|
+
const r = gate(p, "ci-actually-fails");
|
|
34
|
+
assert.equal(r.code, 1, `три шага под continue-on-error прошли как чистые:\n${r.out}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// У записи две красные ветки, и вторая тоньше первой. Проверяются ОБЕ формы сужения и
|
|
38
|
+
// мутация: снял сужение — гейт обязан замолчать, иначе краснеет он не от этого.
|
|
39
|
+
test("арбитр, который не может провалиться, краснеет в обеих формах", (t) => {
|
|
40
|
+
const mk = (tail) => ({
|
|
41
|
+
"openapi.yaml": SPEC,
|
|
42
|
+
".github/workflows/ci.yml": workflow(
|
|
43
|
+
` - run: schemathesis run openapi.yaml --url http://localhost:8000${tail}\n`),
|
|
44
|
+
});
|
|
45
|
+
for (const narrow of [" -c not_a_server_error", " --checks not_a_server_error"]) {
|
|
46
|
+
const p = project(t, mk(narrow));
|
|
47
|
+
assert.equal(gate(p, "api-contract-has-arbiter").code, 1, `сужение «${narrow}» не опознано`);
|
|
48
|
+
}
|
|
49
|
+
const full = project(t, mk(""));
|
|
50
|
+
assert.equal(gate(full, "api-contract-has-arbiter").code, 0, "полный арбитр покраснел");
|
|
51
|
+
|
|
52
|
+
const loud = project(t, {
|
|
53
|
+
"openapi.yaml": SPEC,
|
|
54
|
+
".github/workflows/ci.yml": workflow(
|
|
55
|
+
" - run: schemathesis run openapi.yaml --url http://localhost:8000\n" +
|
|
56
|
+
" - run: oasdiff breaking base.yaml openapi.yaml\n"),
|
|
57
|
+
});
|
|
58
|
+
assert.equal(gate(loud, "api-contract-has-arbiter").code, 1,
|
|
59
|
+
"oasdiff без --fail-on печатает находки и выходит с нулём — это не проверка");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// НАЙДЕНО АУДИТОМ ФИЧ, а не образцами: красный и зелёный образцы лежат по одному, а в
|
|
63
|
+
// настоящем проекте записи стоят рядом — и соседняя `ci-actually-fails` держит в своём
|
|
64
|
+
// check.sh список запускалок со ВСЕМИ инструментами про API разом. Проверка находила её и
|
|
65
|
+
// выдавала ложное ЗЕЛЁНОЕ там, где договор не держал никто.
|
|
66
|
+
test("держателем не считается определение соседней записи", (t) => {
|
|
67
|
+
const p = project(t, {
|
|
68
|
+
"src/a.py": "def s():\n return 1\n",
|
|
69
|
+
"openapi.yaml": SPEC,
|
|
70
|
+
".github/workflows/ci.yml": workflow(" - run: pytest\n"),
|
|
71
|
+
});
|
|
72
|
+
aqk(p, "init");
|
|
73
|
+
aqk(p, "add", "ci-actually-fails");
|
|
74
|
+
aqk(p, "add", "api-contract-has-arbiter");
|
|
75
|
+
const r = gate(p, "api-contract-has-arbiter");
|
|
76
|
+
assert.equal(r.code, 1, `договор без держателя прошёл зелёным:\n${r.out}`);
|
|
77
|
+
// Второй дефект того же прогона: `find` без завершающего -print печатал обойдённые каталоги.
|
|
78
|
+
assert.doesNotMatch(r.out, /\.git|\.aqk/, `в списке спецификаций каталоги:\n${r.out}`);
|
|
79
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Мини-отчёт в коммите: где его требовать, а где состав коммита определить нельзя.
|
|
2
|
+
//
|
|
3
|
+
// НАЙДЕНО КОНВЕЙЕРОМ 2026-09-09. Коммит, трогающий ТОЛЬКО журнал, отчёта не требует — сама
|
|
4
|
+
// запись и есть отчёт. Локально так и было. В конвейере тот же коммит покраснел, и причина
|
|
5
|
+
// тоньше, чем кажется:
|
|
6
|
+
// · при разборе предложения изменений GitHub выкладывает синтетический коммит слияния,
|
|
7
|
+
// и гейт правильно спрашивает второго родителя — коммит автора;
|
|
8
|
+
// · но выкладка идёт МЕЛКИМ клоном, и родителя второго родителя в нём нет;
|
|
9
|
+
// · `git show --name-only` на коммите без родителя считает его корневым и выдаёт ВСЁ дерево;
|
|
10
|
+
// · значит «трогает только журнал» перестаёт срабатывать — молча, — и гейт требует отчёт там,
|
|
11
|
+
// где не должен.
|
|
12
|
+
// Проверено опытом: глубина 2 — красный, глубина 3 — чисто.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { project, run, gate } from "./_fixture.mjs";
|
|
16
|
+
|
|
17
|
+
function repoWithJournalMerge(t) {
|
|
18
|
+
const p = project(t, {
|
|
19
|
+
"incidents/README.md": "# журнал\n",
|
|
20
|
+
"src/a.py": "код\n",
|
|
21
|
+
".aqk.yml": "lessons: incidents\n",
|
|
22
|
+
});
|
|
23
|
+
run(p, "git", ["add", "-A"]);
|
|
24
|
+
run(p, "git", ["commit", "-q", "-m", "feat: первый", "-m", "Сделано: завёл", "-m", "Не уверен: ни в чём"]);
|
|
25
|
+
run(p, "git", ["checkout", "-q", "-b", "feature"]);
|
|
26
|
+
run(p, "bash", ["-c", "printf '## запись\\n' >> incidents/README.md"]);
|
|
27
|
+
run(p, "git", ["add", "-A"]);
|
|
28
|
+
// Тело НАРОЧНО без «Сделано:»: если освобождение для журнала перестанет работать, гейт
|
|
29
|
+
// покраснеет — то есть проверка умеет отличить исправное от сломанного.
|
|
30
|
+
run(p, "git", ["commit", "-q", "-m", "docs: урок", "-m", "Не уверен: ничего не сломано"]);
|
|
31
|
+
const main = run(p, "bash", ["-c", "git checkout -q master 2>/dev/null || git checkout -q main; git rev-parse --short HEAD"]);
|
|
32
|
+
run(p, "git", ["merge", "-q", "--no-ff", "feature", "-m", `Merge feature into ${main.out.trim()}`]);
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("коммит только в журнал не требует отчёта и в мелком клоне слияния", (t) => {
|
|
37
|
+
const p = repoWithJournalMerge(t);
|
|
38
|
+
assert.equal(gate(p, "commit-explains-itself").code, 0, "полная история: журнальный коммит покраснел");
|
|
39
|
+
|
|
40
|
+
// Ровно то, что делает конвейер при разборе предложения изменений.
|
|
41
|
+
run(p, "bash", ["-c", "git clone -q --depth 2 file://$PWD shallow2 2>/dev/null"]);
|
|
42
|
+
const r = gate(p, "commit-explains-itself", "shallow2");
|
|
43
|
+
assert.equal(r.code, 0, `мелкий клон: гейт требует отчёт там, где состав определить нельзя:\n${r.out}`);
|
|
44
|
+
// Молчать в этом случае нельзя: пропуск обязан называть себя, иначе выключенная проверка
|
|
45
|
+
// неотличима от работающей — тот самый класс, ради которого весь комплект.
|
|
46
|
+
assert.match(r.out, /не определить|журнал/, `пропуск не назвал причину:\n${r.out}`);
|
|
47
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Прогон обязан называть свой вердикт СЛОВАМИ, а не только кодом возврата.
|
|
2
|
+
//
|
|
3
|
+
// НАЙДЕНО АУДИТОМ ФИЧ 2026-09-09. `doctor --run` выходил с единицей и в конце не говорил ни
|
|
4
|
+
// слова о причине: она оставалась в шапке, а внизу человек видел список зелёных гейтов и шёл
|
|
5
|
+
// искать несуществующую поломку. С `--min` вердикт печатался всегда, без него — никогда.
|
|
6
|
+
// Обратная сторона нашего же принципа: молчание неотличимо не только от успеха, но и от отказа.
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { project, aqk } from "./_fixture.mjs";
|
|
12
|
+
|
|
13
|
+
test("прогон называет свой вердикт словами в обоих исходах", (t) => {
|
|
14
|
+
const p = project(t, { "src/a.py": "def s():\n return 1\n" });
|
|
15
|
+
aqk(p, "init");
|
|
16
|
+
// Гейт объявляется ПРЯМО, а не через `add`, и это не лень. `add` выбирает рецепт по тому,
|
|
17
|
+
// что установлено на машине: где есть ruff, у `todo-without-task` берётся родной
|
|
18
|
+
// `ruff check --select FIX,TD`, где нет — переносимый. Значит один и тот же проект получает
|
|
19
|
+
// РАЗНУЮ объявленную команду на разных машинах, и проверка про ВЕРДИКТ начинала зависеть от
|
|
20
|
+
// чужого инструмента. Поймано конвейером: локально зелено, на раннере красно.
|
|
21
|
+
// Здесь проверяется строка вердикта, а не поведение записи каталога, — гейту довольно быть
|
|
22
|
+
// заведомо тихим.
|
|
23
|
+
const man = join(p.dir, ".aqk.yml");
|
|
24
|
+
writeFileSync(man, readFileSync(man, "utf8").replace(/^gates:\s*$/m, 'gates:\n тихий: "true"'), "utf8");
|
|
25
|
+
|
|
26
|
+
// .gitignore нет — прогон красный, и обязан сказать, из-за чего именно.
|
|
27
|
+
const red = aqk(p, "doctor", "--run");
|
|
28
|
+
// Сообщение утверждения обязано нести ВЕСЬ хвост: проверка, которая говорит только «не
|
|
29
|
+
// совпало», отправляет читателя гадать — ровно то, за что мы ругаем чужие проверки.
|
|
30
|
+
const redTail = red.out.trimEnd().split("\n").slice(-6).join("\n");
|
|
31
|
+
assert.notEqual(red.code, 0, `прогон без .gitignore прошёл зелёным:\n${redTail}`);
|
|
32
|
+
assert.match(redTail, /красн/, `вердикт не назван в конце вывода (код ${red.code}):\n${redTail}`);
|
|
33
|
+
|
|
34
|
+
// Причина устранена — и «ничего не сказал» обязано отличаться от «всё проверено».
|
|
35
|
+
writeFileSync(join(p.dir, ".gitignore"), "x\n", "utf8");
|
|
36
|
+
const green = aqk(p, "doctor", "--run");
|
|
37
|
+
const greenTail = green.out.trimEnd().split("\n").slice(-6).join("\n");
|
|
38
|
+
assert.equal(green.code, 0, `прогон остался красным:\n${greenTail}`);
|
|
39
|
+
assert.match(greenTail, /зелён/, `успех не назван словами:\n${greenTail}`);
|
|
40
|
+
});
|