Files
memo/docs/orders/13-modelstore-coverage.md
subochev 69a0ebe0fa tests: ModelStoreTest — закрыты дыры покрытия Range и Content-Range
resumesPartialDownload: счётчики Range-запросов и байт; сервер отдаёт 400
без Range для .part — запасной путь «начать заново» закрыт.

failsOnSizeMismatch разделена на два:
  - rejectsWrongTotalFromContentRange — сервер соврал в Content-Range,
    бьёт в проверку actualTotal != expectedTotal (вторая, достижимая);
  - truncatedResponseDoesNotProduceFile — оборванный ответ, по факту ловит
    ветку IOException → «не удалось скачать» (а не first size check).

В mutations.tsv убрана строка про if (declaredBody >= 0 && written !=
declaredBody) — через java.net/http недостижимо (оборванный ответ даёт
IOException, не EOF). Добавлена строка про if (expectedTotal >= 0 &&
actualTotal != expectedTotal) — теперь убивает rejectsWrongTotalFromContentRange.

Все 18 мутаций (4 из ModelStore) убиты прогоном mutation_check.py.
2026-10-02 04:48:35 +03:00

105 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Проект: /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`.