#!/usr/bin/env bash set -euo pipefail APP_URL="${APP_URL:-http://localhost:52080}" BACKEND_URL="${BACKEND_URL:-http://localhost:52000}" ADMIN_BACKEND_URL="${ADMIN_BACKEND_URL:-http://localhost:52800}" ADMIN_AUTH="${ADMIN_AUTH:-admin:admin}" SMOKE_AUDIO="${SMOKE_AUDIO:-0}" COOKIE_JAR="$(mktemp "${TMPDIR:-/tmp}/dreamweaver-cookie.XXXXXX")" cleanup() { rm -f "$COOKIE_JAR" } trap cleanup EXIT need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "Missing required command: $1" >&2 exit 1 fi } need_cmd curl need_cmd jq say() { printf '\n[%s] %s\n' "$(date '+%H:%M:%S')" "$*" } post_json() { local url="$1" local payload="$2" curl -fsS -b "$COOKIE_JAR" -H 'Content-Type: application/json' -d "$payload" "$url" } get_json() { local url="$1" curl -fsS -b "$COOKIE_JAR" "$url" } assert_jq() { local json="$1" local filter="$2" local message="$3" if ! jq -e "$filter" >/dev/null <<<"$json"; then echo "Assertion failed: $message" >&2 echo "$json" | jq '.' >&2 exit 1 fi } wait_for_job_story() { local job_id="$1" local attempts="${2:-60}" for ((i=1; i<=attempts; i++)); do local job_json job_json="$(get_json "$APP_URL/api/generations/jobs/$job_id")" if jq -e '.story_id != null' >/dev/null <<<"$job_json"; then printf '%s\n' "$job_json" return 0 fi if jq -e '.is_terminal == true and .story_id == null' >/dev/null <<<"$job_json"; then echo "Generation job finished without a persisted story: $job_id" >&2 echo "$job_json" | jq '.' >&2 exit 1 fi sleep 1 done echo "Timed out waiting for generation job to persist a story: $job_id" >&2 exit 1 } say "Checking backend health" curl -fsS "$BACKEND_URL/health" | jq -e '.status == "ok"' >/dev/null curl -fsS "$ADMIN_BACKEND_URL/health" | jq -e '.status == "ok"' >/dev/null say "Logging in with dev auth" curl -fsS -c "$COOKIE_JAR" -o /dev/null -L "$APP_URL/auth/dev/signin" session_json="$(get_json "$APP_URL/auth/session")" assert_jq "$session_json" '.user.id == "github:dev_user_001"' "dev session should be active" say "Checking provider capability policy" capabilities_json="$(curl -fsS -u "$ADMIN_AUTH" "$ADMIN_BACKEND_URL/admin/providers/capabilities")" assert_jq "$capabilities_json" 'map(.capability) | sort == ["image","storybook","text","tts"]' "capabilities should include text/image/tts/storybook" say "Generating text story without assets" story_json="$(post_json "$APP_URL/api/generations" '{ "output_mode": "story", "type": "keywords", "data": "金色书签, 小鹿, 学会复盘", "education_theme": "复盘与成长", "generate_images": false }')" story_job_id="$(jq -r '.generation_job_id' <<<"$story_json")" assert_jq "$story_json" '.mode == "generated" and .generation_status == "queued" and .text_status == "generating"' "story request should be accepted for background execution" assert_jq "$story_json" '.generation_job_id != null and .generation_job_id != ""' "story generation should expose a job id" echo "$story_json" | jq '{generation_job_id,mode,generation_status,text_status}' say "Waiting for story main record to be persisted" story_job_json="$(wait_for_job_story "$story_job_id")" story_id="$(jq -r '.story_id' <<<"$story_job_json")" story_json="$(get_json "$APP_URL/api/generations/$story_id")" assert_jq "$story_json" '.mode == "generated" and .generation_status == "partial_ready" and .text_status == "ready"' "story should be readable before assets" assert_jq "$story_json" '(.retryable_assets | index("image")) != null and (.retryable_assets | index("audio")) != null' "story should expose image/audio as retryable assets" echo "$story_json" | jq '{id,title,mode,generation_status,image_status,audio_status,retryable_assets}' say "Checking story generation job events" assert_jq "$story_job_json" '.id == "'"$story_job_id"'" and .story_id == '"$story_id"'' "story generation job should be queryable" assert_jq "$story_job_json" '.progress_percent == 100 and .is_terminal == true' "story generation job should expose progress summary" assert_jq "$story_job_json" '([.events[].event_type] | index("worker_started")) != null and ([.events[].event_type] | index("context_prepared")) != null and ([.events[].event_type] | index("narrative_generated")) != null and ([.events[].event_type] | index("story_saved")) != null' "story generation job should include workflow events" assert_jq "$story_job_json" '([.events[].event_type] | index("provider_call_succeeded")) != null' "story generation job should include provider call events" echo "$story_job_json" | jq '{id,status,current_step,events:([.events[].event_type] | unique)}' say "Checking story provider stats" story_provider_stats_json="$(get_json "$APP_URL/api/generations/$story_id/provider-stats")" assert_jq "$story_provider_stats_json" '.total_calls >= 1 and .successful_calls >= 1 and (.by_provider | length) >= 1' "story provider stats should summarize provider calls" echo "$story_provider_stats_json" | jq '{story_id,total_calls,successful_calls,failed_calls,avg_latency_ms,estimated_cost_usd}' say "Retrying story cover image" story_image_json="$(post_json "$APP_URL/api/generations/$story_id/retry-assets" '{"assets":["image"]}')" assert_jq "$story_image_json" '.generation_status == "partial_ready" and .image_status == "ready" and (.image_url != null)' "story cover should be ready after retry" assert_jq "$story_image_json" '(.retryable_assets | index("image")) == null and (.retryable_assets | index("audio")) != null' "story image retry should leave only audio retryable" echo "$story_image_json" | jq '{id,title,generation_status,image_status,audio_status,retryable_assets}' say "Checking story audio cache status" story_audio_status_json="$(get_json "$APP_URL/api/audio/$story_id/status")" assert_jq "$story_audio_status_json" '.audio_ready == false and .cache_exists == false and .audio_status == "not_requested"' "story audio status should not generate audio" echo "$story_audio_status_json" | jq '{story_id,audio_ready,cache_exists,audio_status,retryable_assets}' if [[ "$SMOKE_AUDIO" == "1" ]]; then say "Retrying story audio" story_audio_json="$(post_json "$APP_URL/api/generations/$story_id/retry-assets" '{"assets":["audio"]}')" assert_jq "$story_audio_json" '.audio_status == "ready"' "story audio should be ready after retry" assert_jq "$story_audio_json" '(.retryable_assets | length) == 0' "story should have no retryable assets after image and audio are ready" audio_probe="$(curl -fsS -b "$COOKIE_JAR" -o /tmp/dreamweaver-smoke-audio.mp3 -w '%{http_code} %{content_type} %{size_download}' "$APP_URL/api/audio/$story_id")" if [[ "$audio_probe" != 200\ audio/mpeg* ]]; then echo "Unexpected audio response: $audio_probe" >&2 exit 1 fi story_audio_status_json="$(get_json "$APP_URL/api/audio/$story_id/status")" assert_jq "$story_audio_status_json" '.audio_ready == true and .cache_exists == true and .cache_size_bytes > 0' "story audio should have cache metadata after generation" echo "$story_audio_json" | jq '{id,title,generation_status,image_status,audio_status,retryable_assets}' else say "Skipping audio smoke; set SMOKE_AUDIO=1 to include TTS" fi say "Generating storybook without images" storybook_json="$(post_json "$APP_URL/api/generations" '{ "output_mode": "storybook", "type": "keywords", "data": "彩虹邮局, 小刺猬, 练习说谢谢", "education_theme": "感恩表达", "generate_images": false, "page_count": 6 }')" storybook_job_id="$(jq -r '.generation_job_id' <<<"$storybook_json")" assert_jq "$storybook_json" '.mode == "storybook" and .generation_status == "queued" and .text_status == "generating"' "storybook request should be accepted for background execution" assert_jq "$storybook_json" '.generation_job_id != null and .generation_job_id != ""' "storybook generation should expose a job id" echo "$storybook_json" | jq '{generation_job_id,mode,generation_status,text_status}' say "Waiting for storybook main record to be persisted" storybook_job_json="$(wait_for_job_story "$storybook_job_id")" storybook_id="$(jq -r '.story_id' <<<"$storybook_job_json")" storybook_json="$(get_json "$APP_URL/api/generations/$storybook_id")" assert_jq "$storybook_json" '.mode == "storybook" and .generation_status == "partial_ready" and .text_status == "ready" and .image_status == "not_requested" and (.pages | length) >= 4' "storybook should be readable before images" assert_jq "$storybook_json" '(.retryable_assets | index("image")) != null and (.retryable_assets | index("audio")) == null' "storybook should expose images as retryable assets" echo "$storybook_json" | jq '{id,title,mode,generation_status,image_status,audio_status,retryable_assets,pages:(.pages | length)}' say "Checking storybook generation job events" assert_jq "$storybook_job_json" '.id == "'"$storybook_job_id"'" and .story_id == '"$storybook_id"'' "storybook generation job should be queryable" assert_jq "$storybook_job_json" '.progress_percent == 100 and .is_terminal == true' "storybook generation job should expose progress summary" assert_jq "$storybook_job_json" '([.events[].event_type] | index("worker_started")) != null and ([.events[].event_type] | index("context_prepared")) != null and ([.events[].event_type] | index("narrative_generated")) != null and ([.events[].event_type] | index("story_saved")) != null' "storybook generation job should include workflow events" echo "$storybook_job_json" | jq '{id,status,current_step,events:([.events[].event_type] | unique)}' say "Checking storybook provider stats" storybook_provider_stats_json="$(get_json "$APP_URL/api/generations/$storybook_id/provider-stats")" assert_jq "$storybook_provider_stats_json" '.total_calls >= 1 and .successful_calls >= 1 and (.by_provider | length) >= 1' "storybook provider stats should summarize provider calls" echo "$storybook_provider_stats_json" | jq '{story_id,total_calls,successful_calls,failed_calls,avg_latency_ms,estimated_cost_usd}' say "Retrying storybook images" storybook_image_json="$(post_json "$APP_URL/api/generations/$storybook_id/retry-assets" '{"assets":["image"]}')" assert_jq "$storybook_image_json" '.image_status == "ready" and (.pages | length) >= 4 and ([.pages[] | select(.image_url != null)] | length) == (.pages | length)' "storybook images should be ready after retry" assert_jq "$storybook_image_json" '(.retryable_assets | length) == 0' "storybook should have no retryable assets after images are ready" echo "$storybook_image_json" | jq '{id,title,generation_status,image_status,audio_status,retryable_assets,pages:(.pages | length), ready_pages:([.pages[] | select(.image_url != null)] | length)}' say "Checking story job history" story_jobs_json="$(get_json "$APP_URL/api/generations/$story_id/jobs")" storybook_jobs_json="$(get_json "$APP_URL/api/generations/$storybook_id/jobs")" assert_jq "$story_jobs_json" 'length >= 2 and (map(.id) | index("'"$story_job_id"'")) != null' "story job history should include generation and retry jobs" assert_jq "$storybook_jobs_json" 'length >= 2 and (map(.id) | index("'"$storybook_job_id"'")) != null' "storybook job history should include generation and retry jobs" echo "$story_jobs_json" | jq '[.[] | {id,output_mode,status,current_step}]' echo "$storybook_jobs_json" | jq '[.[] | {id,output_mode,status,current_step}]' say "Checking cross-story provider analytics" provider_analytics_json="$(get_json "$APP_URL/api/generations/provider-analytics")" assert_jq "$provider_analytics_json" '.total_calls >= 2 and .successful_calls >= 2 and .job_count >= 4 and .story_count >= 2 and (.by_provider | length) >= 1' "provider analytics should summarize calls across generated stories" echo "$provider_analytics_json" | jq '{total_calls,successful_calls,failed_calls,job_count,story_count,avg_latency_ms,estimated_cost_usd}' say "Checking filtered provider analytics" filtered_provider_analytics_json="$(get_json "$APP_URL/api/generations/provider-analytics?days=7&capability=text")" assert_jq "$filtered_provider_analytics_json" '.window_days == 7 and .capability == "text" and .total_calls >= 1' "filtered provider analytics should honor days/capability filters" echo "$filtered_provider_analytics_json" | jq '{window_days,capability,total_calls,successful_calls,failed_calls,failure_reasons}' say "Checking generation ops summary" ops_summary_json="$(get_json "$APP_URL/api/generations/ops-summary?hours=24")" assert_jq "$ops_summary_json" '.window_hours == 24 and .active_jobs >= 0 and .stale_running_jobs >= 0 and .failed_jobs >= 0 and .asset_retry_jobs >= 2' "generation ops summary should expose recent task health" echo "$ops_summary_json" | jq '{window_hours,stale_threshold_minutes,active_jobs,stale_running_jobs,failed_jobs,degraded_jobs,asset_retry_jobs,recent_failures}' say "Checking story list" list_json="$(get_json "$APP_URL/api/stories?limit=5")" assert_jq "$list_json" "map(.id) | index($story_id) != null" "story list should include generated story" assert_jq "$list_json" "map(.id) | index($storybook_id) != null" "story list should include generated storybook" echo "$list_json" | jq '.[] | {id,title,mode,generation_status,image_status,audio_status,retryable_assets}' say "DreamWeaver demo smoke passed"