共有スキーマの列挙値を増やす変更は、未対応の消費側が「型で受理して黙って無視」する fail-open を作る
検索条件・フィルタ・コマンドなどで、複数の消費側(backend の実装経路)が同一のスキーマ定義を共有している構成は珍しくない。ここに新しい列挙値(新しい演算子・新しい条件種別・新しいアクション種別)を1つ足すと、そのスキーマを参照するすべての経路が型・バリデーション上その値を受理するようになる。しかし実際に処理するのは新機能を実装した経路だけで、未対応の経路は型ガードや判定関数でその値を落とし、条件を黙って無視する。
これは fail-open の一種として厄介な形をしている。
- バリデーションを通るのでリクエストは拒否されない(エラーにならない)
- 未対応経路では条件が単に消えるので、絞り込みが効かず「全件に近い結果」が正常なレスポンスとして返る
- 型検査もテストも通る。スキーマは共有なので、消費側に分岐を足し忘れてもコンパイルエラーにならない
さらに、入力 UI も共有コンポーネントであることが多い。共有コンポーネントに新しい選択肢を無条件で追加すると、未対応経路の画面でもその選択肢が選べてしまい、ユーザーは指定したはずの条件が効かない結果を見る。
判断基準
- 共有スキーマに列挙値を足すときは、そのスキーマを参照する消費側の実装をすべて列挙し、新しい値を処理するか落とすかを1件ずつ判定する。スキーマ定義から参照を辿るだけでは足りない。入力 UI の呼び出し元から、実際にその条件を解釈するクエリ組み立て・フィルタ変換の実装まで辿る必要がある
- 「型で受理して無視」は最悪の中間状態。少なくとも未対応経路では**受理せず拒否する(バリデーションエラーにする)**か、入力側でその値を選べなくするかのどちらかに倒す
- スキーマを経路ごとに分離する案は、共有スキーマから派生している型・フォーム状態・共通ヘルパーすべてに分岐が波及するのでコストが高い。既存が「スキーマは広く持ち、経路ごとに入力側で絞る」設計になっているなら、その慣例に乗って入力側のゲート(既定 on / 未対応経路だけ opt-out する prop)で塞ぐ方が変更範囲は小さい。ただしこれは緩和策であって、API は値を受理し続けることを理解した上で選ぶ
落とし穴
入力側でゲートしても、すでにその値で保存された条件が残っている場合がある(保存済み検索条件、下書き、URL 共有された状態など)。ゲート後の画面では選択肢からその値が消えるため、演算子の表示が未選択状態になる一方、値そのものは保持されて送信され続けることがある。未リリース機能なら本番データには存在しないが、検証環境で保存した状態は残る。
検証方法
入力側のゲートは、共有コンポーネント単体のテストだけでは不十分。今回の不具合クラスは「共有コンポーネント側は正しいが、呼び出し元がゲート用の prop を渡していない」という配線バグなので、共有コンポーネントに prop を渡したケースのテストを書いても、呼び出し元の1行を消したときに検知できない。
未対応経路の画面レベルのテストで、その選択肢が出ないことを assert する。そして書いたあとに呼び出し元の配線を実際に外してテストが落ちることを確認する(ミューテーション確認)。落ちなければそのテストは配線を守っていない。