AI Design Import — テストケース
このページは apps/design-import/ のテスト計画です。io-definition.md のすべての行 — フィールド、処理ステップ、失敗カテゴリ — は、少なくとも 1 つのテストケースと対応している必要があります。契約の条項に対応するテストがない場合、まずそれを修正してください。
規約: テスト ID は
DI-<層>-<NN>形式。層はU(unit、I/O なし)、C(component、実 S3 / Mongo testcontainer、PydanticAI + OpenAI + Webhook はモック)、E(end-to-end、実クラウド)。各ケースは Given / When / Then を明示。「Then」ブロックがアサーション契約 — そこに書かれていないことは 検証しない(呼び出し元も依存してはならない)。
テスト層とツーリング
これらのケースは 手動 / ローカル テスト計画として設計されています。CI には組み込まれておらず、自動ゲートで強制もされません — 変更が入る際にワーカーが I/O 契約どおりに動くかをエンジニアが確認するためのものです。マージ前にローカルで実行するか、リリースチェックリストの一部として活用してください。
| 層 | スコープ | ツーリング |
|---|---|---|
| Unit (U) | 純粋関数: SQS body パース、body アンラップ、S3 URL パース、画像フォーマット検出、ドキュメントビルダ、Webhook ペイロードビルダ | pytest、ネットワークなし。PydanticAI / OpenAI / httpx は SDK 境界でフェイク |
| Component (C) | process_record の end-to-end。実 localstack S3 + testcontainer Mongo(DocumentDB API 互換イメージ)+ モック PydanticAI agent + モック openai.embeddings.create + モック Webhook httpx client |
pytest、testcontainers、localstack、respx/unittest.mock |
| End-to-end (E) | 実 dev AWS — 実 SQS、S3、DocumentDB、OpenAI dev キー、実 Webhook エンドポイント |
pytest の @e2e マーカー、スモークのみ |
共有フィクスチャ
| フィクスチャ | 提供内容 | 使用層 |
|---|---|---|
sqs_record(**overrides) |
上書き可能な、正しく整形された SQS レコード dict | U, C |
s3_bucket |
360×707 PNG と Figma JSON サンプルを事前投入した localstack バケット | C |
mock_vision_agent(visual_features=..., semantic_words=[...]) |
agent.run() から固定の DesignDescription を返す |
U, C |
mock_openai_embed(vector=[0.1]*512) |
visual、semantic、structural 入力に対して決定的な 512-float ベクトルを返す | U, C |
mock_webhook(http_status=200) |
外向き Webhook ペイロードをキャプチャし、指定したステータスを返す | C |
sqs_record(...) がペイロード形状の唯一の真実の源です — 個別の dict を手書きするのではなく、必ずここから合成してください。
カバレッジマトリクス
横軸: io-definition.md の契約条項。縦軸: 結果。各セルは 1 つ以上のテスト ID を指します。
| ハッピーパス | 失敗パス | 冪等性 | |
|---|---|---|---|
| ペイロードスキーマ | DI-U-01 | DI-U-02 | — |
| Body アンラップ | DI-U-03 | DI-U-04 | — |
| 画像フォーマット検出 | DI-U-05 | — | — |
| S3 画像 GET | DI-C-01 | DI-C-02, DI-C-03 | — |
| Vision エージェント | DI-C-01 | DI-U-06, DI-C-04 | — |
| Embedding(バッチ) | DI-C-01 | DI-U-07, DI-C-05 | — |
| ドキュメントビルダ | DI-U-08 | — | — |
| DocumentDB upsert | DI-C-01 | DI-C-06 | DI-C-07 |
| 成功 Webhook | DI-C-01 | DI-C-08 | DI-C-09 |
| 失敗 Webhook | — | DI-C-02, DI-C-03 | — |
| 失敗時の SQS への再 raise | — | DI-C-02, DI-C-03 | — |
Unit (U) ケース
ネットワークなし。純粋な Python。
DI-U-01 — 有効な SQS body がパースされる
- Given 必須フィールド(
organization_id,project_id,img_url,node_id,file_id,design_id,design_name,json_schema_url)を含む正しい body。 - When パーサが実行される。
- Then モデルに
organization_id,project_id,img_url,node_id,file_id,design_id,design_name,json_schema_urlが揃っている。
DI-U-02 — 必須フィールド欠落で拒否
- 各必須フィールドをパラメトライズする。
- Given 該当フィールドを 1 つ削除した body。
- Then
ValidationError。フィールド名がエラーに現れる。
DI-U-03 — Body アンラップ: SQS-in-SQS
- Given
body = json.dumps({"Records": [{"body": json.dumps(payload)}]})。 - Then パーサは 1 段アンラップし、内側のペイロードモデルを返す。
DI-U-04 — Body アンラップ: API Gateway + base64
- Given
body = {"body": base64(json.dumps(payload)), "isBase64Encoded": true}。 - Then デコード、パース、モデル返却。
DI-U-05 — 画像フォーマット検出(magic bytes)
- 実装に magic-byte フォーマット検出器が含まれている場合のみ適用。契約は検出器を必須としない。
- パラメトライズ: PNG、JPEG、GIF、WEBP サンプル、未知のバイト列 1 つ。
- Then 検出器がそれぞれ
"png","jpeg","gif","webp",Noneを返す。 - And 実装が検出結果の MIME をデータ URL に入れているならそれをアサート。固定 MIME ならそれをアサート。実装が選択した方をピン留めする。
DI-U-06 — Vision エージェントが空の semantic_words を返す
- Given モック Vision エージェントが
DesignDescription(visual_features="x", semantic_words=[])を返す。 - Then 下流の embedding が実行できない(入力が空)。ワーカーが raise し、ハンドラが再 raise の前に失敗 Webhook を送出する。
DI-U-07 — Embedding レスポンスの次元数が不正
- Given モック embeddings client が 512 ではなく 1536 個の float ベクトルを返す。
- Then (ドキュメントビルダに長さチェックがある場合)ワーカーが拒否し、ハンドラが失敗 Webhook を送出して再 raise する。
- 注: このアサーションは実装が長さチェックを含む場合にのみ適用される。契約は必須としていない。実装にない場合は、合格を偽装するよりこのケースを削除すること。
DI-U-08 — ドキュメントビルダのフィールドコピー
- Given パース済み SQS body と
DesignDescription。 - When ドキュメントビルダが実行される。
- Then 結果の dict に以下が含まれる:
_id == body.design_idorganization_id == body.organization_idproject_id,name == body.design_namevisual.metadata.name == body.design_namevisual.metadata.image_url == body.img_urlvisual.vector_embeddingの長さは 512semantics.metadata.words == agent.semantic_wordssemantics.vector_embeddingの長さは 512structural.vector_embeddingの長さは 512query_termsが入っているfigma_node.file_id == body.file_idfigma_node.node_id == body.node_idjson_schemaキーは 存在しないcomponentsキーは 存在しない- トップレベルの
idまたはdesign_idキーは 存在しない - トップレベル
img_url— 実装が overview.ja.md の DocumentDB ドキュメント構造に従う場合は不在をアサートする。実装で 1 つの方針を決め、テストをピン留めする。 typeとbased_onは I/O contract の default に従う。
ドキュメントのトップレベルに
img_urlを置くかどうかは実装で決定し、このテストをその方針に合わせて更新してください。overview.ja.md の DocumentDB ドキュメント構造を正とします。
Component (C) ケース
実 localstack S3 + testcontainer Mongo。PydanticAI agent、OpenAI embeddings、Webhook httpx client は SDK 境界でモックする。
DI-C-01 — フルハッピーパス
- Given:
- S3 の
img_urlに画像がある。 json_schema_urlが S3 上の有効な Figma node JSON object を指す。mock_vision_agentがDesignDescription(visual_features="blue rounded card", semantic_words=["login","auth","form"])を返す。mock_openai_embedが 3 つの入力すべてに[0.1] * 512を返す。mock_webhook(http_status=200)。- When ハンドラがメッセージを処理する。
- Then:
- Mongo
designコレクションに_id == design_id、organization_id == body.organization_id、visual/semantic/structural vector 長さ 512、semantics.metadata.words == ["login","auth","form"]、SQS のimg_url/json_schema_urlと一致、figma_node.node_id == node_id、query_termsが入っている、name == design_nameのドキュメントがある。 - Webhook が
{"type": "design-import", "recordId": design_id, "projectId": <pid>, "organizationId": <org_id>, "status": "success", "result": {"keywords": ["login","auth","form"], "embeddingDims": 512, "vectorsProduced": ["visual","semantic","structural"]}}で ちょうど 1 回 呼ばれた。 - 例外は再 raise されない。
- worker は
json_schema_urlを GET し、Figma node JSON をパースできない場合は record を失敗させる。
DI-C-02 — S3 画像が 404 (NoSuchKey)
- Given
img_urlが存在しないキーを指す。 - Then:
- 失敗 Webhook が
{"type":"design-import","recordId":...,"projectId":...,"organizationId":...,"status":"failed"}で呼ばれる(resultなし)。 - ハンドラが再 raise し、SQS が失敗を見る。
- Mongo 書き込みなし。
- 成功 Webhook なし。
DI-C-03 — S3 画像が 500
- Given localstack chaos が画像キーで 500 を返す。
- Then:
- 失敗 Webhook 送出。
- 例外再 raise(SQS リトライ)。
- Mongo 書き込みなし。
DI-C-04 — Vision エージェントが raise
- Given
mock_vision_agentが PydanticAI 例外(プロバイダ 5xx など)を raise する。 - Then:
- 失敗 Webhook 送出。
- 例外再 raise。
- Embeddings 呼び出しなし。Mongo 書き込みなし。
DI-C-05 — Embeddings API が raise
- Given Vision エージェントは成功、
mock_openai_embedが一時的なAPIErrorを raise する。 - Then:
- 失敗 Webhook 送出。
- 例外再 raise。
- 部分的な Mongo ドキュメントは存在しない —
db.design.find_one({"_id": design_id}) is Noneをアサート。
DI-C-06 — Mongo 書き込みが raise(一時的)
- Given Vision + embeddings が成功。Mongo testcontainer を一時停止する。
- Then:
- 失敗 Webhook 送出。
- 例外再 raise。
DI-C-07 — 再配送がドキュメントをアトミックに上書きする
- Given DI-C-01 が 1 度実行され、ベクトル
vAのドキュメントが残っている。 - And 同じ
design_idで同じメッセージが再配送されるが、今度はモックがベクトルvBを返す。 - Then:
- Mongo
designコレクションには_id == design_idのドキュメントがちょうど 1 つ。 - ドキュメントの
visual.vector_embeddingがvBを反映している(最後の書き込みが勝つ)。 - 2 回の実行を通じて
successWebhook が 2 回観測される(at-least-once)。
DI-C-08 — 成功パスで Webhook POST が失敗
- Given Mongo 書き込み成功。
mock_webhook(http_status=500)。 - Then:
- WARN ログライン(
design_import.webhook.failedまたは同等)。 - ハンドラは再 raise する、または record を
batchItemFailuresに追加する。backend が authoritative status notification を受け取っていないため、SQS が retry する。
DI-C-09 — Webhook は試行を跨いだ at-least-once
- Given 試行 1: Vision エージェントが raise(DI-C-04)。
- And 試行 2: すべて成功(DI-C-01)。
- Then:
- 2 回の Webhook が観測される:
failed1 件、success1 件。 - Mongo には試行 2 のドキュメントのみ。
DI-C-10 — 永続化されたドキュメントに json_schema フィールドがない
- Given DI-C-01 のハッピーパス。
- Then
db.design.find_one({"_id": design_id})にjson_schemaキーが 存在しない。DocumentDB ドキュメントは embedding とメタデータのみ。Figma JSON は S3 にあり、SQS body のjson_schema_urlから参照される。
DI-C-11 — 永続化されたドキュメントに components フィールドがない
- Given DI-C-01 のハッピーパス。
- Then ドキュメントに
componentsキーが 存在しない。コンポーネントマッチングは下流のdes2codeで計算されるため、design ドキュメントはそれを保持しない。
End-to-end (E) ケース
スモークセットのみ。実 dev 環境、実サービス、実 OpenAI dev キーで手動実行。コストはゼロではないため、スイートは小さく保つこと。
DI-E-01 — 実 dev SQS の往復
- Given guinness-backend dev が実際の design-create フロー経由でメッセージを enqueue する。
- Then 90 秒以内に:
- DocumentDB の
designコレクションに、512-float のベクトル 2 つを持つドキュメントが存在する。 - バックエンドが
successWebhook を受信した(バックエンドのログまたはdesign行状態で確認)。 - CloudWatch にワーカーのライフサイクルログラインが現れる。
- (バックエンド
status = 1は record_id の解決 次第。解決するまで informational として扱う。)
DI-E-02 — 実 OpenAI 429 でのリトライ
- (手動のみ。スロットリングが起きている時間帯に実行。)
- Given dev OpenAI キーがタイトなレートリミット下にある。
- Then メッセージは SQS が再配送し、最終的に成功する。最後の Webhook は
success。
横断的チェック
該当する全ケースで成り立つべき条件。単独のテスト行ではない — 上記のテストをレビュー / 実行する際に確認する。
- 部分的な DocumentDB ドキュメントは存在しない: Vision と Mongo 書き込みの間で失敗するテストはすべて
db.design.find_one({"_id": design_id}) is Noneをアサート。 - Embedding 長は常に 512: 永続化を行うテストはすべて両方のベクトル長をアサート。
process_record1 回の呼び出しにつき Webhook はちょうど 1 回:successまたはfailedのいずれかで、1 回の呼び出し内で両方は送らない。- 失敗 Webhook は再 raise の 前 に送る: Webhook モックと
raise地点の両方をインターセプトし、順序をアサートする。 - スタックトレース文字列が Webhook body に漏れない: Webhook ペイロードのアサーションは完全一致 — 余計なフィールドなし。
- PostgreSQL / MySQL 接続は決して開かれない:
localhostで DB ドライバが到達できない状態でテストを実行する(またはインポート時に大きな声で失敗するガード環境変数を設定する)。誤って DB ドライバを使う経路が即座に表面化する。 - ドキュメントの識別子は 1 つだけ: 永続化されるドキュメントは
_idのみを保持する。トップレベルのidまたはdesign_idキーがあれば失敗させる — design id は_idとして流れ、他の場所には現れない。
意図的にスコープ外
将来のテストが正しいバケットに着地するよう、明示的に列挙する。
- バックエンドの
design.statusステートマシン: guinness-backend のテストが所有。ワーカーは Webhook を送出するだけ。 - ベクトル検索 / Des2Code のフュージョン:
apps/des2code/のテストスイートが所有。 - 認証: ワーカーには HTTP 入口がなく、認証は上流。
- DLQ 運用 / SQS インフラ: インフラテストが所有。ワーカーは再 raise の挙動のみ検証。
- Code preview rendering: code import/rendering pipeline が所有。このワーカーは code entry をレンダリングしない。
- テナント間分離: 純粋に RBAC。バックエンドで起こる。ワーカーのテストはメッセージが既に認可済みであることを前提とする。
record_id解決戦略: 現在 未解決の契約事項。テストは 現状 の挙動をピン留めし、将来の修正がテスト変更として可視化されるようにする。