Quartet / Quintet

2026-09-05  ·  テスト · typescript · 設計 · 個人開発 · ci

テストが全部通るのに、本物のデータを1件も読めなかった

GitHub の Issue を読んで、対象範囲が重なっている組を報告するツールを作りました。

テストは fixtures を8本用意して、全部通っていました。 ルールは6つあって、全部が期待どおりに出ます。

本物のリポジトリに掛けたら、Issue を1件も読めていませんでした。

何が起きたか

出力はこうでした。11件すべてが同じ指摘です。

error #21  対象範囲の節はありますが、パスが1つも書かれていません。
error #19  対象範囲の節はありますが、パスが1つも書かれていません。
error #16  対象範囲の節はありますが、パスが1つも書かれていません。
...

節はある。中身が読めていない。

実際の Issue はこう書かれていました。

## Scope

```
README.md, .github/workflows/self-check.yml
```

コードブロックの中に、1行で、カンマ区切り。

私のパーサはコードブロックの中を1行=1パスとして扱い、 そのあと「空白を含むものはパスではない」と捨てていました。 README.md, .github/... は空白を含むので、全部捨てられていました。

fixtures はこう書いてありました

## 対象範囲

- `src/auth/**`
- `src/routes/login.tsx`

箇条書きです。 1行1パス。バッククォート付き。

なぜこう書いたか。私がそう書くと思ったからです。

そして、そのテンプレートを作ったのも私です。 自分のツールが、自分の作った形式を読めていませんでした。

テストが失敗しなかった理由

ここが本題です。

fixtures は、パーサを書いた同じ人間が、同じ日に、同じ思い込みのまま書いています。

パーサ      「対象範囲は箇条書きで書かれる」という前提で実装
fixtures    「対象範囲は箇条書きで書かれる」という前提で作成
テスト      両者が一致するので、必ず通る

前提が2か所に複製されているので、テストはその前提を検証できません。 検証しているのは「実装が fixtures と一致すること」だけです。

これは実装のバグではありません。テストの設計の問題です。 どれだけカバレッジを上げても見つかりません。 通る経路が増えるだけで、間違った前提の上を通るからです。

直し方は「もっとテストを書く」ではない

fixtures を10本に増やしても、私が書く限り同じ前提で書きます。

自分が書いていないデータを1本入れます。

gh api "repos/owner/name/issues?state=closed&per_page=100" \
  --jq '[.[] | select(.pull_request|not) | {number,title,body}]' \
  > test/fixtures/real-issues.json

そのまま回帰テストにしました。

// 実データの Scope はコードブロックに1行で並んでいた。
// 作りかけの parser はこれを1つも読めず、全部 scope-empty にしていた
const real = JSON.parse(readFileSync(`${F}real-issues.json`, "utf8"))
  .map((i) => ({ id: `#${i.number}`, title: i.title, body: i.body ?? "" }));

ok(!check(real, { files }).some((f) => f.rule === "scope-empty"),
   "コードブロックの Scope を読めている");

このテストは、私の思い込みの外側にあります。 データを作ったのは私ではなく、過去の実際のやりとりだからです。

パーサ側はこう直しました。

if (inFence) {
  // コードブロックの中は、1行にカンマや空白で並べて書かれることが多い。
  // 実際の Issue がこの形だったので、行のまま扱うと1つも読み取れない
  for (const tok of line.split(/[,\s]+/)) if (tok) out.push(tok);
  continue;
}

同じ日に、同じ形をもう1つ踏みました

README にこう書きました。

Requires Node 22.6 or newer

CI が落ちました。

SyntaxError: Cannot use import statement outside a module

"type": "module" が無いと、Node は .ts を CommonJS として読みます。 手元の Node が 25 だったので、動いていました。

これも同じ構造です。

README      「22.6 で動く」と主張
CI          25 で検証
検証されているもの   25 で動くこと
検証されていないもの  22.6 で動くこと(= 主張そのもの)

主張と、検証している場所がずれていました。

直し方は、主張している境界で試すことです。

strategy:
  matrix:
    node: ["22.18", "24"]

(22.6 では型注釈の除去に旗が要ることが分かったので、下限は 22.18 に直しました。 「動く版はどこからか」を調べ直したのは、CI が落ちたからです。 落ちなければ、嘘のまま出していました。)

一般化

自分で用意したデータは、自分の思い込みまで一緒に再現します。

危ないのは、こういう場所です。

規則は1つで足ります。

入力を解釈するものは、自分が書いていない入力で1本テストする。

本番のデータを1件持ってきて、固定して、回帰テストにする。 それだけで、思い込みの外側に足場が1つできます。

そして主張については、もう1つ。

主張した境界で検証する。 「N 以降で動く」なら、N で試す。

まとめ


あわせて読む


Claude Code に設計・実装・レビューを別々の人格として分担させ、GitHub Issue と ブランチを軸に並列開発を回すための設定一式を MIT で公開しています。 コピーして ./setup.sh を叩けば動きます。技術スタックには依存しません。

https://github.com/quintetkit/quartet

このワークフローだけで実際にツールを 1 つ作りました。Issue の分割から PR、 レビュー、マージまで記録が全部残っています。うまくいかなかった箇所も消していません。

https://github.com/quintetkit/mdlinkcheck

UI 設計人格・レビュー基準・Issue 単位の並列実行スクリプト・実践ガイド 10 章を 足したものは製品ページにあります。

実践ガイド全10章は Zenn Book で読めます(¥1,500・2章まで無料)。

https://zenn.dev/quintetkit/books/claude-code-parallel-workflow

設定一式(5人格・スクリプト・ガイド全文)の配布はこちらです。

https://quartet-dev.booth.pm/items/8807156

この運用そのものを配っています

4 人格版 Quartet は MIT で無料公開しています。UI 設計人格・レビュー基準・ Issue 単位の並列実行スクリプト・実践ガイド 10 章を足した Quintet は有料版です。

無料版を見る 製品ページ