dsh-telegram-multiagent 1.3.0 → 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 +151 -108
- package/cordis.patch.yml +9 -8
- package/package.json +8 -4
- package/src/index.js +334 -151
- package/test/stend-imena.mjs +133 -0
- package/test/stend-yashchika-shiny.mjs +184 -0
- package/test/test-goal-control.mjs +16 -4
|
@@ -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);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Приёмка постановки цели из
|
|
3
|
+
* Приёмка постановки цели из канала.
|
|
4
4
|
*
|
|
5
5
|
* Правила, по которым он написан:
|
|
6
6
|
* — текст проверяемых функций берётся ИЗ ФАЙЛА пакета, а не пишется заново;
|
|
@@ -39,7 +39,11 @@ try {
|
|
|
39
39
|
? pass(`манифест обещает ${f} — файл на месте`)
|
|
40
40
|
: fail(`манифест обещает ${f}, а его на диске НЕТ`);
|
|
41
41
|
}
|
|
42
|
-
|
|
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}`);
|
|
43
47
|
} catch (e) { blin('манифест', e.message); }
|
|
44
48
|
|
|
45
49
|
// ── 1. Счётчик постановок ─────────────────────────────────────────────────────
|
|
@@ -195,9 +199,17 @@ try {
|
|
|
195
199
|
: fail(`${field} описан не везде`, `код=${inCode} readme=${inReadme} патч=${inPatch}`);
|
|
196
200
|
}
|
|
197
201
|
const maxInCode = extractConst(src, 'GOAL_MAX_PER_HOUR');
|
|
198
|
-
|
|
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
|
|
199
211
|
? pass('предел в README и в коде сходится', `код=${maxInCode}`)
|
|
200
|
-
: fail('предел в README разошёлся с кодом', `код=${maxInCode}
|
|
212
|
+
: fail('предел в README разошёлся с кодом', `код=${maxInCode}, в README не найден рядом со словом «час»`);
|
|
201
213
|
} catch (e) { blin('README', e.message); }
|
|
202
214
|
|
|
203
215
|
// ── 6. Проверка «ничего нашего не уехало» ВЫНЕСЕНА ИЗ ПАКЕТА ───────────────
|