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:
parent
6977a9130a
commit
c491b5fad1
9 changed files with 312 additions and 88 deletions
|
|
@ -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: ...".
|
||||
|
|
@ -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`):
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
|||
43
docs/decisions/071-pr-169-sdd-validation-headings.md
Normal file
43
docs/decisions/071-pr-169-sdd-validation-headings.md
Normal 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 строки) — главное наличие заголовка.
|
||||
28
docs/handoff/pr-169-sdd-validation-headings.md
Normal file
28
docs/handoff/pr-169-sdd-validation-headings.md
Normal 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 (можно кратко)
|
||||
|
|
@ -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 рендерит `` после 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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue