telegram-test-specs について
telegram-test-specsは、Telegram Botの対話テスト仕様をJSONで作成するためのガイド兼スキルです。agntdevの公開ゲート向けに、トークン不要のプロセス内テストでAPI呼び出しを記録し、期待値と照合します。
主な機能
- 01BotSpec JSONで対話を定義
- 02合成Telegram更新を再生
- 03ネットワークなしでAPI呼び出しを記録
- 04部分列または厳密な順序で呼び出しを照合
- 05宣言済みコマンドのカバレッジを確認
- 06機能ごとに仕様ファイルを整理
活用シーン
- 01TelegramトークンなしでBotの応答をテスト
- 02コールバックやメッセージのAPI呼び出しを検証
- 03有効なテストが不足するコマンドを特定
- 04機能別仕様を使って公開ゲートのテストを実行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agntdev/ skills --skill telegram-test-specsインストール後、Claude Code で/telegram-test-specsを入力して読み込まれたか確認します。
詳細
telegram-test-specs スキル
Telegram ボットのダイアログテスト仕様の書き方 — トークンなしテストを行う理由、ハーネスの仕組み、仕様の形式について説明します。
agntdev パイプライン向けに構築されています。 tests-gate は客観的な公開ゲートです — ボットを公開するには、すべての仕様に合格する必要があります。ビルドループとゲートの位置付けについては、agnt-cli-builder をご覧ください。
⚠️ 安全上の注意 — ボットのプロセス内実行。 このスキルでは、ハーネスが
Updateオブジェクトを再生し、実際の BotFather トークンなしで API 呼び出しをキャプチャできるよう、テストハーネス内でボットのmakeBot()をプロセス内で実行する方法を説明します。このプロセス内インポートはハーネスの要点ですが、インポートしたボットのコードがアクセスできるもの(ファイルシステム、process.env、ネットワーク)をハーネスも利用できることを意味します。自分で作成または監査していないボットのコードをハーネスに指定しないでください。隔離されたディレクトリで実行し、同じシェルで機密性の高い環境変数を設定せず、最初のnpm testの前にsrc/handlers/*とsrc/middleware/*を確認してください。stdout に出力される判定用 nonce(GATE:<nonce>:...、§4)は結果をパブリッシャーに対して認証します — nonce はデプロイ用シークレットとして扱い、チャットに書き込んだり、共有チャンネルにログを記録したりしないでください。
1. トークンなしテストを行う理由
通常、Telegram ボットのテストには実際のボットトークンと api.telegram.org へのネットワーク呼び出しが必要です。つまり、次のような問題があります。
- テストごとに BotFather トークンが必要です
- テストで実際の API を呼び出すため、時間がかかり、レート制限もあります
- シークレットなしでは CI で実行できません
- API 呼び出しを正確にアサートするのが困難です
ハーネスを使う方法
Telegram の API を呼び出す代わりに、ハーネスは次の処理を行います。
- ボットをプロセス内でビルドします(
makeBot()をインポートするだけです) - 合成した Update を渡します(ネットワークは使用しません)
- ボットが試みるすべての送信 API 呼び出しをキャプチャします
- キャプチャした呼び出しを期待される呼び出しと比較します
BotSpec JSON → harness feeds synthetic Updates → bot handles them → captures API calls → compares vs expected
Telegram なし。トークンなし。ネットワークなし。どこでも実行できます。決定論的です。
ゲートの判定
ハーネスは stdout に、機械で読み取り可能な行を 1 行だけ出力します。
GATE:<nonce>:{"ok":true,"total":3,"passed":3,"failed":0,"coverage":{...},"results":[...]}
ok: true→ すべての仕様に合格し、宣言されたすべてのコマンドがカバーされています- 終了コードは常に
0(判定は JSON に含まれ、ゼロ以外はハーネスのクラッシュを意味します) - nonce が判定を認証します(ボットのコードでは偽造できません)
2. ハーネスの仕組み
ボットファクトリー
ハーネスは makeBot() をインポートし、仕様ごとに新しく呼び出します。
import { makeBot } from "./src/index";
// Harness does this internally for each spec:
const bot = makeBot(); // fresh bot, fresh session, fresh state
キャプチャトランスフォーマー
ハーネスは grammY トランスフォーマーをインストールし、送信されるすべての API 呼び出しをインターセプトします。
bot.api.config.use(async (prev, method, payload) => {
// Instead of calling api.telegram.org:
calls.push({ method, payload }); // record it
return { ok: true, result: stub }; // return fake success
});
これにより、ctx.reply("Hi")、ctx.editMessageText(...)、ctx.answerCallbackQuery() はすべてキャプチャされ、ネットワークには送信されません。
偽の botInfo
通常、grammY は起動時に getMe を呼び出します。ハーネスではこれをスキップします。
bot.botInfo = { id: 1, is_bot: true, first_name: "TestBot", username: "test_bot", ... };
合成 Update
ハーネスは、仕様から grammY 互換の Update オブジェクトを構築します。
// { "send": { "text": "/start" } } becomes:
{
update_id: 1,
message: {
message_id: 1,
chat: { id: 1, type: "private" },
from: { id: 1, first_name: "User" },
text: "/start",
entities: [{ type: "bot_command", offset: 0, length: 6 }]
}
}
/commandテキストにはbot_commandエンティティが自動的に付与され、grammY のコマンドルーターに一致しますchatIdのデフォルトは1、userIdのデフォルトも1です- コールバッククエリには元のメッセージが含まれるため、
editMessageTextが機能します
3. BotSpec の形式
仕様ファイルは、ダイアログを記述する JSON オブジェクトです。
{
"name": "start command greets user",
"strict": false,
"steps": [
{
"send": { "text": "/start" },
"expect": [
{ "method": "sendMessage", "payload": { "text": "Welcome!" } }
]
}
]
}
フィールド
| フィールド | 型 | 必須 | 備考 |
|---|---|---|---|
name | string | はい | 一意で、人が読みやすい名前 |
strict | boolean | いいえ | デフォルトは false |
steps | SpecStep[] | はい | ユーザーの操作と期待される応答を順番に指定します |
SpecStep — send(ユーザーの操作)
3 つのバリエーションがあります。
// 1. Text message
{ "send": { "text": "/start" } }
// 2. Text with specific chat/user
{ "send": { "text": "/book", "chatId": 42, "userId": 99 } }
// 3. Callback button tap
{ "send": { "callback": "menu:book", "messageId": 100 } }
// 4. Raw Update object (advanced)
{ "send": { "update": { "update_id": 1, "message": {...} } } }
SpecStep — expect(ボットが返信する内容)
// Assert method was called with specific payload (deep-subset match)
{ "method": "sendMessage", "payload": { "text": "Welcome!" } }
// Assert method was called, any payload
{ "method": "editMessageText" }
// Assert method was called (no payload check)
{ "method": "answerCallbackQuery" }
ディープサブセット照合: payload: { text: "Welcome!" } は { chat_id: 1, text: "Welcome!", ... } に一致します。chat_id、message_id、parse_mode など自動入力されるフィールドを固定せず、確認したい内容だけをアサートできます。
照合モード
部分列(デフォルト、strict: false): 期待される呼び出しはすべて順番どおりに現れる必要がありますが、追加の呼び出しは許可されます。
{
"send": { "callback": "menu:next" },
"expect": [{ "method": "editMessageText" }]
}
// pass — answerCallbackQuery fired too, but was incidental
厳密(strict: true): 個数と位置が完全に一致する必要があります。「それ以外は何もない」ことが重要な場合に使用します。
{
"strict": true,
"steps": [
{ "send": { "callback": "menu:next" }, "expect": [
{ "method": "editMessageText" }
] }
]
}
// fail — answerCallbackQuery fired but wasn't in expect[]
推奨:ほとんどの仕様では部分列を使用し、厳密モードは対象を絞ったアサーションにのみ使用します。
4. コマンドのカバレッジルール
ゲートでは、宣言されたすべてのコマンドが、それを実行する意味のある仕様を1つ以上持つことを確認します。
コマンドに対して仕様が「意味のある」ものとなるのは、次の条件を満たす場合です。
sendステップに/commandのテキストが含まれている- そのステップの
expect[]に1つ以上の項目がある
// ✅ Counts toward /book coverage:
{ "send": { "text": "/book" }, "expect": [{ "method": "sendMessage" }] }
// ❌ Does NOT count (empty expect — no assertion):
{ "send": { "text": "/book" }, "expect": [] }
// ❌ Does NOT count (not a command — no bot_command entity added):
{ "send": { "text": "hello" }, "expect": [{ "method": "sendMessage" }] }
コマンドでは大文字と小文字が区別されます。/Book と /book は別のコマンドです。grammY はそれぞれを別個にルーティングし、カバレッジも個別に追跡します。
カバレッジレポート(GATE の判定結果から):
{
"declared": ["book", "cancel", "start"],
"covered": ["book", "start"],
"missing": ["cancel"],
"fraction": 0.666
}
ゲートを通過するには fraction: 1 が必要です(コマンドが宣言されていない場合は、自動的に1になります)。
5. ハーネス CLI
インライン化されたハーネス CLI を介して呼び出します(npm run build により src/toolkit/harness/ から
dist/toolkit/harness/cli.js にビルドされます)。
AGNTDEV_BOT_MODULE=./src/index.ts # module exporting makeBot()
AGNTDEV_SPECS_FILE=./specs.json # JSON array of BotSpec (legacy single-file) OR
AGNTDEV_SPECS_GLOB=./tests/specs/*.json # per-feature pattern (whole_bot, canonical)
AGNTDEV_COMMANDS_FILE=./commands.json # string[] of declared commands (optional)
AGNTDEV_GATE_NONCE=abc123 # nonce for verdict auth
whole_bot プロジェクトでは、ハーネスが機能ごとのすべての tests/specs/<slug>.json をグロブしてゲート実行時にマージするよう、AGNTDEV_SPECS_GLOB を設定します。機能ごとに分割するパターンの詳細は、以下のセクション6を参照してください。
完全な例:予約ボットの仕様
[
{
"name": "/start greets user",
"steps": [
{ "send": { "text": "/start" }, "expect": [{ "method": "sendMessage", "payload": { "text": "Welcome!" } }] }
]
},
{
"name": "/book flow",
"steps": [
{ "send": { "text": "/book" }, "expect": [{ "method": "sendMessage", "payload": { "text": "Choose a service:" } }] },
{ "send": { "callback": "select:cut" }, "expect": [{ "method": "editMessageText", "payload": { "text": "Pick a time:" } }] },
{ "send": { "callback": "slot:14:00" }, "expect": [{ "method": "editMessageText", "payload": { "text": "Booked!" } }] }
]
},
{
"name": "/cancel flow",
"steps": [
{ "send": { "text": "/cancel" }, "expect": [{ "method": "sendMessage" }] },
{ "send": { "callback": "confirm:cancel:yes" }, "expect": [{ "method": "editMessageText", "payload": { "text": "Cancelled." } }] }
]
}
]
クイックリファレンス
| 概念 | 実装 |
|---|---|
| ボットファクトリー | export function makeBot() — 仕様ごとに新しいボットを作成 |
| ネットワークを使用しない | キャプチャトランスフォーマー + 偽の botInfo |
| 合成入力 | { text: "/cmd" }、{ callback: "data" }、{ update: {...} } |
| 期待される出力 | { method: "sendMessage", payload: { text: "Hi" } }(ディープサブセット) |
| 判定結果 | 標準出力に GATE:<nonce>:{"ok":bool, ...} |
| カバレッジ | 宣言されたすべてのコマンドに、空でない expect 仕様が1つ以上必要 |
6. 機能ごとの仕様ファイル(whole_bot の規約)
whole_bot プロジェクトでは、テスト仕様を tests/specs/<slug>.json に機能ごとに1つの JSON
ファイルとして整理します。src/handlers/<slug>.ts ごとに1つのファイルを用意します。プラットフォームはゲート実行時にこれらをグロブしてマージするため、各ファイルは独立しています。
ファイル構成
my-bot/
├── tests/
│ ├── specs/
│ │ ├── T01.json # one file per feature task
│ │ ├── T02.json
│ │ └── T03.json
│ ├── helpers.ts # if you use programmatic tests (see advanced skill)
│ └── start.test.ts
各ファイルは BotSpec オブジェクトの JSON 配列です(上記セクション3と同じ形式です)。tests/specs/T02.json の例:
[
{
"name": "T02: /balance shows the user's balance",
"steps": [
{ "send": { "text": "/balance" }, "expect": [
{ "method": "sendMessage", "payload": { "text": "Balance: 42.5" } }
] }
]
}
]
なぜ1つの大きな specs.json ではなく、ファイルごとに分けるのですか?
- マージ時の競合がありません。 複数のエージェント(または将来のPR)が異なる機能を並行して作業できます。機能ごとに仕様ファイルを分けることで、共有ファイルによる作業の停滞を防ぎます。
- 機能ごとに分離できます。 ゲートは各仕様ファイルをファイル名の機能スラッグに対応付けます。
tests/specs/<slug>.jsonが失敗した場合、修正が必要なハンドラーを直接特定できます。 - 手作業で記述します。 bot-starter テンプレートには機能ごとのディレクトリが含まれますが、仕様の内容は自分で記述します。ハンドラーの返信テキストを
expect.payload.textと完全に一致させてください。
機能ごとの仕様の書き方
仕様ファイルの内容は、上記セクション3と同じ BotSpec の JSON 形式です。違いはファイルの場所だけです。大きな specs.json を1つ置く代わりに、tests/specs/<slug>.json に配置します。ファイル名のスラッグは、タスクのスラッグと完全に一致している必要があります(大文字と小文字を区別)。
ゲートの実行時
プラットフォームが公開時に tests-gate を実行すると(whole_bot のビルドループ内で、完全性レビューが収束した後に実行)、tests/specs/*.json をグロブして1つのインメモリ配列にマージし、すべての仕様をまとめてハーネスでテストします。仕様ごとの GATE の結果は元のファイルに紐付けられるため、失敗した場合は修正が必要なハンドラーを特定できます。
ゲートはフェイルクローズです。 機能ごとの仕様ファイルが見つからない、または読み取れない場合、
GATE:<nonce>:{"ok":false,...,"error":"..."}というハードエラーになります。黙ってスキップされることはありません。スキップされた仕様(プラットフォームが取得または解析できない仕様ファイルなど)は、理由とともに判定結果に表示されます。ゲートの判定結果で「skipped」の行が原因で失敗している場合、修正すべきなのはゲートではなく仕様ファイルそのものです。
プロジェクトにレガシー仕様と機能ごとの仕様の両方がある場合(移行中など)は、機能ごとのパターンが優先されます。tests/specs/*.json が存在する場合、プラットフォームは最上位の specs.json を無視します。
よくある間違い
- コマンド送信時に空の
expect[]を指定する — カバレッジは上がりますが、何も検証しません。ハーネスで拒否されます。 answerCallbackQueryを呼び忘れる — 部分列マッチングでは見逃されますが、実際のユーザーにはスピナーが止まらないように見えます。- 付随する呼び出しを含めずに
strict: trueを指定する — ほとんどすべてのコールバックハンドラーでanswerCallbackQueryが呼び出されます。strict にする場合は expect に含めてください。 - スペック間でセッションが維持されると想定する — ハーネスはスペックごとに新しいボットを作成します。セッションは毎回
initial()から始まります。 - すべてのコマンドを宣言しない — カバレッジゲートは宣言されたリストを使用します。ハンドラーに対応するスペックがないと、ゲートで失敗します。
- 大文字と小文字を一致させない —
/Bookを宣言し、スペックで/bookを送信すると、カバレッジ上は別のコマンドとして扱われます。