Repository navigation
Description
何が起きたか
全体テストが重すぎて、テストを使う工程が時間の枠に収まらない。2026-10-31 の開発ループでは、スプリントの PR(#2283 )のリファクタリングの工程で、変更前に流す全体テストに約 9 分かかった。その結果、リファクタリングを一度も検討できずに工程が終わった(#2287 )。
今の重さ:
| 測ったもの | 値 | 出所 |
|---|---|---|
| CI の pytest のテストの step の合計 | 1,113 秒(約 18.5 分) | .ndf/project.json の test_duration.measured(.github/workflows/pytest.yml の run 40160663747) |
| CI の pytest のワークフローの経過時間 | 616 秒 | .ndf/project.json の ci.workflows |
同上(この要求を書いたときの .ndf/project.json) |
テストの step 4 個の合計 1,548 秒(run 41193796288)・経過時間 922 秒(4 ジョブ) | .ndf/project.json(2026-10-31 11:01 UTC の解析) |
| 手元の全体テスト(pytest と factory の vitest) | 約 9 分。1 回目は 180 秒の上限で打ち切られた | 上の PR のリファクタリングの工程の記録 |
手元の pytest だけ(-n 4、高負荷の下) |
580 秒で打ち切った時点で約 40%。1 件の失敗が出た(どのテストかは打ち切りで出力されず未確認) | この要求を書くときに測った(2026-10-31 22:19〜22:29 JST、load average 17.8〜20.9、CPU 8 個・メモリ 30 GB) |
| 各テストの所要の合計 | 10,483 件・2,003 秒。plugins/ndf/scripts/tests が 1,373 秒(69%)、遅い方から 1% のテストで全体の 26% |
2026-10-30 の手元の全体テスト(#2204 の本文) |
| テストのファイルの数 | 229 本 | plugins/ndf/scripts/tests・scripts/tests・tests |
打ち切りの後に子のプロセスが残ることも、この要求を書くときに再現した。上の pytest を timeout 580 で止めた後、中継のテストの plugins/ndf/scripts/relay.py run と plugins/ndf/scripts/tests/fixtures/relay_fake_claude.py が動き続けていた(手で止めた)。NDF のテストの実行の入口(plugins/ndf/scripts/lib/test_triage.py の run_command)は打ち切るときにプロセスグループごと止めるが、テストの中で自分の session を作って起動した子はそのグループの外に出る。
依頼(原文)
手元の全体テストを 3 分以内にする
(書き手: 利用者(起票者がこの課題の本文に引用)、日付: 2026-10-31)
あまり意味のないテストは徹底的に削る
(書き手: 利用者(2026-10-31 の NDF の方針 5 点の 5 番目。#2204 が受け皿)、日付: 2026-10-31)
PR の pytest(
Python testsワークフロー)の所要時間を 3 分以内にする
(書き手: 利用者(起票者が #2154 の本文に「利用者の指定」として引用)、日付: 2026-10-28)
上の 3 つ以外の題・本文(「何が起きたか」の表、元の受け入れ条件 1〜4、「この課題の中で扱うもの」)は起票者(課題を書いた AI、2026-10-31)の文で、利用者の指示の原文としては扱わない。起票者が足した条件は次の「起票者の推定」に並べる。
起票者の推定
承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。
| # | 条件 | 推した出所 | この要求での扱い |
|---|---|---|---|
| S1 | 「全体テスト」は .ndf/project.json の test.suites の全部(pytest・factory・script-structure・lint)を、工程が使う並列の数で流したもの |
元の受け入れ条件 1(起票者) | 受け入れ条件 1・前提 1〜3 |
| S2 | 3 回測った値を PR に残す | 元の受け入れ条件 1(起票者) | 受け入れ条件 1 |
| S3 | 削ったテスト・まとめたテストは、守っていた振る舞いを別のテストが守っていることを PR に示す | 元の受け入れ条件 2(起票者)。方針 5 は「削る」だけを言う | 受け入れ条件 4 |
| S4 | 遅いテストの上位を測った表と、何をして何秒減ったかを PR に残す | 元の受け入れ条件 3(起票者) | 受け入れ条件 3 |
| S5 | 時間切れで打ち切ったとき子のプロセスが残らないことを、この課題で直す | 元の受け入れ条件 4・#2285 (起票者) | 受け入れ条件 6・7 |
| S6 | 削る基準を決めて消す(#2204)・PR の pytest を 3 分以内にする(#2154)・子のプロセスが残る(#2285)をこの課題の中で扱う | 本文の「この課題の中で扱うもの」(起票者) | 対象範囲。#2154 は受け入れ条件 8・9 |
| S7 | 削る基準は「重複・消えた機能の残骸・.md の文言の固定・実装の細部の固定・総当たり」の 5 つで、AGENTS.md か docs/ に置く |
#2204 の「削る基準(設計で確定する)」(起票者) | 受け入れ条件 4。基準の中身は設計で確定する |
| S8 | 並列の数を上げて 3 分に入れることはしない。3 分は今の並列の数(pytest は -n 4)で測る |
この要求を書いた AI の判断。同じマシンで最大 6 本の作業が並ぶ運用で、全体テストを 6 本同時に流したとき残りのメモリが 7.4 GB だった(2026-09-25 の実測) | 前提 2 |
| S9 | 既定の実行で飛ばすテスト(skip・deselect・marker で外すもの)を増やして 3 分に入れることはしない | この要求を書いた AI の判断。飛ばしたテストは全体テストから消えるのに、受け入れ条件 4 の対応を残さずに済んでしまう | 受け入れ条件 5 |
| S10 | 測るのは開始時の 1 分の load average が CPU の数以下のとき | この要求を書いた AI の判断。高負荷の下では同じ pytest が 580 秒で 40% までしか進まなかった | 前提 1 |
目的
- テストを使う工程(リファクタリングの着手前のテスト・最終ゲート・完了判定)が、時間の枠のほとんどを全体テストに使わずに本来の作業へ時間を回せるようにする
- PR ごとの CI の待ちを縮め、1 回の配布までの待ちを減らす
- テストの数を、守る振る舞いに見合う数まで減らし、保守の費用を下げる
前提
- 前提 1: 手元で測るマシンは、この開発コンテナ(CPU 8 個・メモリ 30 GB)である。開始時の 1 分の load average が 8 以下のときに測り、その値を記録に添える。8 を超える負荷の下で測った値は参考として別に残し、受け入れ条件 1 の判定に使わない
- 前提 2: 並列の数は今の宣言の値のまま(pytest の
-n 4)にする。並列の数を変えるのは受け入れ条件の手段に含めない - 前提 3: 「3 分」は壁時計の 180 秒で、
.ndf/project.jsonのtest.suitesのcommandを宣言の順に 1 本ずつ流し、最初の起動から最後の終了までを測る。uvの仮想環境は作成済みの状態から測る(作成の時間は数えない)。factory の suite のコマンドに含まれるnpm ciの時間は数える - 前提 4: リファクタリングの工程で着手前のテストが時間切れになると、作業場所が「使用中」のまま残り、テストのプロセスも残る #2285 の 1 項目目(打ち切りで止まったときに作業場所の持ち主の記録へ終わった時刻を書く)は、この課題では扱わず リファクタリングの工程で着手前のテストが時間切れになると、作業場所が「使用中」のまま残り、テストのプロセスも残る #2285 に残す。この課題が扱うのは リファクタリングの工程で着手前のテストが時間切れになると、作業場所が「使用中」のまま残り、テストのプロセスも残る #2285 の 2 項目目(子のプロセス)だけである
- 前提 5: リファクタリングの工程が、変更前のテストに時間の枠を使い切り、照合と起票の依頼の受け口を一度も検討できなかった #2287 (閉じた課題)の「工程の時間の枠の決め方」は、この課題では変えない。全体テストが縮むことで枠に収まるようにする
対象範囲
含む:
- 意味の薄いテストを削る基準を文書にし、基準に当たるテストを消す・まとめる(意味の薄いテストを徹底的に削る: 削る基準を決めて要らないテストを消す(全体テストを 3 分以内にする #2291 の削る側) #2204)
- 遅いテストを速くする(待ちの短縮・重い準備の共有など。手段は設計で決める)
- PR の pytest の CI を 3 分以内にし、同じ PR への新しい push で古い実行を止める(PR の pytest が 1 回 10〜13 分かかり、push のたびに古い実行も止まらず走り直す #2154)
- 全体テストを上限で打ち切ったとき、テストが起こした子孫のプロセスを残さない(リファクタリングの工程で着手前のテストが時間切れになると、作業場所が「使用中」のまま残り、テストのプロセスも残る #2285 の 2 項目目)
含まない:
- 並列の数を上げて 3 分に入れること(前提 2)
- テストを既定の実行から外して 3 分に入れること(受け入れ条件 5)
- 作業場所の持ち主の記録に終わった時刻を書くこと(リファクタリングの工程で着手前のテストが時間切れになると、作業場所が「使用中」のまま残り、テストのプロセスも残る #2285 の 1 項目目。前提 4)
- リファクタリングの工程の時間の枠の決め方の変更(前提 5)
- テストの対象のコード(プラグインの振る舞い)の変更。テストを速くするためにテストの対象を変える必要が出たら、その変更は止めて設計で扱いを決める
ドメインイベント
| # | イベント | 引き金 | 失敗したとき | 順序の前提 |
|---|---|---|---|---|
| E1 | 着手前の遅いテストの表と件数・合計時間を測った | 実装の着手 | 測れない(高負荷・失敗)なら、前提 1 の条件が揃うまで測り直す。揃わなければ高負荷の値を参考として残し、その旨を書く | — |
| E2 | 削る基準を文書にした | E1 の表 | 基準を決められないテストは消さずに残し、PR に理由を書く | E1 |
| E3 | 基準に当たるテストを消した・まとめた | E2 | 守っていた振る舞いを守るテストを示せなければ消さない | E2 |
| E4 | 遅いテストを速くした | E1 の表の上位 | 速くすると振る舞いの確かめが弱まるなら、速くせずに残し、PR に理由を書く | E1 |
| E5 | 全体テストを上限で打ち切った | test-run.py・cross-refactoring の着手前のテストの上限 | 子孫のプロセスが残る(受け入れ条件 6 が落とす) | — |
| E6 | 打ち切った全体テストのプロセスがすべて止まった | E5 | 止まらないプロセスがあれば、そのプロセスを起こしたテストを直す | E5 |
| E7 | CI の pytest が新しい push で走り直し、同じ PR の古い実行が止まった | PR への push | 古い実行が止まらない・必須のチェック pytest が取り消しで落ちたままになる(受け入れ条件 9 が落とす) |
— |
| E8 | 着手後の全体テストを 3 回測った | E3・E4 の後 | 3 分に入らなければ、届いた値と残りの差を PR に書き、未決の 1 に従う | E3・E4 |
用語
| 用語 | 意味 |
|---|---|
| 全体テスト | 用語集のとおり。この課題では .ndf/project.json の test.suites の全部(前提 3) |
| 着手前のテスト | 用語集のとおり(cross-refactoring の init が改修の前に 1 回走らせるテスト) |
| 全体テストの実測 | 用語集のとおり(同じリポジトリで先に走った全体テストの壁時計の秒) |
| 範囲テスト | 用語集のとおり |
今の状態(2026-11-07)
- 手元の全体テストは 962.9 秒から 635.8 秒になった。目標 180 秒までの残りは 全体テストを 636 秒から 180 秒へ縮める: 子のプロセスを起こす結合テストとスクリプトの起動の時間を減らす #2309 が持つ
- 同じ PR の古い実行の取り消しはコミット 768126b で入った。PR の
pytestの所要の目標は受け入れ条件 8・9 が持つ
受け入れ条件
- 1. 前提 1〜3 の条件で手元の全体テストを 3 回流し、3 回とも全 suite が通り、壁時計が 180 秒以内に収まる。3 回の秒・suite ごとの秒・開始時の load average を PR に残す
- 2. 着手前と着手後の両方で、全体テストの件数・各テストの所要の合計・壁時計を、同じマシン・同じ並列の数で測った値が PR に並ぶ
- 3. 着手前の遅いテストの上位 30 件(1 本あたりの秒の長い順。
pytest --durations=30の出力)と、ファイルごとの合計の上位 10 本の表が PR にあり、各行に「何をしたか(消した・まとめた・速くした・そのまま)」と着手後の秒が書かれている - 4. テストを削る基準が
AGENTS.mdかdocs/の文書にあり、消したテスト・まとめたテストの 1 件ごとに、当たった基準と、守っていた振る舞いを守るテストの名前(守らなくてよいならその理由)が PR から追える - 5. 着手前に既定の実行で走っていたテストのうち、着手後に既定の実行で skip・deselect されるものが 0 件である(消したテストは受け入れ条件 4 の対応に載り、この数に入れない)
- 6. 全体テストを NDF の上限で打ち切ったとき(
plugins/ndf/scripts/test-run.py whole --limit <秒>、cross-refactoring の着手前のテスト)、打ち切りから 10 秒後に、その worktree のパスをコマンドラインに含むプロセスが 0 件である(pgrep -f <worktree のパス>で確かめる)。中継のテストのrelay_fake_claude.pyのように、テストの中で自分の session を作って起動する子も数える - 7. pytest の実行がシグナル(SIGTERM)で止められたとき、テストが起こした子孫のプロセスが 10 秒以内に止まる(受け入れ条件 6 と同じ確かめ方。NDF を通さない打ち切りでも残さない)
- 8. PR の
Python testsワークフロー(.github/workflows/pytest.yml)の、ワークフローの開始から必須のチェックpytestの結果が付くまでの時間が、実装の PR の 3 回の実行で 3 回とも 180 秒以内である。3 回の run の番号と秒を PR に残す - 9. 同じ PR に、
Python testsの実行中に新しい push をすると、古い実行が取り消され、新しい実行の結果だけが必須のチェックpytestに付く(取り消された実行でpytestが失敗のまま残らない) - 10. 着手後も、必須のチェック(
.ndf/project.jsonのci.required_checks)がすべて通る
非機能の条件
| 大項目 | 条件 |
|---|---|
| 性能・拡張性 | 手元の全体テストが 180 秒以内(受け入れ条件 1)。PR の pytest の CI が 180 秒以内(受け入れ条件 8) |
| 運用・保守性 | 打ち切ったテストのプロセスが残らない(受け入れ条件 6・7)。削る基準が文書にあり、後からテストを書く側が参照できる(受け入れ条件 4) |
| システム環境 | 並列の数は -n 4 のまま(前提 2)。メモリ 30 GB のマシンで、ほかの作業と並んでも全体テストがメモリを使い切らない |
影響
| 対象 | 影響 |
|---|---|
| 公開インタフェース | 変わらない。test-run.py などの結果 JSON・終了コードの契約は変えない |
| データ | 無し |
| 既存の振る舞い | テストの件数と所要が変わる。.github/workflows/pytest.yml の実行の取り消しと分割が変わる。テストの道具(fixture・偽の CLI)の子のプロセスの止め方が変わる |
検証手段
| 項目 | 手段 |
|---|---|
| テスト | .ndf/project.json の test.suites の command を宣言の順に流し、全体を time で測る(受け入れ条件 1・2)。遅いテストは uv run --frozen --project . --all-extras pytest . -q -n 4 --durations=30(受け入れ条件 3) |
| 静的解析・型検査 | bash scripts/check-lint.sh・python3 scripts/check-script-structure.py |
| 打ち切りの確かめ | python3 plugins/ndf/scripts/test-run.py whole --limit 30 で打ち切り、10 秒後に pgrep -f <worktree のパス> が 0 件(受け入れ条件 6)。pytest を kill -TERM で止めて同じ確かめ(受け入れ条件 7) |
| CI | 実装の PR の Python tests の run を gh run list -w "Python tests"・gh run view <番号> --json startedAt,updatedAt,jobs で読む(受け入れ条件 8)。実行中に空のコミットを push し、古い run の conclusion が cancelled で、必須のチェック pytest が新しい run の結果になることを確かめる(受け入れ条件 9) |
前提とする取り決め
| 項目 | 参照先 / 決めたこと |
|---|---|
| プロジェクト構造 | テストは今の置き場(plugins/ndf/scripts/tests・plugins/*/skills/*/tests・scripts/tests・tests・factory)のまま。削る基準の文書は AGENTS.md か docs/(#2204) |
| コーディング規約 | AGENTS.md の「最小限のコード実装」。.md の文言を照合するテストを書かない(AGENTS.md の DON'T)。ruff(pyproject.toml) |
| テスト戦略 | テストを消すときは、守っていた振る舞いを残るテストが守るかを 1 件ずつ示す(受け入れ条件 4)。速くするときは、確かめる振る舞いを弱めない |
境界
| 区分 | 内容 |
|---|---|
| 常に行う | 着手前と着手後の実測(受け入れ条件 2・3)。消したテストの対応の記録(受け入れ条件 4)。既存のテストと静的解析の実行 |
| 確認してから行う | .ndf/project.json の変更(C7。承認ゲート 2 で人が見る)。必須のチェックの名前・集約ジョブに触れる CI の変更(承認ゲート 2) |
| 行わない | 並列の数を上げる・テストを既定の実行から外すことで 3 分に入れる。テストの対象のコードの変更。#2285 の 1 項目目と #2287 の時間の枠の変更。依頼の範囲外のリファクタリング |
未決
| 項目 | 誰が決めるか | 期限 |
|---|---|---|
| 1. 削る・速くするを尽くしても 180 秒に届かないときに、届いた値で受け入れるか、残りを別の課題へ回すか(プロジェクト MVV の Value 10 は、上限を超える品質の向上を別の課題へ回すとする。ただしこの 3 分は利用者が指定した目標である) | 利用者 | 承認ゲート 1(設計の承認) |
| 2. #2154 の CI の 3 分(受け入れ条件 8・9)をこの課題の受け入れ条件に入れてよいか(起票者の推定 S6) | 利用者 | 承認ゲート 1 |
| 3. #2285 の 1 項目目を #2285 に残す扱い(前提 4)でよいか | 利用者 | 承認ゲート 1 |
未決の答え(2026-11-01 JST、開発ループ l20261031 の周回 5)
- 未決 1: 届いた値で受け入れ、残りを別の課題へ回す。 手段を尽くした着手後の全体テストは 635.8 秒(着手前 962.9 秒)。180 秒までの残りは 全体テストを 636 秒から 180 秒へ縮める: 子のプロセスを起こす結合テストとスクリプトの起動の時間を減らす #2309 で、テストの対象のコードを速くする側から扱う。根拠: 利用者が承認ゲート 1 で設計を承認し、未決 1 の期限はその承認だった。項目が挙げる Value 10(上限を超える品質の向上は別の課題へ回す)に従う。対象のコードを速くする案は要求の「含まない」に当たるため、この課題では採らない(設計の決定 3)
- この課題の PR の本文に、届いた値と 180 秒までの差を書く
関連
- リファクタリングの工程が、変更前のテストに時間の枠を使い切った リファクタリングの工程が、変更前のテストに時間の枠を使い切り、照合と起票の依頼の受け口を一度も検討できなかった #2287
- 時間切れで作業場所が「使用中」のまま残り、テストのプロセスも残る リファクタリングの工程で着手前のテストが時間切れになると、作業場所が「使用中」のまま残り、テストのプロセスも残る #2285
この課題の中で扱うもの
- 意味の薄いテストを削る基準を決めて消す 意味の薄いテストを徹底的に削る: 削る基準を決めて要らないテストを消す(全体テストを 3 分以内にする #2291 の削る側) #2204
- PR の pytest が 1 回 10〜13 分かかる PR の pytest が 1 回 10〜13 分かかり、push のたびに古い実行も止まらず走り直す #2154
- 時間切れのときに子のプロセスが残る リファクタリングの工程で着手前のテストが時間切れになると、作業場所が「使用中」のまま残り、テストのプロセスも残る #2285
🤖 Generated with Claude Code
進行
モード: — / 進め方: auto
- 要求と受け入れ条件 — 2026-10-31 13:19
- 作業場所の用意 — 2026-10-31 14:19
- 設計 — 2026-10-31 13:33
- 素材の収集と出典の確定
- ドキュメント再構成
- ドキュメントレビュー
- 計画 — 2026-10-31 14:19
- 実装 — 2026-10-31 14:19
- 構造改善 — 2026-10-31 17:29
- 実装レビュー — 2026-10-31 17:33
- 完了判定 — 2026-10-31 17:37
- Pull Request — 2026-10-31 17:14
- 確定仕様化
- 後片付け
- 配布 — 2026-10-31 17:44
- 体裁レビュー
- リリース後テスト
- 振り返り
- 棚卸し
人の判断待ち(開発ループ l20261031)
- 判定: unknown(承認ゲート 1)
- 理由: 設計の中身は MVV に沿っている。全体テストを 3 分以内にする目的は、利用者の手を止めず時間を減らす Mission と Vision に合う。決定 1・6 は実測の数値を根拠にしており Value 3、予算の中で品質を扱う点は Value 10 に沿う。後始末に既存の lib/procs.py の child_pids・stop_tree を使い、同じ役割の関数を作らない点は Value 6、計測のスクリプトを実験版に置き台帳に残す点は Value 9 に沿う。/ただし、設計の『未確認のまま残ること』に『未決 2・3(PR の pytest が 1 回 10〜13 分かかり、push のたびに古い実行も止まらず走り直す #2154 を取り込むか、リファクタリングの工程で着手前のテストが時間切れになると、作業場所が「使用中」のまま残り、テストのプロセスも残る #2285 の 1 項目目を残す扱い)は承認ゲート 1 で利用者が決める』とある。設計が利用者の判断をこの関門に明示して預けている。MVV 判定で関門を省くと、利用者が決めるはずの範囲の選択を AI が代わりに決めることになる。共通原則の『方向を決めるのは人』と『決められないときは人へ戻す』に照らし、自動では通せない。/テストの requirements-check が終了コード 2 で止まっている。この PR で追加した issues/issue-2291-requirements.md を読めておらず、要求と受け入れ条件の照合が済んでいない。承認の材料の正しさがそろっていない状態で(Value 2・Value 10 の但し書き)、材料から follow とは決められない。/変更は文書 2 ファイルの追加だけである。実装の予定(conftest・後始末の部品・.github/workflows/pytest.yml・docs・テスト)にも、秘密・認証・利用者のデータ・戻せない操作・他リポジトリへの公開・.ndf/ や CLAUDE.md の運用の節・配布の取得元と版の規則(C1〜C8・P1)に当たるものは無い。
- 根拠の項目: Mission, Vision, Value 2, Value 3, Value 6, Value 9, Value 10
- 設計 Pull Request: 全体テスト: 手元と PR の CI の両方で 3 分以内に終わり、打ち切ってもプロセスを残さない(#2291) #2304
- 答え: approved
- 答え方:
python3 /work/ai-plugins/plugins/ndf/scripts/sprint-state.py loop answer l20261031 2291 --approved|--rejected
Activity
Docs: 後始末の部品のゾンビの回収と自分を止めない呼び方、push の concurrency の組を設計に書き足す(#2291)
Docs: 子の起動から pid の記録までの間は SIGCHLD の回収を見送る契約を設計に書き足す(#2291)
Merge pull request #2304 from devbasex/design/issue-2291
Merge pull request #2311 from devbasex/feat/issue-2291-l20261031-5