agent-quality-kit 0.14.0 → 0.15.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 +29 -5
- package/README.ru.md +31 -6
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/tool/commands/context.mjs +36 -40
- package/tool/commands/doctor.mjs +18 -26
- package/tool/commands/feedback.mjs +157 -0
- package/tool/commands/project.mjs +11 -13
- package/tool/commands/prompt.mjs +2 -1
- package/tool/i18n/en-docs.mjs +1 -0
- package/tool/i18n/en-gates.mjs +27 -0
- package/tool/i18n/en.mjs +5 -0
- package/tool/i18n/ru-docs.mjs +1 -0
- package/tool/i18n/ru-gates.mjs +28 -0
- package/tool/i18n/ru.mjs +7 -0
- package/tool/lib/ask.mjs +118 -0
- package/tool/lib/brief.mjs +17 -38
- package/tool/lib/core.mjs +7 -6
- package/tool/lib/gate-worker.mjs +4 -1
- package/tool/lib/run.mjs +120 -7
- package/tool/program.mjs +6 -0
- package/tool/selfcheck/smoke/fail-closed.test.mjs +96 -1
- package/tool/selfcheck/units-ask.mjs +85 -0
- package/tool/selfcheck/units-brief.mjs +3 -13
- package/tool/selfcheck/units-context.mjs +2 -1
- package/tool/selfcheck/units-feedback.mjs +137 -0
|
@@ -20,7 +20,7 @@ import assert from "node:assert/strict";
|
|
|
20
20
|
import { cpSync, writeFileSync, mkdirSync } from "node:fs";
|
|
21
21
|
import { join, dirname } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import { project, run, aqk } from "./_fixture.mjs";
|
|
23
|
+
import { project, run, aqk, aqkEnv } from "./_fixture.mjs";
|
|
24
24
|
|
|
25
25
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
26
26
|
const posix = (p) => String(p).replace(/\\/g, "/");
|
|
@@ -110,3 +110,98 @@ test("doctor называет человеку, сколько правил не
|
|
|
110
110
|
assert.ok(/человек/i.test(r.out),
|
|
111
111
|
`doctor не сказал про сторожа-человека ни слова, хотя таких правил здесь два.\n${r.out}`);
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
// СБОЙ САМОЙ ПРОВЕРКИ НЕ СУЖАЕТСЯ ДИФОМ. Написано ДО кода 2026-09-14.
|
|
116
|
+
//
|
|
117
|
+
// ЗАЧЕМ. Внешний разбор (аудит Runcap, 2026-09-13) искал у нас тот же класс отказа, что нашёл
|
|
118
|
+
// у соседа, и нашёл: `--since` фильтрует вывод ПО ПУТЯМ, и гейт, который НЕ СМОГ отработать,
|
|
119
|
+
// печатался зелёным, если названный им путь в диф не попал. То есть
|
|
120
|
+
//
|
|
121
|
+
// проверка сломалась → прогон говорит «чисто»
|
|
122
|
+
//
|
|
123
|
+
// — ровно та тишина, против которой построен весь стандарт, только внутри нашего же флага.
|
|
124
|
+
// Договор уже записан в `execution.mjs` и уже соблюдается в `prove`: 0 чисто, 1 находка (у
|
|
125
|
+
// vulture 3, у pylint битовой маской), ВСЁ ОСТАЛЬНОЕ — «не смогли». Сужать дифом можно только
|
|
126
|
+
// НАХОДКУ: у сбоя нет места в коде, которое он называет, — есть только сам сбой.
|
|
127
|
+
// Цвет снимается: в терминале между знаком и именем гейта стоит управляющая
|
|
128
|
+
// последовательность, и проверка, написанная «по глазам», не совпала бы ни разу.
|
|
129
|
+
const plain = (s) => String(s).replace(/\u001b\[[0-9;]*m/g, "");
|
|
130
|
+
|
|
131
|
+
function scopeProject(t, { code, path }) {
|
|
132
|
+
const p = project(t, {
|
|
133
|
+
"AGENTS.md": "# проект\n",
|
|
134
|
+
".aqk.yml": 'aqk: 1\nentry:\n - AGENTS.md\ngates:\n broken: "bash broken.sh"\n',
|
|
135
|
+
// Команда — ФАЙЛОМ, а не строкой `echo …; exit 2`: на Windows строку исполняет cmd.exe,
|
|
136
|
+
// где `;` не разделитель, и гейт выходит нулём. Поймано конвейером 2026-09-11.
|
|
137
|
+
"broken.sh": `echo "${path}: infrastructure failure"\nexit ${code}\n`,
|
|
138
|
+
});
|
|
139
|
+
run(p, "git", ["add", "-A"]);
|
|
140
|
+
run(p, "git", ["commit", "-qm", "база"]);
|
|
141
|
+
// Изменение ПОСЛЕ коммита: ровно один файл в дифе, и путь из вывода гейта в него не входит.
|
|
142
|
+
mkdirSync(join(p.dir, "src"), { recursive: true });
|
|
143
|
+
writeFileSync(join(p.dir, "src", "a.mjs"), "export const a = 1;\n", "utf8");
|
|
144
|
+
return p;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
test("гейт, выпавший с непонятным кодом, остаётся красным и при --since", (t) => {
|
|
148
|
+
const p = scopeProject(t, { code: 2, path: "outside/old.mjs:1" });
|
|
149
|
+
const r = aqk(p, "doctor", "--run", "--since", "HEAD");
|
|
150
|
+
assert.match(plain(r.out), /✘\s+broken/,
|
|
151
|
+
"гейт вышел с кодом 2 — «не смогли проверить» — и показан зелёным, потому что названный " +
|
|
152
|
+
`им путь не попал в диф. Вывод:\n${r.out}`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("контроль: НАХОДКА вне дифа по-прежнему сужается", (t) => {
|
|
156
|
+
const p = scopeProject(t, { code: 1, path: "outside/old.mjs:1" });
|
|
157
|
+
const r = aqk(p, "doctor", "--run", "--since", "HEAD");
|
|
158
|
+
assert.match(plain(r.out), /✔\s+broken/,
|
|
159
|
+
`находка вне дифа обязана сужаться — иначе --since не делает ничего. Вывод:\n${r.out}`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("контроль: находка ВНУТРИ дифа остаётся красной", (t) => {
|
|
163
|
+
const p = scopeProject(t, { code: 1, path: "src/a.mjs:1" });
|
|
164
|
+
const r = aqk(p, "doctor", "--run", "--since", "HEAD");
|
|
165
|
+
assert.match(plain(r.out), /✘\s+broken/, `находка в изменённом файле пропала. Вывод:\n${r.out}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("контроль: нулевой код остаётся «чисто»", (t) => {
|
|
169
|
+
const p = scopeProject(t, { code: 0, path: "outside/old.mjs:1" });
|
|
170
|
+
const r = aqk(p, "doctor", "--run", "--since", "HEAD", "--verbose");
|
|
171
|
+
assert.match(plain(r.out), /✔\s+broken/, `нулевой код перестал означать «чисто». Вывод:\n${r.out}`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("параллельный прогон говорит о сбое то же самое, что одиночный", (t) => {
|
|
175
|
+
const p = scopeProject(t, { code: 2, path: "outside/old.mjs:1" });
|
|
176
|
+
const one = plain(aqk(p, "doctor", "--run", "--since", "HEAD").out);
|
|
177
|
+
const many = plain(aqk(p, "doctor", "--run", "--since", "HEAD", "--jobs", "2").out);
|
|
178
|
+
const line = (out) => (out.split("\n").find((l) => /broken/.test(l) && /[✘✔!]/.test(l)) || "").trim();
|
|
179
|
+
assert.match(line(many), /✘/, `при --jobs сбой не покраснел. Вывод:\n${many}`);
|
|
180
|
+
assert.equal(line(many).replace(/\d+\.\ds/, ""), line(one).replace(/\d+\.\ds/, ""),
|
|
181
|
+
`одно и то же событие названо по-разному в одиночном и параллельном прогоне:\n${line(one)}\n${line(many)}`);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Совещательный — это «показать, но не останавливать». Причина остановки тут ни при чём: сбой
|
|
185
|
+
// такой проверки обязан быть НАЗВАН и обязан не ронять прогон. Раньше на таймауте ронял.
|
|
186
|
+
test("сбой СОВЕЩАТЕЛЬНОГО гейта назван, но прогон не уронен", (t) => {
|
|
187
|
+
const p = scopeProject(t, { code: 2, path: "outside/old.mjs:1" });
|
|
188
|
+
writeFileSync(join(p.dir, ".aqk.yml"),
|
|
189
|
+
'aqk: 1\nentry:\n - AGENTS.md\ngates:\n broken: "bash broken.sh"\nadvisory:\n - broken\n', "utf8");
|
|
190
|
+
const r = aqk(p, "doctor", "--run", "--since", "HEAD");
|
|
191
|
+
const out = plain(r.out);
|
|
192
|
+
assert.match(out, /!\s+broken/, `совещательный сбой не назван вовсе. Вывод:\n${out}`);
|
|
193
|
+
assert.match(out, /не смогли проверить/, `сбой назван находкой, а не «не смогли». Вывод:\n${out}`);
|
|
194
|
+
assert.doesNotMatch(out, /красных гейтов/,
|
|
195
|
+
`совещательный уронил прогон — список advisory перестал что-либо значить. Вывод:\n${out}`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Пометка в pull request у сбоя — ОБЩАЯ, без file=: путь из вывода упавшего гейта назвал не
|
|
199
|
+
// виноватого, а того, до кого гейт успел дойти.
|
|
200
|
+
test("в GitHub Actions сбой помечается гейтом, а не строкой чужого файла", (t) => {
|
|
201
|
+
const p = scopeProject(t, { code: 2, path: "outside/old.mjs:1" });
|
|
202
|
+
const r = aqkEnv(p, { GITHUB_ACTIONS: "true" }, "doctor", "--run", "--since", "HEAD");
|
|
203
|
+
const ann = plain(r.out).split("\n").filter((l) => l.startsWith("::error"));
|
|
204
|
+
assert.equal(ann.length, 1, `пометок про сбой не одна:\n${ann.join("\n")}`);
|
|
205
|
+
assert.doesNotMatch(ann[0], /file=/, `пометка повешена на файл, которого сбой не касается: ${ann[0]}`);
|
|
206
|
+
assert.match(ann[0], /title=aqk%3A broken/, `пометка не называет гейт: ${ann[0]}`);
|
|
207
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// tool/selfcheck/units-ask.mjs — КОГДА КОМПЛЕКТ ОБРАЩАЕТСЯ К ЧЕЛОВЕКУ: ограничитель обращений.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ЭТОТ ФАЙЛ И ЭТОТ МОДУЛЬ. Знание «мы это человеку уже показывали» жило в комплекте
|
|
4
|
+
// ДВАЖДЫ и по-разному: `~/.config/aqk/feedback-shown` — файл-флаг, раз на машину, заведён в
|
|
5
|
+
// core.mjs и прочитан в project.mjs; `.aqk/advice-shown` и `.aqk/update-checked` — отметка
|
|
6
|
+
// временем плюс `adviceDue()` на сутки, заведённые в brief.mjs. Одно решение — «не долби» — в
|
|
7
|
+
// двух местах с двумя форматами хранения. Третье обращение (просьба об отзыве по делу) завело
|
|
8
|
+
// бы третий формат, и дальше они расходятся молча: именно так и умирает ограничитель.
|
|
9
|
+
//
|
|
10
|
+
// Поэтому здесь один модуль на все обращения и таблица видов. Вид объявляет ДВЕ вещи, и обе
|
|
11
|
+
// нельзя угадать по имени: где живёт отметка (проект или дом) и как часто можно повторять.
|
|
12
|
+
//
|
|
13
|
+
// node --test tool/selfcheck/units-ask.mjs
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { askDue, askFile, ASKS, ASK_FILES } from "../lib/ask.mjs";
|
|
17
|
+
import { RUNTIME_FILES } from "../lib/core.mjs";
|
|
18
|
+
|
|
19
|
+
const NOW = Date.parse("2026-09-14T20:00:00Z");
|
|
20
|
+
|
|
21
|
+
// --- «раз в сутки»: совет и проверка версии ----------------------------------
|
|
22
|
+
// Тот же договор, что был у adviceDue, и он обязан остаться прежним: совет на каждом коммите
|
|
23
|
+
// превращается в шум, а шум пролистывают вместе с настоящими находками.
|
|
24
|
+
test("суточное обращение: не показывали — пора", () => {
|
|
25
|
+
assert.equal(askDue("advice", null, NOW), true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("суточное обращение: час назад — рано, сутки назад — пора", () => {
|
|
29
|
+
assert.equal(askDue("advice", "2026-09-14T19:00:00Z", NOW), false);
|
|
30
|
+
assert.equal(askDue("advice", "2026-09-13T19:00:00Z", NOW), true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Испорченная отметка — это «не знаем, когда показывали». Молчать из-за нечитаемого файла
|
|
34
|
+
// состояния значит потерять обращение навсегда и не сказать почему.
|
|
35
|
+
test("испорченная отметка читается как «не знаем» и обращение показывается", () => {
|
|
36
|
+
assert.equal(askDue("advice", "не дата", NOW), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// --- «один раз»: просьба об отзыве -------------------------------------------
|
|
40
|
+
// Разовое обращение отличается от суточного не числом, а СМЫСЛОМ: его нельзя повторить никогда,
|
|
41
|
+
// сколько бы времени ни прошло. Считать его «раз в очень много часов» — значит однажды получить
|
|
42
|
+
// повтор у человека, который поставил комплект год назад.
|
|
43
|
+
test("разовое обращение: без отметки — пора, с любой отметкой — никогда", () => {
|
|
44
|
+
assert.equal(askDue("install", null, NOW), true);
|
|
45
|
+
assert.equal(askDue("install", "shown\n", NOW), false);
|
|
46
|
+
assert.equal(askDue("install", "2020-01-01T00:00:00Z", NOW), false);
|
|
47
|
+
assert.equal(askDue("install", "мусор", NOW), false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// --- опечатка в виде обращения -----------------------------------------------
|
|
51
|
+
// Неизвестный вид обязан падать, а не превращаться в «показывать всегда» или «не показывать
|
|
52
|
+
// никогда». Оба умолчания — молчаливый выбор за человека, и оба неверны в половине случаев.
|
|
53
|
+
test("неизвестный вид обращения — отказ, а не тихое умолчание", () => {
|
|
54
|
+
assert.throws(() => askDue("опечатка", null, NOW), /опечатка/);
|
|
55
|
+
assert.throws(() => askFile("опечатка", { project: "/p", home: "/h" }), /опечатка/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- где лежит отметка --------------------------------------------------------
|
|
59
|
+
// Разовая на машину отметка НЕ ИМЕЕТ ПРАВА лежать в проекте: внутри `.aqk/` она либо уедет в
|
|
60
|
+
// чужой git как наш мусор, либо пропадёт при `init --force` — и то и другое врёт о том, видел
|
|
61
|
+
// человек обращение или нет. Довод записан в core.mjs с 2026-09; проверки у него не было.
|
|
62
|
+
test("разовая на машину отметка лежит вне проекта", () => {
|
|
63
|
+
const p = askFile("install", { project: "/p", home: "/h" });
|
|
64
|
+
assert.ok(!p.startsWith("/p"), `отметка машины уехала в проект: ${p}`);
|
|
65
|
+
assert.ok(p.startsWith("/h"), `отметка машины легла мимо дома: ${p}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("отметки про этот проект лежат в проекте", () => {
|
|
69
|
+
for (const kind of Object.keys(ASKS).filter((k) => ASKS[k].where === "project")) {
|
|
70
|
+
const p = askFile(kind, { project: "/p", home: "/h" });
|
|
71
|
+
assert.ok(p.startsWith("/p"), `${kind}: отметка проекта легла мимо проекта: ${p}`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// --- СВЯЗЬ ДВУХ СПИСКОВ, КОТОРУЮ ИНАЧЕ НЕ СТОРОЖИТ НИКТО ----------------------
|
|
76
|
+
// Отметка, лежащая в `.aqk/` и не названная в RUNTIME_FILES, не попадёт в .gitignore — и уедет
|
|
77
|
+
// в чужой коммит. Это уже случалось с `.aqk/last-run.md` на живом проекте 2026-09-11, и оттуда
|
|
78
|
+
// же взялся сам список. Список и таблица видов — два разных файла, и связь между ними держится
|
|
79
|
+
// только этой проверкой.
|
|
80
|
+
test("каждая отметка проекта названа в RUNTIME_FILES — иначе уедет в чужой git", () => {
|
|
81
|
+
for (const f of ASK_FILES) {
|
|
82
|
+
assert.ok(RUNTIME_FILES.includes(f),
|
|
83
|
+
`«${f}» не назван в RUNTIME_FILES: init не положит его в .gitignore, и отметка уедет в коммит`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// node --test tool/selfcheck/units-brief.mjs
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
|
-
import { briefLine,
|
|
11
|
+
import { briefLine, pickAdvice, updateNotice, updateWanted } from "../lib/brief.mjs";
|
|
12
12
|
import { CATALOGS } from "../i18n/index.mjs";
|
|
13
13
|
|
|
14
14
|
const T = CATALOGS.ru;
|
|
@@ -35,18 +35,8 @@ test("невычисленный уровень не выдумывается",
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
// --- ограничитель совета -----------------------------------------------------
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
test("совет не повторяется чаще раза в сутки", () => {
|
|
41
|
-
const now = Date.parse("2026-09-08T20:00:00Z");
|
|
42
|
-
assert.equal(adviceDue(null, now), true, "первый раз показывается");
|
|
43
|
-
assert.equal(adviceDue("2026-09-08T19:00:00Z", now), false, "час назад — рано");
|
|
44
|
-
assert.equal(adviceDue("2026-09-07T19:00:00Z", now), true, "сутки прошли");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("испорченная отметка времени не мешает показать совет", () => {
|
|
48
|
-
assert.equal(adviceDue("не дата", Date.parse("2026-09-08T20:00:00Z")), true);
|
|
49
|
-
});
|
|
38
|
+
// Сам ограничитель переехал в `ask.mjs` — он общий на все обращения комплекта к человеку, и
|
|
39
|
+
// проверки на него лежат в units-ask.mjs. Здесь остаётся то, что про краткий режим.
|
|
50
40
|
|
|
51
41
|
// --- выбор совета ------------------------------------------------------------
|
|
52
42
|
// Один совет за раз, а не список: список читается как «у вас всё плохо» и не помогает выбрать.
|
|
@@ -9,7 +9,8 @@
|
|
|
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,
|
|
12
|
+
import { contextBlock, countArbiters, withHook, hasOurHook, portableSelf, nextSteps } from "../commands/context.mjs";
|
|
13
|
+
import { parseLastRun } from "../lib/run.mjs";
|
|
13
14
|
import { CATALOGS } from "../i18n/index.mjs";
|
|
14
15
|
import { commandRows } from "../lib/core.mjs";
|
|
15
16
|
import { readFile } from "node:fs/promises";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// tool/selfcheck/units-feedback.mjs — О ЧЁМ комплект просит человека и что кладёт в отчёт.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ЭТО ВООБЩЕ. Замер 2026-09-14: 1342 скачивания в неделю в npm, из них ни одного
|
|
4
|
+
// пользователя — версии качаются равномерно, включая прожившую двадцать пять минут, то есть это
|
|
5
|
+
// зеркала и сканеры. На GitHub за две недели семь уникальных посетителей, две звезды, ноль чужих
|
|
6
|
+
// комментариев за всё время. Обратной связи нет не потому, что люди молчат, — её не у кого
|
|
7
|
+
// просить, и просить мы не умеем: единственная просьба печаталась при `init`, то есть ДО того,
|
|
8
|
+
// как комплект сделал хоть что-то полезное.
|
|
9
|
+
//
|
|
10
|
+
// ПОЭТОМУ ЗДЕСЬ ДВЕ ЧИСТЫЕ ФУНКЦИИ И ДВА ПРАВИЛА.
|
|
11
|
+
// 1. Просим, ТОЛЬКО когда есть что рассказать. «Оставьте отзыв» без содержания — шум, а шум
|
|
12
|
+
// выключают вместе с хуком, в котором он приехал. Самое ценное — «AQK не смог»: жалоба
|
|
13
|
+
// даётся людям легче похвалы, и она же говорит нам, где инструмент врёт.
|
|
14
|
+
// 2. Отчёт не несёт ни путей, ни кода. Человек отправляет его сам, и он обязан видеть глазами
|
|
15
|
+
// всё, что отправляет, — иначе первый же внимательный читатель назовёт это телеметрией.
|
|
16
|
+
//
|
|
17
|
+
// node --test tool/selfcheck/units-feedback.mjs
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { feedbackAsk, reportText, issueUrl, askLine, feedbackWanted } from "../commands/feedback.mjs";
|
|
21
|
+
|
|
22
|
+
// --- о чём просим ------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
// Рассказывать нечего — не просим. Это главное правило: просьба без содержания превращает
|
|
25
|
+
// ограничитель в шум, а следующую просьбу — в то, что пролистывают не читая.
|
|
26
|
+
test("сказать нечего — просьбы нет", () => {
|
|
27
|
+
assert.equal(feedbackAsk({ cannot: [], blind: [], red: [] }), null);
|
|
28
|
+
assert.equal(feedbackAsk({}), null);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// «Не смогли проверить» — самое ценное, что у нас бывает: это отказ прибора, а не находка о
|
|
32
|
+
// коде. Такое человек рассказывает охотнее, чем похвалу, и чинить надо именно это.
|
|
33
|
+
test("«не смогли проверить» важнее всего остального", () => {
|
|
34
|
+
const a = feedbackAsk({ cannot: ["smoke"], blind: ["swallowed-error"], red: ["lint"] });
|
|
35
|
+
assert.equal(a.reason, "cannot");
|
|
36
|
+
assert.deepEqual(a.names, ["smoke"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("слепой класс важнее просто красного гейта", () => {
|
|
40
|
+
const a = feedbackAsk({ cannot: [], blind: ["swallowed-error"], red: ["lint"] });
|
|
41
|
+
assert.equal(a.reason, "blind");
|
|
42
|
+
assert.deepEqual(a.names, ["swallowed-error"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Красный гейт — это момент, когда комплект принёс пользу: он поймал то, ради чего стоит.
|
|
46
|
+
test("пойманное красным — тоже повод, но последний", () => {
|
|
47
|
+
const a = feedbackAsk({ cannot: [], blind: [], red: ["lint", "smoke"] });
|
|
48
|
+
assert.equal(a.reason, "red");
|
|
49
|
+
assert.deepEqual(a.names, ["lint", "smoke"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// --- что в отчёте ------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
// ПУТЕЙ НЕ БЫВАЕТ. Проба знает файл, в который подсаживала брак («blind-class: slug path»), и
|
|
55
|
+
// соблазн положить его в отчёт велик — он же объясняет находку. Нельзя: путь внутри чужого
|
|
56
|
+
// репозитория рассказывает о чужом проекте больше, чем его владелец собирался рассказать.
|
|
57
|
+
test("отчёт называет класс, но не файл, в котором его нашли", () => {
|
|
58
|
+
const text = reportText({
|
|
59
|
+
version: "0.15.0", node: "v22.0.0", platform: "linux", level: 3,
|
|
60
|
+
langs: ["javascript"], gates: 31, red: ["lint"], cannot: [],
|
|
61
|
+
blind: [{ slug: "swallowed-error", file: "src/secret/internal.js" }],
|
|
62
|
+
}).join("\n");
|
|
63
|
+
assert.match(text, /swallowed-error/, "класс не назван — отчёт бесполезен");
|
|
64
|
+
assert.doesNotMatch(text, /src\/secret\/internal\.js/, "в отчёт попал путь из чужого репозитория");
|
|
65
|
+
assert.doesNotMatch(text, /internal/, "в отчёт попал кусок пути из чужого репозитория");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// МЕТКА ИСТОЧНИКА. Отзыв в GitHub ничем не связан с просьбой, и понять, работает ли канал,
|
|
69
|
+
// иначе нельзя. Метка видна человеку в том же тексте, который он отправляет: это не слежка,
|
|
70
|
+
// а подпись. Слежки у комплекта нет и не будет — исходящий запрос ровно один, про версию.
|
|
71
|
+
test("отчёт подписан командой и версией — иначе не узнать, работает ли канал", () => {
|
|
72
|
+
const text = reportText({ version: "0.15.0", node: "v22.0.0", platform: "linux" }).join("\n");
|
|
73
|
+
assert.match(text, /aqk feedback/, "нет метки источника");
|
|
74
|
+
assert.match(text, /0\.15\.0/, "нет версии");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Незнание называется словом, а не пропускается: пустая строка в отчёте читается как «всё
|
|
78
|
+
// хорошо» — тот же порок, против которого написан весь комплект, только в нашем же письме.
|
|
79
|
+
test("чего не знаем — сказано словом, а не пустым местом", () => {
|
|
80
|
+
const text = reportText({ version: "0.15.0", node: "v22.0.0", platform: "linux", level: null }).join("\n");
|
|
81
|
+
assert.doesNotMatch(text, /уровень:\s*$/m, "уровень пропущен молча");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// --- ссылка ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
// Предзаполнение задачи параметрами `title` и `body` описано в документации GitHub (Creating an
|
|
87
|
+
// issue from a URL query, сверено 2026-09-14). Кодирование обязательно: в теле переносы строк,
|
|
88
|
+
// решётки и пробелы, и незакодированная ссылка обрывается на первом же из них.
|
|
89
|
+
test("ссылка предзаполнена и закодирована", () => {
|
|
90
|
+
const u = issueUrl("https://github.com/o/r", "Заголовок про AQK", "строка\nвторая #2");
|
|
91
|
+
assert.ok(u.startsWith("https://github.com/o/r/issues/new?"), `не та ссылка: ${u}`);
|
|
92
|
+
assert.match(u, /title=/);
|
|
93
|
+
assert.match(u, /body=/);
|
|
94
|
+
assert.doesNotMatch(u, /\n/, "перенос строки не закодирован — ссылка оборвётся");
|
|
95
|
+
assert.doesNotMatch(u, /#2/, "решётка не закодирована — всё после неё отвалится как якорь");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// --- как это звучит ----------------------------------------------------------
|
|
99
|
+
// Строку строит feedback.mjs, а печатают её `context` (агенту) и `doctor` (человеку): один
|
|
100
|
+
// текст на два места. Собери его в каждом месте отдельно — и через месяц они разойдутся, как
|
|
101
|
+
// разошлись бы `context` и `prompt` без общего `readAdvice`.
|
|
102
|
+
test("строка называет причину, имена и команду", () => {
|
|
103
|
+
const s = askLine({ reason: "cannot", names: ["smoke"] }, "npx agent-quality-kit");
|
|
104
|
+
assert.match(s, /smoke/, "не названо, что именно не смогли");
|
|
105
|
+
assert.match(s, /npx agent-quality-kit feedback/, "нет команды — просьбу нечем выполнить");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// АГЕНТУ — ОТДЕЛЬНАЯ ОГОВОРКА. Мы кладём строку в контекст ЧУЖОГО агента, и он исполнит то, что
|
|
109
|
+
// там написано. Без «не настаивай» это превращается в рекламу в чужом окне — и хук, которым она
|
|
110
|
+
// приехала, снесут в первый же день вместе со всей затеей.
|
|
111
|
+
test("агенту сказано не настаивать, человеку — как выключить", () => {
|
|
112
|
+
const forAgent = askLine({ reason: "red", names: ["lint"] }, "aqk", { agent: true });
|
|
113
|
+
const forHuman = askLine({ reason: "red", names: ["lint"] }, "aqk", { agent: false });
|
|
114
|
+
assert.notEqual(forAgent, forHuman, "агенту и человеку сказано одно и то же");
|
|
115
|
+
assert.match(forHuman, /AQK_FEEDBACK=0/, "человеку не сказано, как это выключить");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("просить нечего — строки нет, а не пустая", () => {
|
|
119
|
+
assert.equal(askLine(null, "aqk"), null);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// У всего, что случается само, обязан быть выключатель — то же правило, что у совета
|
|
123
|
+
// (AQK_ADVICE=0), у пробы (AQK_PROBE=0) и у проверки версии (AQK_UPDATE=0).
|
|
124
|
+
test("выключатель уважается", () => {
|
|
125
|
+
assert.equal(feedbackWanted({ AQK_FEEDBACK: "0" }), false);
|
|
126
|
+
assert.equal(feedbackWanted({}), true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ПРОБА НЕ ДЕЛАЛАСЬ — «НЕИЗВЕСТНО», А НЕ «НЕТ». Поймано на первом же живом запуске команды:
|
|
130
|
+
// в письме стояло «классы, которые здесь не ловит никто: нет», хотя проба не запускалась ни
|
|
131
|
+
// разу. То есть наш собственный отчёт об инструменте против тишины сам выдавал незнание за
|
|
132
|
+
// чистоту — и автор, читающий такое письмо, сделал бы неверный вывод о чужом проекте.
|
|
133
|
+
test("проба не делалась — в отчёте «неизвестно», а не «нет»", () => {
|
|
134
|
+
const text = reportText({ version: "0.15.0", blind: null }).join("\n");
|
|
135
|
+
assert.doesNotMatch(text, /не ловит никто: нет/, "незнание выдано за чистоту");
|
|
136
|
+
assert.match(text, /не ловит никто: неизвестно/, "незнание не названо словом");
|
|
137
|
+
});
|