fix(issue): align headings with create-issue validation and add SDD sections (#169)

* fix(issue): align headings with create-issue validation and add SDD sections

* docs(issue): add handoff, ADR, and fix CI for SDD validation

* docs(pr-169): fix handoff sections

---------

Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
Sergey 2026-07-31 19:42:14 +03:00 committed by GitHub
parent 6977a9130a
commit c491b5fad1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 312 additions and 88 deletions

View file

@ -10,6 +10,6 @@ If a bug is found during work that is outside the scope of the current task:
1. Check `gh issue list` for duplicates.
2. Create a GitHub issue via `create-issue` tool (NOT raw `gh issue create`).
3. Title: `fix(scope): short description` in English.
4. Body: `## Контекст` / `## Задача` / `## Критерии приемки` (in Russian).
4. Body: `## Контекст` / `## Задача` / `## Контракты` / `## Инварианты` / `## Граничные случаи` / `## Вне scope` / `## Критерии приемки` (in Russian).
5. Continue the current task. Do NOT fix the bug yourself.
6. Report to orchestrator: "Created issue #N: ...".

View file

@ -21,39 +21,27 @@ Issue должно содержать всё необходимое, чтобы
```markdown
## Контекст
(зачем это нужно, какая проблема решается)
Зачем: [мотивация — почему это нужно]
Контекст: [текущее состояние, что есть сейчас]
## Что сделать
(пошагово, с путями к файлам)
## Задача
[Что делаем — пошагово, с путями к файлам и номерами строк]
### Шаг 1: ...
- Файл: `path/to/file.py`
- Изменить: ...
## Контракты
[Ожидаемое поведение: API, форматы запросов/ответов, коды ошибок]
### Шаг 2: ...
## Инварианты
[Правила без исключений: лимиты, ограничения, выбранные технологии]
## Проверка
(команды и ожидаемый результат)
- `pytest tests/test_xxx.py -x -q --no-cov` → all passed
- `ruff check path/to/file.py` → All checks passed
- `mypy path/to/file.py` → no issues
## Граничные случаи
[Что при ошибках: невалидный вход, отказ внешнего сервиса, превышение лимита]
## Acceptance criteria
(явный чек-лист — что должно быть верно в результате, не команды проверки)
- [ ] Эффект A работает в случае B
- [ ] Файл C не содержит паттерн D
- [ ] Тест E покрывает ветку F
- [ ] Coverage ≥ 80% на изменённых файлах
## Вне scope
[Что НЕ делаем в этой итерации]
## Dependencies
(связи с другими issue/PR — блокировки и порядок)
- Blocked by #N (этот PR нельзя начать пока #N не смержен)
- Do not merge until #N merges (этот PR готов, но ждёт #N)
- Part of #N (подзадача родительского issue)
## Связанные ресурсы
- Ref #33
- [PR #34](https://github.com/...)
## Критерии приемки
- [ ] Проверяемый сценарий 1: "пользователь делает X → видит Y"
- [ ] Проверяемый сценарий 2
```
## Правило дробления
@ -79,8 +67,8 @@ Issue создаёт **subagent** (general type), а не основной аг
**Subagent (полная ответственность):**
1. Загрузи навык `issue`
2. Собери контекст — прочитай файлы из intent summary, пойми задачу, оцени объём (правило дробления ниже)
3. Составь self-contained body по шаблону (Контекст → Что сделать → Проверка → Acceptance criteria → Dependencies → Связанные ресурсы)
4. Запусти `create-issue({ title: "...", body: "...", labels: ["..."] })` tool (НЕ raw `gh issue create` — заблокирован deny; tool валидирует conventional title format и headings `## Контекст`/`## Задача`/`## Критерии приемки`)
3. Составь self-contained body по шаблону (Контекст → Задача → Контракты → Инварианты → Граничные случаи → Вне scope → Критерии приемки)
4. Запусти `create-issue({ title: "...", body: "...", labels: ["..."] })` tool (НЕ raw `gh issue create` — заблокирован deny; tool валидирует conventional title format и headings `## Контекст`/`## Задача`/`## Контракты`/`## Инварианты`/`## Граничные случаи`/`## Вне scope`/`## Критерии приемки`)
5. Верни URL созданного issue основному агенту
Main agent НЕ пишет body и НЕ запускает `create-issue` — всё через subagent. Это согласовано с `run-pipeline` skill (Phase 0: "через subagent с `issue` skill") и `AGENTS.md` (Dev Workflow, step 2: "delegate to `task` subagent").
@ -89,27 +77,40 @@ Main agent НЕ пишет body и НЕ запускает `create-issue` — в
```markdown
## Контекст
Zoom breathing падает при включённом geometry crop — crop использует probe.width вместо iw.
Зачем: API эндпоинт /api/videos/analyze отвечает 2-5 секунд из-за повторного обращения к Claude API для тех же видео. Кеширование результата сократит время ответа до <100мс для повторных запросов.
Контекст: сейчас AnalysisService обращается к Claude API при каждом вызове, кеша нет.
## Что сделать
### Шаг 1: Заменить probe dimensions на iw/ih выражения
- Файл: `src/video_uniq/effects/camera.py:72`
- Заменить `w, h = probe.width, probe.height` на `iw`/`ih` выражения
## Задача
1. В `services/analysis_service.py:45` — добавить проверку кеша перед вызовом Claude API
2. В `utils/cache.py` — использовать RedisCache (уже есть в проекте)
3. TTL результата анализа — 30 дней
4. При cache hit — пропустить обращение к TranscriptService и AnalysisService
## Проверка
- `pytest tests/test_effects.py -x -q --no-cov` → all passed
- `pytest tests/test_new_effects_real.py::test_geometry_crop_with_zoom_breathing_real` → passed
## Контракты
- POST /api/videos/analyze — без изменений в API
- При cache hit: 200 OK, время ответа <100мс
- При cache miss: 200 OK, время ответа 2-5 сек (как сейчас)
## Acceptance criteria
- [ ] Geometry crop использует `iw`/`ih`, не `probe.width`/`probe.height`
- [ ] Zoom breathing не падает при включённом geometry crop
- [ ] Регрессионный тест покрывает комбинацию zoom breathing + geometry crop
## Инварианты
- Кеш только через Redis (RedisCache из utils/cache.py)
- TTL результата анализа — 30 дней (2592000 сек)
- Невалидный ответ Claude НЕ кешируется
## Dependencies
- Closes #33
## Граничные случаи
- Redis недоступен → логировать warning, продолжить без кеша (cache miss)
- Кеш содержит устаревший формат → invalidate, пересчитать
- Конкурентные запросы на одно видео → первый пишет в кеш, последующие берут из кеша
## Связанные ресурсы
- Closes #33
## Вне scope
- ❌ Кеширование субтитров (отдельная задача)
- ❌ Инвалидация по времени просмотра видео
- ❌ Админ-панель для управления кешем
## Критерии приемки
- [ ] Повторный анализ того же видео → результат мгновенно (<100мс)
- [ ] Новое видео → результат через 2-5 сек (как раньше)
- [ ] Redis недоступен → API работает (без кеша), в логах warning
- [ ] pytest tests/test_analysis_service.py проходит
```
## Пример плохого issue
@ -130,8 +131,9 @@ create-issue({ title: "type(scope): description", body: "...", labels: ["<label>
```
Tool валидирует: title соответствует conventional format (type(scope): desc,
≤80 chars, English), body содержит `## Контекст`, `## Задача`, `## Критерии
приемки` headings и на русском (Cyrillic обязательна). При ошибке валидации
≤80 chars, English), body содержит `## Контекст`, `## Задача`, `## Контракты`,
`## Инварианты`, `## Граничные случаи`, `## Вне scope`, `## Критерии приемки`
headings и на русском (Cyrillic обязательна). При ошибке валидации
tool возвращает ошибку и НЕ вызывает gh — почини формат и повтори.
Label выбирай по типу задачи (совпадает с commit `type`):

View file

@ -303,8 +303,13 @@ Default stack для типа (хардкод, добавить всегда):
3. Для каждого пункта roadmap (по порядку):
- Сформируй самодостаточный issue body (issue-skill format):
## Контекст
## Что сделать (пошагово с путями к файлам)
## Проверка (команды)
(Зачем: мотивация; Контекст: текущее состояние)
## Задача (пошагово с путями к файлам)
## Контракты (ожидаемое поведение / API)
## Инварианты (правила без исключений)
## Граничные случаи (что при ошибках)
## Вне scope (что НЕ делаем)
## Критерии приемки (как проверяем)
## Связанные ресурсы (Part of spec, ref к docs/spec/roadmap.md)
- Issue #1 (scaffolding) body ДОЛЖЕН включать:
"Используй repo-init skill для: pyproject.toml, CI, .gitignore, LICENSE, dependabot, pre-commit. Структура — из ## Структура в docs/spec/modules.md."

View file

@ -5,20 +5,21 @@ const TITLE_REGEX = /^(feat|fix|chore|docs|refactor|test|style|perf)\([^)]+\): .
const CYRILLIC = /[\u0400-\u04FF]/
const RULES = `Rules:
- Title: type(scope): description (<=80 chars description)
- Types: feat, fix, chore, docs, refactor, test, style, perf
- Scope is mandatory
- Title in English only
- Body must contain '## Контекст' heading
- Body must contain '## Задача' heading
- Body must contain '## Критерии приемки' heading
- Body in Russian (must contain Cyrillic)`
- Title: conventional format type(scope): description (80 chars, English only)
- Body must contain '## Контекст' heading (зачем + текущее состояние)
- Body must contain '## Задача' heading (что делаем)
- Body must contain '## Контракты' heading (ожидаемое поведение / API)
- Body must contain '## Инварианты' heading (правила без исключений)
- Body must contain '## Граничные случаи' heading (что при ошибках)
- Body must contain '## Вне scope' heading (что НЕ делаем)
- Body must contain '## Критерии приемки' heading (как проверяем)
- Body must contain Cyrillic (Russian language required)`
export default tool({
description: "Create a GitHub issue with title/body validation. Validates: title format type(scope): description (<=80), English title, body headings (## Контекст, ## Задача, ## Критерии приемки), body in Russian. Optional labels. Returns issue URL on success.",
description: "Create a GitHub issue with title/body validation. Validates: title format type(scope): description (<=80), English title, body headings (## Контекст, ## Задача, ## Контракты, ## Инварианты, ## Граничные случаи, ## Вне scope, ## Критерии приемки), body in Russian. Optional labels. Returns issue URL on success.",
args: {
title: tool.schema.string().describe("Issue title (conventional format: type(scope): description, <=80 chars)"),
body: tool.schema.string().describe("Issue body in Russian with ## Контекст, ## Задача, ## Критерии приемки headings"),
body: tool.schema.string().describe("Issue body in Russian with ## Контекст, ## Задача, ## Контракты, ## Инварианты, ## Граничные случаи, ## Вне scope, ## Критерии приемки headings"),
labels: tool.schema.array(tool.schema.string()).optional().describe("Labels to assign (e.g. ['bug', 'enhancement'])"),
repo: tool.schema.string().optional().describe("Optional repo (owner/name). If omitted, gh auto-detects from context.worktree."),
},
@ -41,6 +42,18 @@ export default tool({
if (!body.includes("## Критерии приемки")) {
return `❌ Issue body must contain '## Критерии приемки' heading\n\n${RULES}`
}
if (!body.includes("## Контракты")) {
return `❌ Issue body must contain '## Контракты' heading\n\n${RULES}`
}
if (!body.includes("## Инварианты")) {
return `❌ Issue body must contain '## Инварианты' heading\n\n${RULES}`
}
if (!body.includes("## Граничные случаи")) {
return `❌ Issue body must contain '## Граничные случаи' heading\n\n${RULES}`
}
if (!body.includes("## Вне scope")) {
return `❌ Issue body must contain '## Вне scope' heading\n\n${RULES}`
}
if (!CYRILLIC.test(body)) {
return `❌ Issue body must be in Russian\n\n${RULES}`
}

View file

@ -0,0 +1,43 @@
# ADR-071: 7 mandatory SDD sections for all issues
## Статус
Accepted (2026-07-31)
## Контекст
Шаблоны issue body в `issue/SKILL.md` и `spec/SKILL.md` использовали заголовки (`## Что сделать`, `## Проверка`, `## Acceptance criteria`), не совпадающие с валидацией `create-issue.ts` (`## Контекст`, `## Задача`, `## Критерии приемки`). Issues, созданные по шаблону скилла, проваливали валидацию tool'а — агент получал ошибку вместо создания issue.
Кроме того, 3 обязательные секции (Контекст, Задача, Критерии приемки) не покрывали SDD-аспекты: контракты (ожидаемое поведение/API), инварианты (правила без исключений), граничные случаи (что при ошибках), out-of-scope (что НЕ делаем). Без этих секций агент угадывает границы задачи, что приводит к scope creep и неполным результатам.
## Решение
Унифицировать все шаблоны и валидацию на 7 обязательных SDD-секций — всегда, без флагов и режимов:
1. `## Контекст` — зачем + текущее состояние
2. `## Задача` — что делаем
3. `## Контракты` — ожидаемое поведение / API
4. `## Инварианты` — правила без исключений
5. `## Граничные случаи` — что при ошибках
6. `## Вне scope` — что НЕ делаем
7. `## Критерии приемки` — как проверяем
Изменения:
- `create-issue.ts`: 4 новые проверки заголовков (всегда, без флагов).
- `issue/SKILL.md`: шаблон 7 секций, обновлённые примеры.
- `spec/SKILL.md`: шаблон issue body обновлён до 7 секций.
- `bug-discovery/SKILL.md`: описание заголовков обновлено.
- Тесты (Python + TS): `VALID_BODY` 7 секций, 4 новых missing-heading теста.
## Альтернативы
1. **Двухрежимный подход (sdd flag)**`create-issue` принимает `sdd?: boolean`, при `true` проверяет 7 секций, при `false` — 3. Отклонено: флаги создают два стандарта, агенты путаются какой режим выбрать, SDD-секции полезны для всех задач (даже кратко).
2. **Оставить 3 секции** — отклонено: не покрывает контракты, инварианты, edge cases, out-of-scope. Агент угадывает границы → scope creep.
3. **Опциональные SDD-секции (предупреждение, не error)** — отклонено: предупреждения игнорируются агентами. Только жёсткая валидация гарантирует качество.
## Последствия
- Все новые issues должны содержать 7 секций — включая баг-репорты (bug-discovery скилл обновлён).
- Существующие issues в репо не соответствуют новому формату — это не влияет на функциональность, только на создание новых.
- SDD-секции можно заполнять кратко (1-2 строки) — главное наличие заголовка.

View file

@ -0,0 +1,28 @@
---
pr: 169
title: fix(issue): align headings with create-issue validation and add SDD sections
---
## Что сделано
- `create-issue.ts`: добавлена валидация 4 новых SDD-секций (`## Контракты`, `## Инварианты`, `## Граничные случаи`, `## Вне scope`) — всегда, без флагов. Итоговые обязательные заголовки: 7 секций.
- `issue/SKILL.md`: шаблон обновлён до 7 секций SDD. Структура `## Контекст` включает «Зачем» (мотивация) и «Контекст» (текущее состояние). Старые заголовки (`## Что сделать`, `## Проверка`, `## Acceptance criteria`, `## Dependencies`, `## Связанные ресурсы`) убраны. Примеры обновлены.
- `spec/SKILL.md`: шаблон issue body (строки 305-308) обновлён — `## Что сделать``## Задача`, `## Проверка``## Критерии приемки`, добавлены новые SDD-секции.
- `bug-discovery/SKILL.md`: описание заголовков issue body обновлено до 7 секций.
- `tests/test_create_issue_tool.py`: `VALID_BODY` обновлён до 7 секций. Тела существующих missing-heading тестов обновлены. Добавлены 4 новых теста: `test_sdd_missing_contracts`, `test_sdd_missing_invariants`, `test_sdd_missing_edge_cases`, `test_sdd_missing_out_of_scope`. Длинные строки переведены на implicit string concatenation (E501 fix).
- `tests/test_create_issue_tool.ts`: синхронизирован с Python-тестами.
## Почему
Шаблоны в `issue/SKILL.md` и `spec/SKILL.md` использовали заголовки (`## Что сделать`, `## Проверка`, `## Acceptance criteria`), не совпадающие с валидацией `create-issue.ts` (`## Контекст`, `## Задача`, `## Критерии приемки`) — issues, созданные по шаблону скилла, проваливали валидацию tool'а.
SDD-секции (контракты, инварианты, граничные случаи, out-of-scope) исключают угадывание агента и улучшают качество результатов с первого промпта. Единый стандарт для всех issues — без флагов и режимов.
## Pending
PR #2: создать `feature-spec` скилл — SDD Q&A перед реализацией. Будет использовать обновлённый `create-issue` tool с 7 секциями.
## Watch out
- Существующие issues в репо не соответствуют новому формату — но это не влияет на функциональность, только на создание новых issues
- `bug-discovery` скилл теперь требует 7 секций — баг-репорты должны включать контракты, инварианты, edge cases и out-of-scope (можно кратко)

View file

@ -26,12 +26,12 @@ opencode-config/
│ │ └── spec.md # /spec — 9-phase spec generation
│ ├── skills/
│ │ ├── add-skill/SKILL.md # Create new opencode skill
│ │ ├── bug-discovery/SKILL.md # Bug Discovery Protocol — create issue for out-of-scope bugs (create-issue tool, duplicate check) — PR#120
│ │ ├── bug-discovery/SKILL.md # Bug Discovery Protocol — create issue for out-of-scope bugs (create-issue tool, 7 SDD sections, duplicate check) — PR#120, PR#169
│ │ ├── branch/SKILL.md # Branch naming conventions
│ │ ├── code-standards/SKILL.md # Universal code style rules
│ │ ├── configure-opencode/SKILL.md # Canonical rule: write to .opencode/
│ │ ├── get-project-map/SKILL.md # Maintain docs/project-map/
│ │ ├── issue/SKILL.md # GitHub issue creation
│ │ ├── issue/SKILL.md # GitHub issue creation (7 SDD sections template) — PR#169
│ │ ├── memory/SKILL.md # opencode-memory usage guide
│ │ ├── run-pipeline/SKILL.md # 7-phase pipeline orchestration
│ │ ├── python-development/SKILL.md # Python dev patterns
@ -40,11 +40,11 @@ opencode-config/
│ │ ├── repo-readme/SKILL.md # Standardized README generation (create-readme tool: create vs validate, bilingual Why/What + Features table, GitHub metadata, quick_start_steps clickable steps + conditional bash block; tagline_en/tagline_ru required + delimiter-теги, breaking change note; workflow: create → draw-image (template "cover", slots icon/sub-icon/badge, brand-logos vs Lucide) → validate → fix-цикл) — PR#112, PR#116, PR#118, PR#130, PR#146, PR#151, PR#158 (cover pipeline в workflow)
│ │ ├── tunnel/SKILL.md # Cloudflare tunnel toggle (tool `tunnel()`: 1-й вызов start, 2-й stop) — PR#63 (восстановлен, удалён в PR#42)
│ │ ├── run-tests/SKILL.md # Test runner guide
│ │ └── spec/SKILL.md # 9-phase spec generation
│ │ └── spec/SKILL.md # 9-phase spec generation (issue body template: 7 SDD sections) — PR#169
│ ├── tools/
│ │ ├── _shared.ts # Shared module for GitHub tools: parseRepo(repo?), runGh(args, repo?, opts?), formatResult(r, toolName) — PR#65
│ │ ├── commit.ts # commit tool wrapper (1 arg message, validates format+staged) — PR#38
│ │ ├── create-issue.ts # create-issue tool wrapper (3 args, validates format+labels; optional repo?: string) — PR#38, PR#65
│ │ ├── create-issue.ts # create-issue tool wrapper (3 args, validates 7 SDD headings+format+labels; optional repo?: string) — PR#38, PR#65, PR#169
│ │ ├── create-pr.ts # create-pr tool wrapper (3 args, validates format+Closes #N; optional repo?: string) — PR#38, PR#65
│ │ ├── create-readme.ts # create-readme tool (TS plugin, modes: create/validate; standardized bilingual README with features table, include_clone/development_en/ru/quick_start_steps_en/ru optional params, clickable access_url [url](url), conditional bash block via hasBashBlock, RU heading 'Русский' + anchor checks, 6 delimiter pairs for slaid098.dev (summary-en/ru, features-en/ru, tagline-en/ru); tagline_en+tagline_ru required (BREAKING: tagline removed PR#151), content-валидация repo_name (lowercase kebab-case) + tagline_en (no Cyrillic) + tagline_ru (require Cyrillic), validateReadme H1 prefix check `# 🚀 `; generateReadme рендерит `![Cover](assets/cover.png)` после H1 перед tagline-разделителями; validateReadme substring-чек `assets/cover.png` (без file existence — для remote gh api режима); local fs + remote gh api) — PR#112, PR#116, PR#118, PR#130, PR#146, PR#149 (quick_start optional + guard), PR#151 (bilingual tagline + repo_name validation, breaking), PR#158 (cover image reference в template + validation)
│ │ ├── draw-image.ts # draw-image tool wrapper (opencode plugin, 5 args: template/title/subtitle?/slots?/out?; spawnSync node cli.ts render → sharp PNG) — PR#133
@ -127,8 +127,8 @@ opencode-config/
│ ├── test_dockerfile.py # Dockerfile npm install (opencode-ai + repomix; @mathew-cf/opencode-memory REMOVED PR#103, assertions inverted) — PR#57, PR#103
│ ├── test_commit_tool.py # .opencode/tools/commit.ts (via _ts_loader.mjs exec_stub_json) — PR#38
│ ├── test_commit_tool.ts # TS wrapper test (mjs loader) — PR#38
│ ├── test_create_issue_tool.py # .opencode/tools/create-issue.ts (via _ts_loader.mjs exec_stub_json; +repo explicit/omitted/invalid) — PR#38, PR#65
│ ├── test_create_issue_tool.ts # TS wrapper test (mjs loader; +repo cases) — PR#38, PR#65
│ ├── test_create_issue_tool.py # .opencode/tools/create-issue.ts (via _ts_loader.mjs exec_stub_json; 7 SDD headings, 4 new missing-heading tests, +repo) — PR#38, PR#65, PR#169
│ ├── test_create_issue_tool.ts # TS wrapper test (mjs loader; +repo cases, 7 SDD headings) — PR#38, PR#65, PR#169
│ ├── test_create_pr_tool.py # .opencode/tools/create-pr.ts (via _ts_loader.mjs exec_stub_json; +repo explicit/omitted/invalid) — PR#38, PR#65
│ ├── test_create_pr_tool.ts # TS wrapper test (mjs loader; +repo cases) — PR#38, PR#65
│ ├── test_draw_image_tool.py # .opencode/tools/draw-image.ts (via _ts_loader.mjs exec_stub_json; 5 tests: load/valid/failure/cwd/all-args) — PR#133

View file

@ -32,7 +32,13 @@ TS_FILE_REL = ".opencode/tools/create-issue.ts"
VALID_TITLE = "feat(tools): add create-issue tool validation"
VALID_BODY = (
"## Контекст\nНужен tool\n\n## Задача\nСоздать tool\n\n## Критерии приемки\nTool работает"
"## Контекст\nНужен tool\n\n"
"## Задача\nСоздать tool\n\n"
"## Контракты\nTool валидирует 7 секций\n\n"
"## Инварианты\nВсе секции обязательны\n\n"
"## Граничные случаи\nНет секций → error\n\n"
"## Вне scope\nИзменение gh CLI\n\n"
"## Критерии приемки\nTool работает"
)
ISSUE_URL = "https://github.com/slaid098/opencode-config/issues/39"
ISSUE_OK_RESPONSE = {"status": 0, "stdout": ISSUE_URL + "\n", "stderr": ""}
@ -106,11 +112,16 @@ def test_title_80_chars_ok():
def test_missing_kontekst():
"""execute() with body missing '## Контекст' → error mentioning heading."""
body = (
"## Задача\nСделать\n\n"
"## Контракты\nКонтракты\n\n"
"## Инварианты\nПравила\n\n"
"## Граничные случаи\nОшибки\n\n"
"## Вне scope\nНе делаем\n\n"
"## Критерии приемки\nГотово"
)
out = _run_exec(
{
"title": VALID_TITLE,
"body": "## Задача\nСделать\n\n## Критерии приемки\nГотово",
},
{"title": VALID_TITLE, "body": body},
[ISSUE_OK_RESPONSE],
)
result = out["result"]
@ -119,11 +130,16 @@ def test_missing_kontekst():
def test_missing_zadacha():
"""execute() with body missing '## Задача' → error mentioning heading."""
body = (
"## Контекст\nКонтекст\n\n"
"## Контракты\nКонтракты\n\n"
"## Инварианты\nПравила\n\n"
"## Граничные случаи\nОшибки\n\n"
"## Вне scope\nНе делаем\n\n"
"## Критерии приемки\nГотово"
)
out = _run_exec(
{
"title": VALID_TITLE,
"body": "## Контекст\nКонтекст\n\n## Критерии приемки\nГотово",
},
{"title": VALID_TITLE, "body": body},
[ISSUE_OK_RESPONSE],
)
result = out["result"]
@ -132,17 +148,86 @@ def test_missing_zadacha():
def test_missing_kriterii():
"""execute() with body missing '## Критерии приемки' → error mentioning heading."""
body = (
"## Контекст\nКонтекст\n\n"
"## Задача\nСделать\n\n"
"## Контракты\nКонтракты\n\n"
"## Инварианты\nПравила\n\n"
"## Граничные случаи\nОшибки\n\n"
"## Вне scope\nНе делаем"
)
out = _run_exec(
{
"title": VALID_TITLE,
"body": "## Контекст\nКонтекст\n\n## Задача\nСделать",
},
{"title": VALID_TITLE, "body": body},
[ISSUE_OK_RESPONSE],
)
result = out["result"]
assert "## Критерии приемки" in result, f"expected heading error, got: {result!r}"
def test_sdd_missing_contracts():
"""execute() with body missing '## Контракты' → error mentioning heading."""
body = (
"## Контекст\nКонтекст\n\n"
"## Задача\nСделать\n\n"
"## Инварианты\nПравила\n\n"
"## Граничные случаи\nОшибки\n\n"
"## Вне scope\nНе делаем\n\n"
"## Критерии приемки\nГотово"
)
out = _run_exec({"title": VALID_TITLE, "body": body}, [ISSUE_OK_RESPONSE])
result = out["result"]
assert "## Контракты" in result, f"expected heading error, got: {result!r}"
assert "error" in result.lower() or "" in result
def test_sdd_missing_invariants():
"""execute() with body missing '## Инварианты' → error mentioning heading."""
body = (
"## Контекст\nКонтекст\n\n"
"## Задача\nСделать\n\n"
"## Контракты\nКонтракты\n\n"
"## Граничные случаи\nОшибки\n\n"
"## Вне scope\nНе делаем\n\n"
"## Критерии приемки\nГотово"
)
out = _run_exec({"title": VALID_TITLE, "body": body}, [ISSUE_OK_RESPONSE])
result = out["result"]
assert "## Инварианты" in result, f"expected heading error, got: {result!r}"
assert "error" in result.lower() or "" in result
def test_sdd_missing_edge_cases():
"""execute() with body missing '## Граничные случаи' → error mentioning heading."""
body = (
"## Контекст\nКонтекст\n\n"
"## Задача\nСделать\n\n"
"## Контракты\nКонтракты\n\n"
"## Инварианты\nПравила\n\n"
"## Вне scope\nНе делаем\n\n"
"## Критерии приемки\nГотово"
)
out = _run_exec({"title": VALID_TITLE, "body": body}, [ISSUE_OK_RESPONSE])
result = out["result"]
assert "## Граничные случаи" in result, f"expected heading error, got: {result!r}"
assert "error" in result.lower() or "" in result
def test_sdd_missing_out_of_scope():
"""execute() with body missing '## Вне scope' → error mentioning heading."""
body = (
"## Контекст\nКонтекст\n\n"
"## Задача\nСделать\n\n"
"## Контракты\nКонтракты\n\n"
"## Инварианты\nПравила\n\n"
"## Граничные случаи\nОшибки\n\n"
"## Критерии приемки\nГотово"
)
out = _run_exec({"title": VALID_TITLE, "body": body}, [ISSUE_OK_RESPONSE])
result = out["result"]
assert "## Вне scope" in result, f"expected heading error, got: {result!r}"
assert "error" in result.lower() or "" in result
def test_latin_only_body():
"""execute() with body containing no Cyrillic → error.
@ -190,7 +275,7 @@ def test_execute_uses_cwd_from_context():
"""execute passes cwd=context.worktree to spawnSync (ADR-023 pattern)."""
out = _run_exec({"title": VALID_TITLE, "body": VALID_BODY}, [ISSUE_OK_RESPONSE])
calls = out["calls"]
assert len(calls) == 1, f"expected 1 spawnSync call, got: {len(calls)}"
assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}"
opts = calls[0]["opts"]
assert opts is not None, "spawnSync called without opts — expected cwd kwarg"
assert "cwd" in opts, f"opts missing 'cwd' key — got: {opts}"
@ -212,7 +297,7 @@ def test_repo_explicit_passed_to_gh():
result = out["result"]
assert result == f"Issue created: {ISSUE_URL}", f"expected success, got: {result!r}"
calls = out["calls"]
assert len(calls) == 1, f"expected 1 spawnSync call, got: {len(calls)}"
assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}"
args = calls[0]["args"]
assert args[0] == "--repo", f"expected --repo first, got: {args[0]!r}"
assert args[1] == "foo/bar", f"expected repo value, got: {args[1]!r}"
@ -228,7 +313,7 @@ def test_repo_omitted_no_repo_flag():
"""
out = _run_exec({"title": VALID_TITLE, "body": VALID_BODY}, [ISSUE_OK_RESPONSE])
calls = out["calls"]
assert len(calls) == 1, f"expected 1 spawnSync call, got: {len(calls)}"
assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}"
args = calls[0]["args"]
assert "--repo" not in args, (
f"--repo must NOT be added when repo arg omitted; gh auto-detects. args: {args!r}"

View file

@ -36,7 +36,7 @@ function ctx() {
}
}
const VALID_BODY = "## Контекст\nНужен tool\n\n## Задача\nСоздать tool\n\n## Критерии приемки\nTool работает"
const VALID_BODY = "## Контекст\nНужен tool\n\n## Задача\nСоздать tool\n\n## Контракты\nTool валидирует 7 секций\n\n## Инварианты\nВсе секции обязательны\n\n## Граничные случаи\nНет секций → error\n\n## Вне scope\nИзменение gh CLI\n\n## Критерии приемки\nTool работает"
describe("create-issue tool", () => {
test("test_valid_issue — valid title + body succeeds", async () => {
@ -71,7 +71,7 @@ describe("create-issue tool", () => {
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({
title: "feat(tools): valid title",
body: "## Задача\nСделать\n\n## Критерии приемки\nГотово",
body: "## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
}, ctx())
expect(result).toContain("## Контекст")
})
@ -83,7 +83,7 @@ describe("create-issue tool", () => {
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({
title: "feat(tools): valid title",
body: "## Контекст\nКонтекст\n\n## Критерии приемки\nГотово",
body: "## Контекст\nКонтекст\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
}, ctx())
expect(result).toContain("## Задача")
})
@ -95,11 +95,59 @@ describe("create-issue tool", () => {
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({
title: "feat(tools): valid title",
body: "## Контекст\nКонтекст\n\n## Задача\nСделать",
body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Вне scope\nНе делаем",
}, ctx())
expect(result).toContain("## Критерии приемки")
})
test("test_sdd_missing_contracts — body missing ## Контракты → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({
title: "feat(tools): valid title",
body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
}, ctx())
expect(result).toContain("## Контракты")
})
test("test_sdd_missing_invariants — body missing ## Инварианты → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({
title: "feat(tools): valid title",
body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Граничные случаи\nОшибки\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
}, ctx())
expect(result).toContain("## Инварианты")
})
test("test_sdd_missing_edge_cases — body missing ## Граничные случаи → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({
title: "feat(tools): valid title",
body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
}, ctx())
expect(result).toContain("## Граничные случаи")
})
test("test_sdd_missing_out_of_scope — body missing ## Вне scope → error", async () => {
mock.module("child_process", () => ({
spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
}))
const mod = await import(TOOL_SRC + "?t=" + Date.now())
const result = await mod.default.execute({
title: "feat(tools): valid title",
body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Критерии приемки\nГотово",
}, ctx())
expect(result).toContain("## Вне scope")
})
test("test_latin_only_body — body without Cyrillic → error", async () => {
// NOTE: spec validation order checks headings (## Контекст, ## Задача,
// ## Критерии приемки) BEFORE the Cyrillic check. Since the headings