feat(pipeline): add check_project_map to DOCS phase (#67)
* feat(pipeline): add check_project_map to DOCS phase * test(pipeline): add check_project_map unit tests + update check_docs tests * docs(adr): add ADR-028 project map contract * docs(project-map): mention check_project_map guard in Update Protocol * docs(handoff): add PR handoff for check_project_map * docs(handoff): set PR number 67 * docs(review): fix PR#66 to PR#67 typo in project map and handoff * fix(ci): ruff lint errors in pipeline-status and project map test --------- Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
parent
15fc7d014d
commit
f06d36140b
6 changed files with 280 additions and 3 deletions
|
|
@ -51,6 +51,7 @@ _MEMORY_BASE = os.environ.get(
|
|||
MEMORY_DIR = Path(_MEMORY_BASE) / "repos"
|
||||
HANDOFF_DIR = REPO_ROOT / "docs" / "handoff"
|
||||
ADR_DIR = REPO_ROOT / "docs" / "decisions"
|
||||
PROJECT_MAP_DIR = REPO_ROOT / "docs" / "project-map"
|
||||
|
||||
REQUIRED_SECTIONS = ["## Что сделано", "## Почему", "## Pending", "## Watch out"]
|
||||
|
||||
|
|
@ -264,13 +265,15 @@ def check_implement(pr_number: int) -> PhaseResult:
|
|||
|
||||
|
||||
def check_docs(pr_number: int) -> PhaseResult:
|
||||
"""Phase 3: DOCS — handoff valid (4 sections) + mandatory ADR + docs-reviewer marker.
|
||||
"""Phase 3: DOCS — handoff valid (4 sections) + mandatory ADR + project map
|
||||
+ docs-reviewer marker.
|
||||
|
||||
Deterministic checks:
|
||||
1. Handoff file `docs/handoff/pr-N-*.md` exists.
|
||||
2. 4 sections present in handoff content.
|
||||
3. ADR file `docs/decisions/*-pr-N-*.md` exists.
|
||||
4. PR comment with heading `## Docs Review Summary` from docs-reviewer (proves it ran).
|
||||
4. Project map `docs/project-map/README.md` exists (ADR-028 contract).
|
||||
5. PR comment with heading `## Docs Review Summary` from docs-reviewer (proves it ran).
|
||||
|
||||
Without the docs-reviewer comment, DOCS is NOT_DONE — subagent may have written
|
||||
handoff+ADR without docs-reviewer actually validating them. Symmetric to
|
||||
|
|
@ -289,6 +292,10 @@ def check_docs(pr_number: int) -> PhaseResult:
|
|||
if adr_result.status != PhaseStatus.DONE:
|
||||
return adr_result
|
||||
|
||||
pm_result = check_project_map()
|
||||
if pm_result.status != PhaseStatus.DONE:
|
||||
return pm_result
|
||||
|
||||
return _check_docs_reviewer_comment(pr_number)
|
||||
|
||||
|
||||
|
|
@ -324,6 +331,29 @@ def check_adr(pr_number: int) -> PhaseResult:
|
|||
)
|
||||
|
||||
|
||||
def check_project_map() -> PhaseResult:
|
||||
"""Phase 3 part: docs/project-map/README.md exists (mandatory project map).
|
||||
|
||||
Minimal guard against silent docs-reviewer failure. Does NOT validate
|
||||
frontmatter or module-level files — see ADR-028 for the contract.
|
||||
"""
|
||||
if not PROJECT_MAP_DIR.exists():
|
||||
return PhaseResult(
|
||||
PhaseStatus.NOT_DONE,
|
||||
f"директория {PROJECT_MAP_DIR} не найдена — "
|
||||
"docs-reviewer должен создать initial README.md (docs-reviewer.md:82-84)",
|
||||
)
|
||||
readme = PROJECT_MAP_DIR / "README.md"
|
||||
if readme.exists():
|
||||
size = readme.stat().st_size
|
||||
return PhaseResult(PhaseStatus.DONE, f"project map: README.md ({size} bytes)")
|
||||
return PhaseResult(
|
||||
PhaseStatus.NOT_DONE,
|
||||
"docs/project-map/README.md не найден — "
|
||||
"docs-reviewer должен создать initial map (docs-reviewer.md:244)",
|
||||
)
|
||||
|
||||
|
||||
def check_ci(pr_number: int) -> PhaseResult:
|
||||
"""Phase 4: CI — latest CI run on PR branch completed & success.
|
||||
|
||||
|
|
|
|||
73
docs/decisions/028-pr-67-project-map-contract.md
Normal file
73
docs/decisions/028-pr-67-project-map-contract.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# ADR-028: Project map contract (mandatory README.md, aspirational modules)
|
||||
|
||||
## Статус
|
||||
|
||||
Accepted (2026-07-25)
|
||||
|
||||
## Контекст
|
||||
|
||||
Контракт `docs/project-map/` в репо `slaid098/opencode-config` размазан по 3
|
||||
файлам с внутренними несоответствиями:
|
||||
|
||||
- `.opencode/agents/docs-reviewer.md:158-159,191` — обязывает
|
||||
docs-reviewer обновлять карту при структурных изменениях и предлагает
|
||||
один commit message.
|
||||
- `.opencode/skills/get-project-map/SKILL.md:25-27` — описывает структуру
|
||||
карты (README.md + модульные файлы per module) и frontmatter
|
||||
`last_updated`.
|
||||
- `.opencode/agents/reviewer.md:199-205` — требует от code-reviewer проверять
|
||||
актуальность карты и предлагает *другой* commit message для той же
|
||||
операции (несоответствие с docs-reviewer.md).
|
||||
|
||||
Фактическое состояние карты: только `README.md` (~140 строк, ASCII-дерево
|
||||
структуры репо) без frontmatter и без модульных файлов. Полный контракт
|
||||
(модульные файлы per module) никогда не соблюдался — aspirational документ,
|
||||
не enforcement.
|
||||
|
||||
`pipeline-status.py:check_docs` (DOCS phase) валидировал handoff (4 секции),
|
||||
ADR (filename pattern) и docs-reviewer comment (heading `## Docs Review
|
||||
Summary`), но **не валидировал `docs/project-map/`**. Это позволяло PR проходить
|
||||
DOCS-фазу даже при «тихом провале» docs-reviewer (забыл обновить карту при
|
||||
структурных изменениях) — детерминированный guard отсутствовал. Зафиксировано
|
||||
в memory `technical/pipeline-status-no-project-map-check.md`.
|
||||
|
||||
## Решение
|
||||
|
||||
Минимальный guard в `pipeline-status.py` — новая функция `check_project_map()`
|
||||
проверяет существование `docs/project-map/README.md` (через
|
||||
`PROJECT_MAP_DIR.exists()` + `README.md.exists()`). Встроена в `check_docs`
|
||||
между `check_adr` и `_check_docs_reviewer_comment` (short-circuit на
|
||||
NOT_DONE). Mandatory для pipeline-driven репо: docs-reviewer автосоздаёт
|
||||
initial README.md при отсутствии каталога (`docs-reviewer.md:82-84`), поэтому
|
||||
guard не блокирует первый PR и не требует ручной bootstrap-операции.
|
||||
|
||||
Контракт ослаблен под фактическое состояние:
|
||||
|
||||
- **README.md обязателен** — единственное, что проверяется детерминированно.
|
||||
Возвращает DONE с размером файла в detail, либо NOT_DONE с указанием что
|
||||
docs-reviewer должен создать (с ссылками на строки docs-reviewer.md).
|
||||
- **Модульные файлы per module и frontmatter `last_updated`** — aspirational,
|
||||
НЕ enforced. Полный контракт остаётся в `get-project-map/SKILL.md` как
|
||||
рекомендация для docs-reviewer, но не блокирует пайплайн.
|
||||
|
||||
Возвращает только DONE/NOT_DONE (без AMBIGUOUS) — проверка файловой системы
|
||||
детерминирована, нет внешних API вызовов.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Полный контракт (модульные файлы per module)** — отклонено: потребовало
|
||||
бы создания ~12 новых файлов (по одному на модуль) для существующего репо,
|
||||
блокируя пайплайн до их создания. Фактическое состояние никогда не
|
||||
соответствовало этому контракту — enforcement сломал бы все открытые PR.
|
||||
|
||||
- **Опциональность через env-flag `OPENCODE_REQUIRE_PROJECT_MAP`** —
|
||||
отклонено: docs-reviewer уже автосоздаёт initial README.md при отсутствии
|
||||
каталога (`docs-reviewer.md:82-84`), feature-flag избыточен — guard
|
||||
никогда не блокирует корректно настроенный pipeline-driven репо. Env-flag
|
||||
добавил бы конфигурационную поверхность без выгоды.
|
||||
|
||||
- **Diff-based проверка (карта обновлена в PR через `gh pr view --json
|
||||
files`)** — отклонено: сложнее в реализации (парсинг diff, фильтр по путям),
|
||||
не гарантирует валидность содержимого (файл может быть изменён, но остаться
|
||||
пустым/устаревшим). Существование README.md — более сильный guard (файл
|
||||
присутствует), diff-based — слабее (файл изменён, но может быть невалиден).
|
||||
95
docs/handoff/pr-67-check-project-map.md
Normal file
95
docs/handoff/pr-67-check-project-map.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
pr: 67
|
||||
title: feat(pipeline): add check_project_map to DOCS phase
|
||||
---
|
||||
|
||||
## Что сделано
|
||||
|
||||
- **`.opencode/scripts/pipeline-status.py`** — 3 точечные правки:
|
||||
- Константа `PROJECT_MAP_DIR = REPO_ROOT / "docs" / "project-map"` (после
|
||||
`ADR_DIR`, строка 54).
|
||||
- Функция `check_project_map()` (после `check_adr`) — проверяет
|
||||
`PROJECT_MAP_DIR.exists()` + `README.md.exists()`, возвращает
|
||||
DONE/NOT_DONE (без AMBIGUOUS, чисто файловая система). Шаблон по образцу
|
||||
`check_adr`.
|
||||
- В `check_docs` вставлен вызов между `check_adr` и
|
||||
`_check_docs_reviewer_comment` (short-circuit на NOT_DONE). Docstring
|
||||
`check_docs` обновлён (теперь 5 детерминированных проверок).
|
||||
- **`tests/test_pipeline_status_project_map.py`** — новый файл, 3 unit-теста
|
||||
(по образцу `test_pipeline_status_adr.py`):
|
||||
- `test_project_map_exists` — README.md есть → DONE, "README.md" в detail.
|
||||
- `test_project_map_dir_missing` — директория отсутствует → NOT_DONE,
|
||||
"директория" в detail.
|
||||
- `test_project_map_readme_missing` — директория есть, README.md нет →
|
||||
NOT_DONE, "README.md не найден" в detail.
|
||||
Загрузка скрипта под уникальным module name `pipeline_status_project_map`
|
||||
(не коллидирует с `pipeline_status` из других тестов).
|
||||
- **`tests/test_pipeline_status.py`** — критическая правка существующих
|
||||
тестов:
|
||||
- Добавлен helper `make_project_map(tmp_path)` рядом с `make_handoff` —
|
||||
создаёт `tmp_path/project-map/README.md`.
|
||||
- Во все 10 `test_check_docs_*` тестов добавлен
|
||||
`monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))`.
|
||||
Без этого новая под-проверка возвращала NOT_DONE и ломала assertions
|
||||
(7 тестов с валидным ADR доходили до check_project_map).
|
||||
- **ADR-028** (`docs/decisions/028-pr-67-project-map-contract.md`) —
|
||||
фиксирует контракт project-map: README.md обязателен (mandatory, enforced),
|
||||
модульные файлы и frontmatter `last_updated` — aspirational (не enforced).
|
||||
Альтернативы: полный контракт (отклонено — блокирует пайплайн, ~12 файлов),
|
||||
env-flag (отклонено — docs-reviewer автосоздаёт), diff-based (отклонено —
|
||||
сложнее, не гарантирует валидность).
|
||||
- **`docs/project-map/README.md`** — секция Update Protocol дополнена строкой
|
||||
про новый guard; tree обновлён (новый тест-файл + тег PR#67 на
|
||||
pipeline-status.py).
|
||||
|
||||
## Почему
|
||||
|
||||
Детерминированный guard для project-map отсутствовал —
|
||||
`pipeline-status.py:check_docs` проверял handoff, ADR и docs-reviewer
|
||||
comment, но не валидировал `docs/project-map/`. Это позволяло PR проходить
|
||||
DOCS-фазу даже при «тихом провале» docs-reviewer (забыл обновить карту при
|
||||
структурных изменениях). Memory `technical/pipeline-status-no-project-map-check.md`
|
||||
зафиксировала эту дыру.
|
||||
|
||||
Контракт карты был размазан по 3 файлам (`docs-reviewer.md`,
|
||||
`get-project-map/SKILL.md`, `reviewer.md`) с несоответствиями (2 разных commit
|
||||
message для одной операции). Фактическое состояние — только README.md без
|
||||
frontmatter и модульных файлов. ADR-028 ослабляет контракт под фактическое
|
||||
состояние (README.md mandatory, модульные файлы aspirational) и фиксирует
|
||||
единый source of truth. Полный контракт остаётся в `get-project-map/SKILL.md`
|
||||
как рекомендация.
|
||||
|
||||
Локальная верификация: 404 теста прошли, coverage 89.19% ≥ 80%
|
||||
(pipeline-status.py покрыт на 88%).
|
||||
|
||||
## Pending
|
||||
|
||||
- После merge: фаза MEMORY — обновить memory
|
||||
`technical/pipeline-status-no-project-map-check.md` секцией Closure (guard
|
||||
добавлен в PR#67, контракт зафиксирован в ADR-028).
|
||||
- ADR filename переименован с placeholder `<PR-NUMBER>` на `028-pr-67-project-map-contract.md`
|
||||
после create_pr (коммит `docs(handoff): set PR number`).
|
||||
|
||||
## Watch out
|
||||
|
||||
- **10 существующих тестов правлены** — `test_check_docs_*` в
|
||||
`test_pipeline_status.py` теперь требуют `make_project_map(tmp_path)` +
|
||||
monkeypatch `PROJECT_MAP_DIR`. Если добавить новый `test_check_docs_*` без
|
||||
этого — упадёт на NOT_DONE от `check_project_map`. Паттерн: копируй строку
|
||||
`monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))` из
|
||||
соседнего теста.
|
||||
- **Уникальный module name в новом тесте** — `pipeline_status_project_map`
|
||||
(не `pipeline_status`). Если использовать `pipeline_status`, произойдёт
|
||||
коллизия с `test_pipeline_status.py`/`test_pipeline_status_adr.py` которые
|
||||
грузят скрипт под тем же именем — `sys.modules` уже занят, тесты
|
||||
конфликтуют. Шаблон: новый test-файл для под-проверки → уникальный module
|
||||
name.
|
||||
- **Контракт ослаблен намеренно** — ADR-028 явно фиксирует что модульные
|
||||
файлы и frontmatter `last_updated` НЕ enforced. Если будущий PR захочет
|
||||
ужесточить контракт (полный enforcement) — это будет breaking change,
|
||||
требующий создания ~12 файлов для существующего репо. См. альтернативу в
|
||||
ADR-028.
|
||||
- **scaffold-handoff.sh нумерация** — скрипт считает `ls docs/decisions |
|
||||
grep -E '^[0-9]{3}-' | wc -l + 1`. Если ADR-028 уже существует, следующий
|
||||
вызов даст 029. При создании ADR вручную + через scaffold в одном PR —
|
||||
scaffold создаст дубликат с +1 номером (нужно удалить вручную).
|
||||
|
|
@ -54,7 +54,7 @@ opencode-config/
|
|||
│ │ ├── check-adr-refs.py # ADR cross-reference validator (adr-check.yml)
|
||||
│ │ ├── check-permissions.py # Permissions validator (permissions-check.yml)
|
||||
│ │ ├── observability.py # OTel spans for tools
|
||||
│ │ ├── pipeline-status.py # 7-phase oracle (gh PR + CI polling, NEXT_ACTIONS with subagent_type+template) — PR#42
|
||||
│ │ ├── pipeline-status.py # 7-phase oracle (gh PR + CI polling, NEXT_ACTIONS with subagent_type+template) — PR#42, PR#67 (check_project_map guard)
|
||||
│ │ ├── scaffold-handoff.sh # Scaffold handoff + ADR stubs
|
||||
│ │ ├── setup-memory.sh # opencode-memory bootstrap (deterministic 6-step flow, idempotent) — PR#36
|
||||
│ │ ├── spec-status.py # 9-phase spec oracle
|
||||
|
|
@ -100,6 +100,7 @@ opencode-config/
|
|||
│ ├── test_pipeline_status_adr.py
|
||||
│ ├── test_pipeline_status_ci.py
|
||||
│ ├── test_pipeline_status_next_actions.py # NEXT_ACTIONS subagent_type+template per phase (25 tests) — PR#42
|
||||
│ ├── test_pipeline_status_project_map.py # check_project_map guard (README.md mandatory, ADR-028) — PR#67
|
||||
│ ├── test_pipeline_status_tool.py
|
||||
│ ├── test_pipeline_status_tool.ts # TS wrapper test (mjs loader)
|
||||
│ ├── test_post_docs_review_tool.py # .opencode/tools/post-docs-review.ts (via _ts_loader.mjs exec_stub_json; +repo cases) — PR#46, PR#65
|
||||
|
|
@ -138,3 +139,5 @@ opencode-config/
|
|||
## Update Protocol
|
||||
|
||||
Updated by docs-reviewer subagent on each PR. Reflects tracked files only (`git ls-files`).
|
||||
|
||||
Guarded by `pipeline-status.py:check_project_map` — README.md existence is mandatory.
|
||||
|
|
|
|||
|
|
@ -83,6 +83,17 @@ def make_handoff(
|
|||
return handoff
|
||||
|
||||
|
||||
def make_project_map(tmp_path: Path) -> Path:
|
||||
"""Create docs/project-map/README.md in tmp_path, return the dir path.
|
||||
|
||||
Satisfies the mandatory ``check_project_map`` guard (ADR-028 contract).
|
||||
"""
|
||||
pm_dir = tmp_path / "project-map"
|
||||
pm_dir.mkdir(parents=True, exist_ok=True)
|
||||
(pm_dir / "README.md").write_text("# Project Map\n")
|
||||
return pm_dir
|
||||
|
||||
|
||||
# ── parse_remote_url ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
@ -304,6 +315,7 @@ def test_check_docs_done(tmp_path, monkeypatch):
|
|||
(adr_dir / "002-pr-46-test.md").write_text("# ADR-002")
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46)
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
|
|
@ -325,6 +337,7 @@ def test_check_docs_done(tmp_path, monkeypatch):
|
|||
|
||||
def test_check_docs_not_done_no_handoff(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
result = ps.check_docs(46)
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
assert "не найден" in result.detail
|
||||
|
|
@ -334,6 +347,7 @@ def test_check_docs_not_done_no_adr(tmp_path, monkeypatch):
|
|||
"""ADR missing → DOCS phase NOT_DONE (mandatory)."""
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", tmp_path / "decisions")
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46)
|
||||
result = ps.check_docs(46)
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
|
|
@ -343,6 +357,7 @@ def test_check_docs_not_done_no_adr(tmp_path, monkeypatch):
|
|||
def test_check_docs_not_done_missing_sections(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", tmp_path / "decisions")
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46, sections=["## Что сделано", "## Почему"])
|
||||
result = ps.check_docs(46)
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
|
|
@ -356,6 +371,7 @@ def test_check_docs_done_with_adr(tmp_path, monkeypatch):
|
|||
(adr_dir / "002-pr-46-test.md").write_text("# ADR-002")
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46, extra="Архитектурное изменение, см. ADR-002.")
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
|
|
@ -382,6 +398,7 @@ def test_check_docs_not_done_no_comment(tmp_path, monkeypatch):
|
|||
(adr_dir / "002-pr-46-test.md").write_text("# ADR-002")
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46)
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
|
|
@ -400,6 +417,7 @@ def test_check_docs_not_done_comment_without_marker(tmp_path, monkeypatch):
|
|||
(adr_dir / "002-pr-46-test.md").write_text("# ADR-002")
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46)
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
|
|
@ -426,6 +444,7 @@ def test_check_docs_false_positive_reviewer_comment(tmp_path, monkeypatch):
|
|||
(adr_dir / "002-pr-46-test.md").write_text("# ADR-002")
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46)
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
|
|
@ -452,6 +471,7 @@ def test_check_docs_ambiguous_api_error(tmp_path, monkeypatch):
|
|||
(adr_dir / "002-pr-46-test.md").write_text("# ADR-002")
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46)
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
|
|
@ -470,6 +490,7 @@ def test_check_docs_done_with_fixed_verdict(tmp_path, monkeypatch):
|
|||
(adr_dir / "002-pr-46-test.md").write_text("# ADR-002")
|
||||
monkeypatch.setattr(ps, "HANDOFF_DIR", tmp_path)
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", make_project_map(tmp_path))
|
||||
make_handoff(tmp_path, 46)
|
||||
comment_body = (
|
||||
"## Docs Review Summary\\n- Handoff: fixed: added Pending\\n\\n### Verdict: FIXED"
|
||||
|
|
|
|||
55
tests/test_pipeline_status_project_map.py
Normal file
55
tests/test_pipeline_status_project_map.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Tests for .opencode/scripts/pipeline-status.py — mandatory project map.
|
||||
|
||||
Verifies the deterministic check_project_map: docs/project-map/README.md is
|
||||
required for every pipeline-driven PR (ADR-028). Loaded under a unique module
|
||||
name (``pipeline_status_project_map``) so it does not collide with other test
|
||||
modules that load the same script as ``pipeline_status``.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_PATH = (
|
||||
Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "pipeline-status.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("pipeline_status_project_map", SCRIPT_PATH)
|
||||
ps = importlib.util.module_from_spec(spec)
|
||||
sys.modules["pipeline_status_project_map"] = ps
|
||||
spec.loader.exec_module(ps)
|
||||
|
||||
|
||||
def test_project_map_exists(tmp_path, monkeypatch):
|
||||
"""PROJECT_MAP_DIR + README.md → DONE, "README.md" в detail."""
|
||||
pm_dir = tmp_path / "project-map"
|
||||
pm_dir.mkdir()
|
||||
(pm_dir / "README.md").write_text("# Project Map\n")
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", pm_dir)
|
||||
|
||||
result = ps.check_project_map()
|
||||
|
||||
assert result.status == ps.PhaseStatus.DONE
|
||||
assert "README.md" in result.detail
|
||||
|
||||
|
||||
def test_project_map_dir_missing(tmp_path, monkeypatch):
|
||||
"""PROJECT_MAP_DIR не существует → NOT_DONE, "директория" в detail."""
|
||||
missing_dir = tmp_path / "does-not-exist"
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", missing_dir)
|
||||
|
||||
result = ps.check_project_map()
|
||||
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
assert "директория" in result.detail
|
||||
|
||||
|
||||
def test_project_map_readme_missing(tmp_path, monkeypatch):
|
||||
"""Директория есть, README.md нет → NOT_DONE, "README.md не найден" в detail."""
|
||||
pm_dir = tmp_path / "project-map"
|
||||
pm_dir.mkdir()
|
||||
monkeypatch.setattr(ps, "PROJECT_MAP_DIR", pm_dir)
|
||||
|
||||
result = ps.check_project_map()
|
||||
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
assert "README.md не найден" in result.detail
|
||||
Loading…
Add table
Reference in a new issue