#!/bin/bash # 外挂状态检测的回归测试 # 覆盖:not_installed / env_unavailable / runtime_failed / empty_result / unsupported set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" fail() { echo "FAIL: $1" >&2 exit 1 } assert_file_contains() { local file="$1" local text="$2" if ! grep -F -- "$text" "$file" > /dev/null; then fail "Expected $file to contain: $text" fi } assert_text_contains() { local text="$1" local expected="$2" if ! printf '%s' "$text" | grep -F -- "$expected" > /dev/null; then fail "Expected output to contain: $expected" fi } assert_text_not_contains() { local text="$1" local unexpected="$2" if printf '%s' "$text" | grep -F -- "$unexpected" > /dev/null; then fail "Expected output to not contain: $unexpected" fi } make_stub() { local path="$1" local body="$2" printf '%s\n' "$body" > "$path" chmod +x "$path" } prepare_skill_root() { local skill_root="$1" mkdir -p "$skill_root/baoyu-url-to-markdown" mkdir -p "$skill_root/youtube-transcript" } prepare_source_checkout() { local repo_root="$1" mkdir -p "$repo_root/scripts" "$repo_root/deps/baoyu-url-to-markdown" "$repo_root/deps/youtube-transcript" : > "$repo_root/.git" cp "$REPO_ROOT/scripts/adapter-state.sh" "$repo_root/scripts/adapter-state.sh" cp "$REPO_ROOT/scripts/source-registry.sh" "$repo_root/scripts/source-registry.sh" cp "$REPO_ROOT/scripts/source-registry.tsv" "$repo_root/scripts/source-registry.tsv" cp "$REPO_ROOT/scripts/shared-config.sh" "$repo_root/scripts/shared-config.sh" cp "$REPO_ROOT/scripts/runtime-context.sh" "$repo_root/scripts/runtime-context.sh" } test_bundled_adapters_are_not_treated_as_installed_from_repo_checkout() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/bin" "$tmp_dir/skills" make_stub "$tmp_dir/bin/lsof" '#!/bin/sh exit 1' output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" check web_article 2>&1 )" || fail "adapter-state should check the target skill root, not the repo checkout" assert_text_contains "$output" "not_installed" assert_text_contains "$output" "baoyu-url-to-markdown" } test_source_checkout_uses_repo_deps_for_bundled_adapters() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN prepare_source_checkout "$tmp_dir/repo" mkdir -p "$tmp_dir/bin" make_stub "$tmp_dir/bin/lsof" '#!/bin/sh exit 1' output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$tmp_dir/repo/scripts/adapter-state.sh" check web_article 2>&1 )" || fail "adapter-state should understand bundled deps from a source checkout" assert_text_contains "$output" "available" assert_text_contains "$output" "临时浏览器" } test_adapter_state_distinguishes_not_installed_and_unsupported() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/bin" "$tmp_dir/skills" make_stub "$tmp_dir/bin/uv" '#!/bin/sh exit 0' output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" check wechat_article 2>&1 )" || fail "adapter-state should classify not_installed" assert_text_contains "$output" "not_installed" assert_text_contains "$output" "wechat-article-to-markdown" assert_text_contains "$output" "手动入口" output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" check xiaohongshu_post 2>&1 )" || fail "adapter-state should classify unsupported" assert_text_contains "$output" "unsupported" assert_text_contains "$output" "请先从 App 或网页复制内容" } test_web_capture_available_without_chrome_debug_port() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/bin" "$tmp_dir/skills" prepare_skill_root "$tmp_dir/skills" make_stub "$tmp_dir/bin/lsof" '#!/bin/sh exit 1' output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" check web_article 2>&1 )" || fail "adapter-state should keep Chrome-backed sources available without 9222" assert_text_contains "$output" "available" assert_text_contains "$output" "临时浏览器" assert_text_contains "$output" "9222" } test_web_capture_available_with_chrome_debug_port() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/bin" "$tmp_dir/skills" prepare_skill_root "$tmp_dir/skills" make_stub "$tmp_dir/bin/lsof" '#!/bin/sh exit 0' output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" check web_article 2>&1 )" || fail "adapter-state should report available with Chrome debug session" assert_text_contains "$output" "available" assert_text_contains "$output" "可复用的 Chrome 调试会话" # install_hint 应为"-",不应出现"补充说明" assert_text_not_contains "$output" "9222" } test_uv_backed_source_reports_env_unavailable_without_uv() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/bin" "$tmp_dir/skills" prepare_skill_root "$tmp_dir/skills" make_stub "$tmp_dir/bin/lsof" '#!/bin/sh exit 1' output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" check youtube_video 2>&1 )" || fail "adapter-state should classify env_unavailable for uv-backed sources" assert_text_contains "$output" "env_unavailable" assert_text_contains "$output" "uv" } test_adapter_state_distinguishes_runtime_failed_and_empty_result() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/bin" "$tmp_dir/skills" prepare_skill_root "$tmp_dir/skills" make_stub "$tmp_dir/bin/lsof" '#!/bin/sh exit 0' make_stub "$tmp_dir/bin/uv" '#!/bin/sh exit 0' : > "$tmp_dir/empty.txt" printf 'body\n' > "$tmp_dir/full.txt" output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" classify-run web_article 1 "$tmp_dir/full.txt" 2>&1 )" || fail "adapter-state should classify runtime_failed" assert_text_contains "$output" "runtime_failed" assert_text_contains "$output" "重试" output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" classify-run web_article 0 "$tmp_dir/empty.txt" 2>&1 )" || fail "adapter-state should classify empty_result" assert_text_contains "$output" "empty_result" assert_text_contains "$output" "手动补全文本" output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" classify-run web_article 0 "$tmp_dir/full.txt" 2>&1 )" || fail "adapter-state should keep available runs green" assert_text_contains "$output" "available" } test_classify_run_preserves_preflight_failures() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/bin" "$tmp_dir/skills" printf 'body\n' > "$tmp_dir/full.txt" make_stub "$tmp_dir/bin/uv" '#!/bin/sh exit 0' output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" classify-run wechat_article 1 "$tmp_dir/full.txt" 2>&1 )" || fail "classify-run should preserve not_installed preflight state" assert_text_contains "$output" "not_installed" assert_text_not_contains "$output" "runtime_failed" output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" classify-run xiaohongshu_post 1 "$tmp_dir/full.txt" 2>&1 )" || fail "classify-run should preserve unsupported preflight state" assert_text_contains "$output" "unsupported" assert_text_not_contains "$output" "runtime_failed" } test_install_reports_adapter_states_from_shared_model() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/home/.claude/skills" "$tmp_dir/bin" make_stub "$tmp_dir/bin/bun" '#!/bin/sh mkdir -p node_modules exit 0' make_stub "$tmp_dir/bin/lsof" '#!/bin/sh exit 1' make_stub "$tmp_dir/bin/uv" "#!/bin/sh printf '%s\n' '#!/bin/sh' 'exit 0' > \"$tmp_dir/bin/wechat-article-to-markdown\" chmod +x \"$tmp_dir/bin/wechat-article-to-markdown\" exit 0" output="$( HOME="$tmp_dir/home" \ PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/install.sh" --platform claude --with-optional-adapters 2>&1 )" || fail "install.sh should surface adapter states" assert_text_contains "$output" "外挂状态" assert_text_contains "$output" "网页文章" assert_text_contains "$output" "可用" assert_text_contains "$output" "临时浏览器" assert_text_contains "$output" "微信公众号" assert_text_contains "$output" "可用" } test_skill_routes_ingest_and_status_through_adapter_state_model() { assert_file_contains "$REPO_ROOT/SKILL.md" "scripts/adapter-state.sh" assert_file_contains "$REPO_ROOT/SKILL.md" "not_installed / env_unavailable / runtime_failed / unsupported / empty_result" assert_file_contains "$REPO_ROOT/SKILL.md" "外挂状态" assert_file_contains "$REPO_ROOT/SKILL.md" "--with-optional-adapters" } test_summary_human_shows_supplementary_label_for_available_with_hint() { local tmp_dir output tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' RETURN mkdir -p "$tmp_dir/bin" "$tmp_dir/skills" prepare_skill_root "$tmp_dir/skills" # lsof 返回 1 → Chrome 9222 未监听 → state=available + install_hint 非空 make_stub "$tmp_dir/bin/lsof" '#!/bin/sh exit 1' output="$( PATH="$tmp_dir/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ bash "$REPO_ROOT/scripts/adapter-state.sh" --skill-root "$tmp_dir/skills" summary-human 2>&1 )" || fail "summary-human should succeed" # available + 有 install_hint → 应显示"补充说明" assert_text_contains "$output" "补充说明" } test_adapter_state_distinguishes_not_installed_and_unsupported test_bundled_adapters_are_not_treated_as_installed_from_repo_checkout test_source_checkout_uses_repo_deps_for_bundled_adapters test_web_capture_available_without_chrome_debug_port test_web_capture_available_with_chrome_debug_port test_uv_backed_source_reports_env_unavailable_without_uv test_adapter_state_distinguishes_runtime_failed_and_empty_result test_classify_run_preserves_preflight_failures test_install_reports_adapter_states_from_shared_model test_skill_routes_ingest_and_status_through_adapter_state_model test_summary_human_shows_supplementary_label_for_available_with_hint echo "Adapter state checks passed."