--- title: "Windows docker-тест-харнесс: грабли верификационных скриптов" type: concept tags: [windows, testing, node, docker, vitest, sqlite, gotchas] sources: [] related: [concepts/sched-fnf-r8-verification-stand.md] updated: 2026-08-18 --- # Windows docker-тест-харнесс: грабли Собрано из верификационных прогонов против docker-контейнеров на Windows-хосте (стенд sched F&F r8, 2026-08-18). Переиспользуемо для любой проверки «локальные репро-скрипты против живых docker-БД». ## 1. `rmSync` удаляемого sqlite-файла — EBUSY на Windows `new DatabaseSync('x.db')` создаёт файл, и `rmSync('x.db')` ПОСЛЕ открытия падает `EBUSY` (errno -4082): Windows не удаляет открытый файл (на POSIX unlink открытого — ок, на Windows — нет). **Правило: `rmSync` ДО `DatabaseSync`.** Порядок в тесте: ```js rmSync('x.db', { force: true }); const db = new DatabaseSync('x.db'); ``` Симптом-ловушка: тот же EBUSY бывает, если файл держит ВИСЯЩИЙ node-процесс прошлого прогона (см. п.4) — сначала убить процессы, потом чинить порядок. ## 2. vitest 10s-таймаут vs медленный docker-DDL На этом хосте DDL в docker-mysql8 стоит ~200–1500ms на запрос (CREATE TABLE, ALTER), а `createMysqlStorage` (миграции v1→v5 на свежей БД) — ~11s. vitest-дефолт 10s на тест → каждый mysql-тест падает в `beforeEach` по таймауту, БЕЗ assertion-ошибок. Выглядит как регрессия, а это окружение. Варианты: `--testTimeout=60000`, либо (лучше для стенда) не гонять тяжёлый contract-сьют на mysql/pg/mariadb через vitest — покрывать те же пути отдельными node-скриптами (`adapters-spotcheck`, `adapter-migration-test`). ## 3. Фейк-часы в тестах движка — монотонность и изоляция Инжектируемый `now()` даёт детерминизм, но: - **Часы должны идти МОНОТОННО.** Прыжок назад (например, после тика 10:03 гнать once-расписание на 10:01) ломает: расписание «уже» выстрелило catch-up'ом на более позднем тике, nextRunAt=null. Планируй все времена по возрастанию. - **Отдельный DB на сценарий.** Общий DB + прыгающие часы → соседняя задача становится due на «чужом» тике и стреляет (мешает счётчикам/онRunFinal). - **`rmSync` в начале каждого прогона** — иначе повторный запуск видит старые раны (флак «запусти ещё раз — упало»). - **Блокирующий раннер + `runOnce`** = дедлок, если release после `await`. Тик не await'ить, дать микро-паузу на claim, release, потом `await`. ## 4. Висельники node-процессов держат файлы/порты После таймаутов и убитых прогонов node-процессы живут и держат sqlite-файлы (→ EBUSY) и соединения. `tasklist | grep node` врёт (UTF-16 binary); правильно: ```powershell Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Select ProcessId,CommandLine ``` Убивать точечно по CommandLine (репо-путь), НЕ всех — рядом крутятся dev-серверы юзера (nuxt, pi-агенты, wiki-graph). Таймаут bash убивает шелл, но не дерево node-детей — чистить руками. ## 5. Секвенции assert'ов: «ожидание» vs «поведение» Половина «багов» в репро-скриптах — неверные ожидания в тесте, а не движок: - кумулятивные счётчики (retryCount = consumed-попытки, не «текущая»); - тай-брейки сортировок (priority-равные → id ASC); - событийные хуки на терминальных ранах (success тоже); - легаси-форматы данных (mongo: `config.data` не `data`, `paused`/`disabled` обязательны — иначе синтез даёт null и строка не в due). Перед репортом о «находке» — перепроверить, что ожидание соответствует доке (это часто и есть ответ: поведение документировано, тест был неверен).