dsh-telegram-multiagent 1.4.3 → 1.5.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/CHANGELOG.md +40 -0
- package/package.json +6 -10
- package/src/index.js +66 -3
- package/test/stend-svezhesti-opory.mjs +135 -0
- package/test/vse-stendy.mjs +52 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
## 1.5.0
|
|
2
|
+
|
|
3
|
+
🔴 **`/compact-status` больше не выдаёт устаревшую опору за свежую.** Строка «Опора замера:
|
|
4
|
+
usage (числа провайдера)» была правдива про ИСТОЧНИК и обманчива про АКТУАЛЬНОСТЬ: измеритель
|
|
5
|
+
копирует `baseline` из якоря вместе с его `kind`, а якорь ставится только на
|
|
6
|
+
`assistant/message`. После компакта поверхность пересчитывается, якорь остаётся прежним — и
|
|
7
|
+
команда сообщала «числа провайдера» рядом с числом, которого провайдер не называл. Флаг не
|
|
8
|
+
отсутствовал, он ПРИСУТСТВОВАЛ И УСПОКАИВАЛ, а это хуже отсутствия.
|
|
9
|
+
|
|
10
|
+
Три состояния вместо двух:
|
|
11
|
+
|
|
12
|
+
опора свежая «(числа провайдера)», расстояние до порога печатается
|
|
13
|
+
опора устарела «🔴 ЧИСЛА СНЯТЫ ДО КОМПАКТА», расстояние НЕ печатается вовсе
|
|
14
|
+
снять не удалось «⚠️ свежесть снять НЕ МОГУ (причина)», расстояние не печатается
|
|
15
|
+
|
|
16
|
+
Расстояние до порога от устаревшей опоры не «примерно», а НЕИЗВЕСТНО: вырезанное вычтено по
|
|
17
|
+
эвристической цене (4 знака = 1 токен), а она на кириллице занижает в разы.
|
|
18
|
+
|
|
19
|
+
Интерфейс сессии определяется ПО ФАКТУ, не по номеру версии: в одних редакциях платформы у
|
|
20
|
+
сессии массив `events`, в других `eventAt()` + `seq`. Не сумели снять — говорим «не знаю», а
|
|
21
|
+
НЕ «свежо»: молчаливое «свежо» при пропавшем интерфейсе было бы зелёным, которое не может
|
|
22
|
+
покраснеть.
|
|
23
|
+
|
|
24
|
+
Цена обхода замерена: **0,46 мс** на журнале в 61 866 событий.
|
|
25
|
+
|
|
26
|
+
🔴 **Заведён `scripts.test` — его не было ВОВСЕ.** То есть `npm test` у получателя не гонял ни
|
|
27
|
+
одного из шести стендов, и снаружи пакет был неотличим от пакета без проверок. Прогонщик
|
|
28
|
+
снимает перечень обходом каталога, а не ведёт его рукой; `files` перечисляет каталоги, а не
|
|
29
|
+
файлы поимённо, поэтому новый стенд едет к получателю сам.
|
|
30
|
+
Прогон сразу показал законную слепоту `stend-imena` (имена проверяются только в двух строках
|
|
31
|
+
предмета, стенда по всему составу нет) — она была всегда, её просто некому было увидеть.
|
|
32
|
+
|
|
33
|
+
Новый стенд `stend-svezhesti-opory.mjs`: 12 проб, вырезает признак ИЗ ПРЕДМЕТА, а не хранит
|
|
34
|
+
копию. Две порчи дают РАЗНЫЕ ответы — удаление признака слепоту (2), излом логики при
|
|
35
|
+
сохранной форме расхождение (1).
|
|
36
|
+
|
|
37
|
+
⚠️ Порог по-прежнему считается как `окно × 0.8` — умолчание платформы. Появится
|
|
38
|
+
`thresholdRatio` в составе — число станет ложью, а стенд не покраснеет: он читает наш файл,
|
|
39
|
+
а не чужой состав. Эта граница не тронута и не починена.
|
|
40
|
+
|
|
1
41
|
# Изменения
|
|
2
42
|
|
|
3
43
|
Указатель по версиям. Подробности каждой — отдельным разделом README: здесь
|
package/package.json
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-telegram-multiagent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Канал Telegram для DeepSeek Harness: один модуль на машину обслуживает несколько агентов, у каждого свой бот, свой файл токена и свой список разрешённых. Метка отправителя неподделываема, память владельца и координатора слита в одну, режим доставки переключается файлом вне кода, цели управляются из канала.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test/vse-stendy.mjs"
|
|
9
|
+
},
|
|
7
10
|
"files": [
|
|
8
|
-
"src
|
|
9
|
-
"
|
|
10
|
-
"test/extract.mjs",
|
|
11
|
-
"test/stend-imena.mjs",
|
|
12
|
-
"test/stend-yashchika-shiny.mjs",
|
|
13
|
-
"test/test-goal-control.mjs",
|
|
14
|
-
"test/stend-komand-konteksta.mjs",
|
|
15
|
-
"test/stend-dostupa-sluzhb.mjs",
|
|
16
|
-
"test/stend-pobudki.mjs",
|
|
11
|
+
"src",
|
|
12
|
+
"test",
|
|
17
13
|
"cordis.patch.yml",
|
|
18
14
|
"README.md",
|
|
19
15
|
"CHANGELOG.md"
|
package/src/index.js
CHANGED
|
@@ -1470,11 +1470,63 @@ export function apply(ctx, config = {}) {
|
|
|
1470
1470
|
let m;
|
|
1471
1471
|
try { m = schetchik.measure(sess); }
|
|
1472
1472
|
catch (e) { return { kind: 'error', text: `Измеритель отказал: ${String(e?.message ?? e).slice(0, 160)}` }; }
|
|
1473
|
+
// 🔴 СВЕЖЕСТЬ ОПОРЫ. baseline.kind различает ПРОИСХОЖДЕНИЕ опоры и НЕ различает
|
|
1474
|
+
// её СВЕЖЕСТЬ: при surface-replace (compaction/summary|prune) поверхность
|
|
1475
|
+
// пересчитывается, а якорь остаётся прежним и тащит свой kind='usage'
|
|
1476
|
+
// (token-meter :507 baseline = anchor.baseline; :588 якорь только на
|
|
1477
|
+
// assistant/message). Значит после компакта строка «числа провайдера» говорит
|
|
1478
|
+
// правду про источник и ложь про актуальность — ровно там, где на неё смотрят.
|
|
1479
|
+
// Флаг не отсутствует, он ПРИСУТСТВУЕТ И УСПОКАИВАЕТ; это хуже отсутствия.
|
|
1480
|
+
//
|
|
1481
|
+
// ⚠️ ИНТЕРФЕЙС ОПРЕДЕЛЯЕМ ПО ФАКТУ, НЕ ПО ВЕРСИИ: в 0.1.1-rc.2 у сессии массив
|
|
1482
|
+
// `events`, в 0.1.2-rc.1 и master — `eventAt()` + `seq`. Версия на диске может
|
|
1483
|
+
// отставать от кода; проверяем наличие, а не номер.
|
|
1484
|
+
//
|
|
1485
|
+
// 🔴 И ГЛАВНОЕ: не сумели снять — говорим «НЕ МОГУ», а НЕ «свежо». Молчаливое
|
|
1486
|
+
// «свежо» при пропавшем интерфейсе — зелёное, которое не может покраснеть.
|
|
1487
|
+
const svezhest = (() => {
|
|
1488
|
+
let poslednijKompakt = -1;
|
|
1489
|
+
let poslednijYakor = -1;
|
|
1490
|
+
const uchest = (ev, seq) => {
|
|
1491
|
+
const t = ev?.type;
|
|
1492
|
+
if (t === 'compaction/summary' || t === 'compaction/prune') {
|
|
1493
|
+
if (seq > poslednijKompakt) poslednijKompakt = seq;
|
|
1494
|
+
} else if (t === 'assistant/message' && ev?.data?.usage !== undefined) {
|
|
1495
|
+
if (seq > poslednijYakor) poslednijYakor = seq;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
try {
|
|
1499
|
+
if (Array.isArray(sess.events)) {
|
|
1500
|
+
for (const ev of sess.events) uchest(ev, Number(ev?.seq ?? -1));
|
|
1501
|
+
} else if (typeof sess.eventAt === 'function' && sess.seq !== undefined) {
|
|
1502
|
+
const granica = Number(sess.seq);
|
|
1503
|
+
if (!Number.isFinite(granica)) return { kind: 'ne-znayu', pochemu: 'session.seq не число' };
|
|
1504
|
+
for (let i = 0; i < granica; i++) {
|
|
1505
|
+
const ev = sess.eventAt(i);
|
|
1506
|
+
if (ev === undefined) return { kind: 'ne-znayu', pochemu: 'eventAt вернул undefined — иная сигнатура' };
|
|
1507
|
+
uchest(ev, i);
|
|
1508
|
+
}
|
|
1509
|
+
} else {
|
|
1510
|
+
return { kind: 'ne-znayu', pochemu: 'у сессии нет ни events, ни eventAt+seq' };
|
|
1511
|
+
}
|
|
1512
|
+
} catch (e) {
|
|
1513
|
+
return { kind: 'ne-znayu', pochemu: `обход событий отказал: ${String(e?.message ?? e).slice(0, 90)}` };
|
|
1514
|
+
}
|
|
1515
|
+
if (poslednijKompakt < 0) return { kind: 'svezhaya', kompaktov: 0 };
|
|
1516
|
+
if (poslednijYakor > poslednijKompakt) return { kind: 'svezhaya', kompaktov: 1 };
|
|
1517
|
+
return { kind: 'ustarela', kompakt: poslednijKompakt, yakor: poslednijYakor };
|
|
1518
|
+
})();
|
|
1473
1519
|
const stroki = [
|
|
1474
1520
|
`Контекст: занято ${m.totalTokens} токенов.`,
|
|
1475
1521
|
`Источник — платформенный tokenMeter.measure: то же число, которым платформа решает, пора ли сжимать.`,
|
|
1476
1522
|
`Опора замера: ${m.baseline?.kind ?? '?'}` +
|
|
1477
|
-
(m.baseline?.kind === 'usage'
|
|
1523
|
+
(m.baseline?.kind === 'usage'
|
|
1524
|
+
? (svezhest.kind === 'ustarela'
|
|
1525
|
+
? ' 🔴 ЧИСЛА ПРОВАЙДЕРА СНЯТЫ ДО КОМПАКТА И С ТЕХ ПОР НЕ ОБНОВЛЯЛИСЬ'
|
|
1526
|
+
: svezhest.kind === 'ne-znayu'
|
|
1527
|
+
? ` ⚠️ свежесть опоры снять НЕ МОГУ (${svezhest.pochemu})`
|
|
1528
|
+
: ' (числа провайдера)')
|
|
1529
|
+
: ' (оценка по эвристике)') +
|
|
1478
1530
|
`, прочитано событий журнала: ${m.logRevision}.`,
|
|
1479
1531
|
];
|
|
1480
1532
|
// Порог платформы: contextWindow × thresholdRatio (умолчание 0.8).
|
|
@@ -1525,14 +1577,25 @@ export function apply(ctx, config = {}) {
|
|
|
1525
1577
|
}
|
|
1526
1578
|
if (okno !== undefined) {
|
|
1527
1579
|
const porog = Math.floor(okno * 0.8);
|
|
1528
|
-
|
|
1580
|
+
if (svezhest.kind === 'svezhaya') {
|
|
1581
|
+
stroki.push(`Порог сжатия ~${porog} (окно ${okno} × 0.8 — умолчание платформы), до него ${porog - m.totalTokens}.`);
|
|
1582
|
+
} else if (svezhest.kind === 'ustarela') {
|
|
1583
|
+
// Расстояние до порога от устаревшей опоры — не «примерно», а НЕИЗВЕСТНО:
|
|
1584
|
+
// вырезанное вычтено по эвристической цене (4 знака = 1 токен), а она на
|
|
1585
|
+
// не-латинице занижена в разы (замер 04.09: ×9 на живом компакте).
|
|
1586
|
+
stroki.push(`Порог сжатия ~${porog} (окно ${okno} × 0.8). 🔴 Расстояние до него сказать НЕ МОГУ:`);
|
|
1587
|
+
stroki.push(`опора снята ДО компакта, вырезанное вычтено по эвристике (4 знака = 1 токен), она занижает на кириллице.`);
|
|
1588
|
+
stroki.push(`Верное число появится после первого же ответа модели — тогда якорь переставится.`);
|
|
1589
|
+
} else {
|
|
1590
|
+
stroki.push(`Порог сжатия ~${porog} (окно ${okno} × 0.8). ⚠️ До него — не считаю: свежесть опоры не снята.`);
|
|
1591
|
+
}
|
|
1529
1592
|
} else {
|
|
1530
1593
|
// Умолчание сюда не подставляется намеренно: выдуманное окно выглядит
|
|
1531
1594
|
// как замер и врёт ровно там, где на него посмотрят. Отказ называет
|
|
1532
1595
|
// ПРИЧИНУ — иначе «не могу» неотличимо от «не пробовала».
|
|
1533
1596
|
stroki.push(`🔴 Окно контекста недоступно (${pochemuNet}) — расстояние до порога сказать НЕ МОГУ.`);
|
|
1534
1597
|
}
|
|
1535
|
-
log(`[статус] ${m.totalTokens} токенов, опора ${m.baseline?.kind ?? '?'}`);
|
|
1598
|
+
log(`[статус] ${m.totalTokens} токенов, опора ${m.baseline?.kind ?? '?'}/${svezhest.kind}`);
|
|
1536
1599
|
return { kind: 'success', text: stroki.join('\n') };
|
|
1537
1600
|
},
|
|
1538
1601
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Стенд признака СВЕЖЕСТИ опоры замера в /compact-status.
|
|
2
|
+
//
|
|
3
|
+
// Коды: 0 сошлось | 1 расхождение | 2 слепота (проверить не удалось).
|
|
4
|
+
//
|
|
5
|
+
// ЧТО ЛЕЧИТ ПРЕДМЕТ. baseline.kind различает ПРОИСХОЖДЕНИЕ опоры («числа провайдера»
|
|
6
|
+
// против «оценка по эвристике») и НЕ различает её СВЕЖЕСТЬ. При surface-replace
|
|
7
|
+
// (compaction/summary|prune) поверхность пересчитывается, а якорь остаётся прежним и
|
|
8
|
+
// тащит свой kind='usage'. То есть после компакта строка правдива про источник и
|
|
9
|
+
// обманчива про актуальность — ровно там, где на неё смотрят. Флаг не отсутствует,
|
|
10
|
+
// он ПРИСУТСТВУЕТ И УСПОКАИВАЕТ, и это хуже отсутствия.
|
|
11
|
+
//
|
|
12
|
+
// 🔴 ВЫРЕЗАЕМ ИЗ ПРЕДМЕТА, А НЕ ПЕРЕПИСЫВАЕМ. Копия стареет молча: перепишут предмет —
|
|
13
|
+
// стенд продолжит проверять прежнее и останется зелёным. То же правило, что в
|
|
14
|
+
// stend-komand-konteksta.mjs, и оно там уже окупилось.
|
|
15
|
+
//
|
|
16
|
+
// ГДЕ НЕ ПРИМЕНЯЕТСЯ. Стенд проверяет ТОЛЬКО вычисление свежести на подставных
|
|
17
|
+
// сессиях. Он НЕ проверяет:
|
|
18
|
+
// — что признак верен на ЖИВОМ компакте (это П1/П2 ворот, только в бою);
|
|
19
|
+
// — что расстояние до порога печатается/не печатается (это текст ответа, не эта функция);
|
|
20
|
+
// — цену обхода на большом журнале.
|
|
21
|
+
// «Стенд зелёный» здесь означает «логика различает три состояния», а не «команда не врёт».
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from 'node:fs'
|
|
24
|
+
|
|
25
|
+
const SRC = process.env.SVYAZ_SRC || new URL('../src/index.js', import.meta.url).pathname
|
|
26
|
+
let text
|
|
27
|
+
try { text = readFileSync(SRC, 'utf8') } catch (e) {
|
|
28
|
+
console.log(`СЛЕПОТА: предмет ${SRC} не читается: ${e?.message ?? e}`); process.exit(2)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Вырезаем тело признака ровно так, как оно стоит в предмете.
|
|
32
|
+
const m = text.match(/const svezhest = \(\(\) => \{[\s\S]*?\n {14}\}\)\(\);/)
|
|
33
|
+
if (!m) {
|
|
34
|
+
console.log('СЛЕПОТА: в предмете не найден блок `const svezhest = (() => {...})();` — вырезать нечего.')
|
|
35
|
+
console.log(' Это НЕ «признака нет»: он мог быть переписан в другой форме. Проверить руками.')
|
|
36
|
+
process.exit(2)
|
|
37
|
+
}
|
|
38
|
+
// Оборачиваем вырезанное в функцию от sess: внутри блок обращается только к `sess`.
|
|
39
|
+
let svezhestOt
|
|
40
|
+
try {
|
|
41
|
+
svezhestOt = new Function('sess', m[0].replace('const svezhest = ', 'return ') .replace(/;$/, '') + ';')
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.log(`СЛЕПОТА: вырезанный блок не собирается: ${e?.message ?? e}`); process.exit(2)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let ok = 0, bed = 0
|
|
47
|
+
const t = (imya, delo) => {
|
|
48
|
+
try { delo(); ok++; console.log(` ok ${imya}`) }
|
|
49
|
+
catch (e) { bed++; console.log(` FAIL ${imya} — ${e?.message ?? e}`) }
|
|
50
|
+
}
|
|
51
|
+
const ravno = (bylo, zhdali, chto) => {
|
|
52
|
+
if (bylo !== zhdali) throw new Error(`${chto}: «${bylo}», а ждали «${zhdali}»`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── подставные сессии ────────────────────────────────────────────────────────
|
|
56
|
+
const soobshchenie = (seq) => ({ seq, type: 'assistant/message', data: { usage: { inputTokens: 1 } } })
|
|
57
|
+
const kompakt = (seq, tip = 'compaction/summary') => ({ seq, type: tip })
|
|
58
|
+
const massiv = (evs) => ({ events: evs })
|
|
59
|
+
const cherezEventAt = (evs) => ({
|
|
60
|
+
seq: evs.length,
|
|
61
|
+
eventAt: (i) => evs[i],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ── СТАРЫЙ ИНТЕРФЕЙС: массив events ─────────────────────────────────────────
|
|
65
|
+
t('компакта не было → свежая', () => {
|
|
66
|
+
ravno(svezhestOt(massiv([soobshchenie(0), soobshchenie(1)])).kind, 'svezhaya', 'исход')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
t('компакт был, ответ ПОСЛЕ него → свежая (якорь переставился)', () => {
|
|
70
|
+
ravno(svezhestOt(massiv([soobshchenie(0), kompakt(1), soobshchenie(2)])).kind, 'svezhaya', 'исход')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// 🔴 ГЛАВНАЯ. Ради неё вся заплатка: это состояние сегодня врало «числа провайдера».
|
|
74
|
+
t('компакт был, ответа ПОСЛЕ нет → устарела', () => {
|
|
75
|
+
const r = svezhestOt(massiv([soobshchenie(0), soobshchenie(1), kompakt(2)]))
|
|
76
|
+
ravno(r.kind, 'ustarela', 'исход')
|
|
77
|
+
if (r.kompakt !== 2 || r.yakor !== 1) throw new Error(`числа: компакт ${r.kompakt}, якорь ${r.yakor}, ждали 2 и 1`)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
t('prune двигает поверхность так же, как summary', () => {
|
|
81
|
+
ravno(svezhestOt(massiv([soobshchenie(0), kompakt(1, 'compaction/prune')])).kind, 'ustarela', 'исход')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Ответ БЕЗ usage якорем не является: измеритель ставит якорь по числам провайдера.
|
|
85
|
+
t('ответ без usage после компакта якорем НЕ считается', () => {
|
|
86
|
+
const evs = [soobshchenie(0), kompakt(1), { seq: 2, type: 'assistant/message', data: {} }]
|
|
87
|
+
ravno(svezhestOt(massiv(evs)).kind, 'ustarela', 'исход')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// ── НОВЫЙ ИНТЕРФЕЙС: eventAt + seq ──────────────────────────────────────────
|
|
91
|
+
t('новый интерфейс: компакт последним → устарела', () => {
|
|
92
|
+
ravno(svezhestOt(cherezEventAt([soobshchenie(0), soobshchenie(1), kompakt(2)])).kind, 'ustarela', 'исход')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
t('новый интерфейс: ответ после компакта → свежая', () => {
|
|
96
|
+
ravno(svezhestOt(cherezEventAt([soobshchenie(0), kompakt(1), soobshchenie(2)])).kind, 'svezhaya', 'исход')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// ── ОТКАЗЫ. Каждый обязан дать «не знаю», а НЕ «свежо» ──────────────────────
|
|
100
|
+
// 🔴 Это и есть ветка, которая сработает при обновлении платформы. Без этих проб
|
|
101
|
+
// она не проверена ничем, а молчаливое «свежо» при пропавшем интерфейсе — зелёное,
|
|
102
|
+
// которое не может покраснеть.
|
|
103
|
+
t('нет ни events, ни eventAt → не знаю, а не «свежо»', () => {
|
|
104
|
+
const r = svezhestOt({})
|
|
105
|
+
ravno(r.kind, 'ne-znayu', 'исход')
|
|
106
|
+
if (!/events/.test(r.pochemu)) throw new Error(`причина не названа: «${r.pochemu}»`)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
t('eventAt есть, seq не число → не знаю', () => {
|
|
110
|
+
ravno(svezhestOt({ eventAt: () => undefined, seq: 'много' }).kind, 'ne-znayu', 'исход')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
t('eventAt вернул undefined (иная сигнатура) → не знаю', () => {
|
|
114
|
+
const r = svezhestOt({ eventAt: () => undefined, seq: 3 })
|
|
115
|
+
ravno(r.kind, 'ne-znayu', 'исход')
|
|
116
|
+
if (!/сигнатура/.test(r.pochemu)) throw new Error(`причина не про сигнатуру: «${r.pochemu}»`)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
t('eventAt бросил → не знаю, причина названа', () => {
|
|
120
|
+
const r = svezhestOt({ eventAt: () => { throw new Error('SessionSeq требуется') }, seq: 2 })
|
|
121
|
+
ravno(r.kind, 'ne-znayu', 'исход')
|
|
122
|
+
if (!/отказал/.test(r.pochemu)) throw new Error(`причина не про отказ: «${r.pochemu}»`)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// ── ПАРА К ГЛАВНОЙ: три исхода обязаны РАЗЛИЧАТЬСЯ ──────────────────────────
|
|
126
|
+
// Иначе признак есть, а различать нечем — та же вывеска, только внутри механизма.
|
|
127
|
+
t('три исхода дают три РАЗНЫХ ответа', () => {
|
|
128
|
+
const a = svezhestOt(massiv([soobshchenie(0)])).kind
|
|
129
|
+
const b = svezhestOt(massiv([soobshchenie(0), kompakt(1)])).kind
|
|
130
|
+
const c = svezhestOt({}).kind
|
|
131
|
+
if (new Set([a, b, c]).size !== 3) throw new Error(`исходы слиплись: ${a} / ${b} / ${c}`)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
console.log(`итог: ${ok} из ${ok + bed}`)
|
|
135
|
+
process.exit(bed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Прогон ВСЕХ стендов пакета одной командой.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 ЗАВЕДЁН 04.09.2026 ПО ЗАМЕРУ ЭТОГО ПАКЕТА: `scripts` в манифесте НЕ БЫЛО ВОВСЕ.
|
|
5
|
+
* То есть `npm test` у получателя не гонял НИ ОДНОГО из шести стендов — не «часть»,
|
|
6
|
+
* а ноль. Снаружи это неотличимо от пакета без проверок.
|
|
7
|
+
*
|
|
8
|
+
* Устройство перенесено из dsh-pamyat-core, где оно родилось из своего замера: там
|
|
9
|
+
* перечень вёлся рукой и называл шесть стендов из девяти, а `&&` обрывал цепь на
|
|
10
|
+
* первой законной слепоте. Здесь перечень СНИМАЕТСЯ С ДИСКА, поэтому «забыли вписать
|
|
11
|
+
* новый стенд» невозможно по устройству, а не по памяти.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ ГДЕ НЕ ПРИМЕНЯЕТСЯ: обход берёт только `stend-*.mjs`. Файл `test-goal-control.mjs`
|
|
14
|
+
* назван иначе и НЕ ЗОВЁТСЯ — это не недосмотр обхода, а несовпадение имени с шаблоном.
|
|
15
|
+
* Переименовать его — отдельная правка с отдельной приёмкой, здесь не делается.
|
|
16
|
+
*
|
|
17
|
+
* ИСХОД: расхождение (1) важнее слепоты (2) — так же, как во всех наших стендах.
|
|
18
|
+
* Слепота отдельного стенда НЕ гасится: она названа в сводке и уходит в код возврата,
|
|
19
|
+
* иначе «проверить нечем» стало бы неотличимо от «проверено».
|
|
20
|
+
*/
|
|
21
|
+
import { readdirSync } from 'node:fs';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { join, dirname } from 'node:path';
|
|
25
|
+
|
|
26
|
+
const katalog = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const svoyo = 'vse-stendy.mjs';
|
|
28
|
+
const stendy = readdirSync(katalog)
|
|
29
|
+
.filter((f) => f.startsWith('stend-') && f.endsWith('.mjs') && f !== svoyo)
|
|
30
|
+
.sort();
|
|
31
|
+
|
|
32
|
+
if (!stendy.length) {
|
|
33
|
+
console.log('СЛЕПОТА: стендов рядом не найдено вовсе — прогонять нечего');
|
|
34
|
+
console.log(` область обхода: ${katalog}, шаблон stend-*.mjs`);
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ishody = [];
|
|
39
|
+
for (const s of stendy) {
|
|
40
|
+
const r = spawnSync(process.execPath, [join(katalog, s)], { stdio: 'inherit' });
|
|
41
|
+
ishody.push({ stend: s, kod: r.status ?? 2 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const razoshlis = ishody.filter((i) => i.kod === 1);
|
|
45
|
+
const slepye = ishody.filter((i) => i.kod !== 0 && i.kod !== 1);
|
|
46
|
+
console.log(`\n═══ стендов ${stendy.length} · зелёных ${ishody.length - razoshlis.length - slepye.length}`
|
|
47
|
+
+ ` · расхождений ${razoshlis.length} · слепота ${slepye.length}`);
|
|
48
|
+
console.log(` область обхода: ${katalog}, шаблон stend-*.mjs — перечень СНЯТ С ДИСКА, не записан`);
|
|
49
|
+
for (const i of [...razoshlis, ...slepye]) console.log(` ${i.kod === 1 ? '🔴 разошёлся' : '⚠️ слепота'}: ${i.stend}`);
|
|
50
|
+
|
|
51
|
+
// Расхождение важнее слепоты: «нашли беду» сильнее, чем «проверить нечем».
|
|
52
|
+
process.exit(razoshlis.length ? 1 : (slepye.length ? 2 : 0));
|