Read OSS

25個のインテグレーションをテストする:テストスイートとコントリビューターガイド

中級

前提知識

  • 本シリーズの第1〜5回
  • pytestの基本的な知識
  • GitHub Actions CI/CDの経験

25個のインテグレーションをテストする:テストスイートとコントリビューターガイド

25以上のAIエージェント向けにファイルを生成するシステムは、組み合わせ爆発的なテスト問題を抱えています。インテグレーションごとに出力フォーマット、ファイル拡張子、ディレクトリ構造、frontmatterはすべて異なります。プレースホルダーの置換が一箇所でも壊れると、AIエージェントを混乱させるコマンドがひっそりと生成されてしまいます。Spec Kitのテストスイートはこの問題に対してパターンベースのアプローチで臨んでいます。各エージェントの出力フォーマットに合わせながらも、すべてのインテグレーションテストで同じ不変条件を検証する構造です。この記事ではテストアーキテクチャ全体を解説し、最後に最も一般的なコントリビューション、新しいAIエージェントの追加についての実践的なガイドで締めくくります。

テストアーキテクチャの全体像

テストスイートはソースコードの構造を鏡のように反映しています。

tests/
├── conftest.py                          # ANSIエスケープ除去ヘルパー
├── integrations/
│   ├── conftest.py                      # StubIntegrationテストヘルパー
│   ├── test_base.py                     # 基底クラスのユニットテスト
│   ├── test_integration_claude.py       # Claude固有のテスト
│   ├── test_integration_copilot.py      # Copilot固有のテスト
│   ├── test_integration_windsurf.py     # ...エージェントごとに1ファイル(計27ファイル)
│   ├── test_manifest.py                 # IntegrationManifestのテスト
│   └── test_registry.py                 # INTEGRATION_REGISTRYのテスト
├── test_extensions.py                   # Extensionシステムのテスト
├── test_presets.py                      # Presetシステムのテスト
├── test_agent_config_consistency.py     # インテグレーション横断の一貫性テスト
├── test_merge.py                        # JSONマージロジックのテスト
└── test_branch_numbering.py             # ブランチ命名のテスト

テストファイルは全部で51本あります。最も多いのは tests/integrations/ 配下のエージェントごとのテストで、サポート対象エージェントに1ファイルずつ、さらに基底クラス・manifest・registry向けのテストが加わります。

共有の conftest.py が提供するのはシンプルなユーティリティ1つだけです。

_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")

def strip_ansi(text: str) -> str:
    """Remove ANSI escape codes from Rich-formatted CLI output."""
    return _ANSI_ESCAPE_RE.sub("", text)

CLIはRichを使ってスタイル付きの出力を行うため、これが欠かせません。CLIのstdoutをキャプチャするテストでは、内容をアサートする前に必ずANSIエスケープコードを除去する必要があります。

tests/integrations/conftest.py には StubIntegration が用意されています。実際のエージェントに依存せず基底クラスの挙動をテストするための、最小限の MarkdownIntegration サブクラスです。

class StubIntegration(MarkdownIntegration):
    key = "stub"
    config = {
        "name": "Stub Agent",
        "folder": ".stub/",
        "commands_subdir": "commands",
        "install_url": None,
        "requires_cli": False,
    }

インテグレーションテストのパターン

インテグレーションのテストファイルはすべて同じ構造に従っています。tests/integrations/test_integration_claude.py を例に見ていきましょう。

レジストリへの登録確認:

def test_registered(self):
    assert "claude" in INTEGRATION_REGISTRY
    assert get_integration("claude") is not None

config の検証:

def test_config_uses_skills(self):
    integration = get_integration("claude")
    assert integration.config["folder"] == ".claude/"
    assert integration.config["commands_subdir"] == "skills"

プレースホルダーの置換テスト(最重要):

def test_setup_creates_skill_files(self, tmp_path):
    integration = get_integration("claude")
    manifest = IntegrationManifest("claude", tmp_path)
    created = integration.setup(tmp_path, manifest, script_type="sh")

    content = plan_skill.read_text(encoding="utf-8")
    assert "{SCRIPT}" not in content
    assert "{ARGS}" not in content
    assert "__AGENT__" not in content

このわずか3行のアサーションが、テストスイート全体で最も重要なパターンです。process_template() がすべてのプレースホルダーを置換済みであることを確認します。コマンドファイルに {SCRIPT} が未置換のまま残っていると、AIエージェントはシェルコマンドの代わりにリテラルのプレースホルダーテキストを受け取ることになり、ワークフローがひっそりと壊れてしまいます。

tests/integrations/test_integration_copilot.py では、フォーマット固有のアサーションが加わります。

def test_setup_creates_agent_md_files(self, tmp_path):
    # .agent.md 拡張子を検証
    for f in agent_files:
        assert f.name.endswith(".agent.md")

def test_setup_creates_companion_prompts(self, tmp_path):
    # 対応する .prompt.md ファイルを検証
    for f in prompt_files:
        content = f.read_text(encoding="utf-8")
        assert content.startswith("---\nagent: speckit.")

def test_agent_and_prompt_counts_match(self, tmp_path):
    # 重要: .agent.md ごとに対応する .prompt.md が必要
    assert len(agents) == len(prompts)

ヒント: 新しいインテグレーションのテストを書くときは、最もシンプルな test_integration_windsurf.py をコピーして出発点にするのがおすすめです。そこからエージェント固有のフォーマット要件に合わせてアサーションを調整していきましょう。プレースホルダーのチェック({SCRIPT}{ARGS}__AGENT__)はすべてのインテグレーションテストに含めてください。

すべてのインテグレーションテストは、独立したファイルシステム操作のためにpytestの tmp_path フィクスチャを使います。これによりテスト同士が干渉せず、開発者のマシンに余計なファイルが残ることもありません。

ExtensionとPresetのテスト

tests/test_extensions.py では、extensionのライフサイクル全体をカバーしています。

flowchart TD
    A["Manifest Validation"] --> B["Registry Operations"]
    B --> C["Manager Install/Remove"]
    C --> D["Command Registration"]
    D --> E["Catalog Discovery"]
    E --> F["Hook Execution"]

manifestの検証テストは正常系と異常系の両方を確認します。有効なmanifestが正しくパースされること、無効なものは特定のメッセージを持つ ValidationError を発生させることを検証します。フィクスチャには tmp_path の代わりに tempfile.mkdtemp()shutil.rmtree() を使っていますが、どちらのアプローチも問題ありません。

tests/test_presets.py はextensionテストと同じ構造で、manifest検証・レジストリ操作・テンプレート解決をカバーしています。リゾルバーの優先度スタック(local > presets > extensions > core)については、同じテンプレート名を提供する複数のソースを用意して、正しいものが勝つことを検証することでテストしています。

CIパイプラインとリリースプロセス

CIは main へのプッシュとすべてのプルリクエストで実行されます。test.yml ワークフローには2つのジョブがあります。

Ruffによるlint — Python 3.13のみで uvx ruff check src/ を実行します。lintは複数バージョンで実行する必要がないためです。

pytest — Python 3.11、3.12、3.13の3バージョンでフルテストスイートを実行します。

strategy:
  matrix:
    python-version: ["3.11", "3.12", "3.13"]
steps:
  - name: Install dependencies
    run: uv sync --extra test
  - name: Run tests
    run: uv run pytest
flowchart LR
    A["Push/PR"] --> B["ruff check<br/>(3.13 only)"]
    A --> C["pytest<br/>(3.11)"]
    A --> D["pytest<br/>(3.12)"]
    A --> E["pytest<br/>(3.13)"]
    B --> F{"All green?"}
    C --> F
    D --> F
    E --> F
    F -->|Yes| G["✓ Merge allowed"]

リリースプロセスはタグによってトリガーされます(release.yml)。v* タグがプッシュされると、ワークフローはバージョンを抽出し、直前のタグ以降のコミットログからリリースノートを生成して、GitHub Releaseを作成します。wheelのビルドにはHatchを使用しており、第1回で紹介した force-include の設定によってすべてのアセットをバンドルします。

ツールの選択について注目すべき点があります。依存関係の管理とスクリプト実行には uv のみを使用しています(uv syncuv run)。CIのどこにも pip installrequirements.txt も存在せず、uvが pyproject.toml からの解決とインストールをすべて担います。

コントリビューション:新しいインテグレーションを追加する

最も多いコントリビューションは、新しいAIコーディングアシスタントのサポートを追加することです。AGENTS.md が公式ガイドですが、ここではそのエッセンスをまとめます。

flowchart TD
    A["1. 基底クラスを選ぶ"] --> B{"エージェントのフォーマット"}
    B -->|"標準 .md"| C["MarkdownIntegration"]
    B -->|".toml"| D["TomlIntegration"]
    B -->|"skill dirs"| E["SkillsIntegration"]
    B -->|"完全カスタム"| F["IntegrationBase"]
    C --> G["2. サブパッケージを作成"]
    D --> G
    E --> G
    F --> G
    G --> H["3. _register_builtins() に登録"]
    H --> I["4. scripts/ ディレクトリを追加"]
    I --> J["5. テストを書く"]
    J --> K["6. テストスイートを実行"]

Step 1: 基底クラスを選ぶ。 ほとんどのエージェントは MarkdownIntegration を使います。TOMLが必要なエージェント(Geminiなど)には TomlIntegration、skillディレクトリ構造を持つものには SkillsIntegration を選びます。コンパニオンファイルや設定のマージが必要な場合(Copilotのように)にのみ、IntegrationBase を直接使いましょう。

Step 2: サブパッケージを作成する。 src/specify_cli/integrations/myagent/__init__.py を作成します。

"""MyAgent integration."""
from ..base import MarkdownIntegration

class MyAgentIntegration(MarkdownIntegration):
    key = "myagent"
    config = {
        "name": "My Agent",
        "folder": ".myagent/",
        "commands_subdir": "commands",
        "install_url": "https://myagent.dev/install",
        "requires_cli": True,  # IDEのみのエージェントの場合はFalse
    }
    registrar_config = {
        "dir": ".myagent/commands",
        "format": "markdown",
        "args": "$ARGUMENTS",
        "extension": ".md",
    }
    context_file = ".myagent/rules.md"

key はツール検出が正しく機能するよう、実際のCLIバイナリ名と一致させてください。folder は末尾に / が必要です。registrar_config["dir"]CommandRegistrar がextensionのコマンドを書き込むパスです。

Step 3: 登録する。 integrations/__init__.py にアルファベット順で追加します。

from .myagent import MyAgentIntegration
# ...
_register(MyAgentIntegration())

Step 4: スクリプトを追加する。 src/specify_cli/integrations/myagent/scripts/ を作成し、update-context.shupdate-context.ps1 を配置します。これらはエージェントのコンテキストファイルを更新するシンプルなラッパースクリプトです。

Step 5: テストを書く。 tests/integrations/test_integration_myagent.py を作成し、最低限として「登録確認」「configの検証」「setupによるファイル生成」「プレースホルダー置換テスト」の4つを含めます。

Step 6: テストスイートを実行する。 uv run pytest がパスすることを確認し、続いて uvx ruff check src/ でlintもチェックしましょう。

ヒント: CONTRIBUTING.md によると、大きな変更はメンテナーへの事前相談が必要です。新しいインテグレーションの追加はよく理解されたコントリビューションのパスであり、一般的に歓迎されます。ただし、まずイシュートラッカーで agent_request ラベルの既存イシューを確認しておきましょう。すでに誰かが取り組んでいるかもしれません。

シリーズのまとめ

この6回の連載を通じて、Spec Kitの全体像を追いかけてきました。アーキテクチャの基盤から specify init パイプライン、4層のインテグレーション階層、コマンドテンプレートのワークフローエンジンを見てきました。さらにextensionとpresetのプラグインシステム、そして今回のテストスイートとコントリビューションワークフローまで解説しました。

ここで得られる本質的な洞察は、Spec Kitが同時に2つのものであるということです。一方では、ファイルを生成するPython CLIであり、もう一方では、markdownテンプレートがプログラムでAIエージェントがランタイムとなる宣言的な命令セットです。CLIはコンパイラ、テンプレートはコード、LLMはCPUというわけです。

このコードベースは丁寧に読む価値があります。モノリシックな __init__.py は密度が高いですが、理解する道筋はあります。インテグレーション階層はTemplate Methodパターンの教科書的な応用例です。Hatchの force-include を使ったエアギャップバンドリングは、ランタイムアセットを同梱するCLIツールなら参考にする価値がある手法です。そして、AIがYAML configを読んでランタイムでhookを実行するhookシステムは、AI支援開発の時代に特にフィットした、新しい発想のプラグインアーキテクチャと言えます。