agent-quality-kit 0.2.5 → 0.3.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 +17 -0
- package/README.ru.md +16 -0
- package/kit/gates/_skip.sh +31 -3
- package/kit/gates/commit-explains-itself/gate.yml +1 -0
- package/kit/gates/complexity-limit/gate.yml +1 -0
- package/kit/gates/dead-code/gate.yml +1 -0
- package/kit/gates/deps-are-pinned/gate.yml +1 -0
- package/kit/gates/duplicate-code/gate.yml +1 -0
- package/kit/gates/entry-links-exist/gate.yml +1 -0
- package/kit/gates/file-size-limit/gate.yml +1 -0
- package/kit/gates/gate-has-samples/gate.yml +1 -0
- package/kit/gates/gates-are-runnable/gate.yml +1 -0
- package/kit/gates/gates-run-in-ci/gate.yml +1 -0
- package/kit/gates/lesson-has-outcome/gate.yml +1 -0
- package/kit/gates/no-print-in-prod/check.sh +3 -1
- package/kit/gates/no-print-in-prod/gate.yml +1 -0
- package/kit/gates/secrets-not-in-code/gate.yml +1 -0
- package/kit/gates/swallowed-error/gate.yml +1 -0
- package/kit/gates/todo-without-task/gate.yml +1 -0
- package/package.json +1 -1
- package/tool/commands/doctor.mjs +38 -41
- package/tool/commands/gates.mjs +103 -109
- package/tool/commands/project.mjs +84 -85
- package/tool/commands/report.mjs +194 -0
- package/tool/i18n/en.mjs +423 -0
- package/tool/i18n/index.mjs +33 -0
- package/tool/i18n/ru.mjs +424 -0
- package/tool/i18n/templates-en.mjs +164 -0
- package/tool/i18n/templates-ru.mjs +170 -0
- package/tool/lib/core.mjs +5 -1
- package/tool/lib/manifest.mjs +12 -31
- package/tool/lib/repo.mjs +45 -21
- package/tool/lib/templates.mjs +38 -182
- package/tool/program.mjs +31 -9
- package/tool/selfcheck/gates.sh +7 -1
- package/tool/selfcheck/smoke.sh +97 -0
- package/tool/selfcheck/units.mjs +80 -5
package/tool/program.mjs
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
import { realpathSync } from "node:fs";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { c, SELF } from "./lib/core.mjs";
|
|
20
|
+
import { L } from "./i18n/index.mjs";
|
|
20
21
|
import { cmdInit, cmdNote, cmdBlob, cmdStart } from "./commands/project.mjs";
|
|
21
22
|
import { cmdDoctor } from "./commands/doctor.mjs";
|
|
22
23
|
import { cmdAdd, cmdNew, cmdRatchet, cmdFind, cmdWhy } from "./commands/gates.mjs";
|
|
24
|
+
import { cmdReport } from "./commands/report.mjs";
|
|
23
25
|
|
|
24
26
|
// Разбор аргументов выполняется только при запуске файла как программы. При импорте —
|
|
25
27
|
// а так его читают модульные проверки tool/selfcheck/units.mjs — CLI запускаться не должен.
|
|
@@ -63,19 +65,39 @@ if (IS_MAIN) {
|
|
|
63
65
|
case "blob":
|
|
64
66
|
await cmdBlob();
|
|
65
67
|
break;
|
|
66
|
-
|
|
68
|
+
case "report":
|
|
69
|
+
await cmdReport();
|
|
70
|
+
break;
|
|
71
|
+
default: {
|
|
72
|
+
// Ширина колонки считается, а не подбирается пробелами: строки в двух языках разной
|
|
73
|
+
// длины, и вручную выровненная справка на втором языке разъезжается.
|
|
74
|
+
const h = L.help;
|
|
75
|
+
const rows = [
|
|
76
|
+
[`${SELF} init`, h.init],
|
|
77
|
+
[`${SELF} init --force`, h.initForce],
|
|
78
|
+
[`${SELF} start`, h.start],
|
|
79
|
+
[`${SELF} doctor`, h.doctor],
|
|
80
|
+
[`${SELF} doctor --run`, h.doctorRun],
|
|
81
|
+
[`${SELF} add ${h.name}`, h.add],
|
|
82
|
+
[`${SELF} find "…"`, h.find],
|
|
83
|
+
[`${SELF} why "…"`, h.why],
|
|
84
|
+
[`${SELF} ratchet ${h.name}`, h.ratchet],
|
|
85
|
+
[`${SELF} new ${h.name}`, h.new],
|
|
86
|
+
[`${SELF} note "…"`, h.note],
|
|
87
|
+
[`${SELF} blob`, h.blob],
|
|
88
|
+
[`${SELF} report`, h.report],
|
|
89
|
+
];
|
|
90
|
+
const width = Math.max(...rows.map(([cmdText]) => cmdText.length));
|
|
91
|
+
const lines = rows.map(([cmdText, text]) => ` ${c.bold(cmdText.padEnd(width))} ${text}`);
|
|
67
92
|
console.log(`
|
|
68
|
-
${c.bold("aqk")} —
|
|
93
|
+
${c.bold("aqk")} — ${h.tagline}
|
|
69
94
|
|
|
70
|
-
|
|
71
|
-
${c.bold(`${SELF} init --force`)} перезаписать уже существующие файлы
|
|
72
|
-
${c.bold(`${SELF} start`)} кода ещё нет: сторожа дня 0 и порядок работы
|
|
73
|
-
${c.bold(`${SELF} doctor`)} проверить, что разложено и чего не хватает\n ${c.bold(`${SELF} doctor --run`)} ещё и запустить объявленные гейты\n ${c.bold(`${SELF} add`)} <имя> поставить гейт из каталога в проект\n ${c.bold(`${SELF} find`)} "…" есть ли уже такой гейт — сверка по намерению\n ${c.bold(`${SELF} why`)} "…" поймал ошибку — почему её не поймал сторож\n ${c.bold(`${SELF} ratchet`)} <имя> храповик: старые нарушения — долг, новые не пускать\n ${c.bold(`${SELF} new`)} <имя> заготовка своего гейта для каталога
|
|
74
|
-
${c.bold(`${SELF} note`)} "…" записать урок в общий журнал шишек
|
|
75
|
-
${c.bold(`${SELF} blob`)} собрать методички в один файл GOD_AI.md
|
|
95
|
+
${lines.join("\n")}
|
|
76
96
|
|
|
77
|
-
${c.dim(
|
|
97
|
+
${c.dim(h.noInstall)}
|
|
98
|
+
${c.dim(h.language)}
|
|
78
99
|
`);
|
|
79
100
|
process.exit(cmd ? 1 : 0);
|
|
101
|
+
}
|
|
80
102
|
}
|
|
81
103
|
}
|
package/tool/selfcheck/gates.sh
CHANGED
|
@@ -33,9 +33,15 @@ for GATE in "$CAT"/*/; do
|
|
|
33
33
|
PROOF="$(field "$YML" proof)"
|
|
34
34
|
[ -n "$INTENT" ] || bad "$SLUG: пустое поле intent — по нему идёт дедупликация"
|
|
35
35
|
|
|
36
|
+
# Английское намерение обязательно для НАШЕГО каталога: программа печатает его тем, кто
|
|
37
|
+
# пришёл с англоязычной площадки, и запись без перевода показала бы им кириллицу. Для
|
|
38
|
+
# чужой записи, принесённой со стороны, поле остаётся необязательным — программа тогда
|
|
39
|
+
# покажет то, что есть. Требовать перевод от вкладчика значит закрыть вклад половине.
|
|
40
|
+
[ -n "$(field "$YML" intent_en)" ] || bad "$SLUG: нет intent_en — намерение на английском"
|
|
41
|
+
|
|
36
42
|
# Заготовка от `aqk new` не должна проехать как запись: незаполненный гейт — мёртвое
|
|
37
43
|
# правило, а мёртвое правило учит игнорировать и живые.
|
|
38
|
-
if grep -
|
|
44
|
+
if grep -qE 'ЗАПОЛНИ|FILL IN|FILL_IN' "$YML" "$GATE/README.md" "$GATE/check.sh" 2>/dev/null; then
|
|
39
45
|
bad "$SLUG: заготовка не заполнена — остались метки ЗАПОЛНИ"
|
|
40
46
|
continue
|
|
41
47
|
fi
|
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
set -uo pipefail
|
|
15
15
|
|
|
16
|
+
# Язык вывода закреплён: проверки ниже сверяют русский текст, а без этой строки они зависели бы
|
|
17
|
+
# от локали машины — на англоязычном раннере зелёное стало бы красным без единой правки в коде.
|
|
18
|
+
export AQK_LANG=ru
|
|
19
|
+
|
|
16
20
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
17
21
|
CLI="$ROOT/tool/program.mjs"
|
|
18
22
|
PASS=0
|
|
@@ -527,6 +531,99 @@ else
|
|
|
527
531
|
fi
|
|
528
532
|
rm -rf "$SHDIR" "$CEDIR"
|
|
529
533
|
|
|
534
|
+
# --- 32. вывод действительно на двух языках -----------------------------------
|
|
535
|
+
# Сверка ключей каталогов (units.mjs) доказывает, что строки не разошлись, но не доказывает,
|
|
536
|
+
# что выбор языка вообще доехал до вывода. Это проверяется только запуском.
|
|
537
|
+
EN_OUT=$(AQK_LANG=en node "$CLI" 2>&1)
|
|
538
|
+
RU_OUT=$(AQK_LANG=ru node "$CLI" 2>&1)
|
|
539
|
+
if printf '%s' "$EN_OUT" | grep -q "install a gate from the catalogue" &&
|
|
540
|
+
! printf '%s' "$EN_OUT" | grep -q '[а-яА-ЯёЁ]' &&
|
|
541
|
+
printf '%s' "$RU_OUT" | grep -q "поставить гейт из каталога"; then
|
|
542
|
+
ok "справка печатается на двух языках, в английской нет кириллицы"
|
|
543
|
+
else
|
|
544
|
+
bad "выбор языка не доехал до вывода" "$(printf '%s' "$EN_OUT" | head -4)"
|
|
545
|
+
fi
|
|
546
|
+
|
|
547
|
+
# --- 33. ссылка на репозиторий ведёт в репозиторий -----------------------------
|
|
548
|
+
# Ссылку собирали из имени пакета. Когда имя стало коротким («agent-quality-kit» вместо
|
|
549
|
+
# «github:владелец/репозиторий»), просьба про звезду поехала на github.com/agent-quality-kit —
|
|
550
|
+
# несуществующую страницу. Единственное место, где мы просим человека о чём-то, вело в никуда.
|
|
551
|
+
FBDIR="$(mktemp -d)"; FBPROJ="$(mktemp -d)"
|
|
552
|
+
FB_OUT=$( cd "$FBPROJ" && git init -q . && HOME="$FBDIR" node "$CLI" init 2>&1 )
|
|
553
|
+
if printf '%s' "$FB_OUT" | grep -qE 'https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+'; then
|
|
554
|
+
ok "просьба про звезду ведёт на репозиторий, а не на github.com/<имя пакета>"
|
|
555
|
+
else
|
|
556
|
+
bad "ссылка на репозиторий собрана неверно" "$(printf '%s' "$FB_OUT" | grep -i github | head -2)"
|
|
557
|
+
fi
|
|
558
|
+
rm -rf "$FBDIR" "$FBPROJ"
|
|
559
|
+
|
|
560
|
+
# --- 34. обязательная форма отчёта -------------------------------------------
|
|
561
|
+
# ЗАЧЕМ. Первый чужой прогон дал отчёт «12 гейтов зелёные» — при том что все 12 стояли на
|
|
562
|
+
# слабейшем рецепте, а половина методичек не была прочитана. Пересказ по памяти выбирает
|
|
563
|
+
# удобное; отчёт обязан собираться прогоном.
|
|
564
|
+
REPDIR="$(mktemp -d)"
|
|
565
|
+
(
|
|
566
|
+
cd "$REPDIR" && git init -q . && mkdir -p src &&
|
|
567
|
+
printf 'def f():\n print("debug")\n' > src/a.py &&
|
|
568
|
+
node "$CLI" start >/dev/null 2>&1
|
|
569
|
+
)
|
|
570
|
+
REP_OUT=$( cd "$REPDIR" && node "$CLI" report 2>&1 ); REP_CODE=$?
|
|
571
|
+
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌ no-print-in-prod'; then
|
|
572
|
+
ok "report краснеет кодом возврата и называет упавший гейт"
|
|
573
|
+
else
|
|
574
|
+
bad "report не отличает красное от зелёного" "код $REP_CODE"
|
|
575
|
+
fi
|
|
576
|
+
if [ -f "$REPDIR/.aqk/report.md" ] && grep -q '^## ' "$REPDIR/.aqk/report.md"; then
|
|
577
|
+
ok "report сохраняет .aqk/report.md"
|
|
578
|
+
else
|
|
579
|
+
bad "report не сохранил файл отчёта" "$REPDIR/.aqk/report.md"
|
|
580
|
+
fi
|
|
581
|
+
# Путь к методичке ИЩЕТСЯ: baseline лежит в подпапке ai/, и жёстко вписанный путь уже соврал.
|
|
582
|
+
if printf '%s' "$REP_OUT" | grep -q '📖 .aqk/docs/ai/project-baseline.md'; then
|
|
583
|
+
ok "report находит методичку в подпапке, а не пишет путь наизусть"
|
|
584
|
+
else
|
|
585
|
+
bad "report не нашёл project-baseline.md" "$(printf '%s' "$REP_OUT" | grep -i baseline | head -1)"
|
|
586
|
+
fi
|
|
587
|
+
rm -rf "$REPDIR"
|
|
588
|
+
|
|
589
|
+
# --- 35. note пишет в журнал ЭТОГО проекта, а не в чужой ---------------------
|
|
590
|
+
# Команда требовала клон нашего репозитория и писала урок туда, игнорируя lessons: из
|
|
591
|
+
# манифеста проекта. Найдено первым чужим прогоном: человек завёл журнал руками.
|
|
592
|
+
NOTEDIR="$(mktemp -d)"; NOTEHOME="$(mktemp -d)"
|
|
593
|
+
(
|
|
594
|
+
cd "$NOTEDIR" && git init -q . && git config user.email t@t && git config user.name t &&
|
|
595
|
+
node "$CLI" init >/dev/null 2>&1 &&
|
|
596
|
+
mkdir -p incidents &&
|
|
597
|
+
sed -i 's|^lessons: ""|lessons: incidents|' .aqk.yml &&
|
|
598
|
+
printf '**Вывод.** 🔧 завели проверку\n' | HOME="$NOTEHOME" AQK_HOME="" node "$CLI" note "шишка" >/dev/null 2>&1
|
|
599
|
+
)
|
|
600
|
+
if [ -f "$NOTEDIR/incidents/README.md" ] && grep -q 'шишка' "$NOTEDIR/incidents/README.md"; then
|
|
601
|
+
ok "note пишет в journal этого проекта — lessons: из манифеста"
|
|
602
|
+
else
|
|
603
|
+
bad "note проигнорировал lessons: и ушёл искать чужой клон" "$NOTEDIR/incidents/README.md"
|
|
604
|
+
fi
|
|
605
|
+
rm -rf "$NOTEDIR" "$NOTEHOME"
|
|
606
|
+
|
|
607
|
+
# --- 36. .aqkignore прячет принесённый извне код ------------------------------
|
|
608
|
+
# ЗАЧЕМ. В чужом проекте референс, принесённый из другого репозитория, попадал в находки
|
|
609
|
+
# всех сканирующих гейтов. Единственным лечением была правка КОПИИ _skip.sh в проекте —
|
|
610
|
+
# то есть настройка правкой чужого файла, которую затрёт следующий `aqk add`.
|
|
611
|
+
IGNDIR="$(mktemp -d)"
|
|
612
|
+
mkdir -p "$IGNDIR/third-party/inner" "$IGNDIR/src"
|
|
613
|
+
printf 'def f():\n print("свой")\n' > "$IGNDIR/src/mine.py"
|
|
614
|
+
printf 'def f():\n print("чужой")\n' > "$IGNDIR/third-party/inner/theirs.py"
|
|
615
|
+
OUT_BEFORE="$(bash "$ROOT/kit/gates/no-print-in-prod/check.sh" "$IGNDIR" 2>&1)"
|
|
616
|
+
printf '# принесено из другого репозитория\nthird-party/\n' > "$IGNDIR/.aqkignore"
|
|
617
|
+
OUT_AFTER="$(bash "$ROOT/kit/gates/no-print-in-prod/check.sh" "$IGNDIR" 2>&1)"
|
|
618
|
+
if printf '%s' "$OUT_BEFORE" | grep -q 'theirs.py' &&
|
|
619
|
+
! printf '%s' "$OUT_AFTER" | grep -q 'theirs.py' &&
|
|
620
|
+
printf '%s' "$OUT_AFTER" | grep -q 'mine.py'; then
|
|
621
|
+
ok ".aqkignore прячет чужой код и не трогает свой"
|
|
622
|
+
else
|
|
623
|
+
bad ".aqkignore не работает" "до: $(printf '%s' "$OUT_BEFORE" | head -2) | после: $(printf '%s' "$OUT_AFTER" | head -2)"
|
|
624
|
+
fi
|
|
625
|
+
rm -rf "$IGNDIR"
|
|
626
|
+
|
|
530
627
|
# --- итог -------------------------------------------------------------------
|
|
531
628
|
printf '\n'
|
|
532
629
|
if [ "$FAIL" -eq 0 ]; then
|
package/tool/selfcheck/units.mjs
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import assert from "node:assert/strict";
|
|
14
14
|
import { parseManifest, manifestWithGate } from "../lib/manifest.mjs";
|
|
15
|
-
import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG } from "../lib/repo.mjs";
|
|
15
|
+
import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG, whichSync } from "../lib/repo.mjs";
|
|
16
|
+
import { CATALOGS, pickLang, L } from "../i18n/index.mjs";
|
|
17
|
+
import { dirname } from "node:path";
|
|
16
18
|
|
|
17
19
|
const facts = (over = {}) => ({ langs: new Set(), files: 0, ...over });
|
|
18
20
|
|
|
@@ -52,7 +54,8 @@ test("условия складываются по И: одно ложное с
|
|
|
52
54
|
|
|
53
55
|
test("причина, по которой запись скрыта, называется словами", () => {
|
|
54
56
|
const v = triggerVerdict({ trigger: { langs: "python, typescript" } }, facts({ langs: new Set(["go"]) }));
|
|
55
|
-
|
|
57
|
+
// Сверяем с каталогом, а не с буквами: текст переводится, а выбор причины — нет.
|
|
58
|
+
assert.equal(v.why, L.trigger.noLangs("python, typescript"));
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
test("always: false значит «никогда не применимо», а не «условие пропущено»", () => {
|
|
@@ -64,7 +67,7 @@ test("неизвестное условие скрывает запись, а н
|
|
|
64
67
|
// Молча пропустить незнакомое условие значит показать запись всем подряд.
|
|
65
68
|
const v = triggerVerdict({ trigger: { has_kubernetes: "true" } }, facts());
|
|
66
69
|
assert.equal(v.applies, false);
|
|
67
|
-
assert.
|
|
70
|
+
assert.equal(v.why, L.trigger.unknown("has_kubernetes"));
|
|
68
71
|
});
|
|
69
72
|
|
|
70
73
|
// --- выбор рецепта ------------------------------------------------------------
|
|
@@ -74,7 +77,7 @@ test("без родного языка берётся переносимый р
|
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
test("рецепта нет — так и сказано, а не пустая строка", () => {
|
|
77
|
-
assert.equal(recipeFor({ slug: "x", recipes: {} }, facts()),
|
|
80
|
+
assert.equal(recipeFor({ slug: "x", recipes: {} }, facts()), L.recipe.none);
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
// --- дедупликация по намерению ------------------------------------------------
|
|
@@ -101,5 +104,77 @@ test("гейт дописывается в блок gates и не дублиру
|
|
|
101
104
|
test("без блока gates программа объясняет, чего не хватает", () => {
|
|
102
105
|
const r = manifestWithGate("aqk: 1\n", "x", "bash y.sh");
|
|
103
106
|
assert.equal(r.text, null);
|
|
104
|
-
assert.
|
|
107
|
+
assert.equal(r.why, L.manifest.noGatesBlock);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// --- каталоги строк не расходятся ---------------------------------------------
|
|
111
|
+
// ЗАЧЕМ. «Поддерживаем два языка» — утверждение, которое обязана держать машина, а не память
|
|
112
|
+
// того, кто правил вывод в последний раз. Забытый ключ в одном каталоге даёт `undefined` в
|
|
113
|
+
// выводе — не отказ, а тихую порчу текста ровно у того, кто пришёл на втором языке.
|
|
114
|
+
// В массивы заходим тоже: ступени уровней и многострочные пояснения лежат массивами, и без
|
|
115
|
+
// этого проверка сравнивала бы их как один непрозрачный «object» — то есть не сравнивала.
|
|
116
|
+
// Заодно сверяется длина: пояснение из трёх строк на одном языке и из двух на другом — тоже
|
|
117
|
+
// расхождение.
|
|
118
|
+
function keyPaths(obj, prefix = "") {
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
121
|
+
const path = prefix ? `${prefix}.${k}` : k;
|
|
122
|
+
if (v && typeof v === "object") out.push(...keyPaths(v, path));
|
|
123
|
+
else out.push(`${path}:${typeof v}`);
|
|
124
|
+
}
|
|
125
|
+
return out.sort();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
test("оба каталога строк несут одни и те же ключи одного типа", () => {
|
|
129
|
+
const a = keyPaths(CATALOGS.ru);
|
|
130
|
+
const b = keyPaths(CATALOGS.en);
|
|
131
|
+
const onlyRu = a.filter((k) => !b.includes(k));
|
|
132
|
+
const onlyEn = b.filter((k) => !a.includes(k));
|
|
133
|
+
assert.deepEqual(onlyRu, [], `есть только в ru: ${onlyRu.join(", ")}`);
|
|
134
|
+
assert.deepEqual(onlyEn, [], `есть только в en: ${onlyEn.join(", ")}`);
|
|
135
|
+
assert.ok(a.length > 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("ни одна строка вывода не осталась пустой", () => {
|
|
139
|
+
for (const [lang, cat] of Object.entries(CATALOGS)) {
|
|
140
|
+
for (const path of keyPaths(cat)) {
|
|
141
|
+
const [key, kind] = path.split(":");
|
|
142
|
+
if (kind !== "string") continue;
|
|
143
|
+
const value = key.split(".").reduce((o, k) => o[k], cat);
|
|
144
|
+
assert.ok(value.trim().length > 0, `пустая строка ${lang}.${key}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("язык берётся из AQK_LANG, потом из локали, иначе английский", () => {
|
|
150
|
+
assert.equal(pickLang({ AQK_LANG: "ru" }), "ru");
|
|
151
|
+
assert.equal(pickLang({ AQK_LANG: "en_US.UTF-8", LANG: "ru_RU.UTF-8" }), "en");
|
|
152
|
+
assert.equal(pickLang({ LANG: "ru_RU.UTF-8" }), "ru");
|
|
153
|
+
assert.equal(pickLang({ LC_ALL: "ru_RU.UTF-8", LANG: "en_US.UTF-8" }), "ru");
|
|
154
|
+
assert.equal(pickLang({ LANG: "de_DE.UTF-8" }), "en");
|
|
155
|
+
assert.equal(pickLang({}), "en");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- поиск программы в PATH ---------------------------------------------------
|
|
159
|
+
// ЗАЧЕМ. Раньше наличие программы проверялось через `command -v` в оболочке. На Windows
|
|
160
|
+
// оболочка — cmd.exe, где такой команды нет, и ответ был «не установлено» ДЛЯ ЛЮБОЙ
|
|
161
|
+
// программы: родной рецепт становился недостижим, гейт молча вставал на слабейший
|
|
162
|
+
// переносимый вариант, а прогон показывал зелёное. Нашлось на чужом прогоне, не у нас.
|
|
163
|
+
test("программа в PATH находится, несуществующая — нет", () => {
|
|
164
|
+
assert.ok(whichSync("node"), "node обязан находиться: им же запущена эта проверка");
|
|
165
|
+
assert.equal(whichSync("нет-такой-программы-12345"), null);
|
|
166
|
+
assert.equal(whichSync(""), null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("поиск не зависит от оболочки — работает с пустым окружением", () => {
|
|
170
|
+
// Тот самый случай: оболочки нет или она другая. Ответ обязан быть «не нашли»,
|
|
171
|
+
// а не исключение и не ложное «нашли».
|
|
172
|
+
assert.equal(whichSync("node", { PATH: "" }), null);
|
|
173
|
+
const dir = dirname(process.execPath);
|
|
174
|
+
assert.ok(whichSync(process.platform === "win32" ? "node" : "node", { PATH: dir }));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("команда путём, а не именем, ищется на диске, а не в PATH", () => {
|
|
178
|
+
assert.ok(whichSync(process.execPath));
|
|
179
|
+
assert.equal(whichSync("./нет-такого-файла.sh"), null);
|
|
105
180
|
});
|