---
pr: 197
title: "refactor(draw-image): decouple tests from production cover.svg"
---

## Что сделано
Отвязал тесты движка draw-image от продакшн-шаблона `templates/cover.svg` (issue #196). Тесты теперь опираются на каноничный fixture, а продакшн-шаблон упрощён до чистой обложки — без badge/sub-icon/subtitle-слотов.

1. **`tests/fixtures/cover.svg`** — новый каноничный тестовый шаблон (1024×1024) со всеми типами слотов: `icon` (x=312,y=180,400×400,recolor=accent), `sub-icon` (x=412,y=600,200×200,recolor=accent), `badge` (x=780,y=780,180×180,bg=surface,border=accent,radius=0.5) и subtitle-строка `y="930"`. Стабильный контракт для тестов: badge x=780 y=780 w=180 h=180, subtitle y=930, icon recolor=accent.
2. **`tests/helpers/fixtures.ts`** — helper `loadFixture(name)`: читает из `tests/fixtures/`, не дублирует логику `loadTemplate`. Экспортирует `FIXTURES_DIR_ABS`.
3. **`--template-dir` флаг CLI** — `src/render.ts`: `loadTemplate(drawImageDir, name, templateDir?)` принимает опциональный `templateDir` (дефолт `"templates"`), поддерживает относительный (от `drawImageDir`) и абсолютный путь. `cli.ts`: парсит `--template-dir`, пробрасывает в `loadTemplate`. Без флага → `templates/` (обратная совместимость, продакшн не сломан). Usage-сообщение обновлено.
4. **9 тестов переведены на fixture**: `slot.test.ts`, `render.optional.test.ts`, `render.integration.test.ts`, `render.optional.integration.test.ts`, `e2e.test.ts`, `e2e.optional.test.ts`, `e2e.no-icon.test.ts`, `idempotency.test.ts`, `cleanup.test.ts`. В unit/integration — `loadTemplate(DRAW_IMAGE_DIR,"cover")` → `loadFixture("cover")`. В e2e (CLI) — добавлен `--template-dir tests/fixtures`. `e2e.bad-input.test.ts` не тронут (проверяет несуществующий шаблон, не зависит от cover.svg). В `idempotency.test.ts` убран неиспользуемый импорт `loadTemplate`/`loadBrand`/`buildSvg`/`computeHash`/`writeFileSync`.
5. **`templates/cover.svg` упрощён** — оставлен только icon-слот 520×520 (x=252,y=180) и title y=830. Удалены `sub-icon`, `badge`, subtitle-`<text>` — чистая обложка. Перерендерен `assets/cover.png` (icon=opencode, title="opencode config", без subtitle): PNG побайтово идентичен предыдущему (`cmp` → IDENTICAL, 18077 байт) — удалённые слоты в продакшн-рендере не использовались. Hash в `cover.meta.json` изменился (`d052d8a9...` vs `646197c4...`), т.к. `computeHash` хеширует весь templateSvg-контент, но пиксели идентичны.
6. Добавлен handoff + ADR-087.

## Почему
10 из 15 тестов жёстко привязывались к продакшн `templates/cover.svg` через `loadTemplate(DRAW_IMAGE_DIR,"cover")` и проверяли конкретные продакшн-значения (badge x=780 y=780, subtitle y=930). Любое изменение обложки (размер/позиция иконки, удаление subtitle) ломало тесты — PR#195 уже сталкивался с этим (пришлось сохранить subtitle-слот в шаблоне, чтобы не сломать `render.optional.test.ts`). Отвязка даёт свободу менять продакшн-обложку без правок тестов: тесты получили стабильный fixture-контракт, а продакшн-шаблон упрощён до того, что реально нужно для обложки. Упрощение cover.svg и зелёные тесты после — прямое подтверждение, что отвязка работает (критерий приёмки issue).

## Pending
— (после merge: fixture можно расширять новыми slot-типами без риска для продакшн-обложки)

## Watch out
- **`--template-dir` относительный путь** — `tests/fixtures` в e2e-тестах резолвится от `drawImageDir` (cwd CLI = `DRAW_IMAGE_DIR`), не от cwd запускающего. Абсолютный путь тоже поддерживается через `path.isAbsolute`. Продакшн-вызовы без флага используют `templates/` — обратно совместимо.
- **Hash `cover.meta.json` изменился** — `computeHash` (sha256) зависит от SVG-контента. Упрощённый шаблон короче → хеш другой (`d052d8a9...`), хотя PNG пиксели идентичны. Кэш-инвалидация ожидаема, на рендер не влияет.
- **ADR-087 / handoff файлы** — созданы через scaffold-handoff.sh, переименованы в `pr-197-*` / `087-pr-197-*` после create-pr (PR#197). Frontmatter `pr: 197`.
- **`e2e.bad-input.test.ts` не переведён** на `--template-dir` — он тестирует несуществующий шаблон `nonexistent`, который отсутствует в любом templateDir. Тест корректен для дефолтного `templates/` и не зависит от cover.svg — оставлен как есть (соответствует контракту issue: «оставь как есть»).
- **PNG-идентичность** — упрощение cover.svg не меняет пиксели, т.к. `render.ts` для пустых slots не вставляет SVG (нет `slotSvgs`), а conditional-regex ранее удалял subtitle-`<text>` при falsy `args.subtitle`. Удаление этих элементов из шаблона = удаление мёртвого кода для продакшн-рендера.