---
title: 既知の制限
icon: phosphor-duotone:warning
summary: 正直なギャップ - 実際に動くアプリに対して証明済みのものと、そうでないもの。
description: 正直なギャップ - 実際に動くアプリに対して証明済みのものと、そうでないもの。
tags: [reference, limitations]
---

# 既知の制限

BX Agents は現在も活発に開発が進められています。このページは、正直なギャップ - 実際に動くアプリに対して検証済みの部分、bx-ai の `"mock"` プロバイダーに対してしかまだ実行されていない部分、そしてこのプロジェクトが遭遇した実際の upstream の癖 - を記録しています。

## テストは `mock` プロバイダーに対してのみ実行される

すべての高速レーンのスペック (ビルドパイプライン、ジェネレータ、CLI 動詞) と [ColdBox 統合スイート](#real-coldbox-integration-testing) は、bx-ai に組み込まれた `"mock"` プロバイダーを演習します - LLM への実際のネットワーク呼び出しは決して行いません。これは意図的です (高速で、無料で、決定的な CI)。しかし、これは実際のプロバイダー (OpenAI、Anthropic など) が実際にエンドツーエンドで正しくラウンドトリップすることを証明する自動テストが現時点では存在しないことを意味します。本番でこれに依存する前に、実際のプロバイダーと使い捨ての API キーに対して、少なくとも一度は手動で `chat`/`serve` を実行してください。

## Real ColdBox integration testing

`./gradlew testColdBoxIntegration` は、生成されたアプリ自身の `Application.bx`/Bootstrap に対して**実際の** `boxlang-miniserver` プロセスを起動し、`toAi()` 登録済みルートを通じて本物の HTTP リクエストを行います。これはスイートの中で最も強力な証明ポイントであり、開発中に `ColdBoxAppGenerator.bx` の 3 つの実際のバグ (WireBox の `Binder` の `extends` の欠落、`Bootstrap` コンストラクタの引数順序の誤り、`Binder` に存在しない素の `getInstance()` 呼び出し) を捕まえました。

### `toAi()` の最初のリクエストのレースコンディション

新しく起動したアプリの `toAi()` ルートへの**本当に最初の**HTTP リクエストは、「Function [getInstance] not found」という一時的な失敗をすることがあります - これは、Router 自身の `getInstance` デリゲート上での、本物の ColdBox/WireBox の遅延注入のレースコンディションであり、BX Agents のバグではありません。他の何か (ヘルスチェック、`chat` セッション、別のルート) がすでに `GeneratedAgent` シングルトンを一度構築するよう WireBox に強制した後の、それ以降のすべてのリクエストでは確実に成功します。負荷のかかる状況で新しくデプロイされた `toAi()` ルートに依存する前に、**ウォームアップリクエストを送ってください**。

## 3 つの統合チェック - 元々の計画とは別の経路で解決された

`tests/specs/integration/RuntimeStartupSmokeSpec.bx` には依然として 3 つの `xit()` が残っています。このファイルは CLI ランナー (`runTests.bxs`/`testBx`) 経由で実行され、BoxLang の CLI モードは `cgi` スコープを一切与えないためです - これは、起動時にルーターをロードするためだけでも ColdBox の RoutingService が必要とするものです。これは構造的な事実であり、ギャップではありません。この 3 つのチェックはすべて、実際には別の場所で本物として証明されています。`tests/specs/integration/coldbox/ColdBoxRuntimeSpec.bx` (実際の `boxlang-miniserver` プロセスによって配信される、本物の HTTP リクエストの内部で実行されます) です:

- 実際の ColdBox でルーティングされた HTTP リクエストが、生成されたエージェントにエンドツーエンドで到達すること - `ColdBoxRuntimeSpec.bx` と `runColdBoxIntegrationTests.bxs` 自身の `POST /api/chat/invoke` アサーションによって証明されています。
- `schedules/*` が実際に**ライブな** ColdBox `Scheduler` に登録されること - `SchedulerService.getSchedulers()["appScheduler@coldbox"].hasTask(...)` を、実際の cron 発火を待つことなく (サポートされる最も粗い粒度は 1 分であり、タスクがすでにライブで登録済みであることが確認された後にわずかな追加の証明のために毎回の CI 実行に負荷をかけることになります)、実際の起動に対して証明しています。
- `chat` と `serve` された HTTP ルートが決して食い違わないこと - 元々の書き方 (`chat` は設計上、WireBox を一切起動しないので、WireBox のシングルトン*オブジェクト*を共有することは決してできません) から、実際に重要なことへと修正されました: WireBox の外側で `GeneratedAgentFactory` をインスタンス化すると、WireBox 自身のシングルトンと振る舞い的に等価なエージェントが生成される、ということです。

## 実際の OS プロセスの CLI テスト - そしてそれが見つけた実際のバグ

`ModuleCliProcessTest.java` は、実際にインストール可能なモジュール構造 (`build/modules/bxagents`) のコピーに対して、実際の `modulesDirectory` を指す形で、本物の `java -jar <boxlang-jar> module:bxagents <verb> ...` 子プロセスを起動します - 実際の BoxLang インストールがこのモジュールをロードするのとまさに同じ方法です。他のすべての CLI スペックは代わりに `ModuleConfig.main()`/各動詞の `run()` をインプロセスで呼び出しており、これはより高速ですが、実際にインストールされた後に本当にモジュールが解決されることを一切証明しません。

そのギャップは実際のものでした: このテストは、実際にインストールされたモジュールプロセスを通して実行された瞬間に、すべての CLI 動詞が「class not located」で失敗することを捕まえました。内部の相互参照が、このリポジトリ自身の開発/テスト用 `boxlang.json` (手で宣言された `/bxagents` マッピング) のおかげでのみ解決される、素の `bxagents.models....` というドット区切りのパスを使っていたためです - 実際にインストールされたモジュールは、そのマッピングを決して得られません。すべてのネストされたクラスの内部参照を、`Class@bxagents` というモジュール相対のサフィックス形式に切り替えることで修正されました (`ModuleConfig.bx` 自体は、モジュールのルートに座っており、プレーンな相対パスを問題なく解決するので、変更は不要でした) - 完全な説明は `BuildPipeline.bx` の `init()` の docblock を参照してください。`build.gradle` のモジュール構造の出力先は `build/module` から `build/modules/bxagents` に移動しました (`modulesDirectory` の発見がそれを見つけるためには、フォルダ名がモジュール名と一致する必要があります)。開発/テスト用の `boxlang.json` も今やこれを実際のモジュールとしてロードするので、既存のスイート全体は、単なる便宜的なマッピングではなく、本番と同じ解決パスを演習します。

## テストフレームワークの構築中に見つかった実際の、根本的なバグ: エージェントが自身のツールを一度も受け取っていなかった

`BaseAgentSpec` の `toHaveCalledTool` マッチャー (M15) の構築中に、深刻な、これまで発見されていなかったバグが表面化しました: `ColdBoxAppGenerator` が生成する `aiAgent()` 呼び出しには、`tools:` 引数がまったく渡されていませんでした (実際の bx-ai ソースに照らして確認済みです - `AiAgent.bx` は内部で `aiToolRegistry()` を一切参照しません)。プロジェクトの `tools/` はビルドにコピーされ、MCP 配線のために名前解決可能にはなっていましたが、**BX Agents がこれまでにビルドしたどのエージェントも - 実際に配信されたアプリ、`chat`、テストスペックのどのコンテキストであっても - 自身の宣言されたツールを実際には一度も受け取っていませんでした。** これは、既存のどのテストも実際のツール呼び出しをアサートしたことがなく、空でない応答だけをアサートしていたため、検出されずにいました。

`ColdBoxAppGenerator.renderAgentFactory()` で修正されました: 生成されるすべての `GeneratedAgentFactory.bx` は、新しい `ToolRegistryLoader.bx` を経由して自身の `tools/` ディレクトリをロードするようになりました (生成時に埋め込まれた絶対パスを使い、ロードされるコンテキストで有効な「/」マッピングが何かに依存する相対パスは使いません - `aiToolRegistry().scan("tools")` 自身の相対パス解決が、`chat` のような `DynamicClassLoader` によってロードされたコンテキストから呼び出された際にサイレントに失敗することを確認しました)。その後、すべての `aiAgent()` 呼び出しに `tools: aiToolRegistry().getAll()` を渡します。

3 つ目のトップレベルスクリプトの落とし穴が、CI で苦労して見つかりました: **`var` は `.bxs` スクリプトのトップレベルでは使用できません。** `var` は `local` スコープに宣言しますが、これは関数の内部にしか存在しないため、トップレベルの `var x = ...` は実行時に `Scope [local] is not available in this context` を投げます - パース時ではないので、レビューを生き延び、そこに到達するコードパスでのみ発火します。これは `runColdBoxIntegrationTests.bxs` で 1 回分の CI サイクルを消費しました。問題の行は失敗診断用の分岐の内部にあったため、他の何かがすでにおかしくなった、まさにそのときにクラッシュし、本当の失敗を自分自身のもので置き換えてしまいました。2 つのルールがここから導かれます: `.bxs` の関数の外で `var` を書かないこと、そして診断出力を自身の try/catch で包み、それが説明しようとしている失敗を隠せないようにすることです。

**デフォルトデータソースのための Application 設定は `this.datasource` であり、`this.defaultDatasource` では**ありません。複数形の登録キーは本当に `this.datasources[ "name" ] = { ... }` であるため、`this.defaultDatasource` は誰もが手を伸ばしてしまう名前です - そして BoxLang はそれをサイレントに受け入れ、何もしません。実際にランタイムに対して直接検証されたもので、推測ではありません: `this.defaultDatasource = "testds"` を設定した状態で、修飾子なしの `queryExecute()` は依然として `No default datasource defined in the application or globally or in the query options. Registered datasources are: [testds]` で失敗します。1 行を `this.datasource = "testds"` に変えると、データソースが解決され、呼び出しは実際のドライバの関心事に進みます。書いた設定が無視されたことについての警告もヒントもエラーの中にはなく、メッセージは選択しようとしている登録済みデータソースの名前を挙げているので、選択メカニズムそのものが壊れているように読めてしまいます。

**qb の `moduleSettings.qb.defaultOptions` は、実際の ColdBox の起動では `QueryBuilder` に届いていませんでした。** 生成される `config/ColdBox.bx` は `moduleSettings.qb.defaultOptions = { datasource : "<name>" }` を設定し、qb 自身の `ModuleConfig.cfc` は `onLoad()` の内部で `.initArg( name = "defaultOptions", value = settings.defaultOptions )` によって `QueryBuilder@qb` をマッピングします - これは正しく読めますし、生成された `ChatDb.query()` が元々データソースを一切指定していなかった理由でもあります。これは機能しませんでした: すべてのクエリが `No default datasource defined in the application or globally or in the query options. Registered datasources are: [<name>]` で失敗しました。つまりデータソースは登録済みだったのに、ビルダーは空の options を持ったままでした。このモジュール設定がなぜ届かなかったのかは徹底追求されませんでした - それがどうでもよくなったのは、そのプラミングに依存するよりも、明示的にデータソースを指定するほうが良いからです。`ChatDb.query()` は今や、すべてのビルダーに対して `.mergeDefaultOptions( { datasource : static.DATASOURCE } )` を呼び出します。`SchemaBuilder` に対して `schemaOptions()` がすでに行わなければならなかったことを反映しています (qb は `SchemaBuilder` に `defaultOptions` を一切渡しません)。生成されるコードに新しい qb の呼び出しパスを追加する場合は、そこにデータソースを指定してください。モジュール設定があなたをカバーしてくれると想定しないでください。

**生成されるアプリは今や `this.datasources` だけでなく、`this.defaultDatasource` も宣言します。** データソースが 1 つしかない場合、名前を指定せずにクエリを実行するもの - ColdBox 自体、別のモジュール、プロジェクト自身のコード - は、失敗するのではなくそこに到達するべきです。以前は生成されるデータソースブロックに対して何もアサートされておらず、それがデフォルトなしで出荷された理由です。`ColdBoxAppGeneratorSpec` は今や、Web UI ありのケースと Web UI なしのケース (どちらの行も現れないべきケース) の両方をカバーしています。

**生成される `models/ChatDb.bx` はコンパイルできず、どのユニットスペックもそれに気づきませんでした。** `WebUiGenerator` は、BoxLang の文字列リテラルから BoxLang ソースを組み立てます。リテラルなダブルクォートは二重化されたクォートとして書かれるので、空文字列リテラルには 4 つのクォート文字が必要で、自然に見える 2 つを書くと、行の残りを飲み込んでしまう 1 つの余分なクォートを開いてしまいます。空文字列への elvis 演算子 (`?: ""`) を運ぶ 2 行のテンプレートが `?: "` を出力してしまい、生成されるすべての Web UI プロジェクトが、パースに失敗する `ChatDb.bx` を出荷していました。すべての `WebUiGeneratorSpec` のケースは生成されたテキストの部分文字列をアサートしており、それはすべて依然として一致していました。実際の ColdBox の起動だけがそれを*コンパイル*しようとしており、そのパス自体も下記のハーネスバグによって何サイクルも壊れていたため、実際のバグはそれらの背後に隠れたままでした。生成されたソースのすべての行がダブルクォートを偶数個持つことをアサートする `WebUiGeneratorSpec` のケースが今や存在します - これは、たまたま間違っていた 2 行だけでなく、このクラス全体を捕まえる不変条件です。ここからのより広い教訓: 自身の出力に対してグレップするだけのジェネレータスペックは、出力が有効なコードであることをテストしていません。

**`GET /chat/api/health` は、それが証明しているように見えるよりもずっと少ないことしか証明していません。** 生成される `health()` アクションはリテラルな `{ status: "ok", success: true }` をレンダリングし、それ以外には何にも触れません - つまり 200 は ColdBox が起動し、ルーティングが生成された `ChatUi` ハンドラに到達することを証明するだけで、WireBox、`ChatDb`、SQLite については何も語りません。`runColdBoxIntegrationTests.bxs` のコメントはかつて「ColdBox routing -> ChatUi -> WireBox -> ChatDb」を演習していると主張していましたが、それは誤りであり、実際に誤った安心感を与えていました: ある CI 実行では、`models/ChatDb.bx` がまったくコンパイルできない状態のまま、グリーンなプローブが返っていました。その場で修正されました。ストアのカバレッジとして扱うべきは、プローブではなく統合スペックです。

**TestBox の `JSONReporter` は、ライブなフレームワークのシングルトンに触れるスペックについては報告できません。** ColdBox の統合スペックは実際の `controller`/WireBox/qb オブジェクトを解決し、それらのオブジェクト参照が TestBox の結果メメントに残ります。`JSONReporter` はそのメメントをまるごとシリアライズするため、BoxLang はライブなオブジェクトに対してリフレクションを行い (`StructUtil.objectToStruct` -> `DynamicInteropService.getMethodNames`)、それらの循環参照を JVM が `StackOverflowError` を投げるまでたどってしまいます。これが発火した時にはすでにスペック自体はグリーンで実行を終えていて、まさにそれが混乱を招きました: 合格したスイートが失敗した実行として報告されたのです。そのため `tests/runner-coldbox.bxm` は自身のレポートを組み立てます。すべての値を `toScalar()` ヘルパーに通してオブジェクトがシリアライザに一切届かないようにし、レポートが構築される*前に*進捗マーカーに合格/失敗のカウントを記録します - そのため、シリアライズの失敗が二度とテストの失敗と誤認されることはありません。

**BoxLang の time マスクでは、`nn` は分ではなく NANOSECONDS (ナノ秒) です - `mm` を使ってください。** `dateTimeFormat( now(), "HH:nn:ss" )` は `02:777491298:32` のようなタイムスタンプをサイレントに生成し、誤った書式ではなく破損した出力のように読めます。(逆の落とし穴が date マスクにもあることに注意してください。そこでは `mm` が月です: `yyyy-mm-dd` は日ではなく分を生成します。)

**サーバー側でアイデンティティを導出することは、それに対して認可することと同じではありません。** 生成される `handlers/ChatUi.bx` は、すべてのアクションでリクエストボディからではなくセッションから `userId` を導出することに気を配っていましたが、それでも 2 つのアクションには認可の穴がありました。呼び出し元ではなく `threadId` によってアドレスされていたからです。`/pending` と `/resume` は id によってチェックポイントをロードし、それに対して動作していました。`/resume` はセッションから `decidedBy` を導出さえしていましたが、これはスコープチェックのように読める一方で、実際に統制していたのは*決定に付けられるラベル*だけであり、*どのランが決定されようとしているか*ではありませんでした。他人の `threadId` を持つ訪問者は、その保留中のツール呼び出しを読み、代わりに承認・拒否できてしまいます。両方とも今や、呼び出し元を、エージェントがラン options にチェックポイントした `userId` と比較します。これが覚えておく価値のある一般的なルールです: あるルートが呼び出し元ではなく不透明な id でキー付けされている場合、サーバー側でアイデンティティを導出することは属性の記録にはなりますが、アクセス制御にはなりません - この 2 つは別々に考える必要があり、同じ関数内にサーバー由来の値が座っていると、両方を兼ねていると簡単に誤認されてしまいます。

**`ProjectValidator` のパスチェックは、先頭のセパレータだけでなく `..` も考慮する必要があります。** `database.path` は、生成される `Application.bx` の中で `expandPath()` 呼び出しにそのまま差し込まれるため、絶対パスであるかどうかがチェックされていましたが - `../../var/lib/chat.db` は、そのチェックをきれいに通過しながら、まったく同じようにアプリディレクトリを脱出します。今では両方のセパレータについて拒否され、`..hidden` や `a..b` のような正当な名前は依然として通過するよう、パスセグメント全体をマッチさせています。生成されるパスを守る将来のどのバリデータも、この両方の半分をチェックする必要があります。

**テストスイートをローカルで実行するには、CI とまったく同じように bx-ai をソースからオーバーレイする必要があります。** `./gradlew downloadModules` は*公開済みの* bx-ai スナップショットを取得しますが、これは自身の development ブランチに遅れています - これはベンダリングされたコピー (`src/test/resources/modules/bxai` の下) を上書きし、その後スイートは `Method 'isRunning' not found` と `The method aiGatewayRegistry does not exist` という 2 つの、upstream には存在するが公開ビルドには存在しない API に関するエラーで、約 40 個のスペックで失敗します。これは後退ではなく、後退だと誤解しやすいものです。`testBx` を実行する前に、リビルドしてオーバーレイしてください: `( cd <bx-ai checkout> && ./gradlew createModuleStructure )` の後 `cp -R <bx-ai>/build/module/. src/test/resources/modules/bxai/`。`tests/coldbox/`、`tests/testbox/`、`tests/qb/` は (`tests/` で実行する) `box install` から来ており、この 3 つはすべて gitignore されています。

**`chr()` は BoxLang には存在しません - その BIF は `char()` です。** これはコンテキスト依存ではありません。以前は `ModuleConfig.bx` のコメントが「このモジュール CLI 実行コンテキストでのみ」利用できないと主張していましたが (現在は修正済みです)、実際にランタイムに対して直接検証されています: `char( 10 )` は改行を返しますが、`chr( 10 )` はプレーンな CLI スクリプトでも、配信されるテンプレート内でも同じように `Function [chr] not found` を投げます。これが知っておく価値があるのは、この失敗が*ランタイム*のものだからです - `chr()` はパースは通り、レビューを生き延び、それに到達する最初のコードパスで投げられます。これはここで何回もの CI サイクルを消費しました: `tests/runner-coldbox.bxm` はその最初の進捗マーカーで `chr( 10 )` を呼び出していたため、すべてのリクエストがエントリで 500 を返し、下記の再試行ループが、そのプレーンな 1 行のエラーを不透明な「HTTP 408、応答なし」に変えてしまいました。

同じ CI ハーネスからの 4 つ目の教訓で、最も多くのサイクルを消費したものです: **再試行が上書きしてしまう計装は、何も記録しません。** `tests/runner-coldbox.bxm` は、自身の進捗ファイルを切り詰める (`fileWrite( progressFile, "" )`) ことから始まり、`runColdBoxIntegrationTests.bxs` は、期限まで 1 秒ごとにランナーリクエストを再試行していました。そのため、すべての再試行が、ハングした最初の試行が書いたマーカーを消し去り、失敗診断は忠実に空のファイルを表示しました - これは「ページはどこにも到達しなかった」と読めますが、実際は「証拠が削除された」でした。さらに悪いことに、再試行は診断とは独立して積極的に有害でした: ランナーページは安価でも冪等でもなく (TestBox を起動し、すべての統合スペックを実行します)、そのため再試行は、わずか一握りのスレッドしかない MiniServer のワーカープールに並行したフルテスト実行を積み重ねてしまいました - これはハングから回復する方法ではなく、ハングを引き起こす方法です。両方とも修正されています: オーケストレーターはマーカーファイルを一度だけクリアし、正確に 1 回だけリクエストを行います (それに先立つヘルスプローブがすでにサーバーが起動していることを証明しているので、再試行が待つべきものは何もありません)。そしてこのページは追記のみを行い、各行にリクエストごとの UUID をタグ付けするので、重複した試行が識別可能なまま残ります。一般的なルール: 進捗ログは追記専用であり、それが計装しているものより長生きしなければなりません。そして、高価あるいはステートフルなものは、決して再試行ループの背後に置かれるべきではありません。

これを診断している最中に見つかった、別の関連する BoxLang の落とし穴: **`request` は予約された組み込みスコープ名です。** `request` という名前のローカル/ループ変数は、それをサイレントにシャドウすることがあります - `for ( var request in someArray ) { request.someKey }` は正しい回数だけ反復しましたが、その内部でのすべての `request.someKey` アクセスは、ループ変数の代わりに、エラーもなく空の組み込みスコープをサイレントに読み取っていました。`BaseAgentSpec.bx` のマッチャーで `recordedRequest` にリネームすることで修正されました - このプロジェクトで、`request` という意味の通る名前でループする将来のどんな BoxLang コードでも覚えておく価値があります。

## `serve` の miniserver 検索は PATH のみ

`serve` は `boxlang-miniserver` を `PATH` 上でのみ探します (`MiniServerLauncher.findExecutable()`)。設定されたパスやバンドルされたバイナリへのフォールバックはありません - インストールされておらず `PATH` になければ、`serve` は明確で対処可能なエラーで失敗しますが、それを指す別の方法はありません。`invoke --server` は内部で `serve` を再利用するので、この同じ PATH のみの検索を引き継ぎます - その `InvokeSpec.bx` 自身の実際の HTTP ラウンドトリップ用テストは、まず `MiniServerLauncher.findExecutable()` をチェックし、`PATH` に実際のバイナリがなければ、失敗ではなくスキップします。これは `RuntimeStartupSmokeSpec.bx` がすでに自身の jar 存在チェックで使っているのと同じ慣用です。本番でこの実際の HTTP パスに依存する前に、`boxlang-miniserver` がインストールされたマシンで、少なくとも一度は手動で `bxAgents invoke --message=... --server` を実行してください - このページの他の箇所ですでに使われているのと同じ、正直な枠組みです。このスイートがあらゆる環境で自力で閉じることができないギャップについてです。

## スコープ付き BoxLang ランタイムホームは `serve`/`invoke --server` には無条件に届くが、インプロセスの動詞には BoxLang のインストールが `.env` をロードする場合にのみ届く

`serve` は miniserver 自身の BoxLang ランタイムホームを、`serverHome` 経由で `.build/runtime` にスコープします (これは実在する、確認済みの `MiniServerConfig` フィールドです - `boxlang-web` 自身の `MiniServer` CLI ヘルプテキスト: `-s, --serverHome <PATH>  BoxLang server home directory (default: ~/.boxlang)`)。そのため、各プロジェクトのコンパイル済みクラスキャッシュと、あらゆる config オーバーライドは、グローバルに共有される `~/.boxlang` ではなくプロジェクトごとに分離されます。`invoke --server` は内部で `serve` を再利用するため、これを継承します。この部分は無条件です - 私たちがそのプロセスの起動 config を自分自身で書いているからです。

`chat`、`build`、`test`、そしてデフォルト (インプロセス) の `invoke` は違います: これらはすでに起動している `bxAgents` プロセスの内部で実行され、その自身の BoxLang ランタイム - そしてそのホーム - は、`ModuleConfig.bx` 自身の `main()` を含む、私たちの BoxLang コードが何であれ実行される機会を得るよりも前に解決されています (そのコードを解釈するには、エンジン自体がまず存在しなければならないからです)。`BoxRuntime` は JVM 全体で 1 つのシングルトンで、そのホームは最初の初期化時に固定されるため、すでに実行中のそのプロセスの内部から動詞クラスが何をしても、それを遡って変更することはできません。

これに対する本当のレバーは実際に存在します: `BoxRunner` (BoxLang 自身のコア CLI エントリポイントであり、miniserver だけではありません) は、`BoxRuntime` が初期化される前に、正真正銘の `BOXLANG_HOME` 環境変数を読み取ります - これはドキュメントだけでなく、実際のランタイム jar に対して直接確認済みです: 実際の OS 環境変数として `BOXLANG_HOME=<path>` を設定してこれを実行すると (相対パスは絶対パスと同じように CWD に対して解決されます)、`~/.boxlang` の代わりにそのパスに、毎回、完全なホーム構造が用意されます。`new` は、まさにこの理由で (`ortus-boxlang/bx-ai-intro` 自身の `.env`/`BOXLANG_HOME` コンベンションを反映して)、`serve` が使うのと同じパスである `BOXLANG_HOME=.build/runtime` を宣言する `.env` を、**プロジェクトルート** (つまり、インプロセスの動詞のためにユーザーが実際に `bxAgents <verb>` を実行するディレクトリであり、CWD ベースの `.env` ローダーがそれを見つけるべき正しい場所です) にスキャフォールドします。

**確認できたこと、そしてできなかったこと。** `boxlang-miniserver` (`serve` が起動するもの) には、実際に組み込まれた `.env` の自動ロードがあります - これは `ortus.boxlang.web.MiniServer` を実際にデコンパイルし、その後実際に実行することで確認済みです: `envFile` が設定されていない場合、`.env` はサーバーの**webRoot** (プロジェクトルートではありません) からの相対パスで解決され、見つかれば Java の `Properties` としてロードされ、各キーは `System.setProperty()` 経由で適用されます - この方法でロードされた実際の値は、`getSystemSetting()` を通じて BoxLang コードから見えます。生のコアランタイム jar (`BoxRunner`、`chat`/`build`/`test`/デフォルトの `invoke` が使うもの) には、それに相当するロジックがどこにもありません (jar 内のすべてのクラスを `.env` でグレップして確認済みです) - そのため、これらの動詞は、実際にインストールされている `boxlang` CLI (この生の jar ではなく、BVM が提供するネイティブランチャー) が JVM が起動する前に自身で `.env` のロードを行っている場合にのみ `.env` を拾います。`ortus-boxlang/bx-ai-intro` が頼っているのがその方法です。これはもっともらしく、そのプロジェクトの実世界での使われ方と一致していますが、このサンドボックスには直接検証するための実際のバイナリがありません。

`.env` のロードが行われる場合でさえ、1 つの具体的で確認済みの落とし穴があります: **`BOXLANG_HOME` それ自体は、実際の OS 環境変数でない限り効果を持ちません** - JVM のシステムプロパティでも、`-D` フラグでもありません。実際の jar に対して 2 回、直接検証されています: (1) `.env` が `BOXLANG_HOME=customhome` を宣言している webRoot に対して `boxlang-miniserver` を実行すると、そのファイルはロードされました (自身の "Loaded environment variables from:" というログ行と、`getSystemSetting()` が他の `.env` の値を正しく返すことによって確認済みです)。それでもサーバーは `Logs Directory: /root/.boxlang/logs` というデフォルトのホームをログに出しました。`customhome` ではありません。(2) `BoxRunner` を `-DBOXLANG_HOME=<path>` (JVM のシステムプロパティで、`.env` は関与しません) で直接起動しても、解決されたホームには何の効果もありませんでした。`getSystemSetting( "BOXLANG_HOME" )` は BoxLang コードから喜んでそのフラグの値を返すにもかかわらずです。つまり `BOXLANG_HOME` の解決は、具体的には本物の OS 環境変数だけを読みます - ほとんどの設定とは異なり、`getSystemSetting()` がそれに対して値を返すことは、ランタイムホームが実際に移動したことを意味しません。あなたの `boxlang` CLI の `.env` ローダーが、MiniServer の内部と同じ方法 (JVM がすでに起動した後の `System.setProperty`) で動作し、JVM を起動する前に本物の env 変数をエクスポートしていない場合、`.env` の中の `BOXLANG_HOME` は、`.env` 自体は正常にロードされたにもかかわらず、ランタイムホームには届きません - それ以外のすべてはそのまま機能します。あなたのインストールがこれをまったく提供しない場合は、コマンドを実行する前に自分自身でそれを source してください (例えば `set -a; source .env; set +a`)。そうすれば `serve` がすでに無条件に得ている分離を得られます。

## `new` の `box install` 便宜ステップは自動テストで演習されていない

`new` はデフォルトで、スキャフォールドされた `tests/` フォルダの内部で `box install` を実行するため、`bxAgents test` は ([CLI リファレンス](cli-reference.md) 参照) すぐに動作します。これは実際のネットワーク操作です (CommandBox は `testbox` を ForgeBox に対して解決します) - この開発サンドボックスでは具体的に約 25 秒かかり、証明書エラーで失敗することが確認されています。これは、このプロジェクト自身のツールの他の箇所ですでに指摘されている、ForgeBox に到達不能という同じ制約です。`NewSpec.bx` は高速で決定的な `--skipInstall` パスのみを演習します (OS プロセスの `ModuleCliProcessTest.java`/インプロセスの `ModuleConfigCliSpec.bx` による `new` の呼び出しも、同じ理由で `--skipInstall` を渡します) - デフォルトのインストール試行パス自体は、手動テストによってのみ検証されており、CI ではありません。

## `chat` には本物の TTY が必要

`chat` は BoxLang 自身の `MiniConsole` を使用します。これは raw ターミナルモードをセットアップするために `stty` をシェルアウトします - 本物の対話ターミナルに対してのみ実行できます。パイプ、リダイレクト、あるいは非対話プロセス (CI ジョブ、スクリプト) からは動作しません。非対話のフォールバックモードはありません。

## 修正済み: `chat` とデフォルト (`--server` なし) の `invoke` は、クラスベースの `Agent.bx` に対してかつて失敗していた

以前は、`chat` とデフォルトの `invoke` の両方が、エージェントに到達する前に `The requested class [agent.classes.agentClass] has not been located in any class resolver.` を投げていました。根本原因: どちらの動詞も、生成された `GeneratedAgentFactory.bx` を、ColdBox コンテナを一切介さずに `DynamicClassLoader.instantiate()` (絶対パスに対する生の `RunnableLoader` 呼び出し) 経由でインプロセスにロードしていましたが、生成されたファクトリは、クラスベースの `Agent.bx` を**相対的な**ドット区切りパスの検索 `new "agent.classes.agentClass"()` でインスタンス化しており、これはアプリのルートを解決可能にするマッピングが登録されている場合にのみ解決されるもので、実際の ColdBox の起動の外側では何もそれを登録していませんでした。

`DynamicClassLoader.instantiate()` の直前にスクリプトの途中でマッピングを登録すること (`Configuration.registerMapping( "/", appDir )`) も、これを修正**しません** - スタンドアロンの `.bxs` スクリプトで手作業で同じ手順を再現することで経験的に確認されています。上記ですでに `TestRunnerLauncher` の TestBox 発見について文書化されている、同じ種類の制限です: スクリプトの途中で `Configuration.registerMapping()` によって登録されたマッピングは、その同じプロセス内で行われるクラス自身の相対パス検索には確実には伝播しません。

**修正:** `ColdBoxAppGenerator.copyAgentClass()` は今や (ドット区切りのコンポーネントパスではなく) コピーされたクラス自身の絶対ファイルパスを返し、`renderClassBasedAgentStatement()` は、相対的な `new "..."()` ではなく、`chat`/`invoke` がすでに `GeneratedAgentFactory.bx` 自体をロードするのに使っているのとまったく同じプリミティブである `DynamicClassLoader.instantiate( absolutePath, context )` 経由でそれをインスタンス化します。これはマッピング解決を完全に回避するので、実際の ColdBox コンテナが起動しているかどうかにかかわらず、今では同じように動作します。`examples/class-based-agent/` に対して確認済みです: `chat`、デフォルトの `invoke`、`invoke --server`、`serve` はすべて、今では同じエージェントを正しくビルドして実行します。

## box.json の `executable` インストールのスモークテストはない

`box.json` は `"boxlang": { "executable": "bxAgents" }` を宣言しているので、実際のモジュールインストールはネイティブな `bxAgents` コマンドを生成します ([インストール](getting-started/installation.md) 参照)。この配線自体は自動テストで演習されていません - これは BoxLang のモジュールインストーラー自身が文書化している、実行ファイルラッパーを生成する挙動に依存しており、ソースを読んで確認されたものであって、このリポジトリ自身の CI でのインストール&実行テストによるものではありません。

## `schedules/Scheduler.bx` はビルド時に検証されない

これは実際の手書きの ColdBox コードがそのまま通されているため ([schedules/](conventions/schedules.md) 参照)、`build` は、かつて `{ cron, action }` config がチェックされていたような意味のあるチェックを行うことができません - 構文エラー、`getInstance( "..." )` 呼び出しのタイプミス、あるいは存在しないエージェント名の参照は、すべて `build` をクリーンに通過し、生成されたアプリが実際に起動したとき (`serve`) にのみ表面化します。このプロジェクトが中身を所有していない他のあらゆる実際の BoxLang クラスと同じです。`build` は、これに隣接するより狭い 1 つの間違いはキャッチします: 2 つのエージェント (ルートまたはどんな深さのサブエージェントでも) が同じ `name` を宣言していることです - これはこのプロジェクトが自身で生成する `config/WireBox.bx` のバインディング衝突なので、生成する他のすべてと同じように検証時にチェックされます。

## `test` 動詞の構築中に見つかった実際のバグ: `boxlang-miniserver` がクラスパスにあると「現在の BoxLang jar」の解決が曖昧になる

`TestRunnerService.bx` は、プロジェクトの `tests/specs` を実行するために新しい子プロセスを起動し、どの jar でそれを起動するかを知る必要があります。最初の実装は「このクラスがロードされた jar はどれか」という標準的なトリック (`BoxRuntime.class.getProtectionDomain().getCodeSource().getLocation()`) を使っていました - これは分離した手動テストでは機能しましたが、`serve`/`MiniServerLauncher` 関連のスペックのために `boxlang-miniserver-*.jar` もクラスパスに必要とする、このプロジェクト自身の完全な `testBx` スイートの一部として実行すると、予測不能に失敗しました。直接調査して確認されました: **`boxlang-miniserver-*.jar` は、`ortus.boxlang.runtime.BoxRuntime` 自身のコピーをバンドルしたファット jar です** - 両方の jar がクラスパスにあると、クラスローダーは `BoxRuntime.class` を、本物のランタイム jar ではなく miniserver jar に解決してしまうことがあり、`BoxRunner.main` の代わりに `MiniServer.main` (BoxLang スクリプトやテストレポートが一切生成される前に `--bx-config` を拒否して即座に終了コード 1 で終了します) をサイレントに起動してしまいます。これは `tests/specs/cli/TestSpec.bx` の実際のプロセスのケースが `exitCode=1` と空のレポートで失敗するという形で表面化しましたが、これは分離した状態ではなく、フルスイートを通して実行した場合にのみ再現され、それが見逃しやすくしていました。

`java.class.path` から jar を解決するように修正されました - `miniserver` を含ま**ない** `boxlang-*.jar` エントリを探し、そのようなエントリが見つからない場合にのみ、古い codeSource のトリックにフォールバックします。

## `deploy` の `ssh`/`docker`/`digitalocean` ターゲットは実際のプロセスによる自動テストで演習されていない

`SshTargetSpec.bx`/`DockerTargetSpec.bx` は、実際のバイナリを一切呼び出さずに、各ターゲットが構築する正確な `scp`/`ssh`/`docker` コマンド (実際の `ProcessBuilder` 引数配列) をアサートします - このスイートの他の箇所で、CI で `PATH` にないかもしれないバイナリが必要なものに対して使われているのと同じ「キャプチャするだけで実行しない」アプローチです (`MiniServerLauncherTest` の `assumeTrue` によるスキップを参照)。`local` のコピーロジックは本当に演習されています (`LocalTargetSpec.bx`)。このリファクタリングが修正した実際の潜在バグに対する回帰テストも含みます: 元々の `Deploy.bx` は「最新」の `.bxa` をファイル名の字句ソートで選んでおり、プロジェクトが 2 桁のバージョンに達すると (`v9.0.0` は `v10.0.0` の後にソートされます) サイレントに誤って選んでいました - 実際のファイル更新時刻でソートすることで修正されました (`DistArtifactLocator`)。

`DigitalOceanTargetSpec.bx` も同様に、純粋な `buildAppSpec()`/`findExistingAppId()` のロジックのみをユニットテストしています - 実際の `GET/POST /v2/apps` 呼び出しは CI では一切演習されていません。ライブな DigitalOcean アカウントと API トークンが必要だからです。どちらか一方に本番で依存する前に、実際の使い捨て VM に対して少なくとも一度は手動で `deploy --name=<ssh-entry>` を、実際の DO アカウントに対して使い捨てのアプリで `deploy --name=<digitalocean-entry>` を実行してください - 上記のモックプロバイダーのみのテストのギャップですでに使われているのと同じ、正直な枠組みです。

## `deploy` の `ftp`/`sftp` ターゲット: 実際の接続処理は証明済みだが、実際の成功するアップロードは証明されていない

外部バイナリをシェルアウトする (そのため*コマンド構築*だけが実サーバーなしでテストできる) `ssh`/`docker` とは異なり、`ftp`/`sftp` は実際の [`bx-ftp`](https://github.com/ortus-boxlang/bx-ftp) モジュールの `bx:ftp` コンポーネントを**インプロセスで**呼び出します - 「コマンドをキャプチャして実行しない」という選択肢はありません。`BaseFtpTargetSpec.bx` は代わりに、何も listen していないポートで `127.0.0.1` に対して本物の接続試行を行い、実際の接続拒否エラーが捕捉され、明確な `BxAgents.DeployFailed` として再送出されることをアサートします (bx-ftp 自身のソースにより、すべてのアクションはソフトな `succeeded: false` を返すのではなく失敗時に例外を投げることが確認されています)。これは実際の接続/エラーラップ/クリーンアップのパスをエンドツーエンドで証明していますが、証明できないのは実際の成功するアップロードです。

そのギャップは具体的に、**この開発サンドボックスには生の TCP の送信接続がまったくない**ためです - あるのはこの環境のプロキシを経由した HTTPS のみです (直接確認済みです: `curl ftp://test.rebex.net` と、公開 FTP ホストへの生の `/dev/tcp` 接続の両方がハングしてタイムアウトし、`docker info` は動いているデーモンがないことを示しているため、bx-ftp 自身にバンドルされた Docker の FTP/SFTP テストサーバーもここでは起動できませんでした)。これはサンドボックスの制約であり、コードの制限ではありません - どちらか一方に本番で依存する前に、実際に到達可能なサーバー (あるいは、Docker/ネットワークアクセスのあるマシンからの bx-ftp 自身の `docker-compose up` テストサーバー) に対して、少なくとも一度は手動で `deploy --name=<ftp-entry>` と `deploy --name=<sftp-entry>` を実行してください - 上記の `ssh`/`digitalocean` ですでに使われているのと同じ、正直な枠組みです。

## `build` は、生成の途中でクラッシュした場合に部分的に書き込まれた `.build/app` をロールバックしない

`BuildPipeline.build()` は、事前に `.build/app` を削除して再作成し、その後フェーズ 5 のジェネレータを順番に実行します。フェーズ 3 (`ProjectValidator`) がチェックできる入力はすべて、それが起こる前にチェックされるため、実際の生成途中のクラッシュは実際には稀であるはずです - しかし、もしそれでも発生した場合 (例えば、ロードに失敗する `models/`/`schedules/`/`mcp/` エントリ、あるいは環境/ファイルシステムの問題)、`.build/app` は、以前の内容に復元されたりクリーンアップされたりするのではなく、部分的に書き込まれた状態でディスクに残ります。その後の成功した `build` はそれをきれいに上書きするので、これは固着するものではありませんが、失敗したビルドと次のビルドの間に `.build/app` を検査するもの (CI ステップ、手動の `package` の再試行) は、壊れた半生成のアプリを見ることがあります。ロールバック/一時ディレクトリ経由の入れ替えステップはまだありません。

## `testBx` で観測された、間欠的な `StackOverflowError` (このプロジェクト自身のコードとは無関係)

このマイルストーンの調査中、`./gradlew testBx` は (毎回ではなく) 時折、BoxLang エンジン自身の汎用オブジェクト JSON シリアライゼーション (`DynamicObjectSerializer`/`BoxStructSerializer` が互いを交互に呼び合い、スタックが尽きるまで続きます - `-Xss16m` でも依然として発生することが確認されているので、単に深いだけの有限な構造ではなく、本物の循環です) の内部で `StackOverflowError` を出して JVM 全体をクラッシュさせました。`git stash` (このセッションの変更を一切適用しない状態で、`development` にある同じコミットに対して、クラッシュは同一に再現しました) と、`tests/specs/**` を個別にも組み合わせても、すべてのサブディレクトリに二分探索を行うことで (すべてのサブセット、さらにはすべての「1 つを除くサブセット」も、クリーンに実行できました) 分離を試みましたが、単一の完全な実行だけが時折それを再現し、クラッシュの直後に同一の完全なスイートを再実行すると、時にはクリーンに合格することもあります。これは、(`ExampleScheduler` 自身のライブなバックグラウンドの `everySecond()` タスクが、その時点で実行中のどのスペックとも並行して自身のスレッドプールから出力していることと相互作用している可能性がある) タイミング依存のレースコンディションを示唆しており、どれか 1 つのスペックやこのプロジェクト自身の生成コードのバグではありません。`testBx` が他に説明のつかない `StackOverflowError` で失敗した場合は、本当の後退だと決めつける前に再試行してください - 現時点ではオンデマンドで再現可能ではないため、これに対する自動回帰テストは存在せず、これを単一の根本原因までさらに追求することは今回のスコープ外でした。

## Push-style gateways (Telegram) are tested against a mocked API/scheduler seam only - no live platform integration runs in CI

`TelegramGatewaySpec.bx` は `TelegramGateway` 自身のロジック (受信の正規化、4096 文字上限での送信チャンキング、HITL インラインキーボードの構築、スケジューラタスクの登録/削除) を、注入可能な `apiCaller`/`setScheduler()` テストシームだけに対して演習します - 実際の Telegram Bot API 呼び出しも、実際の ColdBox スケジューラの起動も一度もありません。これはゲートウェイ自身のコードが正しいことを証明しますが、実際に Telegram の実 API やライブな実行中のスケジューラに対してエンドツーエンドで動作することを証明するものではありません。本番でこれに依存する前に、実際の `botTokenEnvVar` に紐付いた Telegram ボットを持つプロジェクトに対して、少なくとも一度は手動で `bxAgents serve` を実行してください - このファイルの他の箇所ですでに使われている、モックプロバイダーのみ/ライブ接続なしのテストギャップと同じ正直な枠組みです。同じ注意事項は、同じ方法で構築される、あらゆる将来の push 型ゲートウェイ (Slack、Discord、Email、WhatsApp) にも当てはまります。

`SlackGatewaySpec.bx` は同じギャップを、さらに一段深く抱えています: `SlackGateway` の永続的な websocket 接続は、注入可能な `setSocketOpener()` シーム (実際の `java.net.http.WebSocket` の代わりとなるフェイクオブジェクト) を通じてテストされているため、スペック内のすべてのフレーム処理/再接続ロジックのアサーションは、実際のネットワーク I/O をゼロで実行されます。実際に (モックではなく) 直接検証されたこと: スタンドアロンのスモークテストが、実際の `SlackSocketListener(gateway)` (これは `implements="java:java.net.java.net.http.WebSocket$Listener"` を直接持ちます - BoxLang はこれを本物の JVM 実装としてコンパイルし、プロキシは不要です) をインスタンス化し、実際の `HttpClient.newWebSocketBuilder().buildAsync(...)` を到達不能なアドレスに対して呼び出し、BoxLang から Java への相互運用自体がネットワーク境界まで正しく機能することを確認しました (キャスト/相互運用エラーではなく、プレーンな `java.net.ConnectException` で失敗しました) - しかし、ここのどのテストも、Slack の実際のサーバーに対して本物の Socket Mode ハンドシェイクを完了させたことは一度もありません。本番でこれに依存する前に、実際の `botTokenEnvVar`/`appTokenEnvVar` に紐付いた Slack アプリの認証情報を持つプロジェクトに対して、少なくとも一度は手動で `bxAgents serve` を実行してください。

`DiscordGatewaySpec.bx` は、まったく同じ理由で、同一のギャップを抱えています: `DiscordGateway` のフレーム処理/ハートビート/再接続ロジックは、注入可能な `setApiCaller()`/`setSocketOpener()` シームに対してのみ演習されており、実際のネットワーク I/O はゼロです。同じスタンドアロンのスモークテストの規律がここにも適用されました - 実際の `HttpClient.newWebSocketBuilder().buildAsync(...)` 呼び出しを到達不能なアドレスに対して駆動する `gateway.onConnect()` は、キャスト/相互運用エラーではなくプレーンな `java.net.ConnectException` で失敗し、相互運用チェーンが機能することを確認しています。検証されなかったこと: Discord の実際のサーバーに対する実際の Gateway ハンドシェイク (`Hello` → `Identify` → `READY`)、Discord 自身の許容範囲下での実際のハートビートのタイミング、そして Discord Developer Portal で実際のボットに対して `MESSAGE_CONTENT` が有効化/承認された後の、デフォルトの `intents` の値 (`GUILDS`+`GUILD_MESSAGES`+`DIRECT_MESSAGES`+`MESSAGE_CONTENT` = `37377`) が実際にメッセージコンテンツを受信するのに十分であるかどうかです。本番でこれに依存する前に、実際の `botTokenEnvVar` に紐付いた Discord ボット (`MESSAGE_CONTENT` を有効化済み) を持つプロジェクトに対して、少なくとも一度は手動で `bxAgents serve` を実行してください。

`EmailGatewaySpec.bx` は、より大きなバージョンの同じギャップを抱えています。受信 IMAP は、注入可能な `setImapPoller()` シーム (缶詰の正規化済みメッセージ構造体、実際のメールボックスなし) を通じて完全にテストされており、送信も注入可能な `setMailService()` シーム (`MailService@cbmailservices` の代わりとなる `FakeMailService`/`FakeMail`) を通じて完全にテストされています - これらのスペックでは `EmailGateway` は実際の WireBox を一切通過しません。実際の ColdBox の起動が存在しないためです。今回のセッションで (モックではなく、想定でもなく) 実際に直接検証されたこと: `fetchInboundMessages()` が依存する実際の `jakarta.mail` API 表面 (`Session.getDefaultInstance()`、`Flags`/`Flags.Flag`/`FlagTerm`、`Store.getStore("imaps")`、`Folder.READ_WRITE`、`MimeMultipart`、`InternetAddress`) を、実際の `jakarta.mail-api`/Angus Mail の jar (これらはこのリポジトリ自身のテストクラスパスにベンダリングされていないため、これのためにスタンドアロンでダウンロードされました) に対して確認し、使われているすべてのクラス/メソッド名が実際に存在し解決されることを確認しました。到達不能なアドレスに対する実際の `Store.connect()` を、(バイパスされたヘルパーではなく) `EmailGateway.pollInbox()` 自体を通して駆動すると、相互運用/キャストエラーではなく、プレーンな接続タイムアウトエラーで失敗し、相互運用チェーンが実際のネットワーク境界に正しく到達することを確認しました。Slack/Discord の websocket スモークテストと同じ規律です。明示的に検証*されなかった*こと、そしてチャットプラットフォームのゲートウェイよりも厳密に大きなギャップであること: 実際のメールボックスに対する実際の IMAP ハンドシェイクはなく、このリポジトリにもそのテストハーネスにも、実際の `cbmailservices`/`bx-mail` モジュールがインストールされていません (`bx-ai`/TestBox のようにはベンダリングされていません - 同じワークアラウンドが別のモジュールに適用されている下記のスナップショット遅延のエントリを参照してください)。そのため WireBox の解決パス (`MailService@cbmailservices` が実際に存在すること、`BXMail` の `bx:mail` 呼び出しが実際に送信すること) は、モックでも実物でも、このコードベースで一度も演習されていません。本番でこのゲートウェイに依存する前に、実際の IMAP 認証情報**と**実際にインストールされた `cbmailservices`/`bx-mail` (`box install` が成功し、`moduleSettings.cbmailservices` が解決することを確認) を持つプロジェクトに対して、少なくとも一度は手動で `bxAgents serve` を実行してください - これは、これまでに出荷された 4 つの push 型ゲートウェイの中で最も検証が薄いものです。

`WhatsAppCloudGatewaySpec.bx` は、モックではなく、ゲートウェイ自身のロジックを本物として十分にカバーしています: 署名検証パスは、実際に計算された HMAC-SHA256 署名 (`javax.crypto.Mac`/`SecretKeySpec`。BoxLang の計算を信頼する前に、今回のセッションで `openssl dgst -hmac` と Python 自身の `hmac` モジュールの両方に対して独立してクロスチェックされました - 実際の参照ベクトルの不一致が捕まりましたが、それは BoxLang のバグではなく、手でコピーした期待値のタイプミスであったことが判明しました。しかしそれはクロス検証だけが捕まえられたものです) で演習されており、verify ハンドシェイク、webhook のディスパッチ/重複排除、送信、インタラクティブなボタン/リストのレンダリングはすべて、送信の Graph API HTTP 呼び出しだけがスタブ化された (`setApiCaller()`) 状態で、ゲートウェイの実際の公開メソッドを通して駆動されています。検証*されなかった*こと: 生成される `handlers/WhatsAppCloud.bx` 自身の ColdBox リクエストコンテキスト呼び出し (`event.getHTTPContent()`/`event.getHTTPHeader()`/`event.renderData()`、GET ハンドシェイク用の `rc` の URL スコープにマージされたドット区切りキーのクエリパラメータアクセス) を、実際の ColdBox の起動に対して行うことです - これらは文書化された標準的な ColdBox REST ハンドラのイディオムです (ColdBox 自身の "Building REST APIs" レシピドキュメントに照らして確認済みで、推測ではありません)。このファイルの他の箇所で誤りだったことが判明した文書化されていない `aiGatewayRegistry()` のキーの想定よりは、意味のある形でより信頼できる出発点ですが、「文書化されている」ことは「この生成されたコンテキストで動作すると証明されている」こととは違います。このプロジェクト自身の実際の `runColdBoxIntegrationTests.bxs`/miniserver ハーネスを拡張してこれをカバーするには、別途起動される miniserver サブプロセスにフェイクの環境変数を渡す (そのための既存の仕組みはありません) か、他の合格しているテストが依存している共有の `e2e-coldbox-route` フィクスチャで実際の config 関連の起動失敗のリスクを冒すかのいずれかが必要であり、レジストリキーのバグよりも「実際に間違っている確度が低い」このギャップのためにその爆発半径を冒すよりも見送られました。本番でこのルートに依存する前に、実際に `bxAgents serve` を行い、`/webhooks/whatsapp-cloud` に対する本物の Meta Webhook テスト (あるいは `curl`) を少なくとも一度は行ってください。実際の Graph API 呼び出しも一度も行われていません - `deliver()`/`requestHumanInteraction()` の HTTP レイヤーは `apiCaller` テストシームを通じてのみ演習されています。

`TeamsGatewaySpec.bx` は、モックではなく、ゲートウェイ自身のロジックを本物として十分にカバーしています: JWT の検証は、実際に生成された 2048 ビットの RSA 鍵ペア (`java.security.KeyPairGenerator`) と、スペック内にすべて組み込まれた手署名のテスト JWT (事前計算されたフィクスチャなし、テスト時の外部 `openssl` 依存なし) に対して演習されており、有効な署名は受理されディスパッチされる一方で、改ざんされた署名、誤った `aud`、誤った `iss`、期限切れの `exp` は、それぞれ独立して 401 で拒否されることが確認されています。invoke アクティビティ (Adaptive Card ボタンクリック) パス、メッセージディスパッチ、個人スコープのみのフィルタリング、`replyToId` によるスレッディング、チャンキング、Adaptive Card のレンダリングは、送信の Connector REST 呼び出しだけがスタブ化 (`setApiCaller()`) され、JWKS/OAuth2 トークンの取得もスタブ化された (`setJwksFetcher()`/`setTokenFetcher()`) 状態で、すべてゲートウェイの実際の公開メソッドを通して駆動されています。検証*されなかった*こと: 生成される `handlers/Teams.bx` 自身の ColdBox リクエストコンテキスト呼び出しを、実際の ColdBox の起動に対して行うこと (WhatsApp Cloud 自身のハンドラと同じカテゴリのギャップで、同じ理由で見送られています - 上記のそのエントリを参照)。実際の OAuth2 トークン取得も Connector REST 呼び出しも、Microsoft の実際のエンドポイントに対して行われたことは一度もありません。そして、インスタンスの生存期間中 JWKS がキャッシュされるというトレードオフ (`docs/conventions/gateways.md` の Teams セクション参照) は、実際の鍵ローテーションのシナリオも一度も演習されていないことを意味します。本番でこのゲートウェイに依存する前に、実際に `bxAgents serve` を行い、実際の Teams アプリ登録 (Azure/Bot Framework ポータルからの App ID/パスワード) と実際の Teams クライアントからの DM 送信を、少なくとも一度は行ってください。

`TwilioGatewaySpec.bx` は、モックではなく、ゲートウェイ自身のロジックを本物として十分にカバーしています: `X-Twilio-Signature` の HMAC-SHA1/base64 検証パスは、スペック内でインラインに組み立てられた実際に計算された署名で演習されており、BoxLang の実装を信頼する前に、今回のセッションで Python 自身の `hmac`/`hashlib` モジュールに対して独立してクロスチェックされました (WhatsApp Cloud 自身の HMAC-SHA256 クロスチェックと同じ規律です) - 既知の認証トークン/URL/パラメータの組み合わせに対する実際の参照署名が Python で計算され、BoxLang の出力と正確に一致することが確認されています。フォームボディのパース (リテラルな `+` が `%2B` パーセントエンコーディングを通して正しく往復することも含む)、プロキシ/トンネルデプロイ用の `publicUrl` オーバーライド、TwiML の即時確認応答と非同期 REST 返信のデュアルパスモデル、送信チャンキング、そして電話番号キーの HITL 返信の紐付けは、送信の Messages API HTTP 呼び出しだけがスタブ化された (`setApiCaller()`) 状態で、すべてゲートウェイの実際の公開メソッドを通して駆動されています。検証*されなかった*こと: 生成される `handlers/Twilio.bx` 自身の `event.getUrl()` 呼び出しを、実際の ColdBox の起動に対して行うこと (WhatsApp Cloud/Teams 自身のハンドラと同じカテゴリのギャップで、同じ理由で見送られています) - `event.getUrl()` は文書化された ColdBox の Routable/Request Context メソッドです (ColdBox のドキュメント MCP 経由で確認済みで、推測ではありません) が、「文書化されている」ことは「この生成されたコンテキストで証明されている」ことではありません。実際の Twilio Messages API 呼び出しも一度も行われていません。本番でこのルートに依存する前に、実際に `bxAgents serve` を行い、実際の Twilio 電話番号の Webhook テストを少なくとも一度は行ってください - そして、電話番号キーの HITL 紐付け (`docs/conventions/gateways.md` の Twilio セクション参照) には、実際の、文書化された制限があることに注意してください: 同じ電話番号への 2 回目の HITL リクエストが、最初のものがまだ答えられていない状態で来ると、最初の `pendingApprovals` エントリを上書きしてしまい、サイレントにそれを孤立させてしまいます。受信 SMS に対するアローリスト/レート制限も構築されていません - 自身の `allowFrom` config が「必須」であると文書化はしている (がコード上では強制していない) Eve とは異なり、この移植版にはそれと同等のゲートがまったくありません。どんな電話番号でもデプロイされた Twilio 番号にメッセージを送ってエージェントに到達できます。

`GitHubGatewaySpec.bx` は、モックではなく、ゲートウェイ自身のロジックを本物として十分にカバーしており、このゲートウェイのコアロジックはさらに、開発中に (恒久的なスペックだけでなく) スタンドアロンの実際の BoxLang によるスモークテストを通して駆動されました - これが、本物のバグがテストスイートに一度も届く前に捕まった経緯です: メンション抽出ヘルパーの部分文字列ロジックは、`@mention` がコメントの本当に先頭にある場合 (非常によくあるケースです) には常に `left( body, 0 )` を呼び出しており、BoxLang の `left()` はゼロ文字数に対して空文字列を返す代わりに `"Count cannot be zero"` を投げます - 実際のコメント本文でこれをトリガーすることで確認され、`left()`/`mid()` がゼロ長を許容すると想定するのではなく、ゼロ長のケースを明示的に分岐させることで修正されました。`X-Hub-Signature-256` の検証、`@mention` の正規表現先読みゲート (`mybot` という名前のボットが `@mybot2` では発火しないことを、専用のスモークテストで確認済み)、ボットループのガード、配信 ID の重複排除、issue とレビュースレッドの会話アイデンティティ、`deliver()`、そして `@mention` から返信への HITL 紐付けは、送信の GitHub REST 呼び出しだけがスタブ化された (`setApiCaller()`) 状態で、すべてゲートウェイの実際の公開メソッドを通して駆動されています。検証*されなかった*こと: 生成される `handlers/GitHub.bx` を実際の ColdBox の起動に対して行うこと (このプロジェクトの他のすべての Webhook ゲートウェイ自身のハンドラと同じカテゴリのギャップで、同じ理由で見送られています)。実際の GitHub API 呼び出しは一度も行われておらず、実際の GitHub App/PAT が実際のリポジトリに対して使われたこともありません - 本番でこのゲートウェイに依存する前に、実際に `bxAgents serve` を行い、テストリポジトリに対して設定された実際の GitHub Webhook を、少なくとも一度は行ってください。

`SignalGatewaySpec.bx` は、モックではなく、ゲートウェイ自身のロジックを本物として十分にカバーしています: `handleSseEvent()` の JSON-RPC/SSE パース (空行/不正な JSON の処理、グループメッセージのフィルタリング、引用スレッディング、表示名がない場合の `sourceUuid` フォールバック)、`deliver()` の送信の形とチャンキング、そして HITL 決定の紐付け/マッチングは、すべて送信の `rpcCaller`/`connector` の I/O 呼び出しだけがスタブ化された状態で、ゲートウェイの実際の公開メソッドを通して駆動されています。このプロジェクトの他のすべてのゲートウェイと同じシームテストの規律です。開発中に 2 つの BoxLang レベルの発見があり、どちらも解決済みで、ゲートウェイ固有のバグというより一般的な地雷として記録する価値があります: (1) ローカル変数を `request` と名付けたスタンドアロンのスモークテストスクリプトが、プレーンな変数を作る代わりに、サイレントに BoxLang 自身の予約済み `request` スコープと相互作用しており、本物の Java 相互運用の制限のように見える (しかし変数名を変えるとまったく消える) 「method not found」/「argument type mismatch」という紛らわしいエラーを `HttpClient.send()` から出していました - `SignalGateway.bx` 自体には一度もバグはありませんでした。(2) スタンドアロンの `.bxs` スモークテストスクリプトの (関数の内部ではなく) トップレベルに直接置かれた `try/catch` が `java.lang.VerifyError: Inconsistent stackmap frames` をトリガーしました。これは BoxLang のトップレベルスクリプトコンパイラの本物のバイトコード検証の制限で、スタックトレースを辿ってテストスクリプト自身の生成されたクラスであることが判明し、`SignalGateway.bx` のものではありませんでした - try/catch を名前付きの関数の内部にラップすることで修正されました。検証*されなかった*こと、そしてこれまでに出荷されたすべての push 型ゲートウェイの中で最大のギャップであること: この環境には実際の `signal-cli` デーモンが一度も利用可能でなかったため、非同期 SSE 接続のライフサイクル全体 - `HttpClient.sendAsync()`+`BodyHandlers.ofLines()` によるストリームのオープン、本当に不安定な接続に対する指数バックオフの再接続ループ、30 秒/120 秒のアイドルウォッチドッグが強制する再接続、そしてライブな JSON-RPC のラウンドトリップ - は、一度もエンドツーエンドで演習されておらず、相互運用のプラミングレベルでスモークテストされているだけです (スタンドアロンのテストが到達不能なアドレスに対して実際の `java.net.ConnectException` に到達し、チェーンが健全であることを証明していますが、ライブなデーモンに対して動作することの証明ではありません)。本番でこのゲートウェイに依存する前に、実際に稼働している `signal-cli` デーモンと実際にリンクされた Signal アカウントを持つプロジェクトに対して、少なくとも一度は手動で `bxAgents serve` を実行してください - これはこのコードベースにおいて本当に新しい転送アーキテクチャであり (Telegram/Slack/Discord/Email/Signal の中で唯一の SSE ベースのゲートウェイです)、すでに証明されている転送形の上の単なる新しいプラットフォームではありません。

## WhatsApp Personal (非公式の個人アカウントブリッジ) - 調査済み、未構築

元々の計画 (Hermes Agent 自身のアーキテクチャに合わせたもの) は、`@whiskeysockets/baileys` (Hermes 自体が使う、マルチデバイス WhatsApp Web プロトコルクライアントで、MIT ライセンスです。その完全な `bridge.js` は要約ではなく、今回のセッションで Hermes の実際のソースから直接読み込まれました) を実行する Node.js サブプロセスを起動することで構築される `WhatsAppPersonalGateway` を求めていました。そのアプローチは、サブプロセスブリッジよりもネイティブな BoxLang/JVM の統合を優先し、ネイティブな Java ライブラリを使うのはそれがオープンソースで GPL でも LGPL でもない場合に限るという直接の指示を受け、セッションの途中で見送られました。

その調査で見つかったのが **Cobalt** (`com.github.auties00:cobalt`、旧 WhatsappWeb4j) です - これは実在する、MIT ライセンスの、活発にメンテナンスされている (900 以上のスター) WhatsApp のマルチデバイス「リンククライアント」プロトコルの Java 実装で、文書化された流暢な API (`WhatsAppClient.builder().linkedApi().webClient()...`、`addNewMessageListener()`、`sendMessage()`) を持ち、単一抽象メソッドのリスナーに対して BoxLang 自身が文書化している Java-SAM 強制からきれいに変換できます (BoxLang のドキュメント MCP 経由で確認済みで、単一抽象メソッドのリスナーに `createDynamicProxy()` は不要です)。検証中に、推測ではなく実際に 2 つの実在するブロッカーが表面化しました:

1. **最初に読んだ pom.xml (Cobalt の `master` ブランチ、進行中のマルチモジュールの書き直し) は Java 25 を要求します** - これは BoxLang 自身が文書化しているベースライン (Java 21 以上、BoxLang のドキュメント MCP 経由で確認され、このプロジェクト自身の `21.0.10` の JDK と一致しています) より 2 メジャーバージョン先です。実際に Maven Central に公開されているアーティファクト (`cobalt:0.0.10`、`<dependency>` が今日実際に解決するもので、未リリースの書き直しではありません) に対して再チェックすると、`<java.version>21</java.version>` が示されました - つまり Java 25 の発見は、間違ったブランチを読んだことによる誤警報であり、本物のブロッカーではありませんでした。注意点として記録する価値があります: GitHub リポジトリのデフォルトブランチの `pom.xml` は、必ずしも Maven Central にあるものと同じではありません。
2. **実際に公開されている `cobalt:0.0.10` は、`com.aspose:aspose-words` をハードなコンパイル時依存として引き込みます** (Word 文書からリンクプレビューのサムネイルを生成するために内部で使われています) - Aspose.Words for Java は商用/プロプライエタリなライセンスであり、オープンソースではありません。そのため、これをバンドルすると、Cobalt 自体がそもそも満たすために選ばれたのと同じライセンス制約に違反することになります。完全な依存関係グラフ (約 15 個の jar: zxing、qr-terminal、curve25519、protobuf-base、バージョンによって jackson か fastjson2、libphonenumber、dd-plist、apk-parser、link-preview、jaffree、ez-vcard、slf4j、そして Aspose) はすべて手動でダウンロードし、このモジュールの `libs/` フォルダにバンドルする必要があります - BoxLang のモジュールには、独自の Maven 風の依存関係解決がありません (BoxLang のドキュメント MCP 経由で確認済みです: サードパーティの jar はモジュールの `libs/` フォルダに直接バンドルされ、モジュールごとのクラスローダーによってロードされます - 依存関係グラフを自動的に解決する `javaLibraries` キーは `box.json` にはありません)。

Aspose によるライセンス汚染と、結果が実際にロードされることを検証する依存関係解決ツールが一切ない中での手作業によるファット jar 組み立ての手間を考慮し、**WhatsApp Personal は、実際のゲートウェイとしても、スタブとしても出荷されず、スコープ外とされました**。`ProjectValidator` の `validGatewayTypes` と `GatewayGenerator` の `TYPE_CLASS_MAP` には `whatsapp-personal` エントリが含まれていません - `type: "whatsapp-personal"` を宣言しようとしたプロジェクトは、誤解を招く半端に構築されたスタブではなく、他のあらゆる未サポートタイプと同じ「unknown gateway type」検証エラーになります。これを見直すのは、Cobalt がいつか Aspose への依存を落とした場合 (これは狭い機能です - Word 文書のリンクプレビューのサムネイル化であり、メッセージングのコアではありません)、あるいは将来のセッションが、(今回はアーキテクチャの好みで却下された、技術的なブロッカーではない) Node/Baileys サブプロセスブリッジアプローチをやはり選ぶと判断した場合には、妥当です。

## `GatewaySession` はプロジェクト全体で 1 つ、かつルートエージェント専用 (v1)

少なくとも 1 つの push 型ゲートウェイエントリを持つプロジェクトは、正確に 1 つの生成された `GatewaySession` を得ます。これはすべての push 型ゲートウェイをまとめ、常にプロジェクトのルートエージェントに束縛されます - `exposes: "agent"` の HTTP 公開も常にルートエージェントのみであるという既存の前例と一致しています ([gateways/](conventions/gateways.md#3-push-style-gateways-type-telegram--slack--discord--email--whatsapp-cloud--teams--twilio--github--signal-and-friends) 参照)。サブエージェントを持つプロジェクトは、まだ異なるゲートウェイを異なるサブエージェントにルーティングすることはできません (例えば「Telegram は SupportBot と話し、Slack は ResearchBot と話す」)。将来的な、エージェントごとのノードの `GatewaySession` に消費される、ゲートウェイごとの `targetAgent: "SubagentName"` キー (プロジェクト全体で 1 つのセッションの代わりに) は、自然な拡張ポイントですが、まだ構築されていません。

## 修正済み: `GatewaySessionBootstrap.bx` は誤った `aiGatewayRegistry()` キーでゲートウェイを検索していた (4 つすべての push 型ゲートウェイにわたって壊れた状態で出荷されており、WhatsApp の調査の過程で発見された)

実際に、以前出荷されていたバグです: 生成されたインターセプターの `aiGatewayRegistry().get(...)` 呼び出しは、発見された `gateways/*` エントリ自身のファイル名 (例えば `gateways/telegramChannel.bx` からの `"telegramChannel"`) を使っていましたが、bx-ai の実際の `GatewayRegistry.register()` は常にゲートウェイ**クラス**自身の固定された `getName()` (例えば `TelegramGateway.init()` で一度だけ設定される `"telegram"`) でキー付けします - 呼び出し元が指定したものは一切使われません。bx-ai のソースを直接読むことと、経験的に (実際のゲートウェイを登録し、その発見されたエントリ名で `.get()` を呼び出すと `"No item found in registry"` が投げられることを確認して) 両方で確認済みです。これはつまり、`GatewaySession` の構築が、push 型ゲートウェイを持つ**すべての**生成プロジェクトについて、ColdBox の起動時 (`afterConfigurationLoad`) に投げてしまうことを意味していました - Telegram、Slack、Discord、Email はすべてこのバグとともに出荷されており、これは既存のテストカバレッジが生成されたファイルの生の文字列内容に対してだけアサートし、ライブなレジストリに対しては一度もアサートしていなかったため、検出されずにいました。

`GatewayGenerator.generate()` で修正されました: このインターセプターは今や、それらのゲートウェイを TYPE 文字列 (これまでに構築されたすべての push 型ゲートウェイにおいて、常に登録された名前と同一です) で、重複排除しながら検索します。`GatewayGeneratorSpec.bx` は恒久的な回帰テストを得ました。これは本物のゲートウェイインスタンスを登録し、ジェネレータがちょうど出力したキーが、ライブな `aiGatewayRegistry()` 経由でそれを解決することを証明します - これは、最初にこれが検出されずに出荷されることを許してしまった、まさにそのギャップを塞ぐものです。

**この修正が表面化させる、実際の恒久的な帰結 (新しい挙動ではなく、正しく到達可能になっただけです)**: レジストリはエントリではなくタイプでキー付けされているため、**同じ push 型タイプの `gateways/*` エントリが 2 つあると、プロジェクト全体で同じレジストリスロットに衝突します** - 例えば `type: "telegram"` の 2 つのエントリ (異なる 2 つのボットトークン) は、2 つ目の登録がサイレントに最初のものを上書きし、`GatewaySession` はそのうちの一方しか一度も見ることがありません。今日時点では、エントリごとのエイリアス/登録名のオーバーライドはありません。プロジェクトあたり push 型タイプごとに 1 インスタンス、というのが実際の v1 の上限です - この修正がそれを可視化するまでは、そのようには文書化されていませんでした。

## `./gradlew downloadModules` が、`GatewayGenerator` の `aiGatewayRegistry()` コード生成に一時的に遅れた bx-ai のスナップショットを取得することがある

bx-ai は `development` ブランチで `gatewayRegistry()` を `aiGatewayRegistry()` にリネームしました (直接確認済みです - `bifs/gatewayRegistry.bx` は完全に削除され、後方互換のエイリアスはありません)。`GatewayGenerator` はこれに一致するよう更新されました。bx-ai がまだリリースをカットしていないため、このプロジェクト自身の方針は、それを回避策で覆い隠すのではなく、そのまま追従することだったからです。落とし穴: `downloadModules` は、固定された、継続的に再公開されるスナップショットアーティファクト (`bx-ai@3.4.0-snapshot`) を `downloads.ortussolutions.com` から取得しますが、その公開済みの zip は bx-ai 自身の git `development` HEAD にある程度遅れることがあります (今回のセッションで直接確認済みです: この upstream のリネームが着地した直後、ダウンロード可能なスナップショットにはまだ古い `gatewayRegistry.bx` が残っていました)。この改名より前のスナップショットを新しい `downloadModules` が取得してしまうと、チャネルアダプタの `gateways/*` エントリを持つプロジェクトは、生成されたコードが今や新しい名前を呼び出しているにもかかわらず、取得されたモジュールがまだ古いものしか持っていないため、`Function 'aiGatewayRegistry' not found` で起動に失敗します。これは BX Agents 側から修正できるものではありません - ForgeBox が bx-ai の現在の `development` からスナップショットを再公開すれば、自然に解決します。このプロジェクト自身の生成コードが、あるいは古くなっているかもしれないダウンロード済みの zip に頼るのではなく、その git ソース (`ortus-boxlang/bx-ai`) から直接ローカルなモジュール構造を構築することで、bx-ai の本当の HEAD に対して正しいことが検証されています。

push 型ゲートウェイ/`GatewaySession` の作業を構築している最中に、同じ遅延を独立して再確認しました: ダウンロードされた `bx-ai@3.4.0-snapshot` は、その時点でまだ `GatewaySession.bx` を持っておらず、`aiGatewaySession()`/`aiGatewayRegistry()` BIF もなく、`onMessage()`/`onError()` を一切持たない `BaseGateway.bx`/`IGateway.bx` でした - この同じリネームよりずっと前のスナップショットです。`testBx` はこの状態で、`src/test/resources/modules/bxai` の `bifs/`/`models/`/`public/`/`ModuleConfig.bx` を、bx-ai 自身の git ソースからの新しいコピーに置き換えることで実行されました (`libs/`/`box.json` はそのままです)。上記と同じワークアラウンドです。これは `build`/`serve` のエンドユーザーが自分で行う必要のあることではなく、ForgeBox が追いつくまでの、このセッション自身の CI なしの検証のために必要だったものです。

**CI は今や、これを自動的に回避します。そうする必要があったからです。** このスイートの最初の実際の GitHub Actions 実行が、この遅延が見た目だけのものではないことを証明しました: 公開されている `bx-ai@3.4.0-snapshot` は依然として `bifs/gatewayRegistry.bx` を出荷しており (upstream ではとうの昔に `aiGatewayRegistry` にリネームされています)、そしてこのモジュールの 9 つすべての push 型ゲートウェイが extend している、**`models/gateway/BaseGateway.bx` を一切含んでいません**。これに対してテストすると、このモジュール自身のコードとは無関係な理由で 41 個のスペックが失敗します (`The method aiGatewayRegistry does not exist`、続いて存在しないベースクラスからカスケードするすべてのゲートウェイスペックです)。そのため `.github/workflows/tests.yml` は bx-ai の `development` ブランチをクローンし、その `createModuleStructure` を実行し、その結果を `downloadModules` が取得したものの上にオーバーレイします - 上記の手動ワークアラウンドを自動化したものです。`bx-ftp` と `bx-sqlite` は安定版リリースであり、依然として `downloadModules` からそのまま取得されます。公開されたスナップショットが追いついたら、このオーバーレイステップを削除してください。それまでは、CI はピン留めされたアーティファクトではなく bx-ai のブランチ HEAD に対してテストしているため、upstream の破壊がここでは bx-agents の失敗として表面化することに注意してください。

Slash `/compact` の配線中に、3 回目としてこれに遭遇しました: bx-ai は `development` (コミット `f9ac7bd`) で `IAiMemory.summarize()` を `userId`/`conversationId` によってスコープしましたが、新しい `downloadModules` は依然として、古い単一引数の `summarize( struct config = {} )` を持つスナップショットを取得しました。同じワークアラウンド - モジュールは bx-ai 自身の git ソースから (そのリポジトリで `./gradlew createModuleStructure` を実行し、`src/test/resources/modules/bxai` に上書きコピーして) 再構築され、スコープされた挙動はそのビルドに対して直接検証されました。以前の 2 回とは異なり、これはエンドユーザーが実際に遭遇しうるランタイム上の帰結を持ちます: `/compact` は `mem.summarize( config, userId, conversationId )` を呼び出しますが、その コミットより前の bx-ai では、追加の引数は単に無視されるため、圧縮は呼び出し元の会話ではなく、そのメモリインスタンスの*デフォルト*スコープを要約してしまいます。そのため、生成されるアプリは `/compact` に限っては `f9ac7bd` 以降の bx-ai を必要とします。他のすべてのルートには影響しません。このリポジトリ自身のテストスイートは、古いスナップショットでも何一つ後退しません。Web UI のスペックは実行するのではなく、生成されたソーステキストに対してアサートしているためです。

## v1 の Web チャット UI (`exposes: "webui"`) - 実物であることと、ドキュメントに照らしてしか確認できず、ライブなサーバーに対してではなかったこと

`WebUiGeneratorSpec.bx` と `BuildPipelineSpec.bx` のエンドツーエンドテストはどちらも、実際のフィクスチャに対して実際の `WebUiGenerator`/`BuildPipeline` クラスを駆動します: 静的な `<path>/index.html` シェルが正しく書き込まれ、正しくテンプレート化されていること (`__API_BASE__`/`__APP_TITLE__` のプレースホルダーが置換され、出力に一切残らないこと) が確認されており、任意の `interceptors/WebUiAuthGate.bx` は `apiKeyEnvVar` が設定されている場合にのみ生成され、`<path>/api/*` だけをゲートし、素の `<path>` シェル自体は決してゲートせず、`config/ColdBox.bx` の `interceptors:[...]` リストに正しく登録されることが、実際の `BuildPipeline` を通してエンドツーエンドで確認されています。生成されたインターセプターは、今回のセッションでスタンドアロンのスモークテスト (ビルドパイプライン自体が使うのと同じ `DynamicClassLoader` プリミティブでロードされました) を通じて、正しくコンパイル・インスタンス化されることも確認されています。

検証**されなかった**こと - そしてこの開発環境ではできなかったこと: 実際の `bxAgents serve` + 実際のブラウザテストによる、ページが実際にロードされ、返信がストリーミングされ、`X-API-Key` ゲートが実際に本物の HTTP 経由でリクエストを拒否/受理することです。このプロジェクト自身の `runColdBoxIntegrationTests.bxs`/`tests/coldbox` ハーネス (`http-gateway-agent` の `toAi()` の `/invoke` ルートがエンドツーエンドで動作することを証明したのと同じもの) は、`tests/` の内部で実際に `box install` を行うことで `tests/coldbox` が存在することを必要としますが、CommandBox と ForgeBox のネットワークアクセスはどちらもこのセッションのサンドボックスでは利用できなかったため、Web UI に対して (あるいは他の何かについても再確認するために) このセッションでこのハーネスを演習することはできませんでした。これには 2 つの直接的な帰結があります:

- このプロジェクト自身の生成されたページ/ドキュメントが使う正確な `/invoke` JSON レスポンス形状 (入力が `{"input": "..."}`、レスポンスに `"success": true` を含む) は、`runColdBoxIntegrationTests.bxs` 自身のすでに合格していた既存のアサーション (完全な形のアサーションではなく、部分文字列チェック) 経由でのみ経験的に確認されており、このセッションで再検証されたものではありません。
- Web UI 自身の JS がパースする `/stream` の SSE ワイヤーフォーマット (`data: {"token":"..."}` 行で、`data: [DONE]` で終端されます) は、ColdBox 自身の公式な「AI Routing」ドキュメントから直接取られたものであり、このセッションでライブなサーバーに対して独立して再確認されたものではありません - このプロジェクトのドキュメントにある他のほとんどすべてのワイヤーフォーマットの主張が、可能な限り実際に動くコードに照らしてクロスチェックされている (このファイルの他の箇所にある HMAC-SHA256/SHA1 の署名方式の独立した Python/openssl のクロスチェックなどを参照) のとは異なります。

本番でこの機能に依存する前に、実際に `bxAgents serve` + 実際のブラウザテスト (メッセージが送信され、返信がストリーミングされ、`X-API-Key` ゲートがキーを欠いたリクエストを実際に 401 で拒否すること) を少なくとも一度は行ってください - このファイルの他の箇所で、生成されるあらゆる Webhook ハンドラ自身の、実際の起動に対して未検証というギャップについてすでに与えられているのと同じ標準的な助言です。

## Web UI の SQLite ストア - ライブラリレベルでは検証済み、実際の ColdBox の起動を通してではない

`models/ChatDb.bx` の背後にある qb + bx-sqlite のスタックは、ドキュメントから推測されるのではなく、今回のセッションで実際の jar に対して直接検証されています: qb 13.1.0 の `.cfc` ソースは `bx-compat-cfml` なしで BoxLang 1.16 上でネイティブにコンパイル・実行されます。`SQLiteGrammar` + `SchemaBuilder` は本当に v1 のテーブルとインデックスを実際の SQLite ファイルに対して作成します。2 回目のマイグレーションパスはクリーンな無操作です。`QueryBuilder` は挿入、フィルタ済み/ソート済みの読み取り、削除を往復させます。そして `preferences` の複合主キーは、本当に `(userId, prefKey)` の重複を拒否します。この過程で、想定ではなく実際に 2 つの制約が見つかりました - qb は**名前付きの**データソースを必要とします (自身の `appendSqlComments()` はその引数を `string` として型付けしているため、インライン構造体は SQL が一切実行される前に例外を投げます)、そして `SchemaBuilder@qb` はその `grammar` 引数のみでマッピングされ、`moduleSettings.qb.defaultOptions` を一切受け取りません - そして生成されるコードは、この両方に沿った形になっています。

検証**されなかった**こと。Web UI の他の部分と同じ理由です: これは実際の ColdBox の起動の内部で一度も実行されていません。`tests/coldbox` には実際の `box install` が必要ですが、CommandBox/ForgeBox のネットワークアクセスはこのセッションのサンドボックスでは利用できませんでした。そのためマイグレーションロジックは証明済みですが、3 つの配線上の想定はエンドツーエンドでは演習されていません: `getInstance( "ChatDb" )` が生成されたアプリ自身の WireBox を通して解決されること、`SchemaBuilder@qb`/`QueryBuilder@qb` が qb が実際の ColdBox モジュールとしてインストールされた際に解決されること (qb は `box.json` の依存関係であり、ここではベンダリングされていません - `cbmailservices` がすでに抱えているのと同じ正直なギャップです)、そして生成される `Application.bx` の中の `this.datasources` が期待通りに拾われることです。本番でこのストアに依存する前に、`qb` と `bx-sqlite` が実際にインストールされた webui プロジェクトに対して一度 `bxAgents serve` を行ってください。これらのいずれかが欠けている場合の失敗モードは、サイレントではなく、起動時に大きく現れます (解決できない WireBox マッピングか、未知の JDBC ドライバです)。

## 実際の HTTP を通した Web UI: 統合ランナーのプローブによって証明されている

`runColdBoxIntegrationTests.bxs` は、実際の `boxlang-miniserver` によって起動される生成済みフィクスチャアプリに対して、外部から `GET /chat/api/health` をフェッチし、200 でなければビルドを失敗させます。CI では次を返します:

```
+ App probe GET /chat/api/health -> status=200
  body: {"success":true,"status":"ok"}
```

その単一のリクエストが、Web UI のサーバー側についてのエンドツーエンドの証明です: ColdBox のルーティングが生成された `handlers/ChatUi.bx` に到達すること、WireBox がハンドラと `ChatDb` を解決すること、`this.datasources` が bx-sqlite に使用可能な SQLite ファイルを与えること、そして qb が実際に有効化された ColdBox モジュールであること - これらのどれ一つとして、ソーステキストのアサーションだけでは確立できません。`WebUiRuntimeSpec.bx` は、その同じ起動の内部で WireBox から実際のオブジェクトを読み出すことで、ストアの挙動 (マイグレーション、スコープされた会話の CRUD、クロスユーザーガード、設定のアップサート) をカバーします。

**依然としてカバーされていないもの:** 他の webui のルート群を実際の HTTP 経由でです。外部からフェッチされるのは `/health` だけです。`WebUiRuntimeSpec` から他のルートを駆動することは、構造上不可能です。このスペックはランナー自体が占有しているのと同じ単一の miniserver によって配信されるリクエストの*内部*で実行されるため、ループバック呼び出しはワーカープールを奪い合ってしまうからです。残りをカバーするには、2 つ目のサーバープロセスか、より大きな miniserver スレッドプールのどちらかが必要で、これは偽装するのではなく自然な次のステップです。

この段落はかつて、その枯渇の証拠として `runner-coldbox.bxm never responded: HTTP 408` を引用していました。**その帰属は誤りであり**、それが数 CI サイクルにわたってこのプロジェクトを誤導した経緯として記録する価値があります: 408 はループバック呼び出しとは一切関係ありませんでした。ランナーページは存在しない BIF である `chr( 10 )` を呼び出していたため、すべてのリクエストのエントリで 500 を返しており、オーケストレーターの再試行ループが最終的なタイムアウトを報告するまで、その 500 を 90 秒間すりつぶし続けていました。両方とも修正されています。上記のループバック枯渇の懸念自体は、そのスペックの内部からより多くのルートを駆動しない、実在する構造的な理由ですが、これはここで実際に失敗が観測されたものというより、根拠のある予想のままです。

## Web UI 自身のフロントエンド: 実際のブラウザで駆動されたが、モック API に対して

出荷されるページの JavaScript は、今回のセッションで、単にソーステキストとしてアサートされただけでなく、実際に演習されました: 生成された `index.html` はヘッドレス Chromium にロードされ、すべての `<path>/api/*` ルートを傍受し現実的なペイロードで応答した状態で、エンドツーエンドで駆動されました。その方法で確認された動作: `GET /conversations` からの会話サイドバーのレンダリングと、自身のルートを通した切り替え/名前変更/削除。`GET /info` によるツールバーの形成 (`capabilities.compact` が true の場合にのみ Compact が現れ、モデル名がヘッダーに収まること)。`/history` によるトランスクリプトの再水和。bx-ai のエンベロープからコンテンツ、推論、ツール呼び出しチップを流す実際の SSE ターン。マークダウンのレンダリング。そして **New chat** がサーバー側で会話を作成し、それを開くこと。JavaScript エラーはゼロ、置換されない `__TOKEN__` プレースホルダーもゼロ、狭い画面のレイアウトは 390px でスクリーンショットされました。

それが証明**しない**もの: この API は Playwright によるモックであり、実際の SQLite ストアに対して実際の ColdBox 起動の下で動く、生成された `handlers/ChatUi.bx` ではありません。リクエストとレスポンスの形は、生成されたハンドラ自身のソースから取られているため、両者のドリフトはこれによっては捕まえられません。上記の既知の制限のエントリでストアについて述べられていることはすべて、ここにも当てはまります - `qb` と `bx-sqlite` が実際にインストールされた webui プロジェクトに対する一度の手動 `bxAgents serve` が、本番利用の前の正直なゲートとして残ります。
