fix обзвон: дверь итога сторожей проверяет связность того, что приняла
Дверь принимала итог, который сам себе противоречил, и записывала «всё хорошо». Годный по форме случай: тело говорит «сторожей 3, красных 2», а в списке лежит одна зелёная строка — двое сторожей исчезали молча. Дыры было две: - длина списка storozha ни с чем не сверялась; - поле storozhey_krasnyh не читалось вовсе — машина присылала своё показание, а дверь его выбрасывала. Теперь сверяются три показания о числе сторожей — настройка, объявленное число и длина списка — плюс объявленное число красных против нашего подсчёта по строкам. Красных считаем ровно по машинному правилу: код не ноль или провалов не ноль. Иначе зелёный сторож с нулём проверок давал бы ложную тревогу, потому что портал ругается на такого, а машина его красным не считает. Сверх заказанного закрыты два места того же рода. Поля, которых дверь не знает, ищутся вычитанием, а не перечислением — и в итоге, и в строке сторожа: тогда забытое поле оказывается под защитой, а не мимо неё. И ловится список нужной длины, набитый повторами одного имени: числа при этом сходятся все до одного, а гоняли одного сторожа трижды. Каждая новая проверка показана красной вырезанием своего куска по одному за раз. Без чтения storozhey_krasnyh случай с объявленной краснотой при трёх зелёных строках не ловит ничто — список бед выходит пустым. Машину не трогал, схему не трогал, остальную дверь не переписывал. Полный прогон: всего 5207, зелёных 5203, красных 0, ошибок 0, пропущено 4.
This commit is contained in:
@@ -50,6 +50,34 @@ class LenaStorozhaWebhookController extends Controller
|
||||
/** Насколько дата итога может опережать наши часы, прежде чем мы ей не поверим. */
|
||||
private const ZAPAS_VPERYOD_MINUT = 10;
|
||||
|
||||
/**
|
||||
* Поля итога, которые дверь знает ПОИМЁННО.
|
||||
*
|
||||
* 🔴 Список нужен, чтобы считать проверенное ВЫЧИТАНИЕМ, а не перечислением.
|
||||
* Перечисление («проверяем вот эти поля») оставляет всё остальное мимо
|
||||
* защиты: добавят на машине новое поле, забудут добавить сюда — и оно
|
||||
* молча проедет. Вычитание («всё, чего нет в этом списке, — новость»)
|
||||
* ставит забытое поле ПОД защиту: дверь заругается, пока человек не
|
||||
* посмотрит на него и не впишет сюда осознанно.
|
||||
*
|
||||
* Цена решения названа честно: безобидная прибавка поля на машине красит
|
||||
* итог до правки этого списка. Так же устроено и число ожидаемых сторожей
|
||||
* в настройке — прибавление подтверждает человек, а не догадка портала.
|
||||
*/
|
||||
private const ZNAKOMYE_POLYA_ITOGA = [
|
||||
'shema', // разновидность итога — проверена отдельно
|
||||
'sobrano_v', // дата сборки — разбирается в receive()
|
||||
'mashina', // кто прислал — знаем, нарочно не проверяем
|
||||
'dlilos_ms', // сколько длился обход — ложится в отметку
|
||||
'storozhey_vsego', // объявленное число сторожей
|
||||
'storozhey_krasnyh', // объявленное число красных
|
||||
'slepok_etalonov', // слепок папки — сверить не с чем, лежит для истории
|
||||
'storozha', // сами строки сторожей
|
||||
];
|
||||
|
||||
/** Поля строки сторожа, которые дверь знает поимённо. Остальное — новость. */
|
||||
private const ZNAKOMYE_POLYA_STOROZHA = ['imya', 'kod', 'proverok', 'provalov'];
|
||||
|
||||
public function receive(Request $request): JsonResponse
|
||||
{
|
||||
if (! $this->podpisVerna($request)) {
|
||||
@@ -93,6 +121,25 @@ class LenaStorozhaWebhookController extends Controller
|
||||
/**
|
||||
* Что не так с итогом. Пустой список = всё зелено.
|
||||
*
|
||||
* 🔴 ЧТО ЗДЕСЬ ЛЕГКО ПОТЕРЯТЬ ПРИ ПРАВКЕ — СВЯЗНОСТЬ ТОГО, ЧТО ПРИНЯЛИ.
|
||||
* Первый заход этой двери стоял на допущении «список storozha ПОЛОН,
|
||||
* поэтому красноту можно пересчитать по нему, а собственное число красных
|
||||
* из итога читать незачем». Допущение оказалось ложным дважды:
|
||||
*
|
||||
* 1. Длина списка ни с чем не сверялась. Тело «сторожей 3», а в списке
|
||||
* одна зелёная строка — двое сторожей исчезали, и портал записывал
|
||||
* «всё хорошо».
|
||||
* 2. Поле storozhey_krasnyh не читалось вовсе. Машина присылала своё
|
||||
* показание, а дверь его выбрасывала: тело «красных 3» при трёх
|
||||
* зелёных строках проезжало насквозь.
|
||||
*
|
||||
* Поэтому здесь сверяются ТРИ независимых показания об одном и том же:
|
||||
* сколько сторожей ждёт настройка, сколько объявил итог и сколько строк
|
||||
* в списке на самом деле. Расхождение любых двух — само по себе беда,
|
||||
* даже когда каждая строка по отдельности зелёная. Это не дублирование:
|
||||
* согласие трёх показаний и есть единственное доказательство, что список
|
||||
* не обрезан и не подменён.
|
||||
*
|
||||
* @param array<string, mixed> $itog
|
||||
* @return list<string>
|
||||
*/
|
||||
@@ -106,6 +153,13 @@ class LenaStorozhaWebhookController extends Controller
|
||||
return $bedy; // Дальше разбирать нечего — устройство файла нам неизвестно.
|
||||
}
|
||||
|
||||
// Вычитанием, а не перечислением: всё, чего дверь не знает поимённо, —
|
||||
// новость, о которой надо сказать вслух. См. ZNAKOMYE_POLYA_ITOGA.
|
||||
$novyePolya = array_values(array_diff(array_keys($itog), self::ZNAKOMYE_POLYA_ITOGA));
|
||||
if ($novyePolya !== []) {
|
||||
$bedy[] = 'в итоге поля, которых дверь не знает: '.implode(', ', $novyePolya);
|
||||
}
|
||||
|
||||
$zhdyom = (int) config('storozha_mashiny.ozhidaem_storozhey');
|
||||
$vsego = (int) ($itog['storozhey_vsego'] ?? -1);
|
||||
|
||||
@@ -122,25 +176,73 @@ class LenaStorozhaWebhookController extends Controller
|
||||
return $bedy;
|
||||
}
|
||||
|
||||
// Второе показание против первого: список обязан быть той длины,
|
||||
// какую итог сам же и объявил. Короткий список = сторожа исчезли.
|
||||
if (count($spisok) !== $vsego) {
|
||||
$bedy[] = 'строк в списке сторожей '.count($spisok).", а объявлено {$vsego}";
|
||||
}
|
||||
|
||||
$krasnyhPoSpisku = 0;
|
||||
$imena = [];
|
||||
$novyePolyaStrok = [];
|
||||
|
||||
foreach ($spisok as $s) {
|
||||
if (! is_array($s)) {
|
||||
$bedy[] = 'строка сторожа не разобрана';
|
||||
// Не разобрали — считаем красной. Звать её зелёной не за что,
|
||||
// и наш счёт красных обязан остаться сравнимым с машинным.
|
||||
$krasnyhPoSpisku++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$novyePolyaStrok = array_merge(
|
||||
$novyePolyaStrok,
|
||||
array_diff(array_keys($s), self::ZNAKOMYE_POLYA_STOROZHA),
|
||||
);
|
||||
|
||||
$imya = (string) ($s['imya'] ?? '(без имени)');
|
||||
$imena[] = $imya;
|
||||
$kod = (int) ($s['kod'] ?? -1);
|
||||
$proverok = (int) ($s['proverok'] ?? -1);
|
||||
$provalov = (int) ($s['provalov'] ?? -1);
|
||||
|
||||
if ($kod !== 0 || $provalov !== 0) {
|
||||
// 🔴 Красным считаем РОВНО ПО ТОМУ ЖЕ правилу, по какому считает
|
||||
// обход на машине (код не ноль ИЛИ провалов не ноль). Иначе
|
||||
// сверка двух чисел ниже ругалась бы на ровном месте.
|
||||
$krasnyhPoSpisku++;
|
||||
$bedy[] = "{$imya}: код {$kod}, проверок {$proverok}, красных {$provalov}";
|
||||
} elseif ($proverok <= 0) {
|
||||
// Зелёный сторож, не сделавший ни одной проверки, — не сторож.
|
||||
// 🔴 В счёт красных НЕ идёт: машина такого красным не считает,
|
||||
// а мы сверяемся именно с её счётом.
|
||||
$bedy[] = "{$imya}: код 0, но проверок {$proverok} — сторож ничего не проверил";
|
||||
}
|
||||
}
|
||||
|
||||
if ($novyePolyaStrok !== []) {
|
||||
$bedy[] = 'в строках сторожей поля, которых дверь не знает: '
|
||||
.implode(', ', array_values(array_unique($novyePolyaStrok)));
|
||||
}
|
||||
|
||||
// Длину списка можно добрать повторами, и тогда все числа сойдутся,
|
||||
// а гоняли одного сторожа трижды. Имена сторожей — имена файлов на
|
||||
// машине, повторяться они не могут.
|
||||
$povtory = array_keys(array_filter(array_count_values($imena), static fn (int $skolko): bool => $skolko > 1));
|
||||
if ($povtory !== []) {
|
||||
$bedy[] = 'в списке сторожей повторяются имена: '.implode(', ', $povtory);
|
||||
}
|
||||
|
||||
// Третье показание: машина сама сказала, скольких считает красными.
|
||||
// Не сошлось с нашим счётом — беда, даже если каждая строка зелена.
|
||||
if (! array_key_exists('storozhey_krasnyh', $itog)) {
|
||||
$bedy[] = 'в итоге нет числа красных — наш счёт сверить не с чем';
|
||||
} elseif ((int) $itog['storozhey_krasnyh'] !== $krasnyhPoSpisku) {
|
||||
$objavleno = (int) $itog['storozhey_krasnyh'];
|
||||
$bedy[] = "красных по списку {$krasnyhPoSpisku}, а объявлено {$objavleno}";
|
||||
}
|
||||
|
||||
return $bedy;
|
||||
}
|
||||
|
||||
|
||||
@@ -208,6 +208,110 @@ it('краснеет на неизвестной разновидности ит
|
||||
expect(otmetka()->last_error)->toContain('непонятная разновидность');
|
||||
});
|
||||
|
||||
// ======================================= СВЯЗНОСТЬ ТОГО, ЧТО ПРИНЯЛИ ========
|
||||
//
|
||||
// 🔴 Нож владельца по допущению, на котором стояла защита: «список storozha
|
||||
// ПОЛОН, поэтому красноту можно пересчитать по нему, а собственное число
|
||||
// красных из итога читать незачем». Оба случая ниже годны по форме —
|
||||
// разновидность 1, дата свежая, подпись верна, адрес разрешён — и оба
|
||||
// проходили насквозь: портал записывал «всё хорошо», хотя машина в том же
|
||||
// теле прямо сказала обратное.
|
||||
|
||||
it('🔴 краснеет, когда в списке сторожей МЕНЬШЕ строк, чем объявлено', function (): void {
|
||||
// Случай Б ножа владельца: объявлено трое, в списке ОДИН зелёный.
|
||||
// Двое сторожей просто исчезли, и до починки портал молчал.
|
||||
$telo = (string) json_encode([
|
||||
'shema' => 1,
|
||||
'sobrano_v' => now()->toIso8601String(),
|
||||
'mashina' => 'fhmg5qpo4jconna4s9jo',
|
||||
'dlilos_ms' => 41000,
|
||||
'storozhey_vsego' => 3,
|
||||
'storozhey_krasnyh' => 2,
|
||||
'slepok_etalonov' => str_repeat('c', 64),
|
||||
'storozha' => [
|
||||
['imya' => 'proverka-raspisaniya', 'kod' => 0, 'proverok' => 26, 'provalov' => 0],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
postuchat($telo)->assertStatus(202);
|
||||
|
||||
// 🪤 Приводим к строке нарочно. На голом toContain() непочиненная дверь даёт
|
||||
// last_error = NULL, и Pest падает «Expected [iterable]» — по такому
|
||||
// красному не видно, дыра это или сторож сам сломан. Со строкой красный
|
||||
// честный: «'' не содержит ...».
|
||||
$o = otmetka();
|
||||
expect((string) $o->last_error)->toContain('строк в списке сторожей 1, а объявлено 3');
|
||||
expect($o->last_success_at)->toBeNull()
|
||||
->and((int) $o->consecutive_failures)->toBe(1);
|
||||
});
|
||||
|
||||
it('🔴 краснеет, когда объявленное число красных не сходится со списком', function (): void {
|
||||
// Случай В ножа владельца: машина объявила «красных 3», а все три строки
|
||||
// зелёные. Портал обязан заметить, что итог сам себе противоречит, а не
|
||||
// выбрасывать показание, которое ему дали.
|
||||
$telo = (string) json_encode([
|
||||
'shema' => 1,
|
||||
'sobrano_v' => now()->toIso8601String(),
|
||||
'mashina' => 'fhmg5qpo4jconna4s9jo',
|
||||
'dlilos_ms' => 41000,
|
||||
'storozhey_vsego' => 3,
|
||||
'storozhey_krasnyh' => 3,
|
||||
'slepok_etalonov' => str_repeat('d', 64),
|
||||
'storozha' => [
|
||||
['imya' => 'proverka-raspisaniya', 'kod' => 0, 'proverok' => 26, 'provalov' => 0],
|
||||
['imya' => 'proverka-tekstovogo-sleda', 'kod' => 0, 'proverok' => 46, 'provalov' => 0],
|
||||
['imya' => 'proverka-chistki', 'kod' => 0, 'proverok' => 27, 'provalov' => 0],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
postuchat($telo)->assertStatus(202);
|
||||
|
||||
expect((string) otmetka()->last_error)->toContain('красных по списку 0, а объявлено 3');
|
||||
});
|
||||
|
||||
it('🔴 контроль ножа: связный зелёный итог остаётся зелёным', function (): void {
|
||||
// Случай А ножа владельца. Без него две проверки выше доказывали бы только
|
||||
// то, что дверь научилась ругаться, — а не то, что она ругается по делу.
|
||||
$otvet = postuchat(itogSTremyaZelenymi());
|
||||
|
||||
$otvet->assertStatus(202)->assertJson(['status' => 'принят', 'bedy' => []]);
|
||||
expect(otmetka()->last_error)->toBeNull();
|
||||
});
|
||||
|
||||
it('🔴 краснеет на поле, о котором дверь не знает', function (): void {
|
||||
// Список «под защитой» считается ВЫЧИТАНИЕМ: всё, чего дверь не знает
|
||||
// поимённо, — новость, о которой надо сказать вслух. Иначе поле, добавленное
|
||||
// на машине и забытое здесь, окажется мимо защиты, а не под ней.
|
||||
$itog = json_decode(itogSTremyaZelenymi(), true);
|
||||
$itog['storozhey_propushcheno'] = 2;
|
||||
|
||||
postuchat((string) json_encode($itog, JSON_UNESCAPED_UNICODE))->assertStatus(202);
|
||||
|
||||
expect((string) otmetka()->last_error)->toContain('storozhey_propushcheno');
|
||||
});
|
||||
|
||||
it('🔴 краснеет на поле строки сторожа, о котором дверь не знает', function (): void {
|
||||
$itog = json_decode(itogSTremyaZelenymi(), true);
|
||||
$itog['storozha'][1]['propushcheno'] = 4;
|
||||
|
||||
postuchat((string) json_encode($itog, JSON_UNESCAPED_UNICODE))->assertStatus(202);
|
||||
|
||||
expect((string) otmetka()->last_error)->toContain('propushcheno');
|
||||
});
|
||||
|
||||
it('🔴 краснеет на список нужной длины, набитый одним и тем же сторожем', function (): void {
|
||||
// Второе место того же рода, о котором нож владельца не говорил: длину
|
||||
// списка можно добрать повторами. Тогда числа сходятся все до одного, а
|
||||
// на деле гоняли одного сторожа трижды.
|
||||
$itog = json_decode(itogSTremyaZelenymi(), true);
|
||||
$itog['storozha'][1]['imya'] = 'proverka-raspisaniya';
|
||||
$itog['storozha'][2]['imya'] = 'proverka-raspisaniya';
|
||||
|
||||
postuchat((string) json_encode($itog, JSON_UNESCAPED_UNICODE))->assertStatus(202);
|
||||
|
||||
expect((string) otmetka()->last_error)->toContain('повторяются имена');
|
||||
});
|
||||
|
||||
it('🔴 разбирает НАСТОЯЩЕЕ тело, снятое с машины, а не только своё выдуманное', function (): void {
|
||||
// 🪤 Шов между двумя честными половинками. Все проверки выше кормят портал
|
||||
// телом, которое сами же и составили. Так можно доказать что угодно и
|
||||
|
||||
@@ -805,6 +805,39 @@ bash bots/lena-golos/proverka-sutochnogo-obhoda.sh
|
||||
3. дописать его в список эталонов сторожа обхода
|
||||
```
|
||||
|
||||
### Прибавили поле в итог — тоже скажите порталу
|
||||
|
||||
С 06.08.2026 портал проверяет не только числа в итоге, но и **связность** того,
|
||||
что принял. Три вещи, о которых стоит знать, прежде чем править вывод обхода:
|
||||
|
||||
```text
|
||||
1. Длина списка обязана сойтись с объявленным числом.
|
||||
Сказали "storozhey_vsego": 3, а строк в списке две —
|
||||
портал заругается. Раньше двое сторожей исчезали молча.
|
||||
|
||||
2. Объявленное число красных обязано сойтись со списком.
|
||||
Поле "storozhey_krasnyh" портал теперь ЧИТАЕТ и сверяет
|
||||
со своим подсчётом по строкам. Считает он красным ровно
|
||||
по тому же правилу, что и обход: код не ноль ИЛИ провалов
|
||||
не ноль. Зелёного сторожа с нулём проверок обход красным
|
||||
не считает — и портал в этом счёте тоже не считает.
|
||||
|
||||
3. Поля, которых портал не знает, — беда, а не мелочь.
|
||||
Портал знает восемь полей итога:
|
||||
shema, sobrano_v, mashina, dlilos_ms,
|
||||
storozhey_vsego, storozhey_krasnyh,
|
||||
slepok_etalonov, storozha
|
||||
и четыре поля строки сторожа:
|
||||
imya, kod, proverok, provalov
|
||||
Всё остальное он назовёт новостью и покраснеет.
|
||||
```
|
||||
|
||||
🔴 **Цена этого названа честно:** прибавите в итог безобидное поле — портал
|
||||
станет красным, пока человек не впишет его в список знакомых полей на
|
||||
портальной стороне. Это сделано нарочно, той же рукой, что и число ожидаемых
|
||||
сторожей: забытое поле должно оказаться **под** защитой, а не мимо неё. Правится
|
||||
одной строкой в списке знакомых полей в двери приёма итога.
|
||||
|
||||
## Как сверить копию настройки с сервером
|
||||
|
||||
`extensions-lena.conf` — копия `/etc/asterisk/extensions.conf` с сервера
|
||||
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
# Отчёт помощника — З-0.10, доделка по ножу владельца
|
||||
|
||||
Дата: 06.08.2026
|
||||
Ветка: `feat/prospects-manual-testing-kp`
|
||||
Чинится: дверь `app/app/Http/Controllers/Api/LenaStorozhaWebhookController.php`, метод `chtoNeTak()`
|
||||
Что чинилось до этого: коммит `67195f0bf` — «машина робота проверяет себя сама, портал следит за свежестью»
|
||||
|
||||
Пишется ПО ХОДУ работы, не задним числом.
|
||||
|
||||
## Задача
|
||||
|
||||
Владелец принял вчерашнюю работу и нашёл в ней дыру своим ножом. Дыр на самом деле две:
|
||||
|
||||
1. Длина списка `storozha` нигде не сверяется с объявленным `storozhey_vsego`.
|
||||
Список может быть короче — сторожа тихо исчезают, портал молчит.
|
||||
2. Поле `storozhey_krasnyh` не читается вообще. Машина прислала своё показание —
|
||||
дверь его выбросила.
|
||||
|
||||
## Ход работы
|
||||
|
||||
### 1. Разведка — что уже есть
|
||||
|
||||
Прочитано:
|
||||
|
||||
- дверь `LenaStorozhaWebhookController.php` (260 строк);
|
||||
- сторож `app/tests/Feature/Lena/StorozhaMashinyTest.php` (338 строк, 18 проверок);
|
||||
- настройка `app/config/storozha_mashiny.php`;
|
||||
- обход на машине `bots/lena-golos/sutochnyy-obhod.sh` — только чтением, машину не трогал;
|
||||
- потребитель отметки `app/app/Console/Commands/LenaProverkaObhodaStorozhey.php`.
|
||||
|
||||
### 2. Замер ДО починки
|
||||
|
||||
Заведено 6 новых проверок, из них 3 — дословно ножом владельца
|
||||
(случаи Б, В и контроль А). Прогон по непочиненной двери:
|
||||
|
||||
```
|
||||
всего 27, зелёных 22, красных 5
|
||||
```
|
||||
|
||||
Красные — ровно пять новых, и все с одним и тем же словом:
|
||||
|
||||
```
|
||||
Failed asserting that '' [ASCII](length: 0) contains "строк в списке сторожей 1, а объявлено 3"
|
||||
Failed asserting that '' [ASCII](length: 0) contains "красных по списку 0, а объявлено 3"
|
||||
Failed asserting that '' [ASCII](length: 0) contains "storozhey_propushcheno"
|
||||
Failed asserting that '' [ASCII](length: 0) contains "propushcheno"
|
||||
Failed asserting that '' [ASCII](length: 0) contains "повторяются имена"
|
||||
```
|
||||
|
||||
Пустая строка слева — это и есть дыра: `last_error` в отметке **пустой**,
|
||||
портал записал «всё хорошо». Контроль А (случай владельца «3 зелёных»)
|
||||
прошёл зелёным и до починки — значит красные краснеют по делу, а не потому,
|
||||
что сторож сломан.
|
||||
|
||||
🪤 Первый заход этих проверок падал не так: на голом `toContain()` Pest
|
||||
выдавал «Invalid expectation value type. Expected [iterable]», потому что
|
||||
`last_error` был `NULL`. По такому красному не различить дыру и сломанного
|
||||
сторожа. Приведение к строке сделало красный честным.
|
||||
|
||||
### 3. Починка
|
||||
|
||||
Правлен один метод — `chtoNeTak()` — и добавлены два списка знакомых полей.
|
||||
Остальная дверь не тронута.
|
||||
|
||||
Теперь сверяются **три независимых показания** об одном и том же:
|
||||
|
||||
1. сколько сторожей ждёт настройка (`ozhidaem_storozhey`);
|
||||
2. сколько объявил сам итог (`storozhey_vsego`);
|
||||
3. сколько строк в списке `storozha` на самом деле.
|
||||
|
||||
Плюс четвёртое, отдельное: объявленное `storozhey_krasnyh` против нашего
|
||||
подсчёта красных по строкам.
|
||||
|
||||
🔴 **Самое тонкое место починки, и оно же — ответ на вопрос владельца о ложных
|
||||
тревогах.** Красных считаем **ровно по тому же правилу, что и обход на машине**:
|
||||
код не ноль ИЛИ провалов не ноль. У портала правило строже машинного — он
|
||||
ругается ещё и на зелёного сторожа с нулём проверок, а машина такого красным
|
||||
НЕ считает. Сравни я объявленное число со счётом жалоб, а не со счётом красных
|
||||
по машинному правилу — портал ругался бы «числа разошлись» на каждом таком
|
||||
случае. Это и была бы та ложная тревога, которой владелец опасался.
|
||||
|
||||
Сверх заказанного добавлены два места того же рода — обоснование в разборе:
|
||||
|
||||
- список знакомых полей **строки сторожа**, а не только верхнего уровня;
|
||||
- повторы имён в списке (длину можно добрать одним и тем же сторожем).
|
||||
|
||||
### 4. Замер ПОСЛЕ починки
|
||||
|
||||
Проверки одного файла сторожей:
|
||||
|
||||
```
|
||||
result=passed всего 27 зелёных 27 красных 0
|
||||
```
|
||||
|
||||
🔑 Важнее числа то, что **21 прежняя проверка осталась зелёной**, и среди них —
|
||||
та, что кормит дверь НАСТОЯЩИМ телом, снятым с машины 06.08.2026. То есть
|
||||
новая строгость не забраковала то, что машина шлёт сегодня.
|
||||
|
||||
### 5. Порча собственной починки
|
||||
|
||||
Каждая проверка показана красной вырезанием своего куска — по одному за раз,
|
||||
чтобы видеть, что сторож охраняет именно своё, а не соседнее.
|
||||
|
||||
| что вырезал | стало красных | что сказал сторож |
|
||||
|---|---|---|
|
||||
| сверку длины списка | 1 | `'красных по списку 0, а объявлено 2' не содержит 'строк в списке сторожей 1, а объявлено 3'` |
|
||||
| сверку числа красных | 1 | `'' не содержит 'красных по списку 0, а объявлено 3'` |
|
||||
| вычитание полей итога + повторы имён | 2 | `'' не содержит 'storozhey_propushcheno'`, `'' не содержит 'повторяются имена'` |
|
||||
| вычитание полей строки сторожа | 1 | `'' не содержит 'propushcheno'` |
|
||||
|
||||
Два наблюдения из этой таблицы:
|
||||
|
||||
- при вырезанной сверке длины случай Б всё равно краснеет — но **другой**
|
||||
проверкой, сверкой числа красных. Две новые защиты ловят случай Б независимо
|
||||
друг от друга, и это видно глазом в тексте красного.
|
||||
- при вырезанной сверке числа красных случай В даёт **пустую** строку бед.
|
||||
То есть без чтения `storozhey_krasnyh` этот случай не ловит НИЧЕГО. Это
|
||||
прямое доказательство, что показание не лишнее.
|
||||
|
||||
**Возврат доказан слепком со снятием невидимых знаков конца строки:**
|
||||
|
||||
```
|
||||
до порчи: 7473756072af309d0b203853fc8c208ca75464fe
|
||||
после возврата: 7473756072af309d0b203853fc8c208ca75464fe
|
||||
```
|
||||
|
||||
Плюс `grep "false &&"` по двери — порчи не осталось.
|
||||
|
||||
### 6. Полный прогон
|
||||
|
||||
Своя база `liderra_testing_z010fix`, заведена и промигрирована отдельно —
|
||||
чужих баз не занимал.
|
||||
|
||||
```
|
||||
всего 5207, зелёных 5203, красных 0, ошибок 0, пропущено 4
|
||||
утверждений 16348, длилось 19 минут
|
||||
```
|
||||
|
||||
Арифметика сходится: `5203 + 0 + 0 + 4 = 5207`.
|
||||
|
||||
🔴 Датчиком брал **число проверок**, а не код возврата: обёртка печатает
|
||||
«выход 0» даже когда прогон умер от нехватки памяти. `php artisan test`
|
||||
не звал вовсе, `composer test` тоже — он режет по 300 секунд, а прогон идёт 19
|
||||
минут. Вывод складывал в файл перенаправлением, не трубой.
|
||||
|
||||
### 7. Соседи
|
||||
|
||||
- **моего имени нет ни в одном выводе ни одного сторожа** — проверено поиском
|
||||
по всем моим правкам и по отчёту: ни метки смены, ни имени помощника, ни
|
||||
имени моей базы в коде и в текстах проверок нет.
|
||||
- **чужие файлы в дереве не тронуты** — я правил ровно четыре пути: дверь,
|
||||
её проверки, README машины и этот отчёт. Соседи в это же время держат
|
||||
правки в `app/.env.example`, `app/config/services.php`,
|
||||
`docs/observer/STATUS.md`, `package.json`, `package-lock.json` и в чужом
|
||||
приёмочном листе `stroyka-5` — ни одного из них я не открывал на запись и
|
||||
ни один не попадёт в мой коммит: кладу поимённо.
|
||||
|
||||
### 8. Зовущие поимённо
|
||||
|
||||
Кто зовёт то, что я правил — смотрено глазами, не числом:
|
||||
|
||||
| что | кто зовёт |
|
||||
|---|---|
|
||||
| `LenaStorozhaWebhookController@receive` | одна строка `routes/web.php:736`, `POST /api/webhook/lena-storozha`. Больше ниоткуда: поиск по всему коду дал только саму дверь и эту строку |
|
||||
| эту дорогу снаружи | обход на машине 51.250.1.97, шаг 5 в `sutochnyy-obhod.sh` (`curl` с подписью), по расписанию `/etc/cron.d/lena-sutochnyy-obhod` |
|
||||
| `chtoNeTak()` | только `receive()` в той же двери, один вызов |
|
||||
| отметку, которую кладёт дверь | команда `lena:proverka-obhoda`, а её зовёт `routes/console.php:471` ежедневно |
|
||||
| текст бед, который я дописал | та же команда, полем `last_error` → в письмо тревоги. Она читает поле целиком, поимённо тексты не разбирает — значит новые беды доедут до письма сами |
|
||||
|
||||
### 9. Разметка, стиль, статанализ
|
||||
|
||||
- `markdownlint` по этому отчёту и по README машины — `0 error(s)`.
|
||||
- `pint --test` по обоим моим файлам PHP — `passed`.
|
||||
- `composer stan` — 6 ошибок, **все шесть в файлах соседа**
|
||||
(`app/Services/Obzvon/ObzvonClient.php`, `tests/Feature/Obzvon/ObzvonClientShovTest.php`),
|
||||
которые мне трогать запрещено. В моих файлах — ноль.
|
||||
|
||||
## Разбор задания
|
||||
|
||||
### Моя ошибка — первой
|
||||
|
||||
Первый заход новых сторожей падал негодным красным: на голом `toContain()`
|
||||
Pest выдавал «Invalid expectation value type. Expected [iterable]», потому что
|
||||
`last_error` был `NULL`. Такой красный не различает «дыра есть» и «сторож сам
|
||||
сломан» — то есть сторож, показанный красным, ничего этим не доказывал. Поймал
|
||||
сам, приведением к строке. Но написал я его сперва неправильно.
|
||||
|
||||
### Картина беды владельца — верна, обе дыры настоящие
|
||||
|
||||
Замерил своим прогоном: до починки оба случая давали `last_error` **пустым** —
|
||||
портал записывал «всё хорошо». Подтверждаю оба пункта.
|
||||
|
||||
### Ответ на прямой вопрос: читать ли `storozhey_krasnyh`
|
||||
|
||||
**Да, читать. Но не так, как напрашивается — иначе ложные тревоги были бы.**
|
||||
|
||||
Показание не лишнее: вырезав его, я получил случай В с **пустым** списком бед.
|
||||
Больше его не ловит ничто.
|
||||
|
||||
А вот опасение владельца про ложные тревоги — обоснованное, и оно бы сбылось
|
||||
при прямолинейной починке. У портала и у машины **разные определения красноты**:
|
||||
портал ругается ещё и на зелёного сторожа с нулём проверок, машина такого
|
||||
красным не считает. Сравнивать объявленное число со счётом жалоб нельзя —
|
||||
разошлись бы на ровном месте. Сравнивать надо со счётом, посчитанным
|
||||
машинным правилом. Так и сделано, и это записано в коде комментарием, потому
|
||||
что при следующей правке это первое, что потеряется.
|
||||
|
||||
### 🔴 Где картина владельца ЗАВЫШЕНА — про подделку
|
||||
|
||||
В задании сказано: автор той работы оставил незакрытым, что «подделать свежий
|
||||
зелёный итог с утёкшим словом односторонний канал не мешает», и что мой нож
|
||||
показывает — «подделывать связный итог даже не нужно, хватит обрезанного
|
||||
списка».
|
||||
|
||||
**Как довод про злоумышленника это неверно.** Чтобы стук вообще дошёл до
|
||||
разбора тела, его надо подписать тайным словом и прийти с разрешённого адреса.
|
||||
У того, кто уже владеет словом, нет никакой причины слать обрезанный список —
|
||||
он пришлёт связный зелёный итог, и моя починка его не поймает. Порог для
|
||||
подделки задаёт подпись, и он этой работой **не поднят ни на сколько**.
|
||||
|
||||
Настоящая цена починки — другая, и она честная: защита от **случайной** порчи
|
||||
и от будущих правок. Обход на машине правится (сегодня же и правился), вывод
|
||||
собирается склейкой строк в оболочке, где потерять кусок легко и незаметно.
|
||||
Вот от этого защита появилась. Предлагаю так и записать в решении, а довод
|
||||
про подделку из обоснования убрать — иначе он даст ложное чувство, что канал
|
||||
защищён лучше, чем он защищён.
|
||||
|
||||
### Второе место того же рода, о котором задание не говорило
|
||||
|
||||
Длину списка можно добрать **повторами**. Пришли три строки с одним и тем же
|
||||
именем — и сходится ВСЁ: и с настройкой, и с объявленным числом, и с числом
|
||||
красных. А гоняли одного сторожа трижды, двое молчат. Проверка на повтор имён
|
||||
добавлена; ложных тревог она дать не может — имена сторожей это имена файлов
|
||||
на машине, повторяться они не умеют.
|
||||
|
||||
Так же добавлено вычитание по полям **строки сторожа**, а не только верхнего
|
||||
уровня: правило владельца про «новое поле должно оказаться под защитой»
|
||||
относится к строкам ровно так же.
|
||||
|
||||
### Что в этой починке стоит денег и от чего можно отказаться
|
||||
|
||||
Вычитание по полям — единственная часть с будущей ценой: прибавят на машине
|
||||
безобидное поле, и портал станет красным, пока человек не впишет его в список.
|
||||
Это сделано нарочно и той же рукой, что число ожидаемых сторожей. Но если
|
||||
владелец решит, что цена не по нему, отказываться надо **именно от вычитания**,
|
||||
а две сверки чисел оставить — они не стоят ничего и ловят настоящее.
|
||||
|
||||
## Что осталось незакрытым
|
||||
|
||||
1. **Живой машины я не касался** — запрещено заданием, и не касался. Значит
|
||||
доказано только то, что дверь стала строже к телу, снятому 06.08.2026.
|
||||
Что обход на машине сегодня шлёт ровно это тело — взято из чужой проверки,
|
||||
своими руками не перемерено.
|
||||
2. **Портальная половина не выкачена.** README машины прямо говорит: файла
|
||||
настроек отправки на машине нет, обход пишет итог файлом и в портал не
|
||||
стучится. То есть вся эта цепочка на бою пока не работает — ни до моей
|
||||
починки, ни после. Моя работа готовит выкат, но выкатом не является.
|
||||
3. **Список знакомых полей сверен глазами, а не программой.** Он списан с
|
||||
`sutochnyy-obhod.sh` и с настоящего тела в проверке. Сторожа, который
|
||||
ронял бы сборку при расхождении списка в PHP и вывода в оболочке, нет.
|
||||
4. **Статанализ по репозиторию не зелёный** — 6 ошибок в файлах соседней
|
||||
смены. Мои файлы чисты, но общий прогон `composer stan` сейчас красный не
|
||||
по моей вине и не мною чинится.
|
||||
5. **Числа проверок 26/46/27** в проверках взяты из вчерашней работы как есть.
|
||||
Изменятся сторожа на машине — проверки начнут врать, и заметит это только
|
||||
человек.
|
||||
Reference in New Issue
Block a user