AI Code Import — テストケース設計
本ドキュメントは、code-import ワーカーのテストに関する 唯一の真実の情報源(Single Source of Truth)。以下を定義する:
- テスト戦略 — カテゴリ、レイヤー、責任範囲。
- テスト環境 — ローカル、CI、dev(デプロイ済 Lambda)、staging。
- テストフィクスチャ — サンプルペイロード、サンプルコード、サンプル画像。
- テストカタログ — 全テストケース(ID、目的、入力、期待出力、優先度)。
- カバレッジ目標 — 「完了」の定義。
- CI 統合 — PR とデプロイ毎の実行方法。
テスト対象の公式 I/O 契約は I/O 定義;技術詳細および処理フローは 概要。
以下のパスはリポジトリ上の apps/code-import/(src/code_import/ の Python パッケージ code_import)を前提とする。__tests__/ は現在のスキャフォールドに対する 目標配置 である。
1. テスト戦略
ピラミッド
┌──────────────────────┐
│ E2E / スモーク (dev) │ 約 3 ケース (実 AWS、実 OpenAI、実 DocDB)
├──────────────────────┤
│ 統合 (CI) │ 約 10 ケース (process_record をモック付きで)
├──────────────────────┤
│ コントラクト (CI) │ 約 6 ケース (スキーマ検証、Webhook 形式)
├──────────────────────┤
│ ユニット (CI) │ 約 25 ケース (純粋関数、エージェント出力)
└──────────────────────┘
カテゴリ
| カテゴリ |
範囲 |
実行場所 |
モック |
| ユニット |
単一関数またはクラス |
CI(push 毎) |
全外部(S3、OpenAI、DocDB) |
| コントラクト |
スキーマ/ペイロード検証 |
CI(push 毎) |
なし — 純粋データ |
| 統合 |
process_record(record) 全体 |
CI(push 毎) |
S3、OpenAI、DocDB、Webhook(HTTP) |
| スモーク (dev) |
実 Lambda 呼び出し |
手動 / 夜間 |
なし |
| 負荷 |
スループット & レイテンシ |
手動 / リリース前 |
OpenAI(録画再生) |
責任範囲
| レイヤー |
責任者 |
ツール |
| ユニット + コントラクト + 統合 |
PLABS.ID(ワーカー開発) |
pytest、pytest-asyncio、moto、mongomock、respx |
| スモーク (dev) |
PLABS.ID 運用 |
aws lambda invoke + pytest アサーション |
| 負荷 |
PLABS.ID + 4D インフラ |
dev Lambda に対して locust |
2. テスト環境
| 環境 |
目的 |
OpenAI |
S3 |
DocDB |
Webhook |
local(開発 PC) |
ユニット + 統合 |
モック |
moto |
mongomock |
respx |
ci(GitHub Actions) |
local と同じ |
モック |
moto |
mongomock |
respx |
dev(デプロイ済 AWS) |
スモーク |
実(gpt-5.4-nano) |
実バケット |
実クラスタ |
実バックエンド |
staging |
リリース前スモーク + 負荷 |
実 |
実 |
実 |
実 |
コストガード: 実 OpenAI を呼び出すテストは @pytest.mark.real_openai マーカーを宣言し、デフォルトでスキップ。pytest -m real_openai で有効化。
3. テストフィクスチャ
全フィクスチャは apps/code-import/__tests__/fixtures/ に配置。
3.1 サンプル SQS レコード
| フィクスチャファイル |
バリアント |
record_minimal.json |
必須フィールドのみ(css_code、img_url なし) |
record_with_screenshot.json |
img_url を含む全フィールド |
record_with_css.json |
img_url 以外の全フィールド |
record_full.json |
css_code と img_url を含む全フィールド |
record_page.json |
type=0(page) |
record_based_on_design.json |
based_on=1 |
record_based_on_wireframe.json |
based_on=2 |
record_invalid_uuid.json |
不正な code_id |
record_missing_field.json |
name 欠落 |
record_invalid_type.json |
type=2 |
record_empty_source_code.json |
source_code="" |
record_huge_source_code.json |
250 KB の source_code |
record_url_encoded_s3.json |
%E3%83%95 形式のエスケープを含む img_url |
record_double_wrapped.json |
Records 配列をネストする body(バックエンドのレガシーバグ対策) |
3.2 サンプルソースコード
| フィクスチャファイル |
説明 |
code_button.tsx |
シンプルな Tailwind ボタン |
code_modal.tsx |
フォーム付きモーダルダイアログ |
code_table.tsx |
ソート可能なデータテーブル |
code_page_login.tsx |
フルページ(type=0) |
code_with_inline_styles.tsx |
クラスなし、インラインスタイルのみ |
3.3 サンプルスクリーンショット
| フィクスチャファイル |
フォーマット |
備考 |
button_primary.png |
PNG |
320x80、通常ケース |
button_primary.jpg |
JPEG |
同シーン、圧縮アーティファクトあり |
large_image.png |
PNG |
4 MB、アップロード上限近く |
corrupt.png |
— |
切り詰められたバイト列 |
3.4 サンプル LLM レスポンス(録画)
pytest-recording(または手書き)で fixtures/llm_responses/ 以下に録画。CI でエージェント出力を決定論的に再現するために使用。
| フィクスチャ |
入力先 |
返却値 |
semantic_button.json |
Code Analysis LLM(arch ノード C) |
["submit","action","primary","cta","button"] |
visual_button.json |
Vision LLM(arch ノード F/G) |
"角丸の青いボタン、白いサンセリフラベル、半径 8px、パディング 12px" |
embedding_512.json |
OpenAI embeddings |
512 次元の float ベクトル 3 本 |
4. テストカタログ
4.1 ユニットテスト — apps/code-import/__tests__/unit/
スキーマ検証(test_schemas.py)
| ID |
目的 |
入力 |
期待 |
| U-SCH-001 |
必須フィールドのみ受理 |
record_minimal.json body |
CodeImportMessage インスタンス、css_code=None、img_url=None |
| U-SCH-002 |
全フィールド正しく代入 |
record_full.json body |
全フィールドが期待値と一致 |
| U-SCH-003 |
不正な UUID を拒否 |
record_invalid_uuid.json body |
code_id で ValidationError |
| U-SCH-004 |
必須フィールド欠落を拒否 |
record_missing_field.json body |
name で ValidationError |
| U-SCH-005 |
type 範囲外を拒否 |
record_invalid_type.json body |
type で ValidationError |
| U-SCH-006 |
based_on 範囲外を拒否 |
based_on=3 |
ValidationError |
| U-SCH-007 |
空 source_code を拒否 |
record_empty_source_code.json |
ValidationError |
| U-SCH-008 |
オプション css_code が None 受理 |
minimal + css_code=None |
OK |
| U-SCH-009 |
オプション img_url が None 受理 |
minimal + img_url=None |
OK |
S3 URL パース(test_s3_helpers.py)
| ID |
目的 |
入力 |
期待 |
| U-S3-001 |
s3:// URL をパース |
s3://bucket/key/file.png |
("bucket", "key/file.png") |
| U-S3-002 |
仮想ホスト形式 HTTPS をパース |
https://bucket.s3.us-east-1.amazonaws.com/file.png |
("bucket", "file.png") |
| U-S3-003 |
パス形式 HTTPS をパース |
https://s3.us-east-1.amazonaws.com/bucket/file.png |
("bucket", "file.png") |
| U-S3-004 |
キーを URL デコード |
s3://bucket/%E3%83%95%E3%82%9A%E3%83%AD.png |
キーに フ°ロ.png を含む(unquote 済) |
| U-S3-005 |
未対応スキームを拒否 |
ftp://bucket/key |
ValueError |
画像フォーマット検出(test_image_helpers.py)
| ID |
目的 |
入力 |
期待 |
| U-IMG-001 |
PNG マジック検出 |
PNG 先頭 8 バイト |
"png" |
| U-IMG-002 |
JPEG マジック検出 |
JPEG 先頭 3 バイト |
"jpeg" |
| U-IMG-003 |
GIF マジック検出 |
b"GIF89a..." |
"gif" |
| U-IMG-004 |
WebP マジック検出 |
b"RIFF....WEBP" |
"webp" |
| U-IMG-005 |
未知フォーマットは None |
ランダムバイト |
None |
埋め込みヘルパー(test_embedding.py)
| ID |
目的 |
入力 |
期待 |
| U-EMB-001 |
3 入力で 3 本の 512 次元ベクトル |
openai.embeddings.create をモック |
長さ 512 の list[float] 3 本のタプル |
| U-EMB-002 |
単一バッチ呼び出し |
クライアント spy |
API 呼び出しは正確に 1 回 |
| U-EMB-003 |
空 semantic_words を綺麗に join |
[] |
入力が "" になる(クラッシュなし) |
エージェント出力モデル(test_agent_outputs.py)
| ID |
目的 |
入力 |
期待 |
| U-AGT-001 |
CodeSemantics が 5 単語を受理 |
5 文字列のリスト |
OK |
| U-AGT-002 |
CodeSemantics が 10 単語を受理 |
10 文字列のリスト |
OK |
| U-AGT-003 |
CodeSemantics が 4 単語を拒否 |
4 文字列のリスト |
ValidationError |
| U-AGT-004 |
CodeSemantics が 11 単語を拒否 |
11 文字列のリスト |
ValidationError |
| U-AGT-005 |
VisualFeatures が短すぎる文字列を拒否 |
10 文字 |
ValidationError |
4.2 コントラクトテスト — __tests__/contract/
| ID |
目的 |
入力 |
期待 |
| C-WH-001 |
成功 Webhook が openapi 風スキーマと一致 |
ワーカーからの成功ペイロード |
ai-status webhook スキーマに準拠 |
| C-WH-002 |
失敗 Webhook がスキーマと一致 |
ワーカーからの失敗ペイロード |
準拠 |
| C-WH-003 |
Webhook の type は常に "code-import" |
任意の Webhook |
type == "code-import" |
| C-WH-004 |
Webhook の recordId が code_id を正確にエコー |
成功パス |
recordId == code_id |
| C-WH-005 |
Webhook scope が SQS の tenant/project をエコー |
成功・失敗ペイロード |
organizationId == organization_id かつ projectId == project_id |
| C-DOC-001 |
DocDB ドキュメントに必須の visual と semantics ブロック |
サンプル実行 |
両キー存在、metadata と vector_embedding を含む |
| C-DOC-002 |
vector_embedding の長さは 512 |
サンプル実行 |
両配列の長さ == 512 |
4.3 統合テスト — __tests__/integration/test_process_record.py
全テストで mongomock、moto、respx、録画 LLM レスポンスを使用。各テストは process_record(record) をエンドツーエンドで呼び出す。
ハッピーパス
| ID |
目的 |
フィクスチャ |
期待 DB 書き込み |
期待 Webhook |
| I-HP-001 |
最小ペイロード、スクショなし、CSS なし |
record_minimal.json |
css_code=null、visual.metadata.image_url=null、両埋め込み 512 次元 |
success、keywords 長さ 5–10 |
| I-HP-002 |
スクショ付き全フィールド |
record_full.json |
visual.metadata.image_url == img_url |
success |
| I-HP-003 |
CSS あり、スクショなし |
record_with_css.json |
css_code 保存、visual.metadata.image_url=null |
success |
| I-HP-004 |
Page(type=0) |
record_page.json |
type=0 保存 |
success |
| I-HP-005 |
based_on=1(design) |
record_based_on_design.json |
based_on=1 保存 |
success |
| I-HP-006 |
based_on=2(wireframe) |
record_based_on_wireframe.json |
based_on=2 保存 |
success |
| I-HP-007 |
URL エンコード S3 キー |
record_url_encoded_s3.json |
S3 キー正しくデコード、画像ダウンロード |
success |
| I-HP-008 |
冪等な再処理 |
record_full.json を 2 回実行 |
2 回目は upsert、ドキュメント形状不変 |
成功 Webhook 2 回 |
| I-HP-009 |
二重ラップ body(レガシー) |
record_double_wrapped.json |
内部 body 抽出、通常処理 |
success |
失敗パス
Repository regression test では write conflict 時に新しい session で transaction 全体を再試行し、index 無効化の原子性を保ち、AI 呼び出しを繰り返さないことを確認する。永続エラーはローカルで再試行せず、再試行上限後は SQS record failure とする。
| ID |
目的 |
フィクスチャ / モック |
期待動作 |
| I-FP-001 |
空の body 文字列 |
{"body":""} |
ValueError、Webhook なし(code_id なし)、例外を再 raise |
| I-FP-002 |
不正な JSON body |
{"body":"{not json"} |
JSONDecodeError、Webhook なし、再 raise |
| I-FP-003 |
Pydantic 検証失敗 |
record_invalid_uuid.json |
ValidationError、部分 Webhook 試行(ベストエフォート)、再 raise |
| I-FP-004 |
S3 NoSuchKey |
ダウンロード時 ClientError(NoSuchKey) をモック |
Webhook failed、再 raise |
| I-FP-005 |
S3 AccessDenied |
ClientError(AccessDenied) をモック |
Webhook failed、再 raise |
| I-FP-006 |
LLM semantic タイムアウト |
semantic agent が AgentRunError を raise |
Webhook failed、再 raise |
| I-FP-007 |
LLM visual タイムアウト |
visual agent が AgentRunError を raise |
Webhook failed、再 raise |
| I-FP-008 |
埋め込み API レート制限 |
openai.RateLimitError |
Webhook failed、再 raise |
| I-FP-009 |
DocumentDB 書き込み失敗 |
mongomock が PyMongoError を raise |
Webhook failed、再 raise |
| I-FP-010 |
Webhook 自体の失敗(5xx) |
respx が 500 を返却 |
log して SQS retry のため再 raise |
| I-FP-011 |
Webhook 4xx(認証不正) |
respx が 401 を返却 |
log して SQS retry のため再 raise |
エッジケース
| ID |
目的 |
フィクスチャ |
期待 |
| I-EC-001 |
ソースコードが 200 KB 上限 |
record_huge_source_code.json |
受理、処理 |
| I-EC-002 |
非 ASCII(日本語、ベトナム語)を含むソースコード |
unicode source_code |
DocDB へ正しくラウンドトリップ |
| I-EC-003 |
画像が JPEG(PNG ではない) |
button_primary.jpg |
警告ログ、vision agent はバイト受信 |
| I-EC-004 |
画像 4 MB 超 |
large_image.png |
受理(実 Lambda は 6 MB ペイロード上限、5 MB で警告) |
| I-EC-005 |
破損画像バイト |
corrupt.png |
vision agent エラー → Webhook failed |
| I-EC-006 |
1 SQSEvent に 2 レコード |
2 レコードのバッチ |
両方処理、最初が失敗しても 2 番目を試行 |
4.4 スモークテスト — __tests__/smoke/
デプロイ済 dev Lambda に対してのみ実行。@pytest.mark.smoke 付き。
| ID |
目的 |
手順 |
期待 |
| S-001 |
実エンドツーエンド(スクショあり) |
1. SQS 送信 record_full.json。2. ≤ 3 分待機。3. DocDB を code_id で照会。4. PG code 行を照会。 |
DocDB ドキュメントに両埋め込み存在、PG 行 status=1(completed) |
| S-002 |
実エンドツーエンド(スクショなし) |
record_with_css.json 送信。同じ待機 + 確認。 |
上記と同じ、visual.metadata.image_url=null |
| S-003 |
実エンドツーエンド(失敗パス) |
不正な S3 URL 付きペイロード送信。 |
3 分以内に PG 行 status=2(failed)、Webhook ログに failed |
4.5 負荷テスト — __tests__/load/
各リリース前に実行。@pytest.mark.load 付き。
| ID |
目的 |
プロファイル |
合格基準 |
| L-001 |
定常スループット |
10 msg/min を 30 分 |
p50 < 90 秒、p95 < 180 秒、エラー率 < 1 % |
| L-002 |
バースト処理 |
1 分で 100 msg、その後アイドル |
10 分以内に全処理完了、DLQ メッセージなし |
| L-003 |
持続バースト |
30 msg/min を 1 時間 |
DLQ なし、OpenAI レート制限超過なし |
5. カバレッジ目標
| レイヤー |
行カバレッジ |
分岐カバレッジ |
備考 |
apps/code-import/src/code_import/handler.py |
≥ 90 % |
≥ 85 % |
Lambda エントリ(lambda_handler) — 全 try/except 分岐をカバーすること |
apps/code-import/src/code_import/schemas.py |
100 % |
100 % |
Pydantic モデル — 網羅的にテスト |
packages/agentic/agents/code_semantics.py |
≥ 80 % |
— |
ビルダーのみ、LLM ランタイムはモック |
packages/agentic/agents/code_visual.py |
≥ 80 % |
— |
同上 |
packages/helpers/embedding.py |
100 % |
— |
単一関数 |
packages/helpers/webhook.py |
≥ 90 % |
≥ 85 % |
リトライパスを含む |
packages/models/documentdb/code.py |
≥ 85 % |
— |
インデックス作成の冪等性 |
実行:
pytest --cov=code_import --cov=packages \
--cov-report=term-missing --cov-fail-under=85
6. CI 統合
PR 毎のパイプライン(.github/workflows/test-code-import.yml)
on:
pull_request:
paths:
- 'apps/code-import/**'
- 'packages/agentic/agents/code_*.py'
- 'packages/helpers/**'
- 'packages/models/documentdb/code.py'
jobs:
test:
steps:
- uv sync
- pytest -m "not real_openai and not smoke and not load" \
--cov --cov-fail-under=85
- ruff check apps/code-import packages
- mypy apps/code-import packages
夜間(dev に対して)
on:
schedule:
- cron: '0 18 * * *' # 03:00 JST
jobs:
smoke:
steps:
- uv sync
- pytest -m smoke
リリース前(staging に対して)
-m "smoke or load" を実行する手動トリガーワークフロー。
失敗時のポリシー
| CI ステージ |
失敗時 |
| Lint / 型 / ユニット / コントラクト / 統合 |
マージ阻止 |
| カバレッジ閾値未満 |
マージ阻止 |
| 夜間スモーク |
#guinness-ai Slack に通知、3 連続失敗で issue 作成 |
| 負荷テスト |
リリース阻止、マージは阻止しない |
7. クイックテストマトリクス(チートシート)
| シナリオ |
テスト ID |
| 必須フィールドのみ |
U-SCH-001、I-HP-001 |
| 全オプションフィールドあり |
U-SCH-002、I-HP-002 |
| Page vs code |
I-HP-004 |
全 based_on バリアント |
I-HP-005、I-HP-006 |
| スクショ欠落 |
I-HP-001、I-HP-003 |
| 不正入力(検証) |
U-SCH-003 .. U-SCH-007、I-FP-003 |
| 外部サービス障害 |
I-FP-004 .. I-FP-009 |
| Webhook 障害の握りつぶし |
I-FP-010、I-FP-011 |
| 冪等性 |
I-HP-008 |
| 実世界エンドツーエンド |
S-001、S-002 |
| スループット |
L-001、L-002 |
8. Definition of Done(PR 毎)
code-import の PR は以下を満たした時点でマージ可能:
- [ ] 該当する全テストケースが実装され、合格している。
- [ ] カバレッジ閾値達成(§5 参照)。
- [ ] 動作変更があれば最低 1 つの新規統合テスト追加。
- [ ] SQS スキーマ、Webhook スキーマ、または DocDB ドキュメント形状が変わった場合、
S-001–S-003 のスモークを dev で手動検証。
- [ ] PR 説明に該当テスト ID をリンク。
リポジトリ全体のリリース準備状況については docs/overview/non-functional.ja.md(SLA / SLO 目標)参照。