dsh-telegram-multiagent 1.2.3 → 1.4.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 +244 -106
- package/cordis.patch.yml +11 -0
- package/package.json +8 -3
- package/src/index.js +536 -44
- package/test/extract.mjs +44 -0
- package/test/stend-imena.mjs +133 -0
- package/test/stend-yashchika-shiny.mjs +184 -0
- package/test/test-goal-control.mjs +224 -0
package/test/extract.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Вырезает тело именованной функции ИЗ ФАЙЛА пакета — стенд обязан проверять
|
|
2
|
+
// установленный код, а не его пересказ. Поиск по ИМЕНИ, не по номеру строки:
|
|
3
|
+
// номер строки протухает от первой же правки выше по файлу.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
|
|
6
|
+
export function readSource(file) {
|
|
7
|
+
return fs.readFileSync(file, 'utf-8');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function extractFunction(src, name) {
|
|
11
|
+
const re = new RegExp(`(?:^|\\n)\\s*(?:async\\s+)?function\\s+${name}\\s*\\(`);
|
|
12
|
+
const m = re.exec(src);
|
|
13
|
+
if (!m) throw new Error(`функция ${name} не найдена в исходнике`);
|
|
14
|
+
// 🔴 Начало берём от `async`, если оно есть: обрезав его, стенд получил бы
|
|
15
|
+
// обычную функцию с await внутри и упал бы с «await is only valid in async
|
|
16
|
+
// functions» — то есть покраснел бы на исправном коде.
|
|
17
|
+
const fnAt = src.indexOf('function', m.index);
|
|
18
|
+
const asyncAt = src.lastIndexOf('async', fnAt);
|
|
19
|
+
const start = (asyncAt !== -1 && /^async\s+function$/.test(src.slice(asyncAt, fnAt + 8).replace(/\s+/g, ' ').trim().replace(/ $/, '')))
|
|
20
|
+
? asyncAt : fnAt;
|
|
21
|
+
const open = src.indexOf('{', src.indexOf('(', start));
|
|
22
|
+
let depth = 0, i = open, inStr = null, esc = false;
|
|
23
|
+
for (; i < src.length; i++) {
|
|
24
|
+
const c = src[i];
|
|
25
|
+
if (inStr) {
|
|
26
|
+
if (esc) { esc = false; continue; }
|
|
27
|
+
if (c === '\\') { esc = true; continue; }
|
|
28
|
+
if (c === inStr) inStr = null;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (c === '"' || c === "'" || c === '`') { inStr = c; continue; }
|
|
32
|
+
if (c === '{') depth++;
|
|
33
|
+
else if (c === '}') { depth--; if (depth === 0) break; }
|
|
34
|
+
}
|
|
35
|
+
if (depth !== 0) throw new Error(`не удалось сбалансировать скобки у ${name}`);
|
|
36
|
+
return src.slice(start, i + 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function extractConst(src, name) {
|
|
40
|
+
const re = new RegExp(`\\n\\s*const\\s+${name}\\s*=\\s*([^;\\n]+);`);
|
|
41
|
+
const m = re.exec(src);
|
|
42
|
+
if (!m) throw new Error(`константа ${name} не найдена`);
|
|
43
|
+
return m[1].trim();
|
|
44
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Стенд правки «имена из настройки». Строки берутся ИЗ файла, не пишутся заново.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
// 🔴 Путь по умолчанию — УСТАНОВЛЕННЫЙ файл рядом со стендом. Без него стенд,
|
|
6
|
+
// запущенный без довода (а в приёмке списком его запускают именно так), падал
|
|
7
|
+
// сырым TypeError и читался кодом 1 — то есть слепота проверки выглядела отказом
|
|
8
|
+
// предмета. Аргументом по-прежнему можно указать другую копию.
|
|
9
|
+
const SRC = process.argv[2]
|
|
10
|
+
|| new URL('../src/index.js', import.meta.url).pathname;
|
|
11
|
+
let s;
|
|
12
|
+
try {
|
|
13
|
+
s = fs.readFileSync(SRC, 'utf-8');
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.log(`СЛЕПОТА: не читается ${SRC} — ${e.code || e.message}`);
|
|
16
|
+
process.exit(2); // 2 = не смогла проверить. Это НЕ «сошлось» и НЕ отказ.
|
|
17
|
+
}
|
|
18
|
+
let ok = 0, fail = 0, slep = 0;
|
|
19
|
+
const t = (name, cond, syrio) => { if (cond) { ok++; console.log(`ok ${name}`); }
|
|
20
|
+
else { fail++; console.log(`FAIL ${name}\n сырьё: ${syrio}`); } };
|
|
21
|
+
|
|
22
|
+
// 1. Объявление who_ есть и берёт значение из настройки.
|
|
23
|
+
const decl = s.match(/const who_ = ([^;]+);/);
|
|
24
|
+
t('who_ объявлена из config.agentName', !!decl && /config\.agentName/.test(decl[1]),
|
|
25
|
+
decl ? decl[1] : '(объявления нет)');
|
|
26
|
+
|
|
27
|
+
// 🔴 СПИСОК ЧАСТНЫХ ИМЁН ЖИВЁТ ФАЙЛОМ РЯДОМ, А НЕ В ГОЛОВЕ ПРОВЕРЯЮЩЕГО.
|
|
28
|
+
// Прецедент 01.09.2026: три обхода одного файла дали 0, 2 и 4 находки. Причём
|
|
29
|
+
// второй обход был ХУЖЕ первого не по списку, а по его СУЖЕНИЮ: первый шёл по
|
|
30
|
+
// полному списку латиницей и дал ноль; после провала проверяющий перезапросил ОДНО
|
|
31
|
+
// имя — то, что уже видел, — нашёл ожидаемое и счёл поиск законченным.
|
|
32
|
+
// 🔴 Список схлопнулся при первой находке. Это опаснее забытого имени: там дыра
|
|
33
|
+
// видна при перечитывании, здесь список БЫЛ полным и стал неполным незаметно.
|
|
34
|
+
// Поэтому: список в файле, прогоняется ВЕСЬ ВСЕГДА, и покрытие по нему печатается —
|
|
35
|
+
// без числа «имён N из N» сужение снова стало бы невидимым.
|
|
36
|
+
const FAJL_IMEN = path.join(path.dirname(fileURLToPath(import.meta.url)), 'chastnye-imena.json')
|
|
37
|
+
let SPISOK_IMEN = null
|
|
38
|
+
try {
|
|
39
|
+
SPISOK_IMEN = JSON.parse(fs.readFileSync(FAJL_IMEN, 'utf-8')).imena
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// У постороннего установившего файла нет и быть не должно: список наших имён —
|
|
42
|
+
// частные данные, он не входит в состав пакета. Это СЛЕПОТА, а не беда.
|
|
43
|
+
console.log(`--- список частных имён не прочитан (${FAJL_IMEN}): ${e.code ?? e.message}`)
|
|
44
|
+
console.log('--- проверка обезличенности НЕ СОСТОЯЛАСЬ; у стороннего установившего это норма')
|
|
45
|
+
slep += 1
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(SPISOK_IMEN) && SPISOK_IMEN.length) {
|
|
48
|
+
const svoj = fs.readFileSync(fileURLToPath(import.meta.url), 'utf-8')
|
|
49
|
+
// Из обхода исключаются строки, где имя И ЕСТЬ предмет проверки: отрицания вида
|
|
50
|
+
// !/Имя/.test(...) — там оно обязано присутствовать, иначе проверять нечего.
|
|
51
|
+
const bezZashity = svoj.split('\n').filter((l) => !/!\/[^/]+\/\.test\(/.test(l)).join('\n')
|
|
52
|
+
const najdeno = SPISOK_IMEN.filter((im) => bezZashity.includes(im))
|
|
53
|
+
t(`в стенде нет частных имён как данных (проверено имён ${SPISOK_IMEN.length} из ${SPISOK_IMEN.length}, файл ${path.basename(FAJL_IMEN)})`,
|
|
54
|
+
najdeno.length === 0, najdeno.length ? `найдено: ${najdeno.join(', ')}` : 'ни одного')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 🔴 НИЖЕ ДВАЖДЫ ВСТРЕЧАЕТСЯ ЧАСТНОЕ ИМЯ — И ЭТО НЕ ГРЯЗЬ, А ЗАЩИТА.
|
|
58
|
+
// Стенд проверяет, что кличка конкретного агента НЕ попала в код: имя стоит
|
|
59
|
+
// внутри отрицания (!/.../.test(...)), то есть проверка краснеет, если имя
|
|
60
|
+
// появится в предмете. Уберёшь эти два вхождения «чтобы обезличить файл» —
|
|
61
|
+
// снимешь единственную проверку от возврата клички. Защита, выглядящая как
|
|
62
|
+
// грязь, вычищается из лучших побуждений; поэтому объяснение стоит здесь, а не
|
|
63
|
+
// в отчёте, который читатель файла не увидит.
|
|
64
|
+
// 2. Ветка отказа чужому: используется who_, а НЕ who (та затенена msg.from).
|
|
65
|
+
const alertLine = s.split('\n').find((l) => l.includes('Чужой написал'));
|
|
66
|
+
t('тревога о чужом использует who_', !!alertLine && /\$\{who_\}/.test(alertLine), String(alertLine));
|
|
67
|
+
t('тревога о чужом без клички соседа', !!alertLine && !/Петрович/.test(alertLine), String(alertLine));
|
|
68
|
+
|
|
69
|
+
// 3. Затенение существует — значит who_ не блажь: проверяем фактом по файлу.
|
|
70
|
+
t('внутри ветки чужого who действительно затенена',
|
|
71
|
+
/const who = msg\.from \?\? \{\};/.test(s),
|
|
72
|
+
`затенений найдено: ${(s.match(/const who = msg\.from \?\? \{\};/g) ?? []).length}`);
|
|
73
|
+
|
|
74
|
+
// 4. Приветствие /start.
|
|
75
|
+
const hi = s.split('\n').find((l) => l.includes('на связи. Пишите задачу'));
|
|
76
|
+
t('приветствие использует who_', !!hi && /\$\{who_\}/.test(hi), String(hi));
|
|
77
|
+
t('приветствие без клички соседа', !!hi && !/Петрович/.test(hi), String(hi));
|
|
78
|
+
|
|
79
|
+
// 5. Поведение подстановки — проверяем ИСПОЛНЕНИЕМ, а не глазом.
|
|
80
|
+
// 🔴 Проверки 5-6 требуют объявления who_ и строки тревоги. Нет их — стенд обязан
|
|
81
|
+
// СКАЗАТЬ, что не смог измерить, а не упасть сырым TypeError: падение читается
|
|
82
|
+
// как «стенд сломан», хотя сломан предмет. Отделяем слепоту от отказа.
|
|
83
|
+
if (!decl || !alertLine) {
|
|
84
|
+
console.log('--- подстановка и умолчание НЕ ПРОВЕРЕНЫ: нет объявления who_ или строки тревоги');
|
|
85
|
+
} else {
|
|
86
|
+
// Фикстура НЕЙТРАЛЬНАЯ: годится любое имя, и частное было выбрано по привычке.
|
|
87
|
+
// Файл едет в пакет — частное имя увидел бы каждый установивший.
|
|
88
|
+
const config = { agentName: 'агент-проба' };
|
|
89
|
+
const who_ = eval(decl[1]);
|
|
90
|
+
const who = { first_name: 'Чужой' }; // то самое затенение
|
|
91
|
+
const alert = eval('`' + alertLine.split('`')[1] + '`');
|
|
92
|
+
t('подстановка даёт имя, а не [object Object]',
|
|
93
|
+
alert.includes('агент-проба') && !alert.includes('[object Object]'), alert.trim());
|
|
94
|
+
|
|
95
|
+
// 6. Умолчание, когда agentName не задан.
|
|
96
|
+
const config2 = {}; const who2 = eval(decl[1].replace('config.', 'config2.'));
|
|
97
|
+
t('без настройки — нейтральное слово, не пусто', who2 === 'агент', String(who2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 7. Команда расшифровки — настройкой, с громким отказом.
|
|
101
|
+
t('расшифровка берётся из config.transcribeCommand',
|
|
102
|
+
/execFileSync\(config\.transcribeCommand,/.test(s), 'execFileSync(config.transcribeCommand,');
|
|
103
|
+
t('зашитого пути расшифровки не осталось',
|
|
104
|
+
!/execFileSync\('\/usr\/local\/bin\//.test(s),
|
|
105
|
+
`зашитых вызовов: ${(s.match(/execFileSync\('\/usr\/local\/bin\/[^']*'/g) ?? []).join(', ') || 'нет'}`);
|
|
106
|
+
t('не задана — говорит в журнал и возвращает null',
|
|
107
|
+
/не задан config\.transcribeCommand/.test(s),
|
|
108
|
+
`вхождений строки отказа: ${(s.match(/не задан config\.transcribeCommand/g) ?? []).length}`);
|
|
109
|
+
|
|
110
|
+
// 8. sayTo: каталог обмена не задан — сказать, а не падать в mkdirSync(undefined).
|
|
111
|
+
const sayTo = s.slice(s.indexOf('async function sayTo'), s.indexOf('async function sayTo') + 600);
|
|
112
|
+
t('sayTo проверяет A2A_OUT ДО mkdirSync',
|
|
113
|
+
sayTo.indexOf('if (!A2A_OUT)') > -1 && sayTo.indexOf('if (!A2A_OUT)') < sayTo.indexOf('mkdirSync'),
|
|
114
|
+
sayTo.split('\n').slice(1, 4).join(' | '));
|
|
115
|
+
|
|
116
|
+
// 9. НАШ вид источника не потерян (его отсутствие сняло бы право в мосте).
|
|
117
|
+
t('source kind a2a сохранён', /source: \{ kind: 'a2a' \}/.test(s),
|
|
118
|
+
`видов источника в файле: ${(s.match(/source: \{ kind: '[^']+' \}/g) ?? []).join(', ') || 'ни одного'}`);
|
|
119
|
+
|
|
120
|
+
// 10. Подсказка о правах не адресует чужой дом.
|
|
121
|
+
t('подсказка о правах не называет чужого владельца',
|
|
122
|
+
!/chown dsh:dsh/.test(s), `вхождений «chown dsh:dsh»: ${(s.match(/chown dsh:dsh/g) ?? []).length}`);
|
|
123
|
+
|
|
124
|
+
// 11. Устаревшее про «общий модуль» вычищено.
|
|
125
|
+
t('нет утверждения «модуль общий для машины»',
|
|
126
|
+
!/модуль общий для машины/.test(s),
|
|
127
|
+
`вхождений: ${(s.match(/модуль общий для машины/g) ?? []).length}`);
|
|
128
|
+
|
|
129
|
+
console.log(`\nИТОГ: ok=${ok} fail=${fail} слепота=${slep}`);
|
|
130
|
+
// 🔴 Код 0 ТОЛЬКО когда всё состоялось и сошлось. Слепота — код 2: «проверить
|
|
131
|
+
// нечем» и «проверил, всё хорошо» это разные новости, и машина, читающая код,
|
|
132
|
+
// не должна принимать первое за второе.
|
|
133
|
+
process.exit(fail ? 1 : slep ? 2 : 0);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Стенд замыкания выхода шины A2A: чтение ящика модулем связи.
|
|
2
|
+
// Текст функций берётся ИЗ файла предмета и ИСПОЛНЯЕТСЯ на подставном ящике.
|
|
3
|
+
// Боевой ящик не трогается ни на одном шаге.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
|
|
8
|
+
const SRC = process.argv[2] || new URL('../src/index.js', import.meta.url).pathname;
|
|
9
|
+
let s;
|
|
10
|
+
try { s = fs.readFileSync(SRC, 'utf-8'); }
|
|
11
|
+
catch (e) { console.log(`СЛЕПОТА: не читается ${SRC} — ${e.code || e.message}`); process.exit(2); }
|
|
12
|
+
|
|
13
|
+
let ok = 0, fail = 0, slep = 0;
|
|
14
|
+
const t = (n, c, syr) => { if (c) { ok++; console.log(`ok ${n}`); }
|
|
15
|
+
else { fail++; console.log(`FAIL ${n}\n сырьё: ${syr}`); } };
|
|
16
|
+
const sl = (n, p) => { slep++; console.log(`СЛЕПОТА ${n}: ${p}`); };
|
|
17
|
+
|
|
18
|
+
// ── извлечение функций ИЗ предмета ───────────────────────────────────────────
|
|
19
|
+
function vynut(imya) {
|
|
20
|
+
const i = s.indexOf(` function ${imya}(`) >= 0
|
|
21
|
+
? s.indexOf(` function ${imya}(`) : s.indexOf(` async function ${imya}(`);
|
|
22
|
+
if (i < 0) return null;
|
|
23
|
+
const j = s.indexOf('\n }\n', i);
|
|
24
|
+
return j < 0 ? null : s.slice(i, j + 5);
|
|
25
|
+
}
|
|
26
|
+
const tSender = vynut('spoolSender');
|
|
27
|
+
const tAge = vynut('spoolAgeHours');
|
|
28
|
+
const tPoll = vynut('pollSpool');
|
|
29
|
+
if (!tSender || !tAge || !tPoll) {
|
|
30
|
+
sl('извлечение', `spoolSender=${!!tSender} spoolAgeHours=${!!tAge} pollSpool=${!!tPoll}`);
|
|
31
|
+
console.log(`ИТОГО: сошлось ${ok}, расхождений ${fail}, слепот ${slep}`);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── 1. настройка, а не константа ─────────────────────────────────────────────
|
|
36
|
+
const declIn = s.match(/const SPOOL_IN = ([^;]+);/);
|
|
37
|
+
t('путь ящика — настройка config.spoolDir, не константа',
|
|
38
|
+
!!declIn && /config\.spoolDir/.test(declIn[1]) && !/\/var\/spool/.test(declIn[1]),
|
|
39
|
+
declIn ? declIn[1] : '(объявления нет)');
|
|
40
|
+
const declB = s.match(/const SPOOL_BATCH = ([^;]+);/);
|
|
41
|
+
t('порог пачки — настройка', !!declB && /config\.spoolBatchCount/.test(declB[1]),
|
|
42
|
+
declB ? declB[1] : '(нет)');
|
|
43
|
+
const declS = s.match(/const SPOOL_STALE_HOURS = ([^;]+);/);
|
|
44
|
+
t('порог возраста — настройка', !!declS && /config\.spoolStaleHours/.test(declS[1]),
|
|
45
|
+
declS ? declS[1] : '(нет)');
|
|
46
|
+
|
|
47
|
+
// ── 2. вызов в цикле опроса ──────────────────────────────────────────────────
|
|
48
|
+
t('pollSpool зовётся из цикла опроса', /await pollSpool\(\);/.test(s),
|
|
49
|
+
`вхождений: ${(s.match(/await pollSpool\(\)/g) ?? []).length}`);
|
|
50
|
+
|
|
51
|
+
// ── 3. удаление ПОСЛЕ передачи, а не до ──────────────────────────────────────
|
|
52
|
+
const posle = tPoll.indexOf('handle.agent.send');
|
|
53
|
+
const unl = tPoll.indexOf('unlinkSync(p.full)');
|
|
54
|
+
t('удаление письма стоит ПОСЛЕ передачи агенту', posle > 0 && unl > posle,
|
|
55
|
+
`send@${posle} unlink@${unl}`);
|
|
56
|
+
t('команды /goal из ящика НЕ принимаются', !/\/goal/.test(tPoll),
|
|
57
|
+
`вхождений /goal в pollSpool: ${(tPoll.match(/\/goal/g) ?? []).length}`);
|
|
58
|
+
|
|
59
|
+
// ── исполнение: разбор имени и возраст ───────────────────────────────────────
|
|
60
|
+
const mkFn = (txt, args) => new Function(...args, `${txt}\n return arguments.callee;`);
|
|
61
|
+
const sender = new Function('fs', `${tSender}\n return spoolSender;`)(fs);
|
|
62
|
+
t('отправитель из имени: dsh', sender('20260829T205711Z-dsh-af8b9699.txt') === 'dsh',
|
|
63
|
+
sender('20260829T205711Z-dsh-af8b9699.txt'));
|
|
64
|
+
t('имя не по форме — сказано прямо, а не пусто',
|
|
65
|
+
sender('pismo.txt') === '(имя не разобрано)', sender('pismo.txt'));
|
|
66
|
+
|
|
67
|
+
const ageFn = new Function('fs', `${tAge}\n return spoolAgeHours;`)(fs);
|
|
68
|
+
const svezh = new Date(Date.now() - 3600000).toISOString().replace(/[-:]/g, '').slice(0, 15) + 'Z';
|
|
69
|
+
const a1 = ageFn(`${svezh}-dsh-x.txt`, '/nonexistent');
|
|
70
|
+
t('возраст свежего письма около 1 часа', a1 > 0.5 && a1 < 1.5, String(a1));
|
|
71
|
+
const a2 = ageFn('20260829T205711Z-dsh-x.txt', '/nonexistent');
|
|
72
|
+
t('возраст письма от 29.08 больше 12 часов', a2 > 12, String(a2));
|
|
73
|
+
|
|
74
|
+
// ── исполнение pollSpool на подставном ящике ─────────────────────────────────
|
|
75
|
+
function progon(pisma, opts = {}) {
|
|
76
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stend-spool-'));
|
|
77
|
+
for (const [name, body] of pisma) fs.writeFileSync(path.join(dir, name), body);
|
|
78
|
+
const zhurnal = [];
|
|
79
|
+
const podano = [];
|
|
80
|
+
const env = {
|
|
81
|
+
fs, path,
|
|
82
|
+
SPOOL_IN: opts.brokenDir ? path.join(dir, 'net-takogo') : dir,
|
|
83
|
+
SPOOL_BATCH: opts.batch ?? 5,
|
|
84
|
+
SPOOL_STALE_HOURS: opts.stale ?? 12,
|
|
85
|
+
A2A_EXT: ['.txt', '.md'],
|
|
86
|
+
A2A_CHAT: 'a2a',
|
|
87
|
+
a2aReported: opts.reported ?? new Set(),
|
|
88
|
+
log: (m) => zhurnal.push(String(m)),
|
|
89
|
+
stripMark: (x) => x,
|
|
90
|
+
lastOrigin: new Map(),
|
|
91
|
+
pushAsk: () => {},
|
|
92
|
+
copyAsk: () => {},
|
|
93
|
+
nudgeUntilClaimed: () => {},
|
|
94
|
+
platform: { createUserMessage: (m) => m },
|
|
95
|
+
agentFor: async () => {
|
|
96
|
+
if (opts.otkazPeredachi) throw new Error('фабрика агентов не поднялась');
|
|
97
|
+
return { handle: { agent: { send: (m) => podano.push(m.content[0].text) } }, sessionId: 's1' };
|
|
98
|
+
},
|
|
99
|
+
spoolSender: sender,
|
|
100
|
+
spoolAgeHours: ageFn,
|
|
101
|
+
};
|
|
102
|
+
const argsN = Object.keys(env);
|
|
103
|
+
const fn = new Function(...argsN, `${tPoll}\n return pollSpool;`)(...argsN.map((k) => env[k]));
|
|
104
|
+
// 🔴 УБИРАЕМ ЗА СОБОЙ. Первая редакция оставила 89 каталогов в /tmp за один
|
|
105
|
+
// заход: стенд создавал mkdtemp и не удалял. Механизм, копящий в ОБЩЕМ
|
|
106
|
+
// каталоге, — наш известный класс (долг 40), и завести его в приёмке,
|
|
107
|
+
// которая гоняется ежедневно, значит копить молча.
|
|
108
|
+
return fn().then(() => {
|
|
109
|
+
const itog = {
|
|
110
|
+
dir, zhurnal, podano,
|
|
111
|
+
ostalos: fs.existsSync(dir) ? fs.readdirSync(dir) : [],
|
|
112
|
+
};
|
|
113
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* уже нет */ }
|
|
114
|
+
return itog;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const R = {};
|
|
119
|
+
// 🔴 Метка времени СВЕЖЕГО письма вычисляется, а не зашивается: с зашитой датой
|
|
120
|
+
// стенд протухает через 12 часов и краснеет на исправном предмете —
|
|
121
|
+
// «свежее письмо несёт пометку возраста». Проверено 01.09.2026: зашитая
|
|
122
|
+
// вчерашняя дата дала возраст 28 ч.
|
|
123
|
+
const svezhaya = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
|
|
124
|
+
R.odno = await progon([[`${svezhaya}-dsh-aaa.txt`, 'привет из ящика']]);
|
|
125
|
+
t('письмо из ящика подано агенту', R.odno.podano.length === 1, JSON.stringify(R.odno.podano));
|
|
126
|
+
t('в тексте метка отправителя', /от агента dsh/.test(R.odno.podano[0] ?? ''), R.odno.podano[0]);
|
|
127
|
+
t('прочитанное удалено из ящика', R.odno.ostalos.length === 0, JSON.stringify(R.odno.ostalos));
|
|
128
|
+
t('в журнале сказано о принятии', R.odno.zhurnal.some((l) => /\[ящик\] принято/.test(l)),
|
|
129
|
+
JSON.stringify(R.odno.zhurnal));
|
|
130
|
+
|
|
131
|
+
R.povtor = await progon([]);
|
|
132
|
+
t('пустой ящик — ни одной строки в журнале', R.povtor.zhurnal.length === 0,
|
|
133
|
+
JSON.stringify(R.povtor.zhurnal));
|
|
134
|
+
|
|
135
|
+
R.staroe = await progon([['20260829T205711Z-dsh-bbb.txt', 'старое письмо']]);
|
|
136
|
+
t('старое письмо несёт пометку возраста',
|
|
137
|
+
/пролежало в ящике \d+ ч/.test(R.staroe.podano[0] ?? ''), R.staroe.podano[0]);
|
|
138
|
+
t('свежее письмо пометки возраста НЕ несёт (зрячесть)',
|
|
139
|
+
!/пролежало в ящике/.test(R.odno.podano[0] ?? ''), R.odno.podano[0]);
|
|
140
|
+
|
|
141
|
+
R.pachka = await progon(Array.from({ length: 6 }, (_, i) =>
|
|
142
|
+
[`2026083109000${i}Z-dsh-c${i}.txt`, `письмо ${i}`]));
|
|
143
|
+
t('шесть писем поданы ОДНИМ ходом', R.pachka.podano.length === 1,
|
|
144
|
+
`подач: ${R.pachka.podano.length}`);
|
|
145
|
+
t('в пачке названо число писем', /6 писем из ящика одной пачкой/.test(R.pachka.podano[0] ?? ''),
|
|
146
|
+
(R.pachka.podano[0] ?? '').slice(0, 80));
|
|
147
|
+
t('пачка убрана из ящика целиком', R.pachka.ostalos.length === 0, JSON.stringify(R.pachka.ostalos));
|
|
148
|
+
|
|
149
|
+
R.pyat = await progon(Array.from({ length: 5 }, (_, i) =>
|
|
150
|
+
[`2026083109000${i}Z-dsh-d${i}.txt`, `письмо ${i}`]));
|
|
151
|
+
t('пять писем поданы ПО ОДНОМУ (зрячесть порога пачки)', R.pyat.podano.length === 5,
|
|
152
|
+
`подач: ${R.pyat.podano.length}`);
|
|
153
|
+
|
|
154
|
+
R.otkaz = await progon([[`${svezhaya}-dsh-eee.txt`, 'письмо']], { otkazPeredachi: true });
|
|
155
|
+
t('при отказе передачи письмо ОСТАВЛЕНО', R.otkaz.ostalos.length === 1,
|
|
156
|
+
JSON.stringify(R.otkaz.ostalos));
|
|
157
|
+
t('при отказе передачи сказано вслух',
|
|
158
|
+
R.otkaz.zhurnal.some((l) => /ОСТАВЛЕНЫ/.test(l)), JSON.stringify(R.otkaz.zhurnal));
|
|
159
|
+
|
|
160
|
+
R.pusto = await progon([[`${svezhaya}-dsh-fff.txt`, ' ']]);
|
|
161
|
+
t('пустое письмо убрано без подачи',
|
|
162
|
+
R.pusto.podano.length === 0 && R.pusto.ostalos.length === 0,
|
|
163
|
+
`подано ${R.pusto.podano.length}, осталось ${JSON.stringify(R.pusto.ostalos)}`);
|
|
164
|
+
|
|
165
|
+
R.chuzhoe = await progon([[`${svezhaya}-dsh-ggg.pdf`, 'бинарь']]);
|
|
166
|
+
t('чужое расширение не подано, имя названо',
|
|
167
|
+
R.chuzhoe.podano.length === 0 && R.chuzhoe.zhurnal.some((l) => /НЕ ВЗЯТ/.test(l)),
|
|
168
|
+
JSON.stringify(R.chuzhoe.zhurnal));
|
|
169
|
+
t('чужое расширение оставлено в ящике, а не стёрто', R.chuzhoe.ostalos.length === 1,
|
|
170
|
+
JSON.stringify(R.chuzhoe.ostalos));
|
|
171
|
+
|
|
172
|
+
R.nedostup = await progon([], { brokenDir: true });
|
|
173
|
+
t('недоступный ящик — громкая строка один раз',
|
|
174
|
+
R.nedostup.zhurnal.filter((l) => /недоступен/.test(l)).length === 1,
|
|
175
|
+
JSON.stringify(R.nedostup.zhurnal));
|
|
176
|
+
|
|
177
|
+
// ── канарейка точного числа ──────────────────────────────────────────────────
|
|
178
|
+
const ZHDYOM = 27;
|
|
179
|
+
console.log(`ИТОГО: сошлось ${ok}, расхождений ${fail}, слепот ${slep}`);
|
|
180
|
+
if (ok + fail !== ZHDYOM) {
|
|
181
|
+
console.log(`СЛЕПОТА: проведено ${ok + fail} проверок, ждали ${ZHDYOM} — часть не состоялась`);
|
|
182
|
+
process.exit(2);
|
|
183
|
+
}
|
|
184
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Приёмка постановки цели из канала.
|
|
4
|
+
*
|
|
5
|
+
* Правила, по которым он написан:
|
|
6
|
+
* — текст проверяемых функций берётся ИЗ ФАЙЛА пакета, а не пишется заново;
|
|
7
|
+
* — рядом с вердиктом печатается сырьё, на котором он основан;
|
|
8
|
+
* — код возврата: 0 всё сошлось, 1 расхождение, 2 стенд не смог проверить.
|
|
9
|
+
*
|
|
10
|
+
* Гоняется и на распакованном тарболе: `node test/test-goal-control.mjs`.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { readSource, extractFunction, extractConst } from './extract.mjs';
|
|
16
|
+
|
|
17
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const PKG = path.resolve(HERE, '..');
|
|
19
|
+
const SRC_FILE = path.join(PKG, 'src', 'index.js');
|
|
20
|
+
|
|
21
|
+
let ok = 0, bad = 0, blind = 0;
|
|
22
|
+
const pass = (t, raw) => { ok++; console.log(` ok ${t}${raw !== undefined ? ` [${raw}]` : ''}`); };
|
|
23
|
+
const fail = (t, raw) => { bad++; console.log(` FAIL ${t}${raw !== undefined ? ` [${raw}]` : ''}`); };
|
|
24
|
+
const blin = (t, raw) => { blind++; console.log(` ?? ${t} — СТЕНД НЕ СМОГ ПРОВЕРИТЬ${raw !== undefined ? ` [${raw}]` : ''}`); };
|
|
25
|
+
const eq = (t, got, want) => (String(got) === String(want) ? pass(t, `got=${got}`) : fail(t, `got=${got} want=${want}`));
|
|
26
|
+
|
|
27
|
+
let src;
|
|
28
|
+
try { src = readSource(SRC_FILE); }
|
|
29
|
+
catch (e) { console.error(`СТЕНД СЛЕП: не читается ${SRC_FILE}: ${e.message}`); process.exit(2); }
|
|
30
|
+
|
|
31
|
+
// ── 0. Манифест не обещает того, чего нет на диске ────────────────────────────
|
|
32
|
+
console.log('0. целость манифеста');
|
|
33
|
+
try {
|
|
34
|
+
const pj = JSON.parse(fs.readFileSync(path.join(PKG, 'package.json'), 'utf-8'));
|
|
35
|
+
const promised = new Set([...(pj.files ?? []), pj.main, pj.dsh?.bundle?.patch].filter(Boolean)
|
|
36
|
+
.map((f) => f.replace(/^\.\//, '')));
|
|
37
|
+
for (const f of promised) {
|
|
38
|
+
fs.existsSync(path.join(PKG, f))
|
|
39
|
+
? pass(`манифест обещает ${f} — файл на месте`)
|
|
40
|
+
: fail(`манифест обещает ${f}, а его на диске НЕТ`);
|
|
41
|
+
}
|
|
42
|
+
// 🔴 НЕ сверяем с зашитым номером: такой стенд краснеет на КАЖДОМ выпуске и
|
|
43
|
+
// его перестают читать. Проверяем форму — версия есть и она трёхчастная.
|
|
44
|
+
/^\d+\.\d+\.\d+$/.test(String(pj.version || ''))
|
|
45
|
+
? pass('версия объявлена и имеет форму X.Y.Z', `got=${pj.version}`)
|
|
46
|
+
: fail('версия объявлена и имеет форму X.Y.Z', `got=${pj.version}`);
|
|
47
|
+
} catch (e) { blin('манифест', e.message); }
|
|
48
|
+
|
|
49
|
+
// ── 1. Счётчик постановок ─────────────────────────────────────────────────────
|
|
50
|
+
console.log('1. счётчик постановок');
|
|
51
|
+
try {
|
|
52
|
+
const MAX = Number(extractConst(src, 'GOAL_MAX_PER_HOUR'));
|
|
53
|
+
eq('предел из файла', MAX, 3);
|
|
54
|
+
const goalStarts = new Map();
|
|
55
|
+
const goalRateOk = new Function('goalStarts', 'GOAL_MAX_PER_HOUR',
|
|
56
|
+
`${extractFunction(src, 'goalRateOk')}; return goalRateOk;`)(goalStarts, MAX);
|
|
57
|
+
let allowed = 0;
|
|
58
|
+
for (let i = 0; i < 5; i++) if (goalRateOk('tg')) { allowed++; goalStarts.get('tg').push(Date.now()); }
|
|
59
|
+
eq(`из пяти подряд пропущено ${MAX}`, allowed, MAX);
|
|
60
|
+
// старые метки выпадают из скользящего часа
|
|
61
|
+
goalStarts.set('tg', [Date.now() - 3600001, Date.now() - 3600001, Date.now() - 3600001]);
|
|
62
|
+
eq('метки старше часа не считаются', goalRateOk('tg'), true);
|
|
63
|
+
// счётчик отдельный на канал
|
|
64
|
+
goalStarts.set('tg', [Date.now(), Date.now(), Date.now()]);
|
|
65
|
+
eq('канал tg исчерпан', goalRateOk('tg'), false);
|
|
66
|
+
eq('канал a2a при этом свободен', goalRateOk('a2a'), true);
|
|
67
|
+
} catch (e) { blin('счётчик', e.message); }
|
|
68
|
+
|
|
69
|
+
// ── 2. Заголовок отправителя ──────────────────────────────────────────────────
|
|
70
|
+
console.log('2. заголовок отправителя');
|
|
71
|
+
try {
|
|
72
|
+
const splitSender = new Function(`${extractFunction(src, 'splitSender')}; return splitSender;`)();
|
|
73
|
+
let r = splitSender('From: coordinator\n/goal сделать дело');
|
|
74
|
+
eq('имя разобрано', r.sender, 'coordinator');
|
|
75
|
+
eq('заголовок отрезан', r.text, '/goal сделать дело');
|
|
76
|
+
r = splitSender('просто текст без заголовка');
|
|
77
|
+
eq('без заголовка — имени нет', r.sender, undefined);
|
|
78
|
+
eq('без заголовка текст цел', r.text, 'просто текст без заголовка');
|
|
79
|
+
r = splitSender('From: coordinator');
|
|
80
|
+
eq('заголовок без тела — пустой текст', r.text, '');
|
|
81
|
+
r = splitSender('From: two words\nтело');
|
|
82
|
+
eq('имя с пробелом заголовком не считается', r.sender, undefined);
|
|
83
|
+
} catch (e) { blin('splitSender', e.message); }
|
|
84
|
+
|
|
85
|
+
// ── 3. Ожидание сервиса ───────────────────────────────────────────────────────
|
|
86
|
+
console.log('3. ожидание сервиса');
|
|
87
|
+
try {
|
|
88
|
+
const lines = [];
|
|
89
|
+
const log = (m) => lines.push(m);
|
|
90
|
+
const mk = (ctx) => new Function('ctx', 'log', `${extractFunction(src, 'waitService')}; return waitService;`)(ctx, log);
|
|
91
|
+
|
|
92
|
+
let calls = 0;
|
|
93
|
+
const late = mk({ get: () => (++calls >= 3 ? { real: true } : undefined) });
|
|
94
|
+
const t0 = Date.now();
|
|
95
|
+
const got = await late('goals', '[t]', 'последствие названо', 5);
|
|
96
|
+
const spent = Date.now() - t0;
|
|
97
|
+
got ? pass(`сервис дождались за ${spent} мс`, `попыток=${calls}`) : fail('сервис не дождались, хотя он появился');
|
|
98
|
+
lines.some((l) => l.includes('ещё не активен')) ? pass('о начале ожидания сказано') : fail('ожидание молчаливое');
|
|
99
|
+
|
|
100
|
+
lines.length = 0;
|
|
101
|
+
const never = mk({ get: () => undefined });
|
|
102
|
+
const none = await never('goals', '[t]', 'ПОСЛЕДСТВИЕ-МЕТКА', 2);
|
|
103
|
+
eq('не дождались — undefined', none, undefined);
|
|
104
|
+
lines.some((l) => l.includes('ПОСЛЕДСТВИЕ-МЕТКА'))
|
|
105
|
+
? pass('последствие названо в журнале', lines[lines.length - 1])
|
|
106
|
+
: fail('последствие НЕ названо', lines.join(' | '));
|
|
107
|
+
|
|
108
|
+
lines.length = 0;
|
|
109
|
+
const now = mk({ get: () => ({ real: true }) });
|
|
110
|
+
await now('goals', '[t]', 'последствие', 5);
|
|
111
|
+
lines.length === 0 ? pass('сервис готов сразу — лишних строк нет') : fail('печатает, когда ждать не пришлось', lines.join(' | '));
|
|
112
|
+
} catch (e) { blin('waitService', e.message); }
|
|
113
|
+
|
|
114
|
+
// ── 4. Команда целиком, на подставных сервисах ───────────────────────────────
|
|
115
|
+
console.log('4. команда /goal на подставных сервисах');
|
|
116
|
+
try {
|
|
117
|
+
const lines = [];
|
|
118
|
+
const log = (m) => lines.push(m);
|
|
119
|
+
const goalStarts = new Map();
|
|
120
|
+
const goalOrigin = new Map();
|
|
121
|
+
const agent = { session: { id: 'sess-1' } };
|
|
122
|
+
let created = null;
|
|
123
|
+
const goals = {
|
|
124
|
+
get: () => created,
|
|
125
|
+
create: (_a, { objective }) => (created = { id: 'g1', revision: 1, phase: 'active', roundsStarted: 0, maxGoalRounds: 12, objective }),
|
|
126
|
+
edit: (_a, _k, { objective }) => (created = { ...created, revision: created.revision + 1, objective }),
|
|
127
|
+
clear: () => { created = null; },
|
|
128
|
+
};
|
|
129
|
+
const marker = { guidance: (id, rev, tools) => `НАСТАВЛЕНИЕ ${id}/${rev} ${tools ? tools.update : 'без-инструментов'}` };
|
|
130
|
+
const bridge = { modelToolName: (n) => `mcp__srv__${n}` };
|
|
131
|
+
const services = { goals, goalMarker: marker, mcpBridge: bridge };
|
|
132
|
+
const ctx = { get: (n) => services[n] };
|
|
133
|
+
|
|
134
|
+
const src4 = `${extractFunction(src, 'waitService')}\n${extractFunction(src, 'goalRateOk')}\n${extractFunction(src, 'goalCommand')}\n; return goalCommand;`;
|
|
135
|
+
const goalCommand = new Function('ctx', 'log', 'goalStarts', 'goalOrigin', 'GOAL_MAX_PER_HOUR', 'GOAL_WAIT_TRIES', src4)(
|
|
136
|
+
ctx, log, goalStarts, goalOrigin, Number(extractConst(src, 'GOAL_MAX_PER_HOUR')), 1);
|
|
137
|
+
|
|
138
|
+
let r = await goalCommand('/goal', 'tg', agent);
|
|
139
|
+
eq('цели нет — так и сказано', r, 'Цели нет.');
|
|
140
|
+
|
|
141
|
+
r = await goalCommand('/goal разобрать отказы', 'tg', agent);
|
|
142
|
+
r.includes('поставлена') ? pass('цель поставлена', r) : fail('цель не поставлена', r);
|
|
143
|
+
created.objective.includes('НАСТАВЛЕНИЕ g1/2 mcp__srv__update_goal')
|
|
144
|
+
? pass('наставление сложено с именем инструмента ОТ МОСТА', created.objective.split('\n').pop())
|
|
145
|
+
: fail('наставление без имени инструмента моста', created.objective);
|
|
146
|
+
eq('происхождение цели запомнено', goalOrigin.get('sess-1')?.channel, 'tg');
|
|
147
|
+
|
|
148
|
+
r = await goalCommand('/goal', 'tg', agent);
|
|
149
|
+
r.startsWith('Цель g1: active') ? pass('состояние показано', r.split('\n')[0]) : fail('состояние не показано', r);
|
|
150
|
+
|
|
151
|
+
r = await goalCommand('/goal stop', 'tg', agent);
|
|
152
|
+
eq('цель снята', r, 'Цель g1 снята.');
|
|
153
|
+
eq('происхождение забыто', goalOrigin.has('sess-1'), false);
|
|
154
|
+
|
|
155
|
+
// предел постановок
|
|
156
|
+
goalStarts.set('a2a', [Date.now(), Date.now(), Date.now()]);
|
|
157
|
+
r = await goalCommand('/goal ещё одна', 'a2a', agent);
|
|
158
|
+
r.includes('Предел') ? pass('четвёртая за час отвергнута', r) : fail('предел не сработал', r);
|
|
159
|
+
|
|
160
|
+
// 🔴 снятие пределом НЕ ограничено — механизм, который нельзя выключить, хуже отсутствия
|
|
161
|
+
created = { id: 'g2', revision: 1, phase: 'active', roundsStarted: 1, maxGoalRounds: 12, objective: 'x' };
|
|
162
|
+
r = await goalCommand('/goal stop', 'a2a', agent);
|
|
163
|
+
eq('снятие при исчерпанном пределе разрешено', r, 'Цель g2 снята.');
|
|
164
|
+
|
|
165
|
+
// сервиса целей нет вовсе
|
|
166
|
+
const lines2 = [];
|
|
167
|
+
const goalCommand2 = new Function('ctx', 'log', 'goalStarts', 'goalOrigin', 'GOAL_MAX_PER_HOUR', 'GOAL_WAIT_TRIES', src4)(
|
|
168
|
+
{ get: () => undefined }, (m) => lines2.push(m), new Map(), new Map(), 3, 1);
|
|
169
|
+
r = await goalCommand2('/goal что-нибудь', 'tg', agent);
|
|
170
|
+
r.includes('не подключён') ? pass('без сервиса целей отказ внятный', r) : fail('без сервиса целей ответ невнятный', r);
|
|
171
|
+
lines2.some((l) => l.includes('невыполнима')) ? pass('и последствие названо в журнале', lines2[lines2.length - 1]) : fail('журнал молчит об отказе', lines2.join(' | '));
|
|
172
|
+
|
|
173
|
+
// маркера нет — цель ставится, но об этом сказано
|
|
174
|
+
const lines3 = [];
|
|
175
|
+
let created3 = null;
|
|
176
|
+
const goals3 = { get: () => created3,
|
|
177
|
+
create: (_a, { objective }) => (created3 = { id: 'g3', revision: 1, phase: 'active', roundsStarted: 0, maxGoalRounds: 12, objective }),
|
|
178
|
+
edit: () => { throw new Error('edit не должен зваться без маркера'); }, clear: () => {} };
|
|
179
|
+
const goalCommand3 = new Function('ctx', 'log', 'goalStarts', 'goalOrigin', 'GOAL_MAX_PER_HOUR', 'GOAL_WAIT_TRIES', src4)(
|
|
180
|
+
{ get: (n) => (n === 'goals' ? goals3 : undefined) }, (m) => lines3.push(m), new Map(), new Map(), 3, 1);
|
|
181
|
+
r = await goalCommand3('/goal без маркера', 'tg', agent);
|
|
182
|
+
r.includes('поставлена') ? pass('без маркера цель всё равно поставлена', r) : fail('без маркера постановка сорвалась', r);
|
|
183
|
+
lines3.some((l) => l.includes('goalMarker') && l.includes('предела раундов'))
|
|
184
|
+
? pass('последствие отсутствия маркера названо')
|
|
185
|
+
: fail('об отсутствии маркера промолчали', lines3.join(' | '));
|
|
186
|
+
} catch (e) { blin('goalCommand', e.message); }
|
|
187
|
+
|
|
188
|
+
// ── 5. Описание не расходится с кодом ────────────────────────────────────────
|
|
189
|
+
console.log('5. README, патч и код говорят одно');
|
|
190
|
+
try {
|
|
191
|
+
const readme = fs.readFileSync(path.join(PKG, 'README.md'), 'utf-8');
|
|
192
|
+
const patch = fs.readFileSync(path.join(PKG, 'cordis.patch.yml'), 'utf-8');
|
|
193
|
+
for (const field of ['goalUsers', 'goalA2ASenders']) {
|
|
194
|
+
const inCode = src.includes(`config.${field}`);
|
|
195
|
+
const inReadme = readme.includes(`\`${field}\``);
|
|
196
|
+
const inPatch = patch.includes(`${field}:`);
|
|
197
|
+
(inCode && inReadme && inPatch)
|
|
198
|
+
? pass(`${field}: код+README+патч`, `код=${inCode} readme=${inReadme} патч=${inPatch}`)
|
|
199
|
+
: fail(`${field} описан не везде`, `код=${inCode} readme=${inReadme} патч=${inPatch}`);
|
|
200
|
+
}
|
|
201
|
+
const maxInCode = extractConst(src, 'GOAL_MAX_PER_HOUR');
|
|
202
|
+
// 🔴 Сверяем ЧИСЛО из кода с числом в README, а не дословную фразу: проверка
|
|
203
|
+
// по формулировке ломается от любой переписки текста, и тогда стенд краснеет
|
|
204
|
+
// на исправном предмете. Ищем число рядом со словом про час.
|
|
205
|
+
// \b и \n в шаблонной строке JS — это backspace и перевод строки, а НЕ границы
|
|
206
|
+
// регулярного выражения: их надо удваивать. Первая редакция этого не делала и
|
|
207
|
+
// краснела на исправном README.
|
|
208
|
+
const predelVReadme = new RegExp(`\\b${maxInCode}\\b[^.\\n]{0,60}час`, 'i').test(readme)
|
|
209
|
+
|| new RegExp(`час[^.\\n]{0,60}\\b${maxInCode}\\b`, 'i').test(readme);
|
|
210
|
+
predelVReadme
|
|
211
|
+
? pass('предел в README и в коде сходится', `код=${maxInCode}`)
|
|
212
|
+
: fail('предел в README разошёлся с кодом', `код=${maxInCode}, в README не найден рядом со словом «час»`);
|
|
213
|
+
} catch (e) { blin('README', e.message); }
|
|
214
|
+
|
|
215
|
+
// ── 6. Проверка «ничего нашего не уехало» ВЫНЕСЕНА ИЗ ПАКЕТА ───────────────
|
|
216
|
+
// Она лежит рядом со сборкой (proverka-sborki.mjs) и в тарбол не едет.
|
|
217
|
+
// Причина: её образцы — это и есть наши имена и пути, то есть проверка
|
|
218
|
+
// обезличенности сама несла их в опубликованный файл. Обнаружено на
|
|
219
|
+
// сборке 1.3.0: единственное вхождение имени агента в тарболе было
|
|
220
|
+
// внутри самой этой проверки. Чужому человеку она бесполезна — это
|
|
221
|
+
// проверка НАШЕГО процесса сборки, а не работы плагина.
|
|
222
|
+
|
|
223
|
+
console.log(`\nитог: ok=${ok} FAIL=${bad} слепота=${blind}`);
|
|
224
|
+
process.exit(bad > 0 ? 1 : (blind > 0 ? 2 : 0));
|