Проект: /root/WORK/memo (Kotlin/JVM). Заказ: закрыть ДВЕ дыры в покрытии `ModelStoreTest`, доказанные мутационной проверкой. Правки только в тестовом файле, боевой код НЕ трогать. Прогон, который их нашёл: python3 e2e/mutation_check.py --gradle ./gradlew Выжившие мутации: 1. `ModelStore.kt`: `if (startAt > 0L) requestBuilder.header("Range", "bytes=$startAt-")` → `if (false) ...` | ожидался провал `resumesPartialDownload` — НЕ упал. 2. `ModelStore.kt`: `if (declaredBody >= 0 && written != declaredBody) {` → `if (false) {` | ожидался провал `failsOnSizeMismatch` — НЕ упал. ## Почему они выжили (разобрано, не догадка) **1. `resumesPartialDownload`.** Тестовый сервер (`serveBytes`) отдаёт `206` с хвостом только если в запросе есть заголовок `Range`; без него он отдаёт `200` и **полное содержимое**. Поэтому при мутации (Range не отправляется) срабатывает штатный запасной путь «начать заново», итоговый файл получается правильным — и тест, проверяющий только «файл в итоге верный», проходит. Тест не проверяет того, ради чего написан: что докачка **действительно** шла хвостом. **2. `failsOnSizeMismatch`.** Сервер объявляет `Content-Length` больше, чем пишет, соединение обрывается — у клиента вылетает `IOException`, который `download` превращает в `IllegalStateException("не удалось скачать ...")`. Тест принимает `msg.contains("не удалось скачать")`, поэтому проверка размера не проверяется вообще: исключение приходит из другого места. **Важное наблюдение по бою.** В `ModelStore.download` две проверки размера: `written != declaredBody` (первая) и `part.length() != expectedTotal` (вторая, из `Content-Range`). Через `java.net.http` первая на практике недостижима: оборванный ответ всегда даёт `IOException`, а не чистый EOF. Значит это защитный код, а не дыра в покрытии — в таблице мутаций его надо заменить на мутацию ВТОРОЙ проверки, которая достижима (см. ниже). ## Что сделать ### Правка теста `resumesPartialDownload` Сделать так, чтобы «докачка» была доказана, а не предположена: - сервер записывает в счётчики: сколько запросов пришло, у скольких был заголовок `Range`, и сколько всего байт тела он отдал; - **если заголовка `Range` нет для файла, у которого уже есть `.part`** — сервер отвечает кодом `400` и тела не отдаёт (докачки без `Range` не бывает; тест не должен иметь запасного пути); - после `ensure` тест обязан утверждать: - `rangeRequests >= 1` — докачка действительно была запрошена; - файл в итоге побайтно равен исходному (оставить); - `.part` переименован (оставить). При мутации №1 (`Range` не отправляется) сервер ответит `400` → `ensure` бросит исключение → тест упадёт. ### Замена сценария `failsOnSizeMismatch` на два теста **`rejectsWrongTotalFromContentRange`** (новый, закрывает мутацию №2) — «сервер соврал про общий размер, файл принимать нельзя»: - в директории лежит `.part` = первые 4000 байт файла из 8000; - сервер на запрос с `Range: bytes=4000-` отвечает `206`: - `Content-Length: 4000`, тело — реальный хвост 4000 байт (то есть транспорт отдаёт ровно столько, сколько объявил — никакой `IOException`); - `Content-Range: bytes 4000-7999/999999` — **итог соврал**; - ожидание: `ModelStore.ensure` бросает `IllegalStateException`, сообщение содержит `размер`, целевого файла нет, `.part` удалён. Проверить, что сценарий действительно бьёт в нужную проверку: при `if (false)` на `part.length() != expectedTotal` тест обязан провалиться (файл будет установлен, исключения не будет). **`truncatedResponseDoesNotProduceFile`** (переименовать бывший `failsOnSizeMismatch`) — оставить как проверку поведения «оборванный ответ не оставляет файла», но **убрать из принимаемых сообщение «не удалось скачать»**, чтобы тест не «зеленел» за счёт сетевой ошибки. Ожидать явно любое `IllegalStateException` с непустым сообщением и отсутствие целевого файла и `.part`. Отдельно проверить и записать в отчёте: какую ветку кода реально ловит этот тест (по сообщению) — то есть является ли он проверкой размера или сетевого обрыва. В отчёте написать прямо. ### Таблица мутаций `e2e/mutations.tsv` - **Убрать** строку, целящуюся в `if (declaredBody >= 0 && written != declaredBody) {` (по разбору выше — недостижимо через `java.net.http`; ложная цель). - **Добавить** мутацию во вторую проверку: `if (expectedTotal >= 0 && actualTotal != expectedTotal) {` → `if (false) {`, обязанный уронить `rejectsWrongTotalFromContentRange`. - Строку про `Range` оставить, но теперь она обязана валить `resumesPartialDownload`. Правило: каждая строка таблицы — либо убитая мутация, либо честное объяснение в комментарии, почему цель недостижима (с доказательством прогоном). ## Обязательная проверка (приложить вывод) ```bash cd /root/WORK/memo python3 e2e/mutation_check.py --gradle ./gradlew 2>&1 | tail -25 echo "--- ожидается: ВСЕ МУТАЦИИ УБИТЫ" ./gradlew test --rerun-tasks -q 2>&1 | tail -3 ``` Плюс отдельно, для каждого из двух новых/изменённых тестов — доказательство, что он валит мутацию: применить мутацию руками, прогнать только `ModelStoreTest`, показать FAIL, откатить (`git checkout -- memo-core/src/main/kotlin/memo/core/ModelStore.kt`), убедиться в PASS. ## СТРОГИЕ ЗАПРЕТЫ - **Не менять боевой код** `ModelStore.kt` (и вообще ничего в `src/main`). Заказ — только тесты и таблица мутаций. - Не менять другие тесты. - Не добавлять зависимости. - Не выводить план текстом; сразу правь файлы. - Не удалять `.gitignore`, не коммитить `models/` и `*.db`.