agent-quality-kit 0.12.0 → 0.14.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 +43 -7
- package/README.ru.md +44 -7
- package/kit/gates/api-contract-has-arbiter/README.md +16 -1
- package/kit/gates/api-contract-has-arbiter/check.sh +66 -23
- package/kit/gates/complexity-limit/gate.yml +4 -0
- package/kit/gates/dead-code/gate.yml +4 -0
- package/kit/gates/entry-commands-exist/README.md +64 -0
- package/kit/gates/entry-commands-exist/check.sh +110 -0
- package/kit/gates/entry-commands-exist/gate.yml +19 -0
- package/kit/gates/entry-commands-exist/green/AGENTS.md +13 -0
- package/kit/gates/entry-commands-exist/green/Makefile +6 -0
- package/kit/gates/entry-commands-exist/green/justfile +2 -0
- package/kit/gates/entry-commands-exist/green/package.json +10 -0
- package/kit/gates/entry-commands-exist/red/AGENTS.md +9 -0
- package/kit/gates/entry-commands-exist/red/Makefile +2 -0
- package/kit/gates/entry-commands-exist/red/package.json +9 -0
- package/kit/gates/env-secrets-not-committed/README.md +73 -0
- package/kit/gates/env-secrets-not-committed/check.sh +139 -0
- package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
- package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
- package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
- package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
- package/kit/gates/env-secrets-not-committed/red/.env +7 -0
- package/kit/gates/no-print-in-prod/gate.yml +4 -0
- package/kit/gates/swallowed-error/gate.yml +4 -0
- package/kit/gates/todo-without-task/gate.yml +4 -0
- package/llms.txt +8 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +81 -9
- package/tool/commands/doctor-catalog.mjs +222 -0
- package/tool/commands/doctor.mjs +81 -209
- package/tool/commands/learn.mjs +119 -19
- package/tool/commands/probe.mjs +50 -68
- package/tool/commands/project.mjs +6 -1
- package/tool/commands/prompt.mjs +69 -0
- package/tool/commands/report.mjs +1 -1
- package/tool/commands/vitals.mjs +15 -11
- package/tool/i18n/en-docs.mjs +22 -1
- package/tool/i18n/en-gates.mjs +6 -1
- package/tool/i18n/en.mjs +78 -4
- package/tool/i18n/index.mjs +42 -3
- package/tool/i18n/ru-docs.mjs +24 -1
- package/tool/i18n/ru-gates.mjs +6 -1
- package/tool/i18n/ru.mjs +87 -4
- package/tool/lib/adopt.mjs +15 -1
- package/tool/lib/advice.mjs +115 -0
- package/tool/lib/annotate.mjs +66 -0
- package/tool/lib/brief.mjs +3 -1
- package/tool/lib/cadence.mjs +40 -1
- package/tool/lib/core.mjs +40 -1
- package/tool/lib/gate-worker.mjs +18 -0
- package/tool/lib/history.mjs +34 -5
- package/tool/lib/manifest.mjs +65 -21
- package/tool/lib/repo.mjs +47 -35
- package/tool/lib/run.mjs +98 -9
- package/tool/program.mjs +4 -0
- package/tool/selfcheck/smoke/_fixture.mjs +8 -3
- package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
- package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
- package/tool/selfcheck/smoke/first-run.test.mjs +144 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +91 -3
- package/tool/selfcheck/smoke.sh +10 -2
- package/tool/selfcheck/units-annotate.mjs +67 -0
- package/tool/selfcheck/units-cadence.mjs +40 -1
- package/tool/selfcheck/units-context.mjs +64 -1
- package/tool/selfcheck/units-learn.mjs +32 -0
- package/tool/selfcheck/units-level.mjs +97 -3
- package/tool/selfcheck/units-probe.mjs +2 -1
- package/tool/selfcheck/units-prompt.mjs +106 -0
- package/tool/selfcheck/units-repo.mjs +44 -1
- package/tool/selfcheck/units-verdict.mjs +76 -0
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// Обратная сторона нашего же принципа: молчание неотличимо не только от успеха, но и от отказа.
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import { project, aqk } from "./_fixture.mjs";
|
|
11
|
+
import { project, aqk, aqkEnv } from "./_fixture.mjs";
|
|
12
12
|
|
|
13
13
|
test("прогон называет свой вердикт словами в обоих исходах", (t) => {
|
|
14
14
|
const p = project(t, { "src/a.py": "def s():\n return 1\n" });
|
|
@@ -23,7 +23,10 @@ test("прогон называет свой вердикт словами в о
|
|
|
23
23
|
const man = join(p.dir, ".aqk.yml");
|
|
24
24
|
writeFileSync(man, readFileSync(man, "utf8").replace(/^gates:\s*$/m, 'gates:\n тихий: "true"'), "utf8");
|
|
25
25
|
|
|
26
|
-
// .gitignore нет — прогон красный, и обязан сказать, из-за чего именно.
|
|
26
|
+
// .gitignore нет — прогон красный, и обязан сказать, из-за чего именно. Удаляется ЯВНО:
|
|
27
|
+
// `init` с 2026-09-11 сам создаёт .gitignore под служебные файлы, и прежняя посылка теста
|
|
28
|
+
// («после init его нет») перестала быть правдой.
|
|
29
|
+
rmSync(join(p.dir, ".gitignore"), { force: true });
|
|
27
30
|
const red = aqk(p, "doctor", "--run");
|
|
28
31
|
// Сообщение утверждения обязано нести ВЕСЬ хвост: проверка, которая говорит только «не
|
|
29
32
|
// совпало», отправляет читателя гадать — ровно то, за что мы ругаем чужие проверки.
|
|
@@ -38,3 +41,88 @@ test("прогон называет свой вердикт словами в о
|
|
|
38
41
|
assert.equal(green.code, 0, `прогон остался красным:\n${greenTail}`);
|
|
39
42
|
assert.match(greenTail, /зелён/, `успех не назван словами:\n${greenTail}`);
|
|
40
43
|
});
|
|
44
|
+
|
|
45
|
+
// --skip ГРУППОЙ: гейт, которому нужен стенд, в быстром прогоне не идёт — и это СКАЗАНО, а не
|
|
46
|
+
// скрыто. Отзыв с живого проекта 2026-09-11 (гейт цены без стенда валил прогон).
|
|
47
|
+
test("--skip группой: прогон зелёный, пропущенный назван в выводе и в отчёте", (t) => {
|
|
48
|
+
const p = project(t, { "src/a.py": "x = 1\n" });
|
|
49
|
+
aqk(p, "init");
|
|
50
|
+
const man = join(p.dir, ".aqk.yml");
|
|
51
|
+
writeFileSync(man, readFileSync(man, "utf8").replace(/^gates:\s*$/m, 'gates:\n тихий: "true"\n cost: "false"') +
|
|
52
|
+
"groups:\n stack: [cost]\n", "utf8");
|
|
53
|
+
const r = aqk(p, "doctor", "--run", "--skip", "stack");
|
|
54
|
+
assert.equal(r.code, 0, `прогон без стенда покраснел:\n${r.out.slice(-600)}`);
|
|
55
|
+
assert.match(r.out, /cost/, "пропущенный гейт не назван");
|
|
56
|
+
const report = readFileSync(join(p.dir, ".aqk", "last-run.md"), "utf8");
|
|
57
|
+
assert.match(report, /^~ cost/m, `в отчёте для агента пропущенный не отмечен:\n${report}`);
|
|
58
|
+
// Опечатка в имени группы — отказ с названной причиной, а не прогон всего подряд.
|
|
59
|
+
const typo = aqk(p, "doctor", "--run", "--skip", "stak");
|
|
60
|
+
assert.notEqual(typo.code, 0);
|
|
61
|
+
assert.match(typo.out, /stak/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// --jobs N: независимые гейты параллельно. Отзыв с живого проекта 2026-09-11: 34 гейта идут
|
|
65
|
+
// друг за другом больше минуты, «а быструю проверку, которую долго ждать, перестают запускать».
|
|
66
|
+
// Параллельность — по флагу: гейты, пишущие в одни файлы (общий dist/), иначе плавали бы.
|
|
67
|
+
test("--jobs 3: три секундных гейта идут одновременно, порядок и вердикт прежние", (t) => {
|
|
68
|
+
const p = project(t, { "src/a.py": "x = 1\n", ".gitignore": "x\n" });
|
|
69
|
+
aqk(p, "init");
|
|
70
|
+
const man = join(p.dir, ".aqk.yml");
|
|
71
|
+
writeFileSync(man, readFileSync(man, "utf8").replace(/^gates:\s*$/m,
|
|
72
|
+
'gates:\n a: "sleep 1"\n b: "sleep 1; exit 1"\n c: "sleep 1"'), "utf8");
|
|
73
|
+
const t0 = Date.now();
|
|
74
|
+
// --verbose: зелёные a и c без него свёрнуты в одну строку, а проверяется порядок всех трёх.
|
|
75
|
+
const r = aqk(p, "doctor", "--run", "--jobs", "3", "--verbose");
|
|
76
|
+
const secs = (Date.now() - t0) / 1000;
|
|
77
|
+
assert.notEqual(r.code, 0, "упавший гейт b не уронил прогон");
|
|
78
|
+
const order = [...r.out.matchAll(/^\s+\S+\s+([abc])\s/gm)].map((m) => m[1]);
|
|
79
|
+
assert.deepEqual(order, ["a", "b", "c"], `порядок вывода не по объявлению: ${order}`);
|
|
80
|
+
// Три секунды последовательно; параллельно — одна плюс запуск node. Запас на медленную машину.
|
|
81
|
+
assert.ok(secs < 2.8, `прогон шёл ${secs.toFixed(1)} с — параллельности нет`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("--jobs без числа или с нулём — отказ с причиной, а не тихий последовательный прогон", (t) => {
|
|
85
|
+
const p = project(t, { "src/a.py": "x = 1\n" });
|
|
86
|
+
aqk(p, "init");
|
|
87
|
+
const r = aqk(p, "doctor", "--run", "--jobs", "0");
|
|
88
|
+
assert.notEqual(r.code, 0);
|
|
89
|
+
assert.match(r.out, /--jobs/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Коротко по умолчанию. Отзыв с живого проекта 2026-09-11: сто строк вывода, семьдесят из них —
|
|
93
|
+
// зелёные галочки, и красное теряется между ними. Свёрнуто только то, что ничего не требует;
|
|
94
|
+
// упавший и совещательный видны всегда, поимённо — по --verbose.
|
|
95
|
+
test("зелёные гейты свёрнуты в строку, упавший и совещательный видны; --verbose — все", (t) => {
|
|
96
|
+
const p = project(t, { "src/a.py": "x = 1\n", ".gitignore": "x\n" });
|
|
97
|
+
aqk(p, "init");
|
|
98
|
+
const man = join(p.dir, ".aqk.yml");
|
|
99
|
+
writeFileSync(man, readFileSync(man, "utf8").replace(/^gates:\s*$/m,
|
|
100
|
+
'gates:\n green1: "true"\n green2: "true"\n broken: "false"\n advice: "true"') + "advisory: [advice]\n", "utf8");
|
|
101
|
+
const short = aqk(p, "doctor", "--run").out;
|
|
102
|
+
assert.doesNotMatch(short, /^\s+\S+\s+green1\s/m, `зелёный гейт не свёрнут:\n${short}`);
|
|
103
|
+
assert.match(short, /^\s+\S+\s+broken\s/m, "упавший гейт спрятан");
|
|
104
|
+
assert.match(short, /^\s+\S+\s+advice\s/m, "совещательный обязан быть назван каждый прогон");
|
|
105
|
+
assert.match(short, /(прошли ещё 2|2 more passed)/, `нет строки со счётом свёрнутых:\n${short}`);
|
|
106
|
+
const full = aqk(p, "doctor", "--run", "--verbose").out;
|
|
107
|
+
assert.match(full, /^\s+\S+\s+green1\s/m, "--verbose не показал зелёный гейт");
|
|
108
|
+
assert.doesNotMatch(full, /(прошли ещё|more passed)/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Находки — пометками у строк файла в pull request, но только в GitHub Actions: в терминале
|
|
112
|
+
// строка `::error …` — мусор. Вердикт от пометок не меняется.
|
|
113
|
+
test("в GitHub Actions упавший гейт даёт пометку у строки файла; вне его — нет", (t) => {
|
|
114
|
+
// Гейт — файлом через bash: строку «echo …; exit 1» Node на Windows отдаёт cmd.exe, где точка с
|
|
115
|
+
// запятой не разделяет команды, — код 0, и проверка падала на оснастке (поймано конвейером).
|
|
116
|
+
const p = project(t, {
|
|
117
|
+
"src/a.py": "x = 1\n", ".gitignore": "x\n",
|
|
118
|
+
"bad.sh": "echo 'src/a.py:1: x — плохо'\necho ' почини: убери x'\nexit 1\n",
|
|
119
|
+
});
|
|
120
|
+
aqk(p, "init");
|
|
121
|
+
const man = join(p.dir, ".aqk.yml");
|
|
122
|
+
writeFileSync(man, readFileSync(man, "utf8").replace(/^gates:\s*$/m, 'gates:\n bad: "bash bad.sh"'), "utf8");
|
|
123
|
+
const gh = aqkEnv(p, { GITHUB_ACTIONS: "true" }, "doctor", "--run");
|
|
124
|
+
assert.notEqual(gh.code, 0, "пометки не должны менять вердикт");
|
|
125
|
+
assert.match(gh.out, /^::error file=src\/a\.py,line=1,title=aqk%3A bad::x — плохо — почини: убери x$/m, gh.out);
|
|
126
|
+
const local = aqkEnv(p, { GITHUB_ACTIONS: "" }, "doctor", "--run");
|
|
127
|
+
assert.doesNotMatch(local.out, /^::error/m, "вне GitHub Actions пометок нет");
|
|
128
|
+
});
|
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -16,6 +16,10 @@ set -uo pipefail
|
|
|
16
16
|
# Язык вывода закреплён: проверки ниже сверяют русский текст, а без этой строки они зависели бы
|
|
17
17
|
# от локали машины — на англоязычном раннере зелёное стало бы красным без единой правки в коде.
|
|
18
18
|
export AQK_LANG=ru
|
|
19
|
+
# Полный вывод: проверки этого файла написаны до того, как doctor стал сворачивать пройденное и
|
|
20
|
+
# неприменимое, и ищут строки из этих списков — в том числе ОТСУТСТВИЕ строки, которое на
|
|
21
|
+
# свёрнутом выводе прошло бы вхолостую. Свёртку проверяют файлы smoke/*.mjs, они флаг сбрасывают.
|
|
22
|
+
export AQK_VERBOSE=1
|
|
19
23
|
|
|
20
24
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
21
25
|
CLI="$ROOT/tool/program.mjs"
|
|
@@ -1903,8 +1907,12 @@ CADP="$(mktemp -d)"
|
|
|
1903
1907
|
node "$CLI" init && node "$CLI" add todo-without-task
|
|
1904
1908
|
) >/dev/null 2>&1
|
|
1905
1909
|
CTX0=$( cd "$CADP" && AQK_LANG=ru node "$CLI" context 2>&1 )
|
|
1906
|
-
|
|
1907
|
-
|
|
1910
|
+
# Путь ЧЕЛОВЕКА: переменные конвейера сняты. С 2026-09-11 в конвейере проба сама не идёт (минуты
|
|
1911
|
+
# сюрпризом в быстрой проверке), и без `env -u` эта проверка была зелёной у нас и красной в CI —
|
|
1912
|
+
# она проверяла бы окружение, а не каденцию. Поведение в конвейере сторожит units-cadence.mjs.
|
|
1913
|
+
NOCI="env -u CI -u GITHUB_ACTIONS -u GITLAB_CI -u BUILDKITE -u JENKINS_URL"
|
|
1914
|
+
RUN1=$( cd "$CADP" && $NOCI AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
|
|
1915
|
+
RUN2=$( cd "$CADP" && $NOCI AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
|
|
1908
1916
|
CTX1=$( cd "$CADP" && AQK_LANG=ru node "$CLI" context 2>&1 )
|
|
1909
1917
|
if printf '%s' "$CTX0" | grep -q "НЕИЗВЕСТНО" &&
|
|
1910
1918
|
printf '%s' "$RUN1" | grep -q "ещё не делали" &&
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// tool/selfcheck/units-annotate.mjs — находки гейтов пометками GitHub Actions.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ. Находка гейта видна только в логе конвейера, куда почти никто не заглядывает. Строка
|
|
4
|
+
// `::error file=…,line=…,title=…::…` в выводе шага становится красной пометкой у строки файла в
|
|
5
|
+
// pull request. Идея — из разбора AgentLint (research/competitors/agentlint-0xmariowu.md).
|
|
6
|
+
// Формат — документация GitHub (workflow-commands); экранирование — официальный
|
|
7
|
+
// @actions/core (packages/core/src/command.ts: escapeData, escapeProperty), а не чужой код.
|
|
8
|
+
//
|
|
9
|
+
// node --test tool/selfcheck/units-annotate.mjs
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { locate, annotations } from "../lib/annotate.mjs";
|
|
13
|
+
|
|
14
|
+
const exists = (f) => ["src/a.py", "AGENTS.md", "kit/x.sh"].includes(f);
|
|
15
|
+
|
|
16
|
+
test("locate: «файл:строка: находка» и «файл: находка»; без пути — ничего", () => {
|
|
17
|
+
assert.deepEqual(locate("src/a.py:12: print(x) в прод-коде"), { file: "src/a.py", line: 12, message: "print(x) в прод-коде" });
|
|
18
|
+
assert.deepEqual(locate("./AGENTS.md: «npm run debug» — такого скрипта нет"), { file: "AGENTS.md", line: null, message: "«npm run debug» — такого скрипта нет" });
|
|
19
|
+
assert.deepEqual(locate("src/a.py:3:7: E501 line too long"), { file: "src/a.py", line: 3, message: "E501 line too long" });
|
|
20
|
+
assert.equal(locate("прогон красный: 2 находки"), null);
|
|
21
|
+
assert.equal(locate(" почини: убери print"), null, "совет — не находка");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("упавший гейт — пометка у строки с советом; совещательный — предупреждение", () => {
|
|
25
|
+
const r = annotations([
|
|
26
|
+
{ name: "no-print", ok: false, out: "src/a.py:12: print(x)\n почини: замени на logger\n" },
|
|
27
|
+
{ name: "todo", ok: false, advisory: true, out: "src/a.py:3: TODO без задачи\n" },
|
|
28
|
+
{ name: "lint", ok: true, out: "src/a.py:1: не важно\n" },
|
|
29
|
+
], { exists });
|
|
30
|
+
assert.equal(r.lines.length, 2, "зелёный гейт пометок не даёт");
|
|
31
|
+
assert.equal(r.lines[0], "::error file=src/a.py,line=12,title=aqk%3A no-print::print(x) — почини: замени на logger");
|
|
32
|
+
assert.match(r.lines[1], /^::warning file=src\/a\.py,line=3,/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Пометка на файл, которого нет, — пометка в никуда: GitHub повесит её на `.github`, и человек
|
|
36
|
+
// будет искать то, чего не существует. Такая находка идёт общей пометкой гейта без файла.
|
|
37
|
+
test("путь, которого нет в репозитории, и находка без пути — одна общая пометка гейта", () => {
|
|
38
|
+
const r = annotations([
|
|
39
|
+
{ name: "gates-run-in-ci", ok: false, out: "гейт smoke не запускается конвейером\n" },
|
|
40
|
+
{ name: "ghost", ok: false, out: "vendor/lib.js:4: чужой файл\n" },
|
|
41
|
+
], { exists });
|
|
42
|
+
assert.deepEqual(r.lines, [
|
|
43
|
+
"::error title=aqk%3A gates-run-in-ci::гейт smoke не запускается конвейером",
|
|
44
|
+
"::error title=aqk%3A ghost::vendor/lib.js:4: чужой файл",
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("экранирование по @actions/core: % \\r \\n в тексте, ещё : и , в свойствах", () => {
|
|
49
|
+
const r = annotations([{ name: "a,b", ok: false, out: "src/a.py:1: 100% готово, но\r нет\n" }], { exists });
|
|
50
|
+
assert.equal(r.lines[0], "::error file=src/a.py,line=1,title=aqk%3A a%2Cb::100%25 готово, но%0D нет");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Потолок. В документации GitHub лимита не нашли; в обсуждении сообщества 2020 года (#26680):
|
|
54
|
+
// «10 warning annotations and 10 error annotations per step». Держимся его, остаток — числом.
|
|
55
|
+
test("не больше 10 ошибок и 10 предупреждений; остаток назван числом", () => {
|
|
56
|
+
const out = Array.from({ length: 14 }, (_, i) => `src/a.py:${i + 1}: находка ${i + 1}`).join("\n");
|
|
57
|
+
const r = annotations([{ name: "g", ok: false, out }], { exists });
|
|
58
|
+
assert.equal(r.lines.filter((l) => l.startsWith("::error")).length, 10);
|
|
59
|
+
assert.equal(r.dropped, 4);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Поймано конвейером на первом прогоне: при --since пометки вешались на находки ВНЕ дифа, потому
|
|
63
|
+
// что брались из сырого вывода. Показанное прогоном — единственный источник.
|
|
64
|
+
test("пометки берутся из показанного прогоном, а не из сырого вывода", () => {
|
|
65
|
+
const r = annotations([{ name: "g", ok: false, out: "src/a.py:1: в дифе\nkit/x.sh:2: вне дифа\n", shown: "src/a.py:1: в дифе" }], { exists });
|
|
66
|
+
assert.deepEqual(r.lines, ["::error file=src/a.py,line=1,title=aqk%3A g::в дифе"]);
|
|
67
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// незачем, а сто коммитов за день перепроверить надо.
|
|
8
8
|
import test from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { probeDue, probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan } from "../lib/cadence.mjs";
|
|
10
|
+
import { probeDue, probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan, autoProbeAllowed, parseCounts, levelLimits } from "../lib/cadence.mjs";
|
|
11
11
|
|
|
12
12
|
test("порог по умолчанию — сто коммитов, и он назван числом, а не спрятан", () => {
|
|
13
13
|
assert.equal(PROBE_EVERY, 100);
|
|
@@ -103,3 +103,42 @@ test("отметка помнит, какие гейты проба ПРОГОН
|
|
|
103
103
|
assert.equal(parseRan("at: 5\nblind: 1\n"), null);
|
|
104
104
|
assert.deepEqual([...parseRan("ran:\n")], []);
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
// Проба, встроенная в `doctor --run`, в конвейере — это +2–3 минуты в случайном прогоне раз в
|
|
108
|
+
// сто коммитов. Отзыв с живого проекта 2026-09-11: «пусть probe будет отдельной командой или
|
|
109
|
+
// задачей, а не сюрпризом внутри быстрой проверки». В конвейере она сама не запускается; у
|
|
110
|
+
// человека — по-прежнему сама: там её и надо не забыть.
|
|
111
|
+
test("проба сама не запускается в конвейере, в коротком режиме и при AQK_PROBE=0", () => {
|
|
112
|
+
assert.equal(autoProbeAllowed({ brief: false, env: {} }), true);
|
|
113
|
+
assert.equal(autoProbeAllowed({ brief: false, env: { CI: "true" } }), false);
|
|
114
|
+
assert.equal(autoProbeAllowed({ brief: false, env: { GITHUB_ACTIONS: "true" } }), false);
|
|
115
|
+
assert.equal(autoProbeAllowed({ brief: true, env: {} }), false);
|
|
116
|
+
assert.equal(autoProbeAllowed({ brief: false, env: { AQK_PROBE: "0" } }), false);
|
|
117
|
+
// Явное AQK_PROBE=1 включает и в конвейере: у всего, что решено за человека, есть способ решить иначе.
|
|
118
|
+
assert.equal(autoProbeAllowed({ brief: false, env: { CI: "true", AQK_PROBE: "1" } }), true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// --- чего уровень НЕ доказывает --------------------------------------------------
|
|
122
|
+
// Отзыв с живого проекта 2026-09-11: «после одного --run у нас AQK-3, All levels reached. При
|
|
123
|
+
// этом CI ни разу не запускался, а probe засчитывает всё подряд. Уровень меряет наличие
|
|
124
|
+
// артефактов, а не то, что они работают». Смысл ступеней не меняем — поднимать планку молча
|
|
125
|
+
// значит отнять опубликованный уровень у всех разом; вместо этого под уровнем стоит, чего он
|
|
126
|
+
// не доказывает, и что про это знает последняя проба.
|
|
127
|
+
test("отметка пробы несёт числа пойманных и недоказанных, старая — «не знаем»", () => {
|
|
128
|
+
assert.deepEqual(parseCounts("at: 5\nblind: 0\ncaught: 7\nunknown: 2\n"), { caught: 7, unknown: 2 });
|
|
129
|
+
assert.equal(parseCounts("at: 5\nblind: 0\n"), null, "старая отметка: чисел нет — это не ноль");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("строка про брак в ваших файлах говорит ровно то, что знает проба", () => {
|
|
133
|
+
assert.equal(levelLimits(null).kind, "never");
|
|
134
|
+
assert.equal(levelLimits({ state: "never", behind: null, classes: [], ran: null }).kind, "never");
|
|
135
|
+
assert.equal(levelLimits({ state: "off" }).kind, "off");
|
|
136
|
+
const blind = levelLimits({ state: "fresh", behind: 3, classes: [{ slug: "swallowed-error", file: "a.py" }], ran: new Set(), counts: { caught: 4, unknown: 0 } });
|
|
137
|
+
assert.deepEqual([blind.kind, blind.names, blind.behind], ["blind", ["swallowed-error"], 3], "непойманное — первым, даже если пойманного больше");
|
|
138
|
+
const part = levelLimits({ state: "fresh", behind: 0, classes: [], ran: new Set(), counts: { caught: 4, unknown: 2 } });
|
|
139
|
+
assert.deepEqual([part.kind, part.caught, part.unknown], ["partial", 4, 2]);
|
|
140
|
+
assert.equal(levelLimits({ state: "stale", behind: 150, classes: [], ran: new Set(), counts: { caught: 5, unknown: 0 } }).kind, "caught");
|
|
141
|
+
assert.equal(levelLimits({ state: "fresh", behind: 1, classes: [], ran: new Set(), counts: { caught: 0, unknown: 0 } }).kind, "nothing");
|
|
142
|
+
assert.equal(levelLimits({ state: "fresh", behind: 1, classes: [], ran: new Set(), counts: null }).kind, "old", "старая отметка — «подробности в пробе», а не «всё поймано»");
|
|
143
|
+
assert.equal(levelLimits({ state: "unknown", behind: null, classes: [], ran: new Set(), counts: { caught: 2, unknown: 0 } }).kind, "caught", "без git отметка всё равно читается");
|
|
144
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// node --test tool/selfcheck/units-context.mjs
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
|
-
import { contextBlock, countArbiters, parseLastRun, withHook, hasOurHook, portableSelf } from "../commands/context.mjs";
|
|
12
|
+
import { contextBlock, countArbiters, parseLastRun, withHook, hasOurHook, portableSelf, nextSteps } from "../commands/context.mjs";
|
|
13
13
|
import { CATALOGS } from "../i18n/index.mjs";
|
|
14
14
|
import { commandRows } from "../lib/core.mjs";
|
|
15
15
|
import { readFile } from "node:fs/promises";
|
|
@@ -210,3 +210,66 @@ test("выключенная и несостоявшаяся проба не в
|
|
|
210
210
|
const unknown = text({ probe: { state: "unknown", behind: null, blind: 0 } });
|
|
211
211
|
assert.match(unknown, /НЕИЗВЕСТНО|неизвестн/i, `несостоявшаяся проба не названа: ${unknown}`);
|
|
212
212
|
});
|
|
213
|
+
|
|
214
|
+
// --- «Дальше» и «Когда что» ------------------------------------------------------------
|
|
215
|
+
// ЗАЧЕМ. Блок отвечал только на «как дела»: уровень, красное, проба. Что делать ДАЛЬШЕ, `doctor`
|
|
216
|
+
// знал — три первых шага, непойманные пробой классы, чужие проверки проекта, — а в контекст это
|
|
217
|
+
// не попадало. Карта «какую команду когда» жила только в полной версии, в восьми тысячах
|
|
218
|
+
// токенов, где агент её пролистывал. Владелец 2026-09-11: «агент читает половину и не
|
|
219
|
+
// пользуется всем функционалом». Документация Claude Code (code.claude.com/docs/en/memory):
|
|
220
|
+
// «The more specific and concise your instructions, the more consistently Claude follows them»;
|
|
221
|
+
// «"Run `npm test` before committing" instead of "Test your changes"».
|
|
222
|
+
test("дальше: не больше трёх шагов, остаток назван числом, свои проверки проекта — первыми", () => {
|
|
223
|
+
const adopt = [{ name: "test", cmd: "make test" }];
|
|
224
|
+
const blind = [
|
|
225
|
+
{ slug: "swallowed-error", file: "src/a.py", command: "ruff check --select BLE ." },
|
|
226
|
+
{ slug: "no-print-in-prod", file: "src/b.py", command: null },
|
|
227
|
+
];
|
|
228
|
+
const start = [
|
|
229
|
+
{ slug: "complexity-limit", command: "ruff check --select C901 ." },
|
|
230
|
+
{ slug: "swallowed-error", command: "ruff check --select BLE ." },
|
|
231
|
+
{ slug: "dead-code", command: "vulture ." },
|
|
232
|
+
];
|
|
233
|
+
const { steps, rest } = nextSteps({ adopt, blind, start });
|
|
234
|
+
assert.deepEqual(steps.map((s) => s.kind), ["adopt", "blind", "blind"]);
|
|
235
|
+
// Класс из пробы и тот же класс в «начните с трёх» — один шаг, а не два.
|
|
236
|
+
assert.equal(rest, 2, "complexity-limit и dead-code остаются; swallowed-error уже учтён пробой");
|
|
237
|
+
assert.deepEqual(nextSteps({}), { steps: [], rest: 0 });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("дальше: блок печатает шаги по номерам, с командами; пусто — заголовка нет", () => {
|
|
241
|
+
const next = nextSteps({ start: [{ slug: "dead-code", command: "vulture ." }] });
|
|
242
|
+
const t = text({ next });
|
|
243
|
+
assert.match(t, new RegExp(T.nextTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
244
|
+
assert.match(t, /1\. .*dead-code/);
|
|
245
|
+
assert.match(t, /vulture \./, "команда, которую можно выполнить сейчас, обязана быть в шаге");
|
|
246
|
+
assert.doesNotMatch(text({ next: { steps: [], rest: 0 } }), new RegExp(T.nextTitle.slice(0, 12)));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("когда что: правило про коммит зависит от того, стоит ли хук", () => {
|
|
250
|
+
const withHook = text({ when: { hook: true } });
|
|
251
|
+
const noHook = text({ when: { hook: false } });
|
|
252
|
+
assert.match(withHook, /--no-verify/, "хук стоит — агенту сказано не обходить его");
|
|
253
|
+
assert.match(noHook, /doctor --run --since main/, "хука нет — агенту дана команда");
|
|
254
|
+
assert.notEqual(withHook, noHook);
|
|
255
|
+
// Каждое правило — «ситуация → команда»: без команды в строке правило непроверяемо.
|
|
256
|
+
for (const line of T.whenRules) assert.match(line, /`[^`]+`/, `в правиле нет команды: ${line}`);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Без манифеста `aqk add` отказывает («сначала init») — шаг «поставь запись» на таком проекте
|
|
260
|
+
// отправлял агента в ошибку. Найдено живым прогоном блока на requests 2026-09-11.
|
|
261
|
+
test("дальше: манифеста нет — первый шаг `aqk init`, остальные после него", () => {
|
|
262
|
+
const { steps } = nextSteps({ init: true, start: [{ slug: "dead-code", command: null }] });
|
|
263
|
+
assert.equal(steps[0].kind, "init");
|
|
264
|
+
assert.equal(steps[1].kind, "start");
|
|
265
|
+
assert.match(T.nextStep.init(steps[0]), /`aqk init`/);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Отзыв с живого проекта 2026-09-11: в `aqk help` у `vitals` вместо описания стояло `undefined` —
|
|
269
|
+
// команду добавили в список, а текст справки забыли. Сторож на весь список, на оба языка.
|
|
270
|
+
test("у каждой команды в справке есть описание на обоих языках", () => {
|
|
271
|
+
for (const lang of ["ru", "en"]) {
|
|
272
|
+
const empty = commandRows(CATALOGS[lang]).filter((r) => typeof r.text !== "string" || !r.text.trim()).map((r) => r.name);
|
|
273
|
+
assert.deepEqual(empty, [], `${lang}: нет описания у ${empty.join(", ")}`);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
@@ -86,3 +86,35 @@ test("typedFrom: реплика блоками собирается из тек
|
|
|
86
86
|
});
|
|
87
87
|
assert.equal(typedFrom(line)[0].text, "всегда так");
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
// ПОВТОР — СИГНАЛ, КОТОРЫЙ ДАЁТ САМ ЧЕЛОВЕК. Разбор AgentLint 2026-09-11: их SS2 сопоставляет
|
|
91
|
+
// поправку с правилом свода по словам. Замер на логах владельца: из 45 поправок к записанным
|
|
92
|
+
// правилам относятся от силы две — такой приём дал бы шум. Зато человек сам помечает повтор:
|
|
93
|
+
// «я же говорил», «опять», «снова». Из 14 таких реплик в одном проекте настоящих норм 5–6, и
|
|
94
|
+
// прежний отбор по маркерам наставления не ловил НИ ОДНОЙ.
|
|
95
|
+
test("isRepeat: «я же говорил», «опять», «снова» — повтор; «опять же» и вопрос-недоумение — нет", async () => {
|
|
96
|
+
const { isRepeat } = await import("../commands/learn.mjs");
|
|
97
|
+
assert.equal(isRepeat("нет старые задачи не трогаем я же говорил только пусть новые так работают"), true);
|
|
98
|
+
assert.equal(isRepeat("стой зачем ты сумму убрал я же не просил этого ты опять не верно меня понял"), true);
|
|
99
|
+
assert.equal(isRepeat("ок давай запишем, только опять не хочу плодить файлы"), true);
|
|
100
|
+
assert.equal(isRepeat("I told you not to touch the lockfile"), true);
|
|
101
|
+
assert.equal(isRepeat("и сказать в чате, опять же, где что смотреть"), false, "«опять же» — связка, а не повтор");
|
|
102
|
+
assert.equal(isRepeat("ок го дальше"), false);
|
|
103
|
+
// Второй прогон на тех же логах: половина шума — вопросы-недоумения и вставленные цитаты агента.
|
|
104
|
+
assert.equal(isRepeat("не понял еще нужно что то опять в env добавить?"), false, "вопрос с «опять» — недоумение, а не норма");
|
|
105
|
+
assert.equal(isRepeat("⬜ Два мира ролей — всплывёт, если снова будем трогать роли. поясни"), false, "цитата ответа агента");
|
|
106
|
+
assert.equal(isRepeat("почему ты так долго тестируешь я же говорю не нужно столько тестов?"), true, "сильная пометка держится и с вопросом");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Повтор, совпавший с правилом свода, — главное, что может сказать эта команда: правило
|
|
110
|
+
// записано, а человек всё равно поправляет. Текстом оно не держится — ему нужен сторож.
|
|
111
|
+
test("repeatedRules: повтор, совпавший с правилом свода, называет это правило", async () => {
|
|
112
|
+
const { repeatedRules } = await import("../commands/learn.mjs");
|
|
113
|
+
const entry = "# Правила\n\n- **Не плодить файлы.** Новый файл — только если без него нельзя. <!-- aqk: человек -->\n" +
|
|
114
|
+
"- **Секреты только в .env.** <!-- aqk: secrets-not-in-code -->\n- обычная строка списка без метки\n";
|
|
115
|
+
const hits = repeatedRules([{ text: "опять ты плодишь файлы, я же говорил", when: "2026-09-03" }], entry);
|
|
116
|
+
assert.equal(hits.length, 1);
|
|
117
|
+
assert.match(hits[0].rule, /Не плодить файлы/);
|
|
118
|
+
assert.equal(hits[0].arbiter, "человек");
|
|
119
|
+
assert.equal(repeatedRules([{ text: "опять не тот цвет кнопки", when: "x" }], entry).length, 0, "повтор не про правило — не сюда");
|
|
120
|
+
});
|
|
@@ -11,8 +11,8 @@ import test from "node:test";
|
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import { commandFor, verdict } from "../lib/prove.mjs";
|
|
13
13
|
import { assessLevel, layoutChecks, unknownKeys, KNOWN_KEYS, parseManifest, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
|
|
14
|
-
import { pickLang, langFromText } from "../i18n/index.mjs";
|
|
15
|
-
import { progress } from "../lib/run.mjs";
|
|
14
|
+
import { pickLang, langFromText, langFromDocs } from "../i18n/index.mjs";
|
|
15
|
+
import { progress, selectGates } from "../lib/run.mjs";
|
|
16
16
|
|
|
17
17
|
// --- строка «идёт» во время прогона ---------------------------------------------------
|
|
18
18
|
// ЗАЧЕМ. Гейт идёт через spawnSync, и строка про него печаталась только по завершении: минута
|
|
@@ -214,6 +214,26 @@ test("мусор в поле lang не молчит, а просто не дей
|
|
|
214
214
|
assert.equal(pickLang({}, { lang: "клингонский" }), "en");
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
+
// Отзыв с живого проекта 2026-09-11: свод правил и методички на русском, машина на Windows без
|
|
218
|
+
// LANG — и весь вывод английский, пока руками не впишешь `lang:`. Язык текста, который проект
|
|
219
|
+
// сам о себе написал, — свойство проекта; поэтому он выше локали и ниже манифеста.
|
|
220
|
+
test("язык свода проекта сильнее локали и слабее манифеста", () => {
|
|
221
|
+
const ruDoc = "# Правила\n\nПеред коммитом запусти `npm run check` и `make gates`. Схемы живут в " +
|
|
222
|
+
"`packages/contract`, их проверяет сервер.\n\n```sh\nnpm run typecheck && npm run lint\n```\n" +
|
|
223
|
+
"Любое изменение договора проходит через ревью и фиксируется в журнале решений.\n";
|
|
224
|
+
const enDoc = "# Rules\n\nBefore committing run `npm run check`. Schemas live in packages/contract " +
|
|
225
|
+
"and the server validates them. Every contract change goes through review and is logged.\n";
|
|
226
|
+
assert.equal(langFromDocs(ruDoc.repeat(2)), "ru", "русский свод с кодом внутри не узнан");
|
|
227
|
+
assert.equal(langFromDocs(enDoc.repeat(2)), "en");
|
|
228
|
+
assert.equal(langFromDocs("# X\n\n`npm test`\n"), "", "по трём словам язык не решается");
|
|
229
|
+
assert.equal(pickLang({ LANG: "en_US.UTF-8" }, null, "ru"), "ru");
|
|
230
|
+
assert.equal(pickLang({}, null, "ru"), "ru", "Windows без LANG: свод решает");
|
|
231
|
+
assert.equal(pickLang({ LANG: "ru_RU.UTF-8" }, null, "en"), "en");
|
|
232
|
+
assert.equal(pickLang({}, { lang: "en" }, "ru"), "en", "манифест сильнее свода");
|
|
233
|
+
assert.equal(pickLang({ AQK_LANG: "en" }, null, "ru"), "en", "переменная сильнее свода");
|
|
234
|
+
assert.equal(pickLang({ LANG: "ru_RU.UTF-8" }, null, ""), "ru", "свод молчит — решает локаль");
|
|
235
|
+
});
|
|
236
|
+
|
|
217
237
|
// Сокращённый разбор языка в i18n/index.mjs существует потому, что каталог строк нужен раньше,
|
|
218
238
|
// чем кто-либо успеет прочитать манифест целиком. Два разбора одного файла — то же, что два
|
|
219
239
|
// свода правил: через месяц они расходятся, и непонятно, какой настоящий. Сверяем ответы.
|
|
@@ -249,7 +269,7 @@ test("заявка подтверждена, когда коды правил е
|
|
|
249
269
|
test("заявка не подтверждена, когда кодов нет нигде", () => {
|
|
250
270
|
const man = parseManifest('gates:\n lint: "ruff check ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
251
271
|
const catalog = [{ slug: "no-print-in-prod", recipes: { python: "ruff check --select T20 {dir}" } }];
|
|
252
|
-
assert.deepEqual(coversUnproven(man, catalog, ""), [{ entry: "no-print-in-prod", gate: "lint", codes: ["T20"] }]);
|
|
272
|
+
assert.deepEqual(coversUnproven(man, catalog, ""), [{ entry: "no-print-in-prod", gate: "lint", codes: ["T20"], linter: "ruff", kind: "unproven" }]);
|
|
253
273
|
});
|
|
254
274
|
|
|
255
275
|
// Правило может стоять не в команде, а в конфиге линтера — это нормальный уклад, и краснеть
|
|
@@ -267,6 +287,49 @@ test("запись без кодов правил в рецепте не пор
|
|
|
267
287
|
assert.deepEqual(coversUnproven(man, catalog, ""), []);
|
|
268
288
|
});
|
|
269
289
|
|
|
290
|
+
// --- заявка сверяется правилами ТОГО линтера, которым закрыт гейт --------------------
|
|
291
|
+
// Отзыв с живого проекта 2026-09-11 (TypeScript на Biome): заявка «lint держит no-print-in-prod»
|
|
292
|
+
// всегда была «не подтверждена» — комплект искал коды ruff (T20, C901, BLE), а у Biome это
|
|
293
|
+
// noConsole, noExcessiveCognitiveComplexity, noEmptyBlockStatements, и они стояли. Поле, которое
|
|
294
|
+
// должно снимать шум, само его производило и подталкивало ставить второй линтер.
|
|
295
|
+
const NP = { slug: "no-print-in-prod", biome_rules: "noConsole",
|
|
296
|
+
recipes: { python: "ruff check --select T20 {dir}", javascript: `eslint --rule '{"no-console":"error"}' {dir}` } };
|
|
297
|
+
const TODO = { slug: "todo-without-task", biome_rules: "none", recipes: { python: "ruff check --select FIX,TD {dir}" } };
|
|
298
|
+
|
|
299
|
+
test("Biome: правило записи стоит в biome.json — заявка подтверждена", () => {
|
|
300
|
+
const man = parseManifest('gates:\n lint: "npx biome check ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
301
|
+
const biome = '{"linter":{"rules":{"suspicious":{"noConsole":"error"}}}}';
|
|
302
|
+
assert.deepEqual(coversUnproven(man, [NP], { biome }), []);
|
|
303
|
+
const miss = coversUnproven(man, [NP], { biome: '{"linter":{"enabled":true}}' });
|
|
304
|
+
assert.equal(miss[0].kind, "unproven");
|
|
305
|
+
assert.deepEqual(miss[0].codes, ["noConsole"], "человеку названо ПРАВИЛО BIOME, а не код ruff");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("линтер гейта узнаётся и через npm-скрипт", () => {
|
|
309
|
+
const man = parseManifest('gates:\n lint: "npm run lint"\ncovers:\n lint: [no-print-in-prod]\n');
|
|
310
|
+
const scripts = { lint: "biome check ." };
|
|
311
|
+
const r = coversUnproven(man, [NP], { biome: '"noConsole": "error"', scripts });
|
|
312
|
+
assert.deepEqual(r, []);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("eslint: правило берётся из рецепта записи и ищется в конфиге eslint", () => {
|
|
316
|
+
const man = parseManifest('gates:\n lint: "eslint ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
317
|
+
assert.deepEqual(coversUnproven(man, [NP], { eslint: "rules: { 'no-console': 'error' }" }), []);
|
|
318
|
+
assert.equal(coversUnproven(man, [NP], { eslint: "rules: {}" })[0].kind, "unproven");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("у линтера нет такого правила вовсе — заявка заведомо неверна, а не «не подтверждена»", () => {
|
|
322
|
+
const man = parseManifest('gates:\n lint: "biome check ."\ncovers:\n lint: [todo-without-task]\n');
|
|
323
|
+
const r = coversUnproven(man, [TODO], { biome: "{}" });
|
|
324
|
+
assert.equal(r[0].kind, "impossible");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("линтер не распознан — «не умею проверить», а не обвинение", () => {
|
|
328
|
+
const man = parseManifest('gates:\n lint: "make lint"\ncovers:\n lint: [no-print-in-prod]\n');
|
|
329
|
+
const r = coversUnproven(man, [NP], {});
|
|
330
|
+
assert.equal(r[0].kind, "unknown");
|
|
331
|
+
});
|
|
332
|
+
|
|
270
333
|
// --- строка манифеста, которую разбор не понял, не исчезает молча ---------------
|
|
271
334
|
// Найдено 2026-09-09 случайно: подсаживал падающий гейт с именем «плохой», чтобы посмотреть
|
|
272
335
|
// на строку присутствия, — и прогон вышел с НУЛЁМ. Гейт не упал: его вообще не было. Разбор
|
|
@@ -321,3 +384,34 @@ test("сбой арбитра отнимает ступень, законная
|
|
|
321
384
|
|
|
322
385
|
assert.equal(verdict([{ state: "proven" }, { state: "broken" }]).ok, false);
|
|
323
386
|
});
|
|
387
|
+
|
|
388
|
+
// --- группы гейтов и --only / --skip ----------------------------------------------------
|
|
389
|
+
// ЗАЧЕМ. Отзыв с живого проекта 2026-09-11: гейт цены меряет план на засеянной базе. Объявишь
|
|
390
|
+
// его — он валит прогон без стенда; уберёшь — promise-has-gate справедливо ругается. Пришлось
|
|
391
|
+
// выкручиваться через advisory. Прогон бывал только «всё или ничего».
|
|
392
|
+
test("выбор гейтов: --skip группой и именем, --only сужает, пропущенные названы", () => {
|
|
393
|
+
const man = parseManifest('gates:\n lint: "x"\n unit: "x"\n cost: "x"\n e2e: "x"\ngroups:\n stack: [cost, e2e]\n');
|
|
394
|
+
const names = Object.entries(man.gates);
|
|
395
|
+
const s = selectGates(names, man, { skip: ["stack"] });
|
|
396
|
+
assert.deepEqual(s.run.map(([n]) => n), ["lint", "unit"]);
|
|
397
|
+
assert.deepEqual(s.skipped, ["cost", "e2e"]);
|
|
398
|
+
const o = selectGates(names, man, { only: ["lint", "cost"] });
|
|
399
|
+
assert.deepEqual(o.run.map(([n]) => n), ["lint", "cost"]);
|
|
400
|
+
assert.deepEqual(o.skipped, ["unit", "e2e"]);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("выбор гейтов: неизвестное имя — ошибка, а не тихое «пропустили ничего»", () => {
|
|
404
|
+
const man = parseManifest('gates:\n lint: "x"\ngroups:\n stack: [lint]\n');
|
|
405
|
+
const s = selectGates(Object.entries(man.gates), man, { skip: ["stak"] });
|
|
406
|
+
assert.deepEqual(s.unknown, ["stak"]);
|
|
407
|
+
// Группа, в которой назван необъявленный гейт, — тоже названа: иначе группа молча пустеет.
|
|
408
|
+
const man2 = parseManifest('gates:\n lint: "x"\ngroups:\n stack: [cots]\n');
|
|
409
|
+
assert.deepEqual(selectGates(Object.entries(man2.gates), man2, { skip: ["stack"] }).unknown, ["cots"]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("выбор гейтов: без флагов гоняется всё, как раньше", () => {
|
|
413
|
+
const man = parseManifest('gates:\n a: "x"\n b: "x"\n');
|
|
414
|
+
const s = selectGates(Object.entries(man.gates), man, {});
|
|
415
|
+
assert.equal(s.run.length, 2);
|
|
416
|
+
assert.deepEqual(s.skipped, []);
|
|
417
|
+
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { isFix, fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
|
|
6
|
-
import { probeableGates, gatesState, extAlternatives, planProbeGates,
|
|
6
|
+
import { probeableGates, gatesState, extAlternatives, planProbeGates, isCode } from "../commands/probe.mjs";
|
|
7
|
+
import { blindAdvice } from "../lib/advice.mjs";
|
|
7
8
|
|
|
8
9
|
// Признак починки берётся из ТЕМЫ коммита, а не из тела: тема — единственное, что пишут все,
|
|
9
10
|
// и единственное, что видно в `git log --oneline`. Три написания, потому что репозитории
|