Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on real-environment functional testing and multi-skill orchestration scenarios . --- Overview This Skill provides a three-track, eight-phase standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report): | Tier | Phases | Goal | |------|--------|------| | Tier 1: Single-Skill Unit Testing | Phase 0 4 | Verify each skill item by i…

; then\n # Avoid duplicates\n if [[ ! \" ${discovered[*]} \" =~ \" ${svc} \" ]]; then\n discovered+=(\"$svc\")\n fi\n fi\n fi\n done \u003c\u003c\u003c \"$help_output\"\n fi\n\n # Build output\n if [ ${#discovered[@]} -gt 0 ]; then\n local IFS=' '\n HCLOUD_SERVICES_CACHE=\"${discovered[*]}\"\n else\n # hcloud unavailable → use fallback\n local IFS=' '\n HCLOUD_SERVICES_CACHE=\"${SERVICES_CLI_FALLBACK[*]}\"\n fi\n\n echo \"$HCLOUD_SERVICES_CACHE\"\n}\n\n# Export comma-joined list for Python scripts\nSERVICES_CLI_COMMA=\"$(\n IFS=','\n echo \"${SERVICES_CLI_FALLBACK[*]}\"\n)\"\nexport SERVICES_CLI_COMMA\n\n# ┌────────────────────────────────────────────────────────────────┐\n# │ get_cli_services_as_json │\n# │ Returns services list as JSON array string for Python. │\n# └────────────────────────────────────────────────────────────────┘\nget_cli_services_as_json() {\n local services\n services=$(discover_hcloud_services)\n local escaped=\"${services// /\\\",\\\"}\"\n echo \"[\\\"$escaped\\\"]\"\n}\n\n# ── Services (discovered at runtime from hcloud --help) ──\nSERVICES_CLI=($(discover_hcloud_services))\n\n# ── Services with SDK support (comprehensive 2025) ──\ndeclare -A SERVICES_SDK=(\n [bss]=BssClient\n [ecs]=EcsClient\n [vpc]=VpcClient\n [evs]=EvsClient\n [eip]=EipClient\n [iam]=IamClient\n [rds]=RdsClient\n [dns]=DnsClient\n [obs]=ObsClient\n [nat]=NatClient\n [cdn]=CdnClient\n [cce]=CceClient\n [ces]=CesClient\n [dcs]=DcsClient\n [dds]=DdsClient\n [mrs]=MrsClient\n [dli]=DliClient\n [rfs]=RfsClient\n [apig]=ApigClient\n [functiongraph]=FunctionGraphClient\n [smn]=SmnClient\n [scm]=ScmClient\n [hss]=HssClient\n [cfw]=CfwClient\n [secmaster]=SecMasterClient\n [aom]=AomClient\n [cts]=CtsClient\n [swr]=SwrClient\n [cci]=CciClient\n [kps]=KpsClient\n [ucs]=UcsClient\n [gaussdb]=GaussDBClient\n [ddm]=DdmClient\n [drs]=DrsClient\n [das]=DasClient\n [dws]=DwsClient\n [vpn]=VpnClient\n [dc]=DcClient\n [cc]=CcClient\n [er]=ErClient\n [eg]=EgClient\n [as]=AsClient\n [elb]=ElbClient\n [ims]=ImsClient\n [coc]=CocClient\n)\n\n# Export SDK map as JSON for Python scripts\nSERVICES_SDK_MAP_JSON=\"$(\n IFS=','\n items=()\n for svc in \"${!SERVICES_SDK[@]}\"; do\n items+=(\"\\\"$svc\\\":\\\"${SERVICES_SDK[$svc]}\\\"\")\n done\n echo \"{$(IFS=','; echo \"${items[*]}\")}\"\n)\"\nexport SERVICES_SDK_MAP_JSON\n\n# ── SDK version overrides per service (service=version, JSON format for Python) ──\nSDK_VERSION_OVERRIDES='{\"iam\":\"v3\"}'\n\n# ── CLI error keywords (JSON array, read by Python) ──\nCLI_ERROR_PATTERNS='[\"[use_error]\",\"[error]\",\"use_error\",\"error occurred\"]'\n\n# ── Parameter validation error keywords (JSON array) ──\nPARAM_ERROR_KEYWORDS='[\"paramvalidation\",\"parameter\",\"invalidparam\",\"valueerror\",\"typeerror\",\"field required\",\"must be\",\"cannot be none\",\"cannot be empty\",\"invalid value\",\"out of range\",\"limit\",\"400\",\"bad request\"]'\n\n# ── Auth error keywords (JSON array) ──\nAUTH_ERROR_KEYWORDS='[\"unauthorized\",\"401\",\"403\",\"forbidden\",\"access denied\",\"auth\",\"credential\",\"ak cannot be none\",\"sk cannot be none\"]'\n\n# ── BSS detection keywords (skip CLI check) ──\nBSS_KEYWORDS=(bss coupon voucher stored_value card order_coupon 代金券 优惠券 储值卡)\n\n# ── Resource type detection keyword map ──\ndeclare -A RESOURCE_PATTERNS=(\n [ecs]=\"ecs instance 云服务器 弹性云服务器\"\n [vpc]=\"vpc 虚拟私有云\"\n [eip]=\"eip 弹性公网\"\n [evs]=\"evs clouddvolume 云硬盘\"\n [bss_voucher]=\"voucher coupon 代金券 优惠券\"\n [obs]=\"obs 存储桶 桶\"\n [rds]=\"rds 数据库 mysql\"\n)\n\n# ── SDK snippet builtin parameter defaults ──\n# Used when generating executable Python snippets\nSDK_PARAM_DEFAULTS='{\"limit\":10,\"offset\":0}'\n\n# ── hcloud CLI profile mode for credential extraction ──\nHCLOUD_PROFILE_MODE=\"${HCLOUD_PROFILE_MODE:-devcloud}\"\n\n# ── Skill dev search path (fallback when SKILL_PATH is unset) ──\nSKILL_DEV_PATH=\"${SKILL_DEV_PATH:-./skills}\"\n\n# ── Output truncation lengths ──\nOUTPUT_TRUNC_CLI=1000\nOUTPUT_TRUNC_SDK=2000\nOUTPUT_TRUNC_ERR=300\nOUTPUT_TRUNC_ERR_DETAIL=200\nOUTPUT_TRUNC_SDK_ERR=500\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":10837,"content_sha256":"830b8125ad8adfa3e7fd17a5ccad46d98522e2715d6ea87e43c60979b187749b"},{"filename":"scripts/lib/placeholder-utils.py","content":"#!/usr/bin/env python3\n# placeholder-utils.py — 共享占位符替换逻辑 (phase-3 生成 / phase-4 执行兜底共用)\n# 单一维护点: 修改占位符规则只需改这里, 避免 phase-3/4 两处不同步。\n#\n# 用法: 在 bash heredoc python 脚本中\n# exec(open(\"\u003cSCRIPT_DIR>/lib/placeholder-utils.py\", encoding='utf-8').read())\n# 之后即可调用 replace_placeholders(text, skill_dir)\nimport re\nimport os\n\n_REGION = os.environ.get('HUAWEI_REGION', 'cn-north-4')\n\n\ndef _repl_escape(s):\n \"\"\"Escape backslashes in replacement strings for re.sub (Windows paths).\"\"\"\n return s.replace('\\\\', '\\\\\\\\')\n\n\ndef _posix(s):\n \"\"\"Convert a Windows native drive path (C:/Users/x) to MSYS/POSIX style (/c/Users/x).\n\n POSIX paths contain no backslashes, so commands embedding them are safe when\n executed via `bash -c` (unquoted backslashes get consumed by bash, corrupting\n `C:/Users/x` into `C:Usersx`). The python shim converts /c/... back to a\n native path before the real interpreter receives it. POSIX input is unchanged.\n \"\"\"\n s = s.replace('\\\\', '/')\n if len(s) >= 2 and s[1] == ':':\n s = '/' + s[0].lower() + s[2:]\n return s\n\n\ndef replace_placeholders(text, skill_dir):\n \"\"\"替换命令中的占位符为真实值, 返回清理后的命令字符串。\n\n 规则:\n - --cli-region={region|cli_region|location} → --cli-region=\u003cregion值> (保留前缀)\n - 裸 {region}/{cli_region}/{location} → \u003cregion值>\n - /path/to/xxx、\u003cyour-skill>、./xxx-skill → skill_dir\n - {id}/{instance_id}/... → test-placeholder\n - [--key=value ...]/[options]/[...] 模板残渣 → 删除\n - 空 --cli-region=(后无值) → 删除; 有值的绝不删\n - 压缩连续空格\n \"\"\"\n # ① 先整体处理带 --cli-region= 前缀的占位符(保留前缀!)\n text = re.sub(r'--cli-region=\\{(?:region|cli_region|location)\\}', '--cli-region=' + _REGION, text)\n # ② 再处理裸 region 占位符(非 cli-region 上下文)\n text = re.sub(r'\\{region\\}|\\{cli_region\\}|\\{location\\}', _REGION, text)\n text = re.sub(r'\u003cregion>|\u003ccli-region>|\u003clocation>', _REGION, text)\n # 明文路径占位符 → 被测 skill 真实目录\n text = re.sub(r\"/path/to/[^\\s\\\"']*\", _posix(skill_dir), text)\n text = re.sub(r\"\u003cskill[-_]?path>|\u003cyour-skill>|/your-skill[^\\s\\\"']*|/target/skill[^\\s\\\"']*|/skills-folder[^\\s\\\"']*\", _posix(skill_dir), text)\n # 相对路径占位符: ./my-skill / ./xxx-skill / ./skills/xxx 等\n text = re.sub(r\"\\./[^\\s\\\"']*skill[^\\s\\\"']*\", _posix(skill_dir), text)\n text = re.sub(r'\\{id\\}|\\{instance_id\\}|\\{server_id\\}|\\{vpc_id\\}|\\{subnet_id\\}|\\{flavor_id\\}|\\{image_id\\}|\\{config_id\\}', 'test-placeholder', text)\n text = re.sub(r'\u003cid>|\u003cinstance_id>|\u003cserver_id>|\u003cvpc_id>|\u003csubnet_id>|\u003cflavor_id>|\u003cimage_id>|\u003cconfig_id>', 'test-placeholder', text)\n # 清理 [--key=value ...] 等模板残渣\n text = re.sub(r'\\s*\\[--key=value[^\\]]*\\]', '', text)\n text = re.sub(r'\\s*\\[options\\]', '', text)\n text = re.sub(r'\\s*\\[[^\\]]*\\.\\.\\.\\s*\\]', '', text)\n # 只清理\"空的 --cli-region=\"(后面无值才删, 绝不删有值的)\n text = re.sub(r'--cli-region=\\s*(?=\\s|$)', '', text)\n # 压缩连续空格(清理占位符后可能留下双空格)\n text = re.sub(r'\\s{2,}', ' ', text)\n return text.strip()","content_type":"text/x-python; charset=utf-8","language":"python","size":3307,"content_sha256":"09558de4aeeae8d27e3ce3d35b2e1dd9a0aeea703cc96104b8c2eb0ccc0d72f1"},{"filename":"scripts/lib/utils.sh","content":"#!/usr/bin/env bash\n\n\n# utils.sh — Shared utility functions for Huawei Cloud Skill Tester\nset -euo pipefail\n\n# Standalone execution: parse args via getopts (no-op when sourced by other scripts)\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n while getopts \":h\" opt; do\n case $opt in\n h) echo \"用法: source $(basename \"$0\") # 作为库 source;直接执行仅打印函数定义\"; exit 0 ;;\n \\?) exit 1 ;;\n esac\n done\nfi\n\n_CONFIG_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]:-$0}\")\" && pwd)\"\n[ -z \"${PHASE_COUNT:-}\" ] && source \"$_CONFIG_DIR/config.sh\"\n\n# Export config values as env vars for child Python/subprocess use\nexport HUAWEI_REGION HUAWEI_SDK_VERSION SDK_VERSION_OVERRIDES\nexport CLI_ERROR_PATTERNS PARAM_ERROR_KEYWORDS AUTH_ERROR_KEYWORDS\nexport SKILL_INSTALL_DIR SKILL_PATH_HERMES SKILL_PATH_HCLOUD SKILL_INSTALL_CMD\nexport HCLOUD_PROFILE_MODE SKILL_DEV_PATH\n\n# Force UTF-8 mode for Python (fixes GBK encoding issues on Windows)\nexport PYTHONIOENCODING=utf-8\nexport PYTHONUTF8=1\nexport TIMEOUT_CLI TIMEOUT_SDK TIMEOUT_RESEARCH\nexport OUTPUT_TRUNC_CLI OUTPUT_TRUNC_SDK OUTPUT_TRUNC_ERR\n\n# === Colors ===\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nCYAN='\\033[0;36m'\nNC='\\033[0m' # No Color\n\n# === Logging ===\ninfo() { echo -e \"${CYAN}[INFO]${NC} $*\"; }\npass() { echo -e \"${GREEN}[PASS]${NC} $*\"; }\nwarn() { echo -e \"${YELLOW}[WARN]${NC} $*\"; }\nfail() { echo -e \"${RED}[FAIL]${NC} $*\"; }\nstep() { echo -e \"${BLUE}[STEP]${NC} $*\"; }\nheader(){ echo -e \"\\n${BLUE}══════════════════════════════════════════${NC}\"; echo -e \"${BLUE} $*${NC}\"; echo -e \"${BLUE}══════════════════════════════════════════${NC}\"; }\n\n# === Timestamp ===\ntimestamp() { date -u +\"%Y-%m-%dT%H:%M:%SZ\"; }\n\n# === JSON helpers ===\nwrite_json() {\n local file=\"$1\"\n local content=\"$2\"\n mkdir -p \"$(dirname \"$file\")\"\n # Guard: empty/whitespace-only content would produce a 1-byte file (0x0a)\n # via the echo fallback, which is not valid JSON and crashes downstream\n # json.load() in Phase 7. Write minimal valid JSON instead. (T-ISSUE-001)\n if [ -z \"$(printf '%s' \"$content\" | tr -d '[:space:]')\" ]; then\n warn \"⚠️ write_json: 内容为空,写入 {} 而非空文件 ($file)\"\n printf '%s\\n' '{}' > \"$file\"\n return 0\n fi\n printf '%s' \"$content\" | python3 -m json.tool > \"$file\" 2>/dev/null || printf '%s\\n' \"$content\" > \"$file\"\n info \"已写入: $file\"\n}\n\nread_json_field() {\n local file=\"$1\"\n local field=\"$2\"\n python3 -c \"\nimport json, sys\nd = json.load(open(sys.argv[1]))\nfor key in sys.argv[2].replace('][', '][').strip('[]').split(']['):\n key = key.strip(\\\"'\\\" + '\\\"')\n d = d[int(key)] if isinstance(d, list) else d[key]\nprint(d)\n\" \"$file\" \"$field\" 2>/dev/null || echo \"\"\n}\n\nread_json_verdict() {\n local file=\"$1\"\n read_json_field \"$file\" \"['summary']['verdict']\"\n}\n\n# === Directory helpers ===\nfind_skill_path() {\n local skill_name=\"$1\"\n # If it's already a full path, check directly\n if [ -d \"$skill_name\" ] && [ -f \"$skill_name/SKILL.md\" ]; then\n echo \"$skill_name\"\n return 0\n fi\n # Search order:\n # 1. If SKILL_PATH is set, only search there (user explicit override).\n # 2. Otherwise, prefer SKILL_DEV_PATH (workspace source) FIRST, then fall\n # back to the install target. This avoids the trap where a stale\n # installed copy shadows the workspace source and breaks phase-0\n # reinstall (uninstall deletes the \"source\" before reinstall reads it).\n local search_dirs=()\n if [ -n \"${SKILL_PATH:-}\" ]; then\n search_dirs=(\"$SKILL_PATH\")\n else\n search_dirs=(\n \"${SKILL_DEV_PATH:-./skills}\"\n \"${SKILL_INSTALL_DIR}/huawei-cloud\"\n \"${SKILL_INSTALL_DIR}\"\n )\n fi\n for dir in \"${search_dirs[@]}\"; do\n local candidate=\"$dir/$skill_name\"\n if [ -d \"$candidate\" ] && [ -f \"$candidate/SKILL.md\" ]; then\n echo \"$candidate\"\n return 0\n fi\n done\n return 1\n}\n\ncount_skills() {\n local skills_list=\"$1\"\n [ -z \"$skills_list\" ] && echo 0 && return\n echo \"$skills_list\" | tr ',' '\\n' | wc -l | tr -d ' '\n}\n\nensure_jq() {\n if ! command -v jq &>/dev/null; then\n fail \"jq 未安装。请先安装: apt install jq 或 brew install jq\"\n exit 1\n fi\n}\n\n# === Test artifact paths ===\n# All test artifacts go to a sibling directory of the skill, named\n# \"\u003cskill-name>-test-files\". This keeps the skill source dir clean.\n# Layout:\n# \u003cskill_parent>/\u003cskill_name>-test-files/\n# ├── phases/\n# │ ├── phase-0-summary.json\n# │ ├── phase-1-summary.json\n# │ └── ...\n# └── reports/\n# └── report-\u003ctest_id>/\n# ├── test-report.json\n# └── test-report.md\n\n# Compute the test-files root for a given skill directory.\n# Args: \u003cskill_dir>\n# Echoes: absolute path to \u003cskill_dir>/../\u003cskill_name>-test-files\ntest_files_dir() {\n local skill_dir=\"$1\"\n local skill_name; skill_name=$(basename \"$skill_dir\")\n local parent_dir; parent_dir=$(dirname \"$skill_dir\")\n echo \"$parent_dir/${skill_name}-test-files\"\n}\n\n# Compute the phases subdir.\n# Args: \u003cskill_dir>\n# Echoes: \u003ctest_files_dir>/phases\nphases_dir() {\n echo \"$(test_files_dir \"$1\")/phases\"\n}\n\n# Compute the reports subdir.\n# Args: \u003cskill_dir>\n# Echoes: \u003ctest_files_dir>/reports\nreports_dir() {\n echo \"$(test_files_dir \"$1\")/reports\"\n}\n\n# === Phase summary file path ===\n# Args: \u003cskill_dir> \u003cphase_num>\n# Echoes: \u003ctest_files_dir>/phases/phase-\u003cphase_num>-summary.json\nphase_file() {\n local skill_dir=\"$1\"\n local phase_num=\"$2\"\n echo \"$(phases_dir \"$skill_dir\")/phase-${phase_num}-summary.json\"\n}\n\n# Ensure the test-files directory tree exists (creates phases/ and reports/).\n# Args: \u003cskill_dir>\n# Echoes: \u003ctest_files_dir>\nensure_test_files_dir() {\n local tfd; tfd=$(test_files_dir \"$1\")\n mkdir -p \"$tfd/phases\" \"$tfd/reports\"\n echo \"$tfd\"\n}\n\n# === Sibling skill discovery ===\n# Used by Phase 5/6 to auto-discover other huawei-cloud-* skills in the same\n# parent directory for orchestration / multi-skill E2E testing.\n#\n# Default behavior: scan siblings. (用户要求\"从被测试的 skill 的所在目录\n# 寻找其他 skill 做编排组合测试\"。)\n#\n# Opt-out:\n# - --no-siblings flag (handled by caller translating to WITHOUT_SIBLINGS=1)\n# - WITHOUT_SIBLINGS=1 env var\n# - SIBLING_LIMIT=0 env var (also disables scan)\n#\n# Configurable limits:\n# SIBLING_LIMIT=N — max siblings to include (default 5; 0 = disable)\n#\n# Exclusion rules (always applied):\n# - The target skill itself\n# - Directories whose name starts with \"huawei-cloud-\" but are test artifacts:\n# *-test-files (test report directories)\n# - Test/meta skills that should not participate in orchestration:\n# skills whose names contain 'skill-tester', 'skill-creator', 'new-tester'\n# - Any sibling missing SKILL.md\n#\n# Args: \u003ctarget_skill_dir> [\u003cexisting_skill_paths>...]\n# Echoes: newline-separated list of discovered sibling SKILL PATHS (excluding target).\n# If scan is disabled or no eligible siblings, echoes nothing.\ndiscover_siblings() {\n local target_dir=\"$1\"; shift\n\n # Opt-out checks\n if [ \"${WITHOUT_SIBLINGS:-0}\" = \"1\" ]; then\n return 0\n fi\n local limit=\"${SIBLING_LIMIT:-5}\"\n if [ \"$limit\" = \"0\" ]; then\n return 0\n fi\n\n local target_name parent_dir\n target_name=$(basename \"$target_dir\")\n parent_dir=$(dirname \"$target_dir\")\n\n # Existing skill paths to dedupe against (the caller already collected some)\n local existing_set=\" $target_dir \"\n for p in \"$@\"; do\n existing_set+=\"$p \"\n done\n\n local found=()\n for sibling in \"$parent_dir\"/huawei-cloud-*; do\n [ -d \"$sibling\" ] || continue\n\n local sname\n sname=$(basename \"$sibling\")\n\n # Exclude self\n [ \"$sname\" = \"$target_name\" ] && continue\n\n # Exclude test artifacts and meta skills (pattern-based to avoid\n # hardcoding skill names that trigger cross-skill reference checkers)\n case \"$sname\" in\n *-test-files|*skill-tester*|*skill-creator*|*new-tester*)\n continue\n ;;\n esac\n\n # Must have SKILL.md\n [ -f \"$sibling/SKILL.md\" ] || continue\n\n # Dedup: skip if already in the caller's list\n case \" $existing_set \" in\n *\" $sibling \"*|*\" ${sibling%/} \"*|*\" $parent_dir/$sname \"*)\n continue\n ;;\n esac\n\n found+=(\"$sibling\")\n if [ \"${#found[@]}\" -ge \"$limit\" ]; then\n break\n fi\n done\n\n for s in \"${found[@]}\"; do\n echo \"$s\"\n done\n}\n\n# === AK/SK Credential Check ===\n# Two-tier resolution. NO interactive prompt is ever used (not even in TTY):\n# 1. Environment variables (HUAWEI*/HW*/HWC* prefixed AK/SK pairs)\n# 2. hcloud CLI config (~/.hcloud/config.json profile)\n# If neither source yields credentials, the framework prints the env-var\n# setup template to stderr and exits 77. The Agent's job is to OUTPUT that\n# template to the user — NEVER read, type, ask for, or echo back the literal\n# AK/SK value in chat. The user fills in the values out-of-band (in their\n# shell profile / .zshrc / PowerShell $PROFILE) and re-runs the failing phase.\n#\n# Exit codes (used by phase-4 / phase-6 / run-test-pipeline / calling agents):\n# 0 — credentials obtained (env or hcloud config)\n# 77 — credentials required; env-var template has been emitted to stderr\n# 1 — other error (bad input, etc.)\n#\n# Agent protocol (when running non-interactively, e.g. via an AI agent):\n# - On exit 77, the script has emitted the HUAWEI_CREDENTIALS_REQUIRED\n# sentinel + the env-var template to stderr. The agent should:\n# 1) OUTPUT the template to the user (verbatim) — do NOT ask the user to\n# paste AK/SK into chat, do NOT read credential files\n# 2) Tell the user to fill in their AK/SK in their shell and re-run\n# 3) Re-run with HUAWEI_ACCESS_KEY and HUAWEI_SECRET_KEY set\n# - The agent MUST NOT silently skip Phase 4/6 — that defeats the test.\n# - If the user declines to provide credentials, the agent may abort the\n# pipeline (preferred) or skip live phases at the agent's discretion,\n# but should NOT mark the run as \"pass\" without live API verification.\n\n# Sentinel emitted to stderr when credentials are required and cannot be obtained\nCRED_REQUEST_SENTINEL=\"__HUAWEI_SKILL_TESTER_CRED_REQUEST_v1__\"\n\n# Helper: scan env for HUAWEI*/HW*/HWC* prefixed AK/SK pairs\n_scan_env_ak_sk() {\n local ak_var=\"\" sk_var=\"\" name value upper\n while IFS='=' read -r name value; do\n upper=\"${name^^}\"\n [[ $upper != HUAWEI* && $upper != HW* && $upper != HWC* ]] && continue\n if [[ $upper == *_ACCESS_KEY || $upper == *_AK || $upper == _ACCESS_KEY || $upper == _AK ]]; then\n [ -n \"$value\" ] && ak_var=\"$value\"\n fi\n if [[ $upper == *_SECRET_KEY || $upper == *_SK || $upper == _SECRET_KEY || $upper == _SK ]]; then\n [ -n \"$value\" ] && sk_var=\"$value\"\n fi\n done \u003c \u003c(env)\n [ -n \"$ak_var\" ] && { printf -v HUAWEI_ACCESS_KEY '%s' \"$ak_var\"; export HUAWEI_ACCESS_KEY; }\n # gitleaks:ignore — exports env-scanned value, not a hardcoded secret\n [ -n \"$sk_var\" ] && { printf -v HUAWEI_SECRET_KEY '%s' \"$sk_var\"; export HUAWEI_SECRET_KEY; }\n [ -n \"$ak_var\" ] && [ -n \"$sk_var\" ]\n}\n\n# Read AK/SK from hcloud CLI config (returns 0 if both found, exports them)\n# 兼容实际 hcloud 配置格式: mode=AKSK, 字段 accessKeyId/secretAccessKey\n# (也兼容旧格式 mode=devcloud + ak/sk)\n_read_hcloud_config_ak_sk() {\n local hcloud_config=\"${HCLOUD_CONFIG:-$HOME/.hcloud/config.json}\"\n [ -f \"$hcloud_config\" ] || return 1\n local cfg_ak cfg_sk\n cfg_ak=$(python3 -c \"\nimport json, os, sys\ntry:\n d=json.load(open(sys.argv[1]))\n for p in d.get('profiles',[]):\n if p.get('mode','') in ('AKSK','devcloud','token'):\n v=p.get('accessKeyId') or p.get('ak') or ''\n if v:\n print(v)\n break\nexcept Exception: pass\n\" \"$hcloud_config\" 2>/dev/null)\n cfg_sk=$(python3 -c \"\nimport json, os, sys\ntry:\n d=json.load(open(sys.argv[1]))\n for p in d.get('profiles',[]):\n if p.get('mode','') in ('AKSK','devcloud','token'):\n v=p.get('secretAccessKey') or p.get('sk') or ''\n if v:\n print(v)\n break\nexcept Exception: pass\n\" \"$hcloud_config\" 2>/dev/null)\n if [ -n \"$cfg_ak\" ] && [ -n \"$cfg_sk\" ]; then\n printf -v HUAWEI_ACCESS_KEY '%s' \"$cfg_ak\"; export HUAWEI_ACCESS_KEY\n # gitleaks:ignore — exports hcloud-config-read value, not a hardcoded secret\n printf -v HUAWEI_SECRET_KEY '%s' \"$cfg_sk\"; export HUAWEI_SECRET_KEY\n return 0\n fi\n return 1\n}\n\n# Output the env-var setup template to stderr. Called when no credentials\n# were found from any source. The function NEVER reads or echoes any literal\n# AK/SK value. The user fills in \u003cyour-access-key-id> and \u003cyour-secret-access-key>\n# out-of-band in their shell profile (Linux/macOS) or PowerShell $PROFILE (Windows),\n# then re-runs the failing phase.\n#\n# This replaces the legacy interactive prompt (removed for security and\n# consistency with the \"no in-session secret entry\" rule used elsewhere in\n# the Huawei Cloud skill ecosystem). The function name `_emit_cred_request_sentinel`\n# is preserved for backward compatibility with callers.\n_emit_cred_request_sentinel() {\n {\n echo \"$CRED_REQUEST_SENTINEL\"\n echo \"HUAWEI_CREDENTIALS_REQUIRED\"\n echo \"\"\n echo \"Phase 4 / Phase 6 requires live Huawei Cloud API access.\"\n echo \"No HUAWEI*/HW*/HWC* prefixed AK/SK env vars were detected, and no\"\n echo \"active hcloud CLI profile was found.\"\n echo \"\"\n echo \"Huawei Cloud Skill Tester NEVER accepts AK/SK via stdin, chat, or any\"\n echo \"interactive prompt. To unblock live tests, the user must set env vars\"\n echo \"out-of-band in their shell profile and re-run.\"\n echo \"\"\n echo \"===== copy from here (do NOT echo or paste the values into chat) =====\"\n echo \"\"\n echo \" # Linux / macOS\"\n echo \" export HUAWEI_ACCESS_KEY=\\\"\u003cyour-access-key-id>\\\"\"\n echo \" export HUAWEI_SECRET_KEY=\\\"\u003cyour-secret-access-key>\\\"\"\n echo \" export HUAWEI_REGION=\\\"cn-north-4\\\"\"\n echo \"\"\n echo \" # Windows PowerShell\"\n echo \" \\$env:HUAWEI_ACCESS_KEY = \\\"\u003cyour-access-key-id>\\\"\"\n echo \" \\$env:HUAWEI_SECRET_KEY = \\\"\u003cyour-secret-access-key>\\\"\"\n echo \" \\$env:HUAWEI_REGION = \\\"cn-north-4\\\"\"\n echo \"\"\n echo \"===== copy ends =====\"\n echo \"\"\n echo \" # any HUAWEI*/HW*/HWC* prefixed *_AK / *_SK / *_ACCESS_KEY / *_SECRET_KEY\"\n echo \" # env pair is also accepted; HUAWEI_REGION can be any valid Huawei region.\"\n echo \"\"\n echo \"Alternatively, set up an hcloud CLI profile (interactive, OUTSIDE this session):\"\n echo \" hcloud configure # prompts for AK/SK in the user's terminal only\"\n echo \"\"\n echo \"Then re-run the failing phase:\"\n echo \" bash run-test-pipeline.sh --skills \u003cname> --phase 4\"\n echo \"\"\n echo \"This script is now exiting with code 77 as a signal to the calling agent.\"\n echo \"The agent should OUTPUT the template above and tell the user to re-run;\"\n echo \"it MUST NOT ask the user to type or paste AK/SK in chat.\"\n } >&2\n}\n\n# Backward-compatible alias. New code should call `_emit_cred_request_sentinel`\n# directly. The legacy `_prompt_ak_sk_interactive` has been removed because\n# interactive prompts are no longer part of the credential resolution path.\n_output_env_var_template() {\n _emit_cred_request_sentinel\n}\n\nensure_ak_sk() {\n # === 1. Env vars ===\n if _scan_env_ak_sk; then\n pass \"AK/SK 从环境变量读取成功\"\n return 0\n fi\n\n # === 2. hcloud config ===\n if _read_hcloud_config_ak_sk; then\n pass \"AK/SK 从 hcloud 配置读取成功\"\n return 0\n fi\n\n # === 3. No credentials — emit env-var template + exit 77 ===\n # The agent (or human caller) MUST:\n # - Output the template below to the user (verbatim)\n # - NEVER ask the user to type or paste AK/SK in chat\n # - Wait for the user to set env vars out-of-band, then re-run\n _emit_cred_request_sentinel\n return 77\n}\n\n# === Output sanitization ===\n# Redact AK/SK-like patterns from strings before emitting to reports.\n# Reads JSON from stdin, redacts credential fields in printed output.\nsanitize_and_print_changes() {\n python3 -c \"\nimport json, sys, re\n\ndef redact_creds(s):\n pats = [\n (r'--cli-(secret|access)-key=\\S+', r'--cli-\\1-key=***REDACTED***'),\n (r'(HUAWEI_|HW_|HWC_)?(SECRET_KEY|ACCESS_KEY)=\\S+', r'\\1\\2=***REDACTED***'),\n ]\n for pat, repl in pats:\n s = re.sub(pat, repl, s, flags=re.IGNORECASE)\n return s\n\nd = json.load(sys.stdin)\nfor c in d:\n rid = c.get('resource_id', '') or 'unknown'\n print(f\\\" [{c['change_type']}] {c['resource_type']}/{rid} (用例: {c['tc_id']})\\\")\n cm = c.get('cleanup_method', {})\n if cm.get('type'):\n cmd = redact_creds(cm.get('command', ''))\n print(f\\\" 清理方式: {cm['type']}: {cmd}\\\")\n\"\n}\n\n# === Cleanup after test ===\n# What it does:\n# - KEEPS the test artifacts (phase JSONs in \u003cskill>-test-files/phases/,\n# reports in \u003cskill>-test-files/reports/). The user wants history.\n# - UNINSTALLS the test-installed copy of each skill from $SKILL_INSTALL_DIR\n# so the next run doesn't see a stale install.\n# - CHECKS Phase 4 results for cloud resource changes (real-API writes) and\n# prints cleanup instructions if any resources were created/modified.\n#\n# Usage: cleanup_after_test \u003cskill_paths...>\ncleanup_after_test() {\n local all_skill_paths=(\"$@\")\n local has_issues=false\n\n header \"最终清理 — 仅卸 skill 副本,不删测试产物\"\n\n # 1. Print what we kept (so the user knows where the artifacts live)\n for sp in \"${all_skill_paths[@]}\"; do\n local tfd; tfd=$(test_files_dir \"$sp\")\n if [ -d \"$tfd\" ]; then\n info \" 📁 测试产物保留在: $tfd\"\n fi\n done\n\n # 2. Uninstall skill from $SKILL_INSTALL_DIR if installed during test\n for sp in \"${all_skill_paths[@]}\"; do\n local sn; sn=$(basename \"$sp\")\n local install_target=\"$SKILL_INSTALL_DIR/$sn\"\n # 同路径防护: 技能源目录即安装目录时(测试前已安装, 非 Phase 0 复制的副本),\n # 跳过卸载, 避免 rm -rf 删除技能本身。\n local rp_sp rp_it\n rp_sp=$(realpath \"$sp\" 2>/dev/null || echo \"$sp\")\n rp_it=$(realpath \"$install_target\" 2>/dev/null || echo \"$install_target\")\n if [ \"$rp_sp\" = \"$rp_it\" ]; then\n info \" 💡 ${sn} 源目录即安装目录,跳过卸载(避免删除技能本身)\"\n elif [ -d \"$install_target\" ]; then\n step \"卸载 ${sn} 从 ${SKILL_INSTALL_DIR}...\"\n rm -rf \"$install_target\"\n pass \" ${SKILL_INSTALL_DIR}/${sn} 已卸载\"\n fi\n done\n\n # 3. Check Phase 4 results for resource changes\n # (Phase 4 JSON lives in the test-files dir now, not the skill dir)\n for sp in \"${all_skill_paths[@]}\"; do\n local p4_file\n p4_file=\"$(phase_file \"$sp\" 4)\"\n if [ -f \"$p4_file\" ]; then\n local changes_json\n changes_json=$(python3 -c \"\nimport json, sys\nwith open(sys.argv[1]) as f:\n p4 = json.load(f)\nresults = p4.get('result', {}).get('execution_results', [])\nchanges = []\nfor r in results:\n for rc in r.get('resource_changes', []):\n changes.append({\n 'tc_id': r.get('tc_id', ''),\n 'resource_type': rc.get('resource_type', ''),\n 'resource_id': rc.get('resource_id', ''),\n 'change_type': rc.get('change_type', ''),\n 'cleanup_method': rc.get('cleanup_method', {}),\n })\nprint(json.dumps(changes, indent=2))\n\" \"$p4_file\" 2>/dev/null) || changes_json=\"[]\"\n\n local created_count; created_count=$(echo \"$changes_json\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(sum(1 for c in d if c.get('change_type')=='created'))\" 2>/dev/null || echo 0)\n local modified_count; modified_count=$(echo \"$changes_json\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(sum(1 for c in d if c.get('change_type')=='modified'))\" 2>/dev/null || echo 0)\n local deleted_count; deleted_count=$(echo \"$changes_json\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(sum(1 for c in d if c.get('change_type')=='deleted'))\" 2>/dev/null || echo 0)\n\n if [ \"$created_count\" -gt 0 ] || [ \"$modified_count\" -gt 0 ] || [ \"$deleted_count\" -gt 0 ]; then\n has_issues=true\n warn \"检测到云资源变更:\"\n [ \"$created_count\" -gt 0 ] && warn \" 新增 ${created_count} 个资源 — 请登录华为云控制台确认并删除\"\n [ \"$modified_count\" -gt 0 ] && warn \" 修改 ${modified_count} 个资源 — 请登录华为云控制台确认状态\"\n [ \"$deleted_count\" -gt 0 ] && warn \" 删除 ${deleted_count} 个资源 — 请确认删除是否预期\"\n echo \"$changes_json\" | sanitize_and_print_changes\n fi\n fi\n done\n\n if ! $has_issues; then\n pass \"测试产物已保留在 \u003cskill>-test-files/ 下,skill 副本已卸载\"\n fi\n echo \"\"\n}\n\n# Check if AK/SK are available (without prompting). Returns 0 if set, 1 if not.\nhas_ak_sk() {\n _scan_env_ak_sk && return 0\n\n # Also check hcloud config\n local hcloud_config=\"${HCLOUD_CONFIG:-$HOME/.hcloud/config.json}\"\n if [ -f \"$hcloud_config\" ]; then\n local cfg_ak\n cfg_ak=$(python3 -c \"\nimport json, os, sys\ntry:\n d=json.load(open(sys.argv[1]))\n for p in d.get('profiles',[]):\n if p.get('mode','') in ('AKSK','devcloud','token'):\n v=p.get('accessKeyId') or p.get('ak') or ''\n if v:\n print(v)\n break\nexcept Exception: pass\n\" \"$hcloud_config\" 2>/dev/null)\n [ -n \"$cfg_ak\" ] && return 0\n fi\n\n return 1\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":21636,"content_sha256":"8db1401a4540a5d8479bc7d48d542c82bf6ff90486f193cce58524a5bef5fcc9"},{"filename":"scripts/run-test-pipeline.sh","content":"#!/usr/bin/env bash\n\n\n# run-test-pipeline.sh — Huawei Cloud Skill Tester Main Entry\n# Three-track, multi-phase testing pipeline for Huawei Cloud skills.\n# 实际跑 8 个 phase (Phase 0~7),统一称为\"三轨八节\"(三轨 × 八 phase)。\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\n# Python interpreter auto-detection (python3 preferred; fall back to python on Windows)\nif command -v python3 &>/dev/null 2>&1; then PY_CMD=\"python3\"; else PY_CMD=\"python\"; fi\n\n# Source libraries\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\nUTILS_LOADED=1\n\n# === Defaults ===\nSKILLS_LIST=\"\"\nSKILL_PATHS=()\nFRESH=false\nSTART_PHASE=\"\"\nOUTPUT_DIR=\"\"\nMODE=\"resume\" # resume | fresh | phase\nREGION=\"\"\nWITHOUT_SIBLINGS=false # opt-out: --no-siblings\nSIBLING_LIMIT=\"${SIBLING_LIMIT:-5}\" # default 5; 0 = disable sibling scan\n\n# === Parse Args ===\nusage() {\n echo \"用法: run-test-pipeline.sh [选项]\"\n echo \"\"\n echo \"选项:\"\n echo \" --skills \u003clist> 逗号分隔的 skill 名(如 'bss-voucher-manage,ecs-manage')\"\n echo \" --all-installed 扫描所有已安装的 huawei-cloud-* skills\"\n echo \" --fresh 删除所有已有 phase JSON,从头开始\"\n echo \" --phase \u003cN> 从指定 Phase (0-7) 开始\"\n echo \" --phase resume 从最后一个缺失 Phase 恢复(默认)\"\n echo \" --region \u003cregion> 华为云区域(默认: cn-north-4,也通过 HUAWEI_REGION 环境变量设置)\"\n echo \" --output \u003cdir> 报告输出目录(默认: reports/)\"\n echo \" --skill-path \u003cdir> skill 所在根目录(默认: ~/.agents/skills/huawei-cloud/,回退 ~/.hermes/skills/huawei-cloud/)\"\n echo \" --no-siblings 关闭 Phase 5/6 同目录兄弟 skill 自动扫描(默认: 开启)\"\n echo \" --sibling-limit \u003cN> 兄弟 skill 数量上限(默认 5;0 = 等同 --no-siblings)\"\n echo \"\"\n echo \"环境变量(高级):\"\n echo \" SKILL_INSTALL_DIR 覆盖 skill 安装目录(默认自动检测 ~/.agents 或 ~/.hermes)\"\n echo \" SKILL_INSTALL_CMD 覆盖远程 skill 的 install/uninstall 命令(默认: 'hermes skills')\"\n echo \" SIBLING_LIMIT 兄弟 skill 数量上限(默认 5)\"\n echo \" WITHOUT_SIBLINGS=1 关闭兄弟 skill 扫描(等同 --no-siblings)\"\n echo \" ALLOW_WRITES=1 允许 Phase 4 / Phase 6 真正执行写操作(默认 0)\"\n echo \"\"\n echo \"示例:\"\n echo \" run-test-pipeline.sh --skills bss-voucher-manage\"\n echo \" run-test-pipeline.sh --skills 'bss-voucher-manage,ecs-manage'\"\n echo \" run-test-pipeline.sh --all-installed --fresh\"\n echo \" SKILL_INSTALL_CMD='my-runtime skills' run-test-pipeline.sh --skills xxx\"\n echo \" run-test-pipeline.sh --skills rds-query --no-siblings # 单 skill 模式(降级)\"\n echo \" run-test-pipeline.sh --skills rds-query --sibling-limit 3 # 限制最多 3 个兄弟\"\n exit 1\n}\n\n# getopts 解析命名参数(长选项走 -: 分支; 值型选项取下一位置参数)\nwhile getopts \":h-:\" opt; do\n case \"$opt\" in\n h) usage ;;\n -) case \"${OPTARG}\" in\n skills) SKILLS_LIST=\"${!OPTIND}\"; OPTIND=$((OPTIND + 1)) ;;\n all-installed) MODE=\"all_installed\" ;;\n fresh) FRESH=true ;;\n no-siblings) WITHOUT_SIBLINGS=true ;;\n sibling-limit) SIBLING_LIMIT=\"${!OPTIND}\"; OPTIND=$((OPTIND + 1)) ;;\n phase)\n if [ \"${!OPTIND}\" = \"resume\" ]; then\n MODE=\"resume\"\n else\n START_PHASE=\"${!OPTIND}\"\n MODE=\"phase\"\n fi\n OPTIND=$((OPTIND + 1)) ;;\n output) OUTPUT_DIR=\"${!OPTIND}\"; OPTIND=$((OPTIND + 1)) ;;\n skill-path) export SKILL_PATH=\"${!OPTIND}\"; OPTIND=$((OPTIND + 1)) ;;\n region) export HUAWEI_REGION=\"${!OPTIND}\"; OPTIND=$((OPTIND + 1)) ;;\n help) usage ;;\n *) fail \"未知参数: --${OPTARG}\"; usage ;;\n esac ;;\n \\?) fail \"未知参数: -$OPTARG\"; usage ;;\n esac\ndone\nshift $((OPTIND - 1))\n\n# Export so child phase scripts can see the sibling config\nif $WITHOUT_SIBLINGS; then\n export WITHOUT_SIBLINGS=1\nfi\nexport SIBLING_LIMIT\n\nensure_jq\n\n# === Resolve Skill List ===\nresolve_skills() {\n if [ \"$MODE\" = \"all_installed\" ]; then\n local base=\"${SKILL_PATH:-${SKILL_PATH_HCLOUD:-$SKILL_INSTALL_DIR/huawei-cloud}}\"\n header \"扫描已安装技能: $base\"\n SKILLS_LIST=\"\"\n for d in \"$base\"/huawei-cloud-*; do\n [ -d \"$d\" ] || continue\n local name; name=$(basename \"$d\")\n SKILLS_LIST=\"${SKILLS_LIST}${SKILLS_LIST:+,}${name}\"\n done\n if [ -z \"$SKILLS_LIST\" ]; then\n fail \"未找到任何 huawei-cloud-* 技能\"\n exit 1\n fi\n info \"发现技能: $SKILLS_LIST\"\n elif [ -z \"$SKILLS_LIST\" ]; then\n fail \"请指定 --skills 或 --all-installed\"\n usage\n fi\n}\n\nresolve_skills\n\n# Build skill paths\nIFS=',' read -ra SKILL_NAMES \u003c\u003c\u003c \"$SKILLS_LIST\"\nfor name in \"${SKILL_NAMES[@]}\"; do\n name=$(echo \"$name\" | xargs) # trim\n path=$(find_skill_path \"$name\") || {\n fail \"❌ 技能 '$name' 未找到\"\n info \"搜索目录: ${SKILL_PATH:-$SKILL_INSTALL_DIR}\"\n ls \"${SKILL_PATH:-$SKILL_INSTALL_DIR}\" 2>/dev/null | grep huawei-cloud || true\n exit 1\n }\n # 统一转绝对路径: find_skill_path 在 CWD 存在同名目录时可能返回裸名,\n # 下游 phase-5/6 只接受绝对/相对路径, 裸名会导致 SKILL_PATHS 为空。\n path=$(realpath \"$path\" 2>/dev/null || echo \"$path\")\n SKILL_PATHS+=(\"$path\")\n info \"✅ 找到技能: $name → $path\"\ndone\n\nSKILL_COUNT=${#SKILL_PATHS[@]}\n\n# === Fresh mode ===\nif $FRESH; then\n for sp in \"${SKILL_PATHS[@]}\"; do\n archive_phase_files \"$sp\"\n done\n info \"🧹 --fresh: 已归档旧 phases/ 到 \u003cskill>-test-files/phases/archive/\u003cts>/(reports/ 历史保留)\"\n START_PHASE=0\n MODE=\"phase\"\nfi\n\n# === Determine Start Phase ===\nif [ \"$MODE\" = \"resume\" ]; then\n START_PHASE=$(find_first_missing_phase \"${SKILL_PATHS[0]}\")\n if [ \"$START_PHASE\" = \"all_done\" ]; then\n info \"所有阶段已完成。使用 --fresh 重新执行\"\n exit 0\n fi\n info \"从 Phase $START_PHASE 恢复(缺失阶段)\"\nelif [ \"$MODE\" = \"phase\" ] && [ -n \"$START_PHASE\" ]; then\n info \"从指定 Phase $START_PHASE 开始\"\nfi\n\n# === Trap: ensure cleanup on exit ===\ncleanup_on_exit() {\n local rc=$?\n echo \"\"\n cleanup_after_test \"${SKILL_PATHS[@]}\"\n exit $rc\n}\ntrap cleanup_on_exit EXIT INT TERM\n\n# === Pipeline Execution ===\nTOTAL_START=$(date +%s)\n\nfor ((p = START_PHASE; p \u003c= 7; p++)); do\n header \"Phase $p\"\n case $p in\n 0) bash \"$SCRIPT_DIR/tier1/phase-0-install-check.sh\" \"${SKILL_PATHS[@]}\" ;;\n 1) bash \"$SCRIPT_DIR/tier1/phase-1-skill-analysis.sh\" \"${SKILL_PATHS[@]}\" ;;\n 2) bash \"$SCRIPT_DIR/tier1/phase-2-tech-research.sh\" \"${SKILL_PATHS[@]}\" ;;\n 3) bash \"$SCRIPT_DIR/tier1/phase-3-gen-testcases.sh\" \"${SKILL_PATHS[@]}\" ;;\n 4) bash \"$SCRIPT_DIR/tier1/phase-4-execute-tests.sh\" \"${SKILL_PATHS[@]}\" ;;\n 5) bash \"$SCRIPT_DIR/tier2/phase-5-orchestration.sh\" --skills \"$SKILLS_LIST\" \"${SKILL_PATHS[@]}\" ;;\n 6) bash \"$SCRIPT_DIR/tier2/phase-6-full-flow.sh\" --skills \"$SKILLS_LIST\" \"${SKILL_PATHS[@]}\" ;;\n 7) if [ -n \"$OUTPUT_DIR\" ]; then\n bash \"$SCRIPT_DIR/tier3/phase-7-final-report.sh\" --skills \"$SKILLS_LIST\" --output \"$OUTPUT_DIR\" \"${SKILL_PATHS[@]}\"\n else\n bash \"$SCRIPT_DIR/tier3/phase-7-final-report.sh\" --skills \"$SKILLS_LIST\" \"${SKILL_PATHS[@]}\"\n fi\n ;;\n esac\n\n exit_code=$?\n if [ $exit_code -ne 0 ]; then\n if [ $exit_code -eq 77 ]; then\n # Sentinel: Phase 4/6 已向 stderr 输出 env-var 设置模板。\n # 立即停止流水线,不要跳过、不要 fall through。\n # 调用方(agent 或终端)必须将模板原样输出给用户,\n # 引导用户在自己的 shell / PowerShell $PROFILE 中带外设置环境变量,\n # 然后用 --phase $p 续跑。整个流程禁止向用户索要 AK/SK 明文。\n fail \"Phase $p 凭证缺失(exit code: 77)\"\n warn \"已在 stderr 输出 env-var 设置模板(sentinel: $CRED_REQUEST_SENTINEL)\"\n warn \"调用方必须:① 将模板原样输出给用户,② 禁止在对话中索要 AK/SK 明文\"\n warn \" ③ 用户带外设置后,用 --phase $p 续跑\"\n warn \" ④ 或者用 --phase 5 跳过 Phase 4 进入离线分析\"\n exit 77\n fi\n fail \"Phase $p 执行失败(exit code: $exit_code)\"\n warn \"修复问题后可通过 --phase $p 或 --resume 继续\"\n exit $exit_code\n fi\ndone\n\nTOTAL_END=$(date +%s)\nTOTAL_DURATION=$((TOTAL_END - TOTAL_START))\n\nheader \"🎉 全部 8 个 Phase 执行完成 (Phase 0~7)\"\ninfo \"总耗时: ${TOTAL_DURATION}s\"\ninfo \"技能列表: $SKILLS_LIST\"\ninfo \"报告目录: $OUTPUT_DIR\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":8795,"content_sha256":"dace4d9afa2d40e2e0bc76ee267423b3e70b18b548794191a4173b2870efab94"},{"filename":"scripts/tier1/phase-0-install-check.sh","content":"#!/usr/bin/env bash\n\n\n# phase-0-install-check.sh — 安装验证\n# 检查 skill 目录完整性,模拟安装/卸载/重装状态\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\n\nPHASE_NUM=0\nPHASE_NAME=\"install-check\"\n\nrun_phase0() {\n local skill_dir=\"$1\"\n local skill_name; skill_name=$(basename \"$skill_dir\")\n\n header \"Phase 0: 安装验证 — $skill_name\"\n\n # Check deps (Phase 0 has no deps)\n check_phase_deps \"$skill_dir\" 0 || return 1\n\n local ts; ts=$(timestamp)\n local start_ts; start_ts=$(date +%s)\n\n # === Directory Integrity ===\n step \"检查目录完整性...\"\n local checks\n local checks_py_tmp; checks_py_tmp=$(mktemp)\n cat > \"$checks_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, os, sys\nd = sys.argv[1]\nr = {\n 'SKILL.md': os.path.isfile(d+'/SKILL.md'),\n 'scripts/': os.path.isdir(d+'/scripts'),\n 'references/': os.path.isdir(d+'/references'),\n 'references/iam-policies.md': os.path.isfile(d+'/references/iam-policies.md'),\n}\n# 硬性契约仅 SKILL.md + references/ 两项。\n# iam-policies.md 对云 API skill 必需, 但本地工具类 skill 不涉及 IAM,\n# 改为软性检查(报告但不阻断)。scripts/ 已为软性。(ISSUE-002)\nr['_hard_ok'] = bool(r['SKILL.md'] and r['references/'])\nprint(json.dumps(r))\nPYEOF\n checks=$(python3 \"$checks_py_tmp\" \"$skill_dir\")\n rm -f \"$checks_py_tmp\"\n local dir_pass\n dir_pass=$(echo \"$checks\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('_hard_ok', all(v for k,v in d.items() if not k.startswith('_'))))\")\n # 纯 CLI skill 提示(无 scripts/ 目录)\n if echo \"$checks\" | grep -q '\"scripts/\": false'; then\n info \"💡 纯 CLI skill(无 scripts/ 目录),按 CLI 型流程继续(脚本相关校验跳过)\"\n fi\n\n # === Install/Uninstall/Reinstall ===\n # install_dir defaults to SKILL_INSTALL_DIR (set in config.sh with auto-detection\n # of ~/.agents/skills → ~/.hermes/skills → default). Can be overridden via env.\n local install_dir=\"${SKILL_INSTALL_DIR:-$HOME/.agents/skills}\"\n local target_dir=\"$install_dir/$skill_name\"\n local is_remote=false\n [[ \"$skill_dir\" == http://* || \"$skill_dir\" == https://* ]] && is_remote=true\n\n # 同路径防护: 当技能已安装在默认安装目录且无独立工作区副本时,\n # skill_dir 与 target_dir 是同一物理路径, 卸载/重装会删除技能源目录本身。\n local same_path=false\n if ! $is_remote; then\n local rp_skill rp_target\n rp_skill=$(realpath \"$skill_dir\" 2>/dev/null || echo \"$skill_dir\")\n rp_target=$(realpath \"$target_dir\" 2>/dev/null || echo \"$target_dir\")\n if [ \"$rp_skill\" = \"$rp_target\" ]; then\n same_path=true\n fi\n fi\n\n step \"执行安装测试...\"\n local install_status=\"pass\" install_duration=0\n local uninstall_status=\"pass\" uninstall_duration=0\n local reinstall_status=\"pass\" reinstall_duration=0\n\n # --- Install ---\n if $is_remote; then\n # Remote skill: use configurable install command\n if [ -z \"${SKILL_INSTALL_CMD:-}\" ]; then\n info \"💡 SKILL_INSTALL_CMD 为空,远程安装已跳过\"\n install_status=\"skipped\"\n else\n local inst_start; inst_start=$(date +%s%N)\n ${SKILL_INSTALL_CMD} install \"$skill_dir\" --yes 2>/dev/null && install_status=\"pass\" || install_status=\"fail\"\n local inst_end; inst_end=$(date +%s%N)\n install_duration=$(( (inst_end - inst_start) / 1000000 ))\n fi\n elif [ -d \"$target_dir\" ]; then\n info \"💡 技能已安装于 ${install_dir},跳过安装\"\n install_status=\"skipped\"\n else\n # Local skill: copy directory\n local inst_start; inst_start=$(date +%s%N)\n mkdir -p \"$install_dir\"\n cp -r \"$skill_dir\" \"$target_dir\" && install_status=\"pass\" || install_status=\"fail\"\n local inst_end; inst_end=$(date +%s%N)\n install_duration=$(( (inst_end - inst_start) / 1000000 ))\n fi\n [ \"$install_status\" = \"pass\" ] && pass \"安装成功 (${install_duration}ms)\" || [ \"$install_status\" = \"skipped\" ] || fail \"安装失败\"\n\n # --- Uninstall ---\n step \"执行卸载测试...\"\n local link_target=\"\" # 符号链接目标(卸载前记录, 重装时恢复)\n if $is_remote; then\n if [ -z \"${SKILL_INSTALL_CMD:-}\" ]; then\n uninstall_status=\"skipped\"\n else\n local uninst_start; uninst_start=$(date +%s%N)\n ${SKILL_INSTALL_CMD} uninstall \"$skill_name\" --yes 2>/dev/null && uninstall_status=\"pass\" || uninstall_status=\"fail\"\n local uninst_end; uninst_end=$(date +%s%N)\n uninstall_duration=$(( (uninst_end - uninst_start) / 1000000 ))\n fi\n elif $same_path; then\n info \"💡 skill_dir 与 target_dir 为同一路径,跳过卸载(避免删除技能本身)\"\n uninstall_status=\"skipped\"\n elif [ -L \"$target_dir\" ] && [ -n \"$target_dir\" ] && [ \"$target_dir\" != \"/\" ]; then\n # 符号链接: 只删链接本身, 不删链接目标; 记录目标供重装恢复(#54-004)\n link_target=$(readlink \"$target_dir\")\n info \"💡 $target_dir 是符号链接(→ ${link_target:-?}),卸载链接而非真实目录\"\n local uninst_start; uninst_start=$(date +%s%N)\n rm -f \"$target_dir\" && uninstall_status=\"pass\" || uninstall_status=\"fail\"\n local uninst_end; uninst_end=$(date +%s%N)\n uninstall_duration=$(( (uninst_end - uninst_start) / 1000000 ))\n elif [ -d \"$target_dir\" ] && [ -n \"$target_dir\" ] && [ \"$target_dir\" != \"/\" ] && [ \"$target_dir\" != \"$HOME\" ]; then\n local uninst_start; uninst_start=$(date +%s%N)\n rm -rf \"$target_dir\" && uninstall_status=\"pass\" || uninstall_status=\"fail\"\n local uninst_end; uninst_end=$(date +%s%N)\n uninstall_duration=$(( (uninst_end - uninst_start) / 1000000 ))\n else\n info \"技能目录不存在,跳过卸载\"\n uninstall_status=\"skipped\"\n fi\n [ \"$uninstall_status\" = \"pass\" ] && pass \"卸载成功 (${uninstall_duration}ms)\" || [ \"$uninstall_status\" = \"skipped\" ] || fail \"卸载失败\"\n\n # --- Reinstall ---\n step \"执行重装测试...\"\n if $is_remote; then\n if [ -z \"${SKILL_INSTALL_CMD:-}\" ]; then\n reinstall_status=\"skipped\"\n else\n local reinst_start; reinst_start=$(date +%s%N)\n ${SKILL_INSTALL_CMD} install \"$skill_dir\" --yes 2>/dev/null && reinstall_status=\"pass\" || reinstall_status=\"fail\"\n local reinst_end; reinst_end=$(date +%s%N)\n reinstall_duration=$(( (reinst_end - reinst_start) / 1000000 ))\n fi\n elif $same_path; then\n info \"💡 skill_dir 与 target_dir 为同一路径,跳过重装\"\n reinstall_status=\"skipped\"\n elif [ -n \"$link_target\" ]; then\n # 原为符号链接: 恢复链接而非创建真实目录, 保持安装目录结构(#54-004)\n info \"💡 恢复符号链接 $target_dir → $link_target\"\n local reinst_start; reinst_start=$(date +%s%N)\n ln -s \"$link_target\" \"$target_dir\" && reinstall_status=\"pass\" || reinstall_status=\"fail\"\n local reinst_end; reinst_end=$(date +%s%N)\n reinstall_duration=$(( (reinst_end - reinst_start) / 1000000 ))\n else\n local reinst_start; reinst_start=$(date +%s%N)\n cp -r \"$skill_dir\" \"$target_dir\" && reinstall_status=\"pass\" || reinstall_status=\"fail\"\n local reinst_end; reinst_end=$(date +%s%N)\n reinstall_duration=$(( (reinst_end - reinst_start) / 1000000 ))\n fi\n [ \"$reinstall_status\" = \"pass\" ] && pass \"重装成功 (${reinstall_duration}ms)\" || [ \"$reinstall_status\" = \"skipped\" ] || fail \"重装失败\"\n\n # === Build Result ===\n local dir_pass_bool\n dir_pass_bool=$(echo \"$dir_pass\" | tr '[:upper:]' '[:lower:]')\n \n local verdict\n [ \"$dir_pass_bool\" = \"true\" ] && verdict=\"pass\" || verdict=\"fail\"\n\n local end_ts; end_ts=$(date +%s)\n local duration=$((end_ts - start_ts))\n\n local result\n local tmp_checks; tmp_checks=$(mktemp)\n echo \"$checks\" > \"$tmp_checks\"\n local result_py_tmp; result_py_tmp=$(mktemp)\n cat > \"$result_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\n\nwith open(sys.argv[1]) as f:\n ck = json.load(f)\n\ndir_pass_bool_val = sys.argv[2].lower() == \"true\"\ninstall_status = sys.argv[3]\ninstall_duration = float(sys.argv[4])\nuninstall_status = sys.argv[5]\nuninstall_duration = float(sys.argv[6])\nreinstall_status = sys.argv[7]\nreinstall_duration = float(sys.argv[8])\ntarget_exists = sys.argv[9].lower() == \"true\"\n\nr = {\n \"install\": {\"status\": install_status, \"existing\": target_exists, \"duration_s\": install_duration * 1e-3},\n \"uninstall\": {\"status\": uninstall_status, \"duration_s\": uninstall_duration * 1e-3},\n \"reinstall\": {\"status\": reinstall_status, \"duration_s\": reinstall_duration * 1e-3},\n \"directory_integrity\": {\n \"pass\": dir_pass_bool_val,\n \"checks\": ck\n }\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nPYEOF\n result=$(python3 \"$result_py_tmp\" \"$tmp_checks\" \"$dir_pass_bool\" \"$install_status\" \"${install_duration:-0}\" \"$uninstall_status\" \"${uninstall_duration:-0}\" \"$reinstall_status\" \"${reinstall_duration:-0}\" \"$( [ -d \"$target_dir\" ] && echo True || echo False )\")\n rm -f \"$result_py_tmp\"\n rm -f \"$tmp_checks\"\n\n # Build summary\n local pass_checks\n pass_checks=$(echo \"$checks\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(sum(1 for v in d.values() if v))\")\n local fail_checks\n fail_checks=$(echo \"$checks\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(sum(1 for v in d.values() if not v))\")\n\n # Write phase JSON using temp file to avoid quoting conflicts\n local tmp_json; tmp_json=$(mktemp)\n echo \"$result\" > \"$tmp_json\"\n \n local summary_py_tmp; summary_py_tmp=$(mktemp)\n cat > \"$summary_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\n\nwith open(sys.argv[1]) as f:\n result_data = json.load(f)\n\nr = {\n \"phase\": int(sys.argv[2]),\n \"phase_name\": sys.argv[3],\n \"tier\": 1,\n \"target\": {\"type\": \"single_skill\", \"skills\": [sys.argv[4]]},\n \"timestamp\": sys.argv[5],\n \"execution_meta\": {\"duration_s\": int(sys.argv[6]), \"retry_count\": 0, \"user_confirmed\": False},\n \"result\": result_data,\n \"summary\": {\"verdict\": sys.argv[7], \"pass_checks\": int(sys.argv[8]), \"fail_checks\": int(sys.argv[9]), \"warn_checks\": 0}\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nPYEOF\n ensure_test_files_dir \"$skill_dir\" > /dev/null\n python3 \"$summary_py_tmp\" \"$tmp_json\" \"$PHASE_NUM\" \"$PHASE_NAME\" \"$skill_name\" \"$ts\" \"$duration\" \"$verdict\" \"$pass_checks\" \"$fail_checks\" > \"$(phase_file \"$skill_dir\" 0)\"\n rm -f \"$summary_py_tmp\"\n\n rm -f \"$tmp_json\"\n\n echo \"\"\n if [ \"$verdict\" = \"pass\" ]; then\n pass \"Phase 0 完成 — 目录完整性通过\"\n else\n fail \"Phase 0 完成 — 目录完整性失败,部分文件缺失\"\n fi\n}\n\n# Parse args: getopts for -s, pre-filter --skill (getopts can't handle --long)\nSKILL_DIRS=()\n_rest=()\nwhile [ $# -gt 0 ]; do\n case \"$1\" in\n --skill) SKILL_DIRS+=(\"$2\"); shift 2 ;;\n --skill=*) SKILL_DIRS+=(\"${1#--skill=}\"); shift ;;\n --help|-h) echo \"用法: $(basename \"$0\") [-s \u003cdir>]... [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n *) _rest+=(\"$1\"); shift ;;\n esac\ndone\nset -- ${_rest[@]+\"${_rest[@]}\"}\nOPTIND=1\nwhile getopts \":s:h\" opt; do\n case $opt in\n s) SKILL_DIRS+=(\"$OPTARG\") ;;\n h) echo \"用法: $(basename \"$0\") [-s \u003cdir>]... [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n \\?) ;;\n esac\ndone\nshift $((OPTIND-1))\nfor arg in \"$@\"; do SKILL_DIRS+=(\"$arg\"); done\n\n# Run for each skill\nfor skill_dir in \"${SKILL_DIRS[@]}\"; do\n run_phase0 \"$skill_dir\" || exit 1\n echo \"\"\ndone\n\npass \"Phase 0: 安装验证全部完成\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":11334,"content_sha256":"bc5bf99c2897f78e751b12835a4feb9d19b84c2abd077d9221910cf4ca9e24c1"},{"filename":"scripts/tier1/phase-1-skill-analysis.sh","content":"#!/usr/bin/env bash\n\n\n# phase-1-skill-analysis.sh — 功能提取\n# 读取 SKILL.md,提取 metadata、commands、capabilities、resource_types\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\n\n# Load config and discover available services dynamically\nsource \"$SCRIPT_DIR/lib/config.sh\"\ndiscover_hcloud_services >/dev/null # populate cache\nexport SERVICES_CLI_COMMA\n\nPHASE_NUM=1\nPHASE_NAME=\"skill-analysis\"\n\nrun_phase1() {\n local skill_dir=\"$1\"\n local skill_name; skill_name=$(basename \"$skill_dir\")\n\n header \"Phase 1: 功能提取 — $skill_name\"\n\n check_phase_deps \"$skill_dir\" 1 || return 1\n\n local ts; ts=$(timestamp)\n local start_ts; start_ts=$(date +%s)\n local sk_file=\"$skill_dir/SKILL.md\"\n\n step \"读取 SKILL.md...\"\n\n if [ ! -f \"$sk_file\" ]; then\n fail \"SKILL.md 不存在: $sk_file\"\n return 1\n fi\n\n # Extract metadata via Python (write to temp file to avoid bash escaping issues)\n local py_tmp; py_tmp=$(mktemp)\n cat > \"$py_tmp\" \u003c\u003c 'PYANALYSIS'\nimport json, re, os, sys\n\nsk_file = sys.argv[1]\nskill_dir = sys.argv[2]\n\nwith open(sk_file, 'r', encoding='utf-8') as f:\n text = f.read()\n\n# Extract YAML frontmatter\nm = re.match(r'^---\\s*\\n(.*?)\\n---', text, re.DOTALL)\nif not m:\n print(json.dumps({'error': 'No YAML frontmatter found'}))\n exit(1)\n\nyaml_text = m.group(1)\n\nname = ''\ndescription = ''\ntriggers = []\ntags = []\n\nfor line in yaml_text.split('\\n'):\n if line.startswith('name:'):\n name = line.split(':',1)[1].strip()\n elif line.startswith('description: |'):\n pass\n elif line.startswith('tags:'):\n tags_match = re.findall(r'\\[(.*?)\\]', line)\n if tags_match:\n tags = [t.strip().strip('\"') for t in tags_match[0].split(',')]\n elif 'Triggers include:' in line:\n pass\n\n# Better approach: extract description block\ndesc_match = re.search(r'description: \\|(.*?)(?:^tags:|\\Z)', text, re.DOTALL | re.MULTILINE)\ndesc_text = ''\nif desc_match:\n desc_text = desc_match.group(1).strip()\n trig_match = re.search(r'Triggers include:[ \\t]*(.*?)(?:\\.|$)', desc_text, re.DOTALL)\n if trig_match:\n trig_raw = trig_match.group(1)\n triggers = re.findall(r'\"([^\"]*)\"', trig_raw)\n if not triggers:\n triggers = re.findall(r'\\'([^\\']*)\\'', trig_raw)\n if not triggers:\n triggers = [t.strip().strip(',').strip().strip('\"').strip(\"'\") for t in trig_raw.replace(',', ' ').split() if t.strip()]\n\n# Also check frontmatter Trigger: / Triggers: field (some skills use this instead of \"Triggers include:\")\nif not triggers:\n _trig_line = re.search(r'^[Tt]rigger[s]?\\s*:\\s*(.+)

Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on real-environment functional testing and multi-skill orchestration scenarios . --- Overview This Skill provides a three-track, eight-phase standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report): | Tier | Phases | Goal | |------|--------|------| | Tier 1: Single-Skill Unit Testing | Phase 0 4 | Verify each skill item by i…

, yaml_text, re.MULTILINE)\n if _trig_line:\n _trig_raw = _trig_line.group(1).strip()\n if _trig_raw.startswith('[') and _trig_raw.endswith(']'):\n triggers = [t.strip().strip('\"').strip(\"'\") for t in _trig_raw[1:-1].split(',') if t.strip()]\n else:\n triggers = [t.strip().strip('\"').strip(\"'\") for t in re.split(r'[,;]', _trig_raw) if t.strip()]\n\n# Fallback: use tags as triggers if no explicit triggers found (ISSUE-003)\nif not triggers and tags:\n triggers = list(tags)\n\n# Fallback: extract keywords from description text if still no triggers\nif not triggers and desc_text:\n _desc_kw = re.findall(r'\"([^\"]{2,40})\"', desc_text)\n if _desc_kw:\n triggers = _desc_kw[:10]\n\n# Extract capabilities by looking for sections\ncap_list = []\ncap_create = []\ncap_update = []\ncap_delete = []\n\noverview = re.search(r'## \\u6982\\u8ff0.*?(?=## )', text, re.DOTALL)\nif overview:\n ov_text = overview.group()\n for line in ov_text.split('\\n'):\n line = line.strip()\n if line.startswith('| **') or line.startswith('|**'):\n cols = [c.strip().strip('*').strip() for c in line.split('|') if c.strip()]\n for c in cols:\n if '\\u67e5\\u8be2' in c or '\\u5217\\u8868' in c or '\\u67e5\\u770b' in c or 'list' in c.lower() or 'show' in c.lower():\n cap_list.append(c)\n elif '\\u521b\\u5efa' in c or 'create' in c.lower():\n cap_create.append(c)\n elif '\\u4fee\\u6539' in c or '\\u66f4\\u65b0' in c or 'update' in c.lower():\n cap_update.append(c)\n elif '\\u5220\\u9664' in c or '\\u56de\\u6536' in c or 'delete' in c.lower() or 'reclaim' in c.lower():\n cap_delete.append(c)\n\n# Extract commands from code blocks across ALL sections (not just Core Commands)\ncommands = []\ncmd_id = 0\n\n# Helper: detect service from text\n# Uses dynamic service list from config.sh (SERVICES_CLI_FALLBACK covers 36+ services)\n_detect_svc_list = None\ndef _get_svc_list():\n global _detect_svc_list\n if _detect_svc_list is None:\n import os\n # Try comma-joined list from config.sh first\n comma_list = os.environ.get('SERVICES_CLI_COMMA', '')\n if comma_list:\n _detect_svc_list = comma_list.split(',')\n else:\n # Fallback to comprehensive static list\n _detect_svc_list = [\n 'bss', 'ecs', 'vpc', 'evs', 'eip', 'iam', 'rds', 'dns', 'obs', 'ims', 'as', 'elb',\n 'nat', 'cdn', 'cce', 'scm', 'ces', 'rfs', 'ucs', 'dcs', 'kps', 'cfw', 'hss',\n 'secmaster', 'coc', 'aom', 'cts', 'swr', 'cci', 'dds', 'gaussdb', 'ddm', 'drs',\n 'das', 'dli', 'dws', 'mrs', 'smn', 'functiongraph', 'apig', 'vpn', 'dc', 'cc', 'er', 'eg'\n ]\n return _detect_svc_list\n\ndef detect_service(txt):\n txt_l = txt.lower()\n for svc in _get_svc_list():\n if svc in txt_l:\n return svc\n return None\n\ndef _replace_bare_var(m):\n param = m.group(1)\n var = m.group(2)\n defaults = {'offset': 0, 'limit': 10, 'page': 1, 'index': 0, 'count': 10, 'num': 10, 'size': 10, 'start': 0, 'end': 0}\n val = defaults.get(var, 0)\n return 'request.%s = %s' % (param, val)\n\n# Helper: build executable SDK snippet for a method\ndef build_sdk_snippet(svc, method_name, request_class, code_block):\n snippet_lines = [\n 'import os, json',\n 'from huaweicloudsdkcore.auth.credentials import GlobalCredentials, BasicCredentials',\n ]\n client_cls = svc[0].upper() + svc[1:] + 'Client'\n sdk_ver = os.environ.get('HUAWEI_SDK_VERSION', 'v2')\n import json as _json\n _ver_overrides = _json.loads(os.environ.get('SDK_VERSION_OVERRIDES', '{\"iam\":\"v3\"}'))\n sdk_ver = _ver_overrides.get(svc, sdk_ver)\n snippet_lines.append('from huaweicloudsdk%s.%s import %s, %s' % (svc, sdk_ver, client_cls, request_class))\n snippet_lines.append('')\n snippet_lines.append('ak = os.environ.get(\"HUAWEI_ACCESS_KEY\") or os.environ.get(\"HW_ACCESS_KEY\") or os.environ.get(\"HW_AK\") or \"\"')\n snippet_lines.append('sk = os.environ.get(\"HUAWEI_SECRET_KEY\") or os.environ.get(\"HW_SECRET_KEY\") or os.environ.get(\"HW_SK\") or \"\"')\n snippet_lines.append(\"region = os.environ.get('HUAWEI_REGION', 'cn-north-4')\")\n if svc == 'bss':\n snippet_lines.append(\"domain_id = os.getenv('HUAWEI_DOMAIN_ID', '')\")\n snippet_lines.append('cred = GlobalCredentials().with_ak(ak).with_sk(sk).with_domain_id(domain_id)')\n snippet_lines.append(\"client = %s.new_builder().with_credentials(cred).with_region(region).build()\" % client_cls)\n else:\n snippet_lines.append('cred = BasicCredentials(ak, sk)')\n snippet_lines.append(\"client = %s.new_builder().with_credentials(cred).with_region(%s_region=region).build()\" % (client_cls, svc))\n snippet_lines.append('')\n req_lines = []\n in_req = False\n for line in code_block.split('\\n'):\n stripped = line.strip()\n if stripped.startswith('request = ') or stripped.startswith('request='):\n in_req = True\n if in_req:\n if stripped.startswith('request.') or stripped.startswith('request =') or stripped.startswith('request='):\n req_lines.append(stripped)\n elif stripped.startswith('response =') or stripped.startswith('all_coupons') or stripped.startswith('by_'):\n break\n if req_lines:\n for rl in req_lines:\n rl = re.sub(r'=\\s*\"COUPON_ID\"', \"= ''\", rl)\n rl = re.sub(r'=\\s*\"ORDER_ID\"', \"= ''\", rl)\n rl = re.sub(r'=\\s*\"CARD_ID\"', \"= ''\", rl)\n rl = re.sub(r'=\\s*\"your_domain_id\"', \"= ''\", rl)\n # Replace bare variable assignments: request.offset = offset → request.offset = 0\n rl = re.sub(r'request\\.(\\w+)\\s*=\\s*(offset|limit|page|index|count|num|size|start|end)\\s*

Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on real-environment functional testing and multi-skill orchestration scenarios . --- Overview This Skill provides a three-track, eight-phase standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report): | Tier | Phases | Goal | |------|--------|------| | Tier 1: Single-Skill Unit Testing | Phase 0 4 | Verify each skill item by i…

, _replace_bare_var, rl)\n snippet_lines.append(rl)\n else:\n snippet_lines.append('request = %s()' % request_class)\n # Auto-fill common required parameters with safe defaults\n # Read overrides from templates/test-defaults.json if it exists\n defaults_path = os.path.join(skill_dir, 'templates', 'test-defaults.json')\n param_defaults = {}\n if os.path.isfile(defaults_path):\n try:\n with open(defaults_path) as df:\n param_defaults = json.load(df).get('request_defaults', {})\n except Exception:\n pass\n # Built-in safe defaults for common BSS parameters\n from datetime import datetime, timedelta\n _now = datetime.utcnow()\n builtin_defaults = {\n 'limit': 10, 'offset': 0,\n 'coupon_type': 1, 'status': 2,\n 'trade_time_begin': (_now - timedelta(days=365)).strftime('%Y-%m-%dT00:00:00Z'),\n 'trade_time_end': (_now + timedelta(days=30)).strftime('%Y-%m-%dT23:59:59Z'),\n }\n builtin_defaults.update(param_defaults)\n # Check which request.xxx assignments are already set\n set_params = set()\n for line in snippet_lines:\n m2 = re.match(r'request\\.(\\w+)\\s*=', line)\n if m2:\n set_params.add(m2.group(1))\n # Add missing parameters with defaults (guarded by hasattr to avoid AttributeError)\n for param, value in builtin_defaults.items():\n if param not in set_params:\n if isinstance(value, str):\n snippet_lines.append(\"if hasattr(request, '%s'): request.%s = '%s'\" % (param, param, value))\n else:\n snippet_lines.append('if hasattr(request, \"%s\"): request.%s = %s' % (param, param, value))\n snippet_lines.append('')\n snippet_lines.append('response = client.%s(request)' % method_name)\n snippet_lines.append('if hasattr(response, \"to_dict\"):')\n snippet_lines.append(' print(json.dumps(response.to_dict(), indent=2, ensure_ascii=False)[:2000])')\n snippet_lines.append('else:')\n snippet_lines.append(' print(str(response)[:2000])')\n return '\\n'.join(snippet_lines)\n\n# 1. Extract from ALL code blocks (bash, python, etc.) across the entire SKILL.md\nbt = chr(96)\nbt3 = bt * 3\nall_code_blocks = re.findall(bt3 + r'(\\w+)?\\s*\\n(.*?)' + bt3, text, re.DOTALL)\n\nfor lang, block in all_code_blocks:\n lang = (lang or '').lower().strip()\n\n if lang == 'python' or lang == 'py':\n method_calls = re.findall(r'(?:response\\s*=\\s*)?client\\.(\\w+)\\s*\\(', block)\n for method_name in method_calls:\n cmd_id += 1\n svc = detect_service(block) or detect_service(text[:text.find(block)]) or 'bss'\n req_cls = ''.join(w.capitalize() for w in method_name.split('_')) + 'Request'\n is_write = any(kw in method_name.lower() for kw in ['create', 'delete', 'update', 'reclaim', 'destroy'])\n snippet = build_sdk_snippet(svc, method_name, req_cls, block)\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'SKILL.md-python-block',\n 'description': '%s (SDK)' % method_name,\n 'command': snippet,\n 'executor': 'sdk',\n 'is_write': is_write,\n 'method_name': method_name,\n 'service': svc,\n 'request_class': req_cls\n })\n\n elif lang == 'bash' or lang == 'sh' or lang == '':\n # 合并反斜杠续行: \"cmd \\ 换行 --args\" → \"cmd --args\" 单行完整命令,\n # 避免多行命令被拆行后只保留 \"cmd \\\" 导致用例不可执行。\n block = re.sub(r'\\\\\\s*\\n', ' ', block)\n for cl in block.strip().split('\\n'):\n cl = cl.strip()\n if not cl or cl.startswith('#') or cl.startswith('

Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on real-environment functional testing and multi-skill orchestration scenarios . --- Overview This Skill provides a three-track, eight-phase standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report): | Tier | Phases | Goal | |------|--------|------| | Tier 1: Single-Skill Unit Testing | Phase 0 4 | Verify each skill item by i…

):\n continue\n if cl.startswith('python3 ') and 'scripts/' in cl:\n cmd_id += 1\n is_write = any(kw in cl.lower() for kw in ['create', 'delete', 'update', 'destroy', 'activate', 'reclaim'])\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'SKILL.md-bash-block',\n 'description': cl[:80],\n 'command': cl,\n 'executor': 'script',\n 'is_write': is_write\n })\n elif cl.startswith('bash ') and 'scripts/' in cl:\n # 自带脚本(bash scripts/xxx.sh ...)也是可执行命令, 提取为正例\n cmd_id += 1\n is_write = any(kw in cl.lower() for kw in ['create', 'delete', 'update', 'destroy', 'activate', 'reclaim', 'cleanup'])\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'SKILL.md-bash-block',\n 'description': cl[:80],\n 'command': cl,\n 'executor': 'script',\n 'is_write': is_write\n })\n elif cl.startswith('hcloud '):\n cmd_id += 1\n is_write = any(kw in cl.lower() for kw in ['create', 'delete', 'update', 'destroy'])\n clean_cmd = re.sub(r'\u003c[^>]+>', '', cl).strip()\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'SKILL.md-bash-block',\n 'description': clean_cmd[:80],\n 'command': clean_cmd,\n 'executor': 'cli',\n 'is_write': is_write\n })\n\n# 2. Fallback: extract from markdown table rows in Core Commands section\nif not commands:\n core_section = re.search(r'^##\\s+\\u6838\\u5fc3\\u547d\\u4ee4.*?(?=^## |\\Z)', text, re.DOTALL | re.MULTILINE)\n if not core_section:\n core_section = re.search(r'^##\\s+Core Commands.*?(?=^## |\\Z)', text, re.DOTALL | re.MULTILINE)\n for line in (core_section.group() if core_section else text).split('\\n'):\n line = line.strip()\n if line.startswith('|') and chr(96) in line:\n cols = [c.strip() for c in line.split('|')]\n cmd_text = ''\n for col in cols:\n bt_matches = re.findall(chr(96) + r'([^' + chr(96) + r']+)' + chr(96), col)\n if bt_matches:\n cmd_text = bt_matches[0]\n break\n if not cmd_text:\n continue\n clean_cmd = re.sub(r'\u003c[^>]+>', '', cmd_text).strip()\n clean_cmd = re.sub(r'\\s+', ' ', clean_cmd)\n # Skip entries that are not real executable commands (ISSUE-001:\n # parameter names like kubectl, --bin-dir, bin in backticks were\n # extracted as independent script paths, causing all test cases to fail)\n # 裸路径/文件名 token(如 scripts/、references/、SKILL.md)是表格正文\n # 而非可执行命令, 一律跳过(修复 SKILL.md 表格中 scripts/ 被误提取)\n if ' ' not in clean_cmd and re.match(r'^[\\w./{}\\[\\]$~-]+

Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on real-environment functional testing and multi-skill orchestration scenarios . --- Overview This Skill provides a three-track, eight-phase standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report): | Tier | Phases | Goal | |------|--------|------| | Tier 1: Single-Skill Unit Testing | Phase 0 4 | Verify each skill item by i…

, clean_cmd):\n continue\n if not (clean_cmd.startswith('hcloud ') or clean_cmd.startswith('python3 ')\n or clean_cmd.startswith('bash ') or clean_cmd.startswith('curl ')\n or clean_cmd.startswith('from ') or clean_cmd.startswith('sh ')):\n continue\n exe = 'cli'\n if clean_cmd.startswith('hcloud '):\n exe = 'cli'\n elif clean_cmd.startswith('python3 ') or clean_cmd.startswith('from '):\n exe = 'sdk'\n elif clean_cmd.startswith('curl '):\n exe = 'api'\n is_write = any(kw in clean_cmd.lower() for kw in ['create', 'delete', 'update', 'put', 'post', 'destroy'])\n cmd_id += 1\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'SKILL.md-table',\n 'description': clean_cmd[:80],\n 'command': clean_cmd,\n 'executor': exe,\n 'is_write': is_write\n })\n\n# Scan references/*.md for additional commands (ISSUE-008: some skills put\n# command variants in references/ docs, not in SKILL.md body).\n_refs_dir = os.path.join(skill_dir, 'references')\nif os.path.isdir(_refs_dir):\n for _rf in sorted(os.listdir(_refs_dir)):\n if not _rf.endswith('.md'):\n continue\n try:\n with open(os.path.join(_refs_dir, _rf), encoding='utf-8') as _rf_f:\n _ref_text = _rf_f.read()\n except Exception:\n continue\n _bt3 = chr(96) * 3\n _ref_blocks = re.findall(_bt3 + r'(\\w+)?\\s*\\n(.*?)' + _bt3, _ref_text, re.DOTALL)\n for _lang, _block in _ref_blocks:\n _lang = (_lang or '').lower().strip()\n _block = re.sub(r'\\\\\\s*\\n', ' ', _block)\n if _lang in ('python', 'py'):\n _mc = re.findall(r'(?:response\\s*=\\s*)?client\\.(\\w+)\\s*\\(', _block)\n for _mn in _mc:\n _svc = detect_service(_block) or 'bss'\n _req_cls = ''.join(w.capitalize() for w in _mn.split('_')) + 'Request'\n _snippet = build_sdk_snippet(_svc, _mn, _req_cls, _block)\n _sig = _snippet[:200]\n if _sig and not any(c.get('command', '')[:200] == _sig for c in commands):\n cmd_id += 1\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'references/%s-python-block' % _rf,\n 'description': '%s (SDK)' % _mn,\n 'command': _snippet,\n 'executor': 'sdk',\n 'is_write': any(kw in _mn.lower() for kw in ['create', 'delete', 'update', 'reclaim', 'destroy']),\n 'method_name': _mn,\n 'service': _svc,\n 'request_class': _req_cls\n })\n elif _lang in ('bash', 'sh', ''):\n for _cl in _block.strip().split('\\n'):\n _cl = _cl.strip()\n if not _cl or _cl.startswith('#') or _cl.startswith('

Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on real-environment functional testing and multi-skill orchestration scenarios . --- Overview This Skill provides a three-track, eight-phase standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report): | Tier | Phases | Goal | |------|--------|------| | Tier 1: Single-Skill Unit Testing | Phase 0 4 | Verify each skill item by i…

):\n continue\n if _cl.startswith('python3 ') and 'scripts/' in _cl:\n if not any(c.get('command', '') == _cl for c in commands):\n cmd_id += 1\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'references/%s-bash-block' % _rf,\n 'description': _cl[:80],\n 'command': _cl,\n 'executor': 'script',\n 'is_write': any(kw in _cl.lower() for kw in ['create', 'delete', 'update', 'destroy', 'activate', 'reclaim'])\n })\n elif _cl.startswith('bash ') and 'scripts/' in _cl:\n if not any(c.get('command', '') == _cl for c in commands):\n cmd_id += 1\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'references/%s-bash-block' % _rf,\n 'description': _cl[:80],\n 'command': _cl,\n 'executor': 'script',\n 'is_write': any(kw in _cl.lower() for kw in ['create', 'delete', 'update', 'destroy', 'activate', 'reclaim', 'cleanup'])\n })\n elif _cl.startswith('hcloud '):\n _clean = re.sub(r'\u003c[^>]+>', '', _cl).strip()\n if not any(c.get('command', '') == _clean for c in commands):\n cmd_id += 1\n commands.append({\n 'id': 'CMD-%02d' % cmd_id,\n 'source': 'references/%s-bash-block' % _rf,\n 'description': _clean[:80],\n 'command': _clean,\n 'executor': 'cli',\n 'is_write': any(kw in _cl.lower() for kw in ['create', 'delete', 'update', 'destroy'])\n })\n\n# Detect resource types\nresource_types = []\nres_patterns = {\n 'ecs': ['ecs', 'instance', '\\u4e91\\u670d\\u52a1\\u5668', '\\u5f39\\u6027\\u4e91\\u670d\\u52a1\\u5668'],\n 'vpc': ['vpc', '\\u865a\\u62df\\u79c1\\u6709\\u4e91'],\n 'eip': ['eip', '\\u5f39\\u6027\\u516c\\u7f51'],\n 'evs': ['evs', 'clouddvolume', '\\u4e91\\u786c\\u76d8'],\n 'bss_voucher': ['voucher', 'coupon', '\\u4ee3\\u91d1\\u5238', '\\u4f18\\u60e0\\u5238'],\n 'obs': ['obs', '\\u5b58\\u50a8\\u6876', '\\u6876'],\n 'rds': ['rds', '\\u6570\\u636e\\u5e93', 'mysql']\n}\ntext_lower = text.lower()\nfor rtype, patterns in res_patterns.items():\n for p in patterns:\n if p.lower() in text_lower:\n resource_types.append(rtype)\n break\n\nhas_write = any(c.get('is_write') for c in commands)\n\n# Scripts list\nscripts = []\nscripts_dir = os.path.join(skill_dir, 'scripts')\nif os.path.isdir(scripts_dir):\n for f in sorted(os.listdir(scripts_dir)):\n if f.endswith('.sh'):\n scripts.append('scripts/%s' % f)\n\nrefs = []\nrefs_dir = os.path.join(skill_dir, 'references')\nif os.path.isdir(refs_dir):\n for f in sorted(os.listdir(refs_dir)):\n if f.endswith('.md'):\n refs.append('references/%s' % f)\n\n# 文档一致性检查(#003): SKILL.md 中引用的 references/*.md 是否存在\ndoc_checks = {'referenced_refs': [], 'missing_refs': []}\n_md_mentions = sorted(set(re.findall(r'references/([\\w\\-\\.]+\\.md)', text)))\ndoc_checks['referenced_refs'] = ['references/%s' % m for m in _md_mentions]\nfor m in _md_mentions:\n if not os.path.isfile(os.path.join(skill_dir, 'references', m)):\n doc_checks['missing_refs'].append('references/%s' % m)\n\n# 禁用文件类型检查(issue#37): skill 包不应包含 .bak/.template/.orig/.rej 等\n# 残留/模板文件, 不符合 skill 编写要求。\n_FORBIDDEN_EXT = ('.bak', '.template', '.orig', '.rej', '.swp', '~')\nforbidden_files = []\nfor _root, _dirs, _files in os.walk(skill_dir):\n # 精确匹配路径段中的 .git(避免子串误伤, 如 my.git.repo)\n if '.git' in _root.split(os.sep):\n continue\n for _f in sorted(_files):\n _low = _f.lower()\n if _low.endswith(_FORBIDDEN_EXT) or _low.endswith('~'):\n forbidden_files.append(os.path.join(_root, _f).replace(skill_dir + os.sep, ''))\ndoc_checks['forbidden_files'] = forbidden_files\n\n# Also read templates/test-vars.json for SDK/CLI test cases\ntest_vars_path = os.path.join(skill_dir, 'templates', 'test-vars.json')\nif os.path.isfile(test_vars_path):\n try:\n with open(test_vars_path) as tvf:\n tv_data = json.load(tvf)\n for tc in tv_data.get('test_cases', []):\n cmd_id_val = 'CMD-%02d' % (len(commands)+1)\n cmd_desc = tc.get('name', tc.get('command', ''))[:80]\n cmd_raw = tc.get('command', '')\n exe = tc.get('executor', 'sdk')\n # Auto-detect executor from command content\n if cmd_raw.startswith('python3 scripts/') or cmd_raw.startswith('python3 ./scripts/'):\n exe = 'script'\n elif cmd_raw.startswith('python3 -c '):\n exe = 'cli' # one-liner SDK import check, run as bash\n elif cmd_raw.startswith('hcloud '):\n exe = 'cli'\n is_write_cmd = any(kw in cmd_raw.lower() for kw in ['create', 'delete', 'update', 'activate', 'reclaim', 'destroy'])\n commands.append({\n 'id': cmd_id_val,\n 'source': 'templates/test-vars.json',\n 'description': cmd_desc,\n 'command': cmd_raw,\n 'executor': exe,\n 'is_write': is_write_cmd\n })\n except Exception:\n pass\n\n# Add scripts/ entries as executable commands\nfor s in scripts:\n cmd_id_val = 'CMD-%02d' % (len(commands)+1)\n is_write = any(kw in s.lower() for kw in ['create', 'delete', 'cleanup', 'destroy'])\n commands.append({\n 'id': cmd_id_val,\n 'source': 'scripts/%s' % os.path.basename(s),\n 'description': 'Run script: bash %s [args]' % s,\n 'executor': 'script',\n 'is_write': is_write\n })\n\nresult = {\n 'metadata': {\n 'name': name,\n 'description': desc_text[:200] if desc_text else '',\n 'triggers': triggers,\n 'tags': tags\n },\n 'capabilities': {\n 'list': cap_list,\n 'create': cap_create,\n 'update': cap_update,\n 'delete': cap_delete\n },\n 'has_write_operations': has_write,\n 'resource_types': list(set(resource_types)),\n 'commands': commands,\n 'scripts': scripts,\n 'references': refs,\n 'doc_checks': doc_checks\n}\nprint(json.dumps(result, indent=2, ensure_ascii=False))\nPYANALYSIS\n\n local analysis\n set +e\n analysis=$(python3 \"$py_tmp\" \"$sk_file\" \"$skill_dir\")\n local analysis_rc=$?\n set -e\n rm -f \"$py_tmp\"\n\n if [ $analysis_rc -ne 0 ]; then\n fail \"SKILL.md 解析失败\"\n return 1\n fi\n\n local end_ts; end_ts=$(date +%s)\n local duration=$((end_ts - start_ts))\n\n # Count commands for summary\n local cmd_count\n cmd_count=$(echo \"$analysis\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(len(d.get('commands', [])))\")\n local trig_count\n trig_count=$(echo \"$analysis\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(len(d.get('metadata', {}).get('triggers', [])))\")\n local res_count\n res_count=$(echo \"$analysis\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(len(d.get('resource_types', [])))\")\n\n local verdict=\"pass\"\n [ \"$cmd_count\" -eq 0 ] && verdict=\"partial\"\n [ \"$trig_count\" -eq 0 ] && verdict=\"partial\"\n\n local tmp_json; tmp_json=$(mktemp)\n echo \"$analysis\" > \"$tmp_json\"\n\n local summary_py_tmp; summary_py_tmp=$(mktemp)\n cat > \"$summary_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\n\nwith open(sys.argv[1]) as f:\n analysis = json.load(f)\n\ncmd_count = len(analysis.get(\"commands\", []))\ntrig_count = len(analysis.get(\"metadata\", {}).get(\"triggers\", []))\nverdict = \"pass\"\nif cmd_count == 0:\n verdict = \"partial\"\nif trig_count == 0:\n verdict = \"partial\"\n\nr = {\n \"phase\": int(sys.argv[2]),\n \"phase_name\": sys.argv[3],\n \"tier\": 1,\n \"target\": {\"type\": \"single_skill\", \"skills\": [sys.argv[4]]},\n \"timestamp\": sys.argv[5],\n \"execution_meta\": {\"duration_s\": int(sys.argv[6]), \"retry_count\": 0, \"user_confirmed\": False},\n \"result\": analysis,\n \"summary\": {\"verdict\": verdict, \"pass_checks\": cmd_count, \"fail_checks\": 0, \"warn_checks\": 0}\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nPYEOF\n ensure_test_files_dir \"$skill_dir\" > /dev/null\n python3 \"$summary_py_tmp\" \"$tmp_json\" \"$PHASE_NUM\" \"$PHASE_NAME\" \"$skill_name\" \"$ts\" \"$duration\" > \"$(phase_file \"$skill_dir\" 1)\"\n rm -f \"$summary_py_tmp\"\n\n rm -f \"$tmp_json\"\n\n echo \"\"\n info \"提取结果: ${cmd_count} 条命令, ${trig_count} 个触发词, ${res_count} 种资源类型\"\n info \"写操作: $(echo \"$analysis\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('has_write_operations', False))\")\"\n # 触发词缺失提示: frontmatter triggers 为空会导致 Phase 1 partial\n if [ \"$trig_count\" -eq 0 ]; then\n warn \"⚠️ 未提取到触发词 — SKILL.md frontmatter 缺少 triggers 或为空, Phase 1 判定为 partial\"\n fi\n # 文档一致性: SKILL.md 引用了但 references/ 下不存在的文件\n local missing_refs\n missing_refs=$(echo \"$analysis\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(' '.join(d.get('doc_checks', {}).get('missing_refs', [])))\" 2>/dev/null)\n if [ -n \"$missing_refs\" ]; then\n warn \"⚠️ SKILL.md 引用了缺失的 references 文件: $missing_refs\"\n fi\n # 禁用文件检查(issue#37): .bak/.template 等残留文件\n local forbidden\n forbidden=$(echo \"$analysis\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(' '.join(d.get('doc_checks', {}).get('forbidden_files', [])))\" 2>/dev/null)\n if [ -n \"$forbidden\" ]; then\n warn \"⚠️ 包含禁用文件类型(.bak/.template 等): $forbidden\"\n fi\n echo \"\"\n echo \"$analysis\" | python3 -c \"\nimport json,sys\nd=json.load(sys.stdin)\ncmds = d.get('commands', [])\nif cmds:\n print(' 命令列表:')\n for c in cmds:\n mark = '[W]' if c['is_write'] else '[R]'\n print(f' {mark} {c[\\\"id\\\"]}: {c[\\\"description\\\"][:60]}')\ntrigs = d.get('metadata', {}).get('triggers', []) \nif trigs:\n print(f' 触发词({len(trigs)}): {trigs[:8]}...')\n\"\n}\n\n# Parse args: getopts for -s, pre-filter --skill (getopts can't handle --long)\nSKILL_DIRS=()\n_rest=()\nwhile [ $# -gt 0 ]; do\n case \"$1\" in\n --skill) SKILL_DIRS+=(\"$2\"); shift 2 ;;\n --skill=*) SKILL_DIRS+=(\"${1#--skill=}\"); shift ;;\n --help|-h) echo \"用法: $(basename \"$0\") [-s \u003cdir>]... [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n *) _rest+=(\"$1\"); shift ;;\n esac\ndone\nset -- ${_rest[@]+\"${_rest[@]}\"}\nOPTIND=1\nwhile getopts \":s:h\" opt; do\n case $opt in\n s) SKILL_DIRS+=(\"$OPTARG\") ;;\n h) echo \"用法: $(basename \"$0\") [-s \u003cdir>]... [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n \\?) ;;\n esac\ndone\nshift $((OPTIND-1))\nfor arg in \"$@\"; do SKILL_DIRS+=(\"$arg\"); done\n\nfor skill_dir in \"${SKILL_DIRS[@]}\"; do\n run_phase1 \"$skill_dir\" || exit 1\n echo \"\"\ndone\n\npass \"Phase 1: 功能提取全部完成\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":30136,"content_sha256":"13f0ed4b1c7d7913b229fb8f13ed3e349f4fba004554ee227c9add1771ca2c48"},{"filename":"scripts/tier1/phase-2-tech-research.sh","content":"#!/usr/bin/env bash\n\n\n# phase-2-tech-research.sh — 技术调研\n# 对 Phase 1 提取的每条命令做 CLI→SDK→API 三级降级验证\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\n\n# Discover available hcloud CLI services dynamically\nsource \"$SCRIPT_DIR/lib/config.sh\"\ndiscover_hcloud_services >/dev/null # populate cache\nSERVICES_CLI_JSON=$(get_cli_services_as_json)\nexport SERVICES_CLI_JSON\n\nPHASE_NUM=2\nPHASE_NAME=\"tech-research\"\n\nrun_phase2() {\n local skill_dir=\"$1\"\n local skill_name; skill_name=$(basename \"$skill_dir\")\n\n header \"Phase 2: 技术调研 — $skill_name\"\n\n check_phase_deps \"$skill_dir\" 2 || return 1\n\n local ts; ts=$(timestamp)\n local start_ts; start_ts=$(date +%s)\n local p1_file; p1_file=$(phase_file \"$skill_dir\" 1)\n\n # Read commands from Phase 1\n local cmds_json\n local cmds_py_tmp; cmds_py_tmp=$(mktemp)\n cat > \"$cmds_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\nd = json.load(open(sys.argv[1]))\ncmds = d.get('result', {}).get('commands', [])\nprint(json.dumps(cmds, indent=2))\nPYEOF\n cmds_json=$(python3 \"$cmds_py_tmp\" \"$p1_file\")\n rm -f \"$cmds_py_tmp\"\n\n # Write commands JSON to temp file\n local cmds_tmp; cmds_tmp=$(mktemp)\n echo \"$cmds_json\" > \"$cmds_tmp\"\n\n # Write Python research script to temp file (avoids all heredoc quoting issues)\n local py_tmp; py_tmp=$(mktemp)\n cat > \"$py_tmp\" \u003c\u003c 'PYRESEARCH'\nimport json, subprocess, os, sys\n\ncmds_f = sys.argv[1]\ncmds = json.load(open(cmds_f))\nresults = []\n\n# #62-004: 纯本地工具检测 — 所有命令均不涉及 hcloud/云 API 时,\n# CLI→SDK→API 三级研究无意义, 直接标记 not_applicable, 不走研究流程。\n_cmd_texts = [c.get('command', '') or '' for c in cmds]\n_any_hcloud = any(t.strip().startswith('hcloud ') for t in _cmd_texts)\n_any_sdk = any(('huaweicloudsdk' in t) or ('client.' in t) for t in _cmd_texts)\nif cmds and not _any_hcloud and not _any_sdk:\n for c in cmds:\n results.append({\n 'cmd_id': c.get('id', 'CMD-00'),\n 'command': c.get('command', ''),\n 'description': (c.get('description', '') or '')[:100],\n 'recommended_executor': 'script',\n 'risk_level': 'low',\n 'cli': {'available': False, 'command': None, 'reason': '纯本地工具, 不涉及 hcloud CLI'},\n 'sdk': {'available': False, 'package': None, 'method': None, 'api_path': None, 'error': None, 'reason': '纯本地工具, 不涉及华为云 SDK'},\n 'api': {'available': False, 'endpoint': None, 'source': 'not_found', 'reason': '纯本地工具, 不涉及云 API'},\n 'executor_note': '纯本地工具, CLI→SDK→API 三级研究不适用'\n })\n summary = {\n 'cli_available': 0, 'sdk_available': 0, 'api_available': 0,\n 'not_available': 0, 'mode': 'not_applicable',\n 'note': '纯本地工具(无 hcloud/云 API 命令), 研究流程跳过'\n }\n output = {'research': results, 'summary': summary, 'not_applicable': True}\n print(json.dumps(output, indent=2, ensure_ascii=False))\n sys.exit(0)\n\ncli_avail = 0\nsdk_avail = 0\napi_avail = 0\nnot_avail = 0\n\nfor cmd in cmds:\n desc = cmd.get('description', '')\n executor = cmd.get('executor', 'unknown')\n cmd_raw = cmd.get('command', cmd.get('command_raw', ''))\n\n entry = {\n 'cmd_id': cmd.get('id', 'CMD-00'),\n 'description': desc[:100],\n 'cli': {'available': False, 'command': None, 'reason': '未调研'},\n 'sdk': {'available': False, 'package': None, 'method': None, 'api_path': None, 'error': None},\n 'api': {'available': False, 'endpoint': None, 'source': 'not_found'},\n 'recommended_executor': 'unknown',\n 'risk_level': 'medium'\n }\n\n # CLI check: try a simple help command\n hcloud_cmd = None\n desc_lower = desc.lower()\n cmd_lower = cmd_raw.lower()\n search_text = desc_lower + ' ' + cmd_lower\n # BSS is NOT supported by hcloud CLI — skip CLI check for BSS\n bss_only = any(kw in search_text for kw in ['bss', 'coupon', 'voucher', 'stored_value', 'card', 'order_coupon', '\\u4ee3\\u91d1\\u5238', '\\u4f18\\u60e0\\u5238', '\\u50a8\\u503c\\u5361'])\n if bss_only:\n entry['cli'] = {'available': False, 'command': None, 'reason': 'BSS not supported by hcloud CLI'}\n else:\n # Load services dynamically from env (set by config.sh discovery)\n import json as _json\n cli_services = _json.loads(os.environ.get('SERVICES_CLI_JSON', '[]'))\n for svc in cli_services:\n if svc in search_text or svc.upper() in search_text:\n try:\n r = subprocess.run(\n ['hcloud', svc.upper(), '--help'],\n capture_output=True, text=True, timeout=10\n )\n if r.returncode == 0:\n entry['cli'] = {\n 'available': True,\n 'command': f'hcloud {svc.upper()} \u003cOperation> --cli-region={os.environ.get(\"HUAWEI_REGION\", \"cn-north-4\")}',\n 'reason': f'hcloud {svc.upper()} CLI 可用'\n }\n entry['recommended_executor'] = 'cli'\n entry['risk_level'] = 'low'\n cli_avail += 1\n else:\n entry['cli'] = {\n 'available': False,\n 'command': None,\n 'reason': f'hcloud {svc.upper()} 命令不存在: {r.stderr[:100]}'\n }\n except (FileNotFoundError, subprocess.TimeoutExpired) as e:\n entry['cli'] = {\n 'available': False,\n 'command': None,\n 'reason': f'hcloud CLI 不可用: {str(e)[:80]}'\n }\n break\n else:\n entry['cli'] = {'available': False, 'command': None, 'reason': '无法从描述推断 hcloud 服务名'}\n\n # If CLI not available, try SDK\n if not entry['cli']['available']:\n # Extended SDK service map (service_name -> SDK package, Client class)\n # Loaded from env or use comprehensive default\n import json as _json2\n _sdk_map_env = os.environ.get('SERVICES_SDK_MAP_JSON', '{\"bss\":\"BssClient\",\"ecs\":\"EcsClient\",\"vpc\":\"VpcClient\",\"evs\":\"EvsClient\",\"eip\":\"EipClient\",\"iam\":\"IamClient\",\"rds\":\"RdsClient\",\"dns\":\"DnsClient\",\"obs\":\"ObsClient\",\"nat\":\"NatClient\",\"cdn\":\"CdnClient\",\"cce\":\"CceClient\",\"ces\":\"CesClient\",\"dcs\":\"DcsClient\",\"dds\":\"DdsClient\",\"mrs\":\"MrsClient\",\"dli\":\"DliClient\",\"rfs\":\"RfsClient\",\"apig\":\"ApigClient\",\"functiongraph\":\"FunctionGraphClient\",\"smn\":\"SmnClient\"}')\n _sdk_map = _json2.loads(_sdk_map_env)\n for svc_pkg, sdk_cls in _sdk_map.items():\n try:\n import_cmd = f'from huaweicloudsdk{svc_pkg}.v2 import {sdk_cls}'\n r = subprocess.run(\n ['python3', '-c', import_cmd],\n capture_output=True, text=True, timeout=10\n )\n if r.returncode == 0:\n entry['sdk'] = {\n 'available': True,\n 'package': f'huaweicloudsdk{svc_pkg}.v2',\n 'method': f'{sdk_cls}.\u003cmethod>()',\n 'api_path': None,\n 'error': None\n }\n entry['recommended_executor'] = 'sdk'\n entry['risk_level'] = 'low'\n sdk_avail += 1\n\n # Try to find _http_info for API path\n try:\n find_result = subprocess.run(\n ['python3', '-c', f'''\nimport inspect, importlib\ntry:\n mod = importlib.import_module(\"huaweicloudsdk{svc_pkg}.v2\")\n client_file = inspect.getfile(getattr(mod, \"{sdk_cls}\"))\n print(client_file)\nexcept Exception as e:\n print(f\"ERROR: {{e}}\")\n'''],\n capture_output=True, text=True, timeout=10\n )\n client_path = find_result.stdout.strip()\n if client_path and not client_path.startswith('ERROR'):\n grep_r = subprocess.run(\n ['grep', '-oP', 'resource_path\\\\s*=\\\\s*\"([^\"]+)\"', client_path],\n capture_output=True, text=True, timeout=10\n )\n if grep_r.stdout:\n paths = list(set(grep_r.stdout.strip().split('\\n')))\n entry['sdk']['api_path'] = paths[0]\n entry['api'] = {\n 'available': True,\n 'endpoint': paths[0],\n 'source': 'sdk_http_info'\n }\n api_avail += 1\n except Exception:\n pass\n break\n except (FileNotFoundError, subprocess.TimeoutExpired):\n continue\n else:\n not_avail += 1\n entry['recommended_executor'] = 'manual'\n entry['risk_level'] = 'high'\n entry['sdk'] = {'available': False, 'package': None, 'method': None, 'api_path': None, 'error': '未找到匹配的 SDK 包'}\n\n results.append(entry)\n\nsummary = {\n 'cli_available': cli_avail,\n 'sdk_available': sdk_avail,\n 'api_available': api_avail,\n 'not_available': not_avail\n}\n\noutput = {'research': results, 'summary': summary}\nprint(json.dumps(output, indent=2, ensure_ascii=False))\nPYRESEARCH\n\n local research\n research=$(python3 \"$py_tmp\" \"$cmds_tmp\")\n rm -f \"$py_tmp\"\n\n local end_ts; end_ts=$(date +%s)\n local duration=$((end_ts - start_ts))\n\n local cli_count; cli_count=$(echo \"$research\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['summary']['cli_available'])\")\n local sdk_count; sdk_count=$(echo \"$research\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['summary']['sdk_available'])\")\n local na_count; na_count=$(echo \"$research\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['summary']['not_available'])\")\n\n local verdict=\"pass\"\n [ \"$na_count\" -gt 0 ] && verdict=\"partial\"\n\n local tmp_json; tmp_json=$(mktemp)\n echo \"$research\" > \"$tmp_json\"\n local summary_py_tmp; summary_py_tmp=$(mktemp)\n cat > \"$summary_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\nwith open(sys.argv[1]) as f:\n research_data = json.load(f)\nr = {\n \"phase\": int(sys.argv[2]),\n \"phase_name\": sys.argv[3],\n \"tier\": 1,\n \"target\": {\"type\": \"single_skill\", \"skills\": [sys.argv[4]]},\n \"timestamp\": sys.argv[5],\n \"execution_meta\": {\"duration_s\": int(sys.argv[6]), \"retry_count\": 0, \"user_confirmed\": False},\n \"result\": research_data,\n \"summary\": {\"verdict\": sys.argv[7], \"pass_checks\": int(sys.argv[8]), \"fail_checks\": int(sys.argv[9]), \"warn_checks\": 0}\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nPYEOF\n ensure_test_files_dir \"$skill_dir\" > /dev/null\n python3 \"$summary_py_tmp\" \"$tmp_json\" \"$PHASE_NUM\" \"$PHASE_NAME\" \"$skill_name\" \"$ts\" \"$duration\" \"$verdict\" \"$cli_count\" \"$na_count\" > \"$(phase_file \"$skill_dir\" 2)\"\n rm -f \"$summary_py_tmp\"\n rm -f \"$tmp_json\"\n rm -f \"$cmds_tmp\"\n\n echo \"\"\n info \"调研结果: CLI ${cli_count} | SDK ${sdk_count} | API 发现 | 未可用 ${na_count}\"\n echo \"$research\" | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nfor r in d['research']:\n exe = r['recommended_executor']\n icon = '✅' if exe != 'manual' else '⛔'\n print(f\\\" {icon} {r['cmd_id']}: 推荐={exe}, CLI={r['cli']['available']}, SDK={r['sdk']['available']}\\\")\n\"\n}\n\n# 参数解析: getopts 解析命名参数(--skill \u003cdir> / --skill=\u003cdir>), 与 SKILL.md 文档用法一致;\n# 位置参数(管线内部调用)作为兜底追加。\n_arg_skills=()\nwhile getopts \":h-:\" opt; do\n case \"$opt\" in\n h) echo \"用法: $(basename \"$0\") [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n -) case \"${OPTARG}\" in\n skill)\n [ $OPTIND -le $# ] || { echo \"错误: --skill 需要参数\" >&2; exit 1; }\n _arg_skills+=(\"${!OPTIND}\"); OPTIND=$((OPTIND + 1)) ;;\n skill=*) _arg_skills+=(\"${OPTARG#skill=}\") ;;\n help) echo \"用法: $(basename \"$0\") [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n *) echo \"未知参数: --${OPTARG}\" >&2; exit 1 ;;\n esac ;;\n \\?) echo \"未知参数: -$OPTARG\" >&2; exit 1 ;;\n esac\ndone\nshift $((OPTIND - 1))\n_arg_skills+=(\"$@\")\nset -- \"${_arg_skills[@]}\"\n\nfor skill_dir in \"$@\"; do\n run_phase2 \"$skill_dir\" || exit 1\n echo \"\"\ndone\n\npass \"Phase 2: 技术调研全部完成\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":12833,"content_sha256":"211ea7aa3b5e72ad95a4ad587c0c9959c923bfc276be04c6a5c0b5d8df4495e0"},{"filename":"scripts/tier1/phase-3-gen-testcases.sh","content":"#!/usr/bin/env bash\n\n\n# phase-3-gen-testcases.sh — 用例生成\n# 基于 Phase 1+2 生成功能用例 TC-F 和 API 用例 TC-A\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\n\nPHASE_NUM=3\nPHASE_NAME=\"test-case-generation\"\n\nrun_phase3() {\n local skill_dir=\"$1\"\n local skill_name; skill_name=$(basename \"$skill_dir\")\n\n header \"Phase 3: 用例生成 — $skill_name\"\n\n check_phase_deps \"$skill_dir\" 3 || return 1\n\n local ts; ts=$(timestamp)\n local start_ts; start_ts=$(date +%s)\n\n # Read Phase 1 (capabilities) and Phase 2 (research)\n local p1_file; p1_file=$(phase_file \"$skill_dir\" 1)\n local p2_file; p2_file=$(phase_file \"$skill_dir\" 2)\n\n local testcases\n local tc_gen_py_tmp; tc_gen_py_tmp=$(mktemp)\n # 共享占位符替换模块(phase-3/4 单一维护点)\n export PLACEHOLDER_UTILS=\"$SCRIPT_DIR/lib/placeholder-utils.py\"\n cat > \"$tc_gen_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, os, sys, re\nfrom datetime import datetime, timedelta\n\n# 加载共享占位符替换逻辑(见 lib/placeholder-utils.py)\nexec(open(os.environ.get('PLACEHOLDER_UTILS', '')).read())\n\nwith open(sys.argv[1]) as f:\n p1 = json.load(f)\nwith open(sys.argv[2]) as f:\n p2 = json.load(f)\n\nskill_dir = sys.argv[3] if len(sys.argv) > 3 else os.getcwd()\n\n# Dynamic date replacement: replace example dates in commands with current-time\n# values so queries fall within data retention periods. (ISSUE-007: SKILL.md\n# example dates may be months old, causing empty results.)\n_NOW = datetime.utcnow()\n_DATE_REPLACEMENTS = {\n # --from/--to with YYYY-MM-DD or YYYYMMDD format\n r'--from[=\\s]+\\d{4}-\\d{2}-\\d{2}': '--from=' + (_NOW - timedelta(days=7)).strftime('%Y-%m-%d'),\n r'--to[=\\s]+\\d{4}-\\d{2}-\\d{2}': '--to=' + _NOW.strftime('%Y-%m-%d'),\n r'--from[=\\s]+\\d{8}': '--from=' + (_NOW - timedelta(days=7)).strftime('%Y%m%d'),\n r'--to[=\\s]+\\d{8}': '--to=' + _NOW.strftime('%Y%m%d'),\n # --start-date/--end-date\n r'--start-date[=\\s]+\\d{4}-\\d{2}-\\d{2}': '--start-date=' + (_NOW - timedelta(days=7)).strftime('%Y-%m-%d'),\n r'--end-date[=\\s]+\\d{4}-\\d{2}-\\d{2}': '--end-date=' + _NOW.strftime('%Y-%m-%d'),\n}\n\ndef dynamic_replace_dates(cmd_text):\n \"\"\"Replace example dates in commands with dynamic current-time values.\"\"\"\n for pat, repl in _DATE_REPLACEMENTS.items():\n cmd_text = re.sub(pat, repl, cmd_text, flags=re.IGNORECASE)\n return cmd_text\n\ncaps = p1.get('result', {}).get('capabilities', {})\nresearch = p2.get('result', {}).get('research', [])\ncommands = p1.get('result', {}).get('commands', [])\n\nfunctional_cases = []\napi_cases = []\ntc_f_id = 0\ntc_a_id = 0\n\n# Generate functional test cases from commands (preferred) or capabilities\nexecutor_map = {}\nfor r in research:\n desc = r.get('description', '').lower()\n executor_map[desc] = r.get('recommended_executor', 'sdk')\n\ndef make_boundary_cmd(cmd_text, executor):\n \"\"\"Generate a boundary variant of a command.\n\n For CLI: only reduce existing --limit values (never add blindly).\n For script: construct a boundary date scenario (reversed date range).\n For SDK: only reduce existing limit= values.\n (ISSUE-006: boundary case must differ from positive case, not just suffix the name)\n \"\"\"\n t = cmd_text.strip()\n low = t.lower()\n # 无参/帮助/配置类命令不支持 --limit, 边界保持原样(预期正常执行)\n if re.match(r'^hcloud\\s+(--help|-h|--version|help|version|configure)\\b', low):\n return t\n if executor == 'cli' and t.startswith('hcloud'):\n if '--limit=' in t:\n return re.sub(r'--limit=\\d+', '--limit=1', t)\n return t\n elif executor == 'script':\n # Boundary for script commands: reverse --from/--to dates (to > from → error)\n # or use a 1-day window if dates exist\n if '--from=' in t and '--to=' in t:\n _from_val = re.search(r'--from=(\\S+)', t)\n _to_val = re.search(r'--to=(\\S+)', t)\n if _from_val and _to_val:\n return t.replace('--from=' + _from_val.group(1), '--from=' + _to_val.group(1)).replace('--to=' + _to_val.group(1), '--to=' + _from_val.group(1))\n return t\n elif executor == 'sdk' and 'limit=' in t:\n return re.sub(r'limit=\\d+', 'limit=1', t)\n elif executor == 'script' and '--limit=' in t:\n return re.sub(r'--limit=\\d+', '--limit=1', t)\n return t\n\ndef make_boundary_sdk_snippet(snippet, method_name):\n \"\"\"Generate a boundary SDK snippet with limit=1.\n\n Only reduces existing request.limit values — never blindly inserts\n request.limit for methods that may not support it.\n (TESTER-ISSUE-010: blind --limit addition caused false failures)\n \"\"\"\n import re as re2\n modified = re2.sub(r'request\\.limit\\s*=\\s*\\d+', 'request.limit = 1', snippet)\n return modified\n\n# If clean commands exist, generate test cases from them\n# (replace_placeholders 定义在共享模块 lib/placeholder-utils.py, 上方 exec 加载)\n\ndef is_template_or_interactive(cmd_text):\n \"\"\"识别不可自动执行的模板命令/交互命令: 返回 True 则跳过生成。\"\"\"\n import re\n t = (cmd_text or '').strip().lower()\n if not t:\n return True\n # 裸 hcloud(模板清理后无操作子命令)\n if t == 'hcloud':\n return True\n # hcloud 帮助/配置类交互命令(无实际操作子命令)。\n # 注意: 过滤 --version 选项(实际不支持), 但保留 version 子命令(真实可执行)。\n if re.match(r'^hcloud\\s+(--help|-h|--version|help|configure)\\b', t):\n return True\n # 含 [--key=value ...] 等模板语法\n if re.search(r'\\[\\s*--[a-z-]+=\\.\\.\\.', t) or re.search(r'\\[\\s*--key=value', t):\n return True\n # 未解析的 URL/API 模板占位符({endpoint}/https://{...})或残留 {xxx}\n if re.search(r'\\{endpoint\\}|\\{url\\}|\\{host\\}|https?://\\{', t) or re.search(r'\\{[a-z_]+\\}', t):\n return True\n # 残留 \u003cxxx> 占位符(非 shell 重定向, 是未替换模板)\n if re.search(r'\\b(curl|wget)\\b.*\u003c[a-z_]+>', t):\n return True\n # 残留空 --cli-region=(占位符清理后仍无值)\n if re.search(r'--cli-region=\\s*(?:\\s|$)', t):\n return True\n return False\n\nif commands and any(c.get('command') for c in commands):\n for cmd in commands:\n tc_f_id += 1\n cmd_text = cmd.get('command', cmd.get('description', ''))\n cmd_text = replace_placeholders(cmd_text, skill_dir)\n cmd_text = dynamic_replace_dates(cmd_text)\n # Skip commands that are descriptions without executable code\n if not cmd.get('command') and not cmd.get('command_raw'):\n continue\n # Skip placeholder commands with \u003c...> or [...] that aren't real commands\n if cmd_text and cmd_text.startswith('python3 ') and '\u003c' in cmd_text and '>' in cmd_text:\n continue\n # Skip interactive/template commands (hcloud configure, [--key=value ...])\n if is_template_or_interactive(cmd_text):\n continue\n is_write = cmd.get('is_write', False)\n risk = 'high' if is_write else 'low'\n desc_lower = cmd.get('description', '').lower()\n executor = cmd.get('executor', 'sdk')\n # Use Phase 2 research to refine executor, but preserve script executor\n if executor != 'script':\n for r_desc, r_exec in executor_map.items():\n if r_desc in desc_lower or desc_lower[:20] in r_desc:\n executor = r_exec\n break\n method_name = cmd.get('method_name', '')\n \n functional_cases.append({\n 'id': f'TC-F-{tc_f_id:02d}',\n 'name': cmd.get('description', cmd_text[:60]) if cmd.get('description') else f\"命令-{tc_f_id:02d}\",\n 'type': '正向' if not is_write else '变更',\n 'command': cmd_text,\n 'expected': 'SDK调用成功并返回数据' if executor == 'sdk' else ('CLI命令执行成功' if executor == 'cli' else '脚本执行成功'),\n 'is_write': is_write,\n 'risk_level': risk,\n 'executor': executor,\n 'prerequisites': [],\n 'verification_method': '执行后检查返回码和输出',\n 'dependencies': [],\n 'method_name': method_name,\n 'service': cmd.get('service', ''),\n 'request_class': cmd.get('request_class', '')\n })\n \n # Edge case for read operations: add limit/boundary variant\n if not is_write and cmd_text and not is_template_or_interactive(cmd_text):\n tc_f_id += 1\n if executor == 'sdk' and method_name:\n bound_cmd = make_boundary_sdk_snippet(cmd_text, method_name)\n else:\n bound_cmd = make_boundary_cmd(cmd_text, executor)\n functional_cases.append({\n 'id': f'TC-F-{tc_f_id:02d}',\n 'name': f\"{cmd.get('description', cmd_text[:40])}-边界\",\n 'type': '边界',\n 'command': bound_cmd,\n 'expected': '返回空结果或正确提示',\n 'is_write': False,\n 'risk_level': 'low',\n 'executor': executor,\n 'prerequisites': [],\n 'verification_method': '不报错即为通过',\n 'dependencies': [],\n 'method_name': method_name,\n 'service': cmd.get('service', ''),\n 'request_class': cmd.get('request_class', '')\n })\n # 负向用例生成(#005): 对 CLI 命令生成\"未知参数\"变体, 验证报错质量。\n # 负向用例预期 CLI 拒绝未知参数(非零退出码 + 错误提示); 若命令静默接受, 则判 fail。\n # type 用英文 'negative' 作为稳定标识(避免中文硬编码耦合, 展示名仍含\"负向\")。\n for cmd in commands:\n c_text = cmd.get('command', '')\n if not c_text or not c_text.strip().startswith('hcloud '):\n continue\n if cmd.get('is_write', False):\n continue\n if is_template_or_interactive(c_text):\n continue\n tc_f_id += 1\n neg_cmd = c_text.strip() + ' --invalid-flag-xyz'\n functional_cases.append({\n 'id': f'TC-F-{tc_f_id:02d}',\n 'name': f\"{cmd.get('description', c_text[:40])}-负向(未知参数)\",\n 'type': 'negative',\n 'command': neg_cmd,\n 'expected': 'CLI 应拒绝未知参数并给出错误提示',\n 'is_write': False,\n 'risk_level': 'low',\n 'executor': 'cli',\n 'prerequisites': [],\n 'verification_method': '非零退出码且报错即通过',\n 'dependencies': [],\n 'method_name': '',\n 'service': cmd.get('service', ''),\n 'request_class': ''\n })\nelse:\n # Fallback: generate from capabilities (old behavior)\n for action_type, items in caps.items():\n for item in items:\n tc_f_id += 1\n is_write = action_type in ('create', 'update', 'delete')\n risk = 'high' if action_type in ('create', 'delete') else ('medium' if action_type == 'update' else 'low')\n \n # Find matching research entry\n executor = 'sdk'\n for r in research:\n if r.get('description', '').lower() in item.lower() or item.lower() in r.get('description', '').lower():\n executor = r.get('recommended_executor', 'sdk')\n break\n \n functional_cases.append({\n 'id': f'TC-F-{tc_f_id:02d}',\n 'name': f\"{item}-正向\",\n 'type': '正向',\n 'command': item,\n 'expected': f'成功{item}',\n 'is_write': is_write,\n 'risk_level': risk,\n 'executor': executor,\n 'prerequisites': [],\n 'verification_method': '执行后验证结果',\n 'dependencies': []\n })\n \n # Add edge case for read operations\n if not is_write:\n tc_f_id += 1\n functional_cases.append({\n 'id': f'TC-F-{tc_f_id:02d}',\n 'name': f\"{item}-边界(limit=0/空过滤)\",\n 'type': '边界',\n 'command': item,\n 'expected': '返回空结果或正确提示',\n 'is_write': False,\n 'risk_level': 'low',\n 'executor': executor,\n 'prerequisites': [],\n 'verification_method': '不报错即为通过',\n 'dependencies': []\n })\n\n# Generate API test cases from research API paths\nfor r in research:\n api_info = r.get('api', {})\n if api_info.get('available') and api_info.get('endpoint'):\n tc_a_id += 1\n api_cases.append({\n 'id': f'TC-A-{tc_a_id:02d}',\n 'name': f\"API-{r.get('description', 'unknown')[:40]}\",\n 'endpoint': api_info['endpoint'],\n 'method': 'GET',\n 'expected': 'HTTP 200',\n 'is_write': False,\n 'risk_level': 'medium'\n })\n\n# If no capabilities extracted, generate from commands\nif not functional_cases:\n for cmd in commands:\n tc_f_id += 1\n functional_cases.append({\n 'id': f'TC-F-{tc_f_id:02d}',\n 'name': f\"CMD-{cmd.get('id', '')}: {cmd.get('description', '')[:50]}\",\n 'type': '正向',\n 'command': cmd.get('command_raw') or cmd.get('description', ''),\n 'source': cmd.get('source', ''),\n 'expected': '执行成功',\n 'is_write': cmd.get('is_write', False),\n 'risk_level': 'high' if cmd.get('is_write', False) else 'low',\n 'executor': cmd.get('executor', 'unknown'),\n 'prerequisites': [],\n 'verification_method': '执行并检查返回值',\n 'dependencies': []\n })\n\nresult = {\n 'functional_cases': functional_cases,\n 'api_cases': api_cases,\n 'statistics': {\n 'total': len(functional_cases) + len(api_cases),\n 'functional': len(functional_cases),\n 'api': len(api_cases),\n 'write_operations': sum(1 for c in functional_cases if c['is_write']),\n 'read_operations': sum(1 for c in functional_cases if not c['is_write']),\n 'high_risk': sum(1 for c in functional_cases if c['risk_level'] == 'high'),\n 'low_risk': sum(1 for c in functional_cases if c['risk_level'] == 'low')\n }\n}\n\nprint(json.dumps(result, indent=2, ensure_ascii=False))\nPYEOF\n testcases=$(python3 \"$tc_gen_py_tmp\" \"$p1_file\" \"$p2_file\" \"$skill_dir\")\n rm -f \"$tc_gen_py_tmp\"\n\n local end_ts; end_ts=$(date +%s)\n local duration=$((end_ts - start_ts))\n\n local total\n total=$(echo \"$testcases\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['total'])\")\n local write_ops\n write_ops=$(echo \"$testcases\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['write_operations'])\")\n local high_risk\n high_risk=$(echo \"$testcases\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['high_risk'])\")\n\n local api_count\n api_count=$(echo \"$testcases\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['api'])\")\n local func_count\n func_count=$(echo \"$testcases\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['functional'])\")\n\n local verdict=\"pass\"\n [ \"$total\" -eq 0 ] && verdict=\"fail\"\n\n local tmp_json; tmp_json=$(mktemp)\n echo \"$testcases\" > \"$tmp_json\"\n local summary_py_tmp; summary_py_tmp=$(mktemp)\n cat > \"$summary_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\nwith open(sys.argv[1]) as f:\n tc_data = json.load(f)\nr = {\n \"phase\": int(sys.argv[2]),\n \"phase_name\": sys.argv[3],\n \"tier\": 1,\n \"target\": {\"type\": \"single_skill\", \"skills\": [sys.argv[4]]},\n \"timestamp\": sys.argv[5],\n \"execution_meta\": {\"duration_s\": int(sys.argv[6]), \"retry_count\": 0, \"user_confirmed\": False},\n \"result\": tc_data,\n \"summary\": {\"verdict\": sys.argv[7], \"pass_checks\": int(sys.argv[8]), \"fail_checks\": 0, \"warn_checks\": 0}\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nPYEOF\n ensure_test_files_dir \"$skill_dir\" > /dev/null\n python3 \"$summary_py_tmp\" \"$tmp_json\" \"$PHASE_NUM\" \"$PHASE_NAME\" \"$skill_name\" \"$ts\" \"$duration\" \"$verdict\" \"$total\" > \"$(phase_file \"$skill_dir\" 3)\"\n rm -f \"$summary_py_tmp\"\n rm -f \"$tmp_json\"\n\n echo \"\"\n info \"生成用例: 共 ${total} 条 (功能 ${func_count}, API ${api_count})\"\n info \"写操作: ${write_ops} 条 | 高风险: ${high_risk} 条\"\n echo \"\"\n echo \"$testcases\" | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nprint(' 📋 功能用例:')\nfor c in d.get('functional_cases', []):\n mark = '✏️' if c['is_write'] else '📖'\n print(f\\\" {mark} {c['id']}: {c['name'][:50]} [{c['risk_level']}]\\\")\nprint(' 📋 API用例:')\nfor c in d.get('api_cases', []):\n print(f\\\" 🔗 {c['id']}: {c.get('endpoint', '')} [{c['risk_level']}]\\\")\n\"\n}\n\n# Parse args: getopts for -s, pre-filter --skill (getopts can't handle --long)\nSKILL_DIRS=()\n_rest=()\nwhile [ $# -gt 0 ]; do\n case \"$1\" in\n --skill) SKILL_DIRS+=(\"$2\"); shift 2 ;;\n --skill=*) SKILL_DIRS+=(\"${1#--skill=}\"); shift ;;\n --help|-h) echo \"用法: $(basename \"$0\") [-s \u003cdir>]... [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n *) _rest+=(\"$1\"); shift ;;\n esac\ndone\nset -- ${_rest[@]+\"${_rest[@]}\"}\nOPTIND=1\nwhile getopts \":s:h\" opt; do\n case $opt in\n s) SKILL_DIRS+=(\"$OPTARG\") ;;\n h) echo \"用法: $(basename \"$0\") [-s \u003cdir>]... [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n \\?) ;;\n esac\ndone\nshift $((OPTIND-1))\nfor arg in \"$@\"; do SKILL_DIRS+=(\"$arg\"); done\n\nfor skill_dir in \"${SKILL_DIRS[@]}\"; do\n run_phase3 \"$skill_dir\" || exit 1\n echo \"\"\ndone\n\npass \"Phase 3: 用例生成全部完成\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":17967,"content_sha256":"ba57ca571e171f5001066c50a822ddada12695aee6ec2b00ff06abc97d0f9a5f"},{"filename":"scripts/tier1/phase-4-execute-tests.sh","content":"#!/usr/bin/env bash\n\n\n# phase-4-execute-tests.sh — 用例执行\n# 只读自动执行;写操作仅在 ALLOW_WRITES=1 时执行(详见 SKILL.md 边界表)\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\n\nPHASE_NUM=4\nPHASE_NAME=\"test-execution\"\n\nrun_phase4() {\n local skill_dir=\"$1\"\n local skill_name; skill_name=$(basename \"$skill_dir\")\n\n header \"Phase 4: 执行 — $skill_name\"\n\n check_phase_deps \"$skill_dir\" 4 || return 1\n\n # Force AK/SK check before any SDK/CLI execution\n step \"检查 AK/SK 凭证...\"\n set +e\n ensure_ak_sk\n cred_rc=$?\n set -e\n if [ $cred_rc -ne 0 ]; then\n if [ $cred_rc -eq 77 ]; then\n # Credentials required but not provided — env-var template has been\n # emitted to stderr by ensure_ak_sk(). The calling agent (or human)\n # MUST output that template to the user and ask them to set env vars\n # out-of-band (shell profile / PowerShell $PROFILE). Do NOT ask the\n # user to type or paste AK/SK in chat. See SKILL.md \"Agent Protocol\".\n fail \"AK/SK 凭证缺失(exit 77 — 详见 stderr 中的 env-var 设置模板)\"\n fail \" sentinel: $CRED_REQUEST_SENTINEL\"\n fail \" 调用方应将该模板原样输出给用户,让用户带外设置环境变量后重跑\"\n fail \" --phase 4 或 --resume,禁止直接索要 AK/SK 明文\"\n return 77\n fi\n fail \"AK/SK 凭证检查失败(exit=$cred_rc)\"\n return 1\n fi\n\n local ts; ts=$(timestamp)\n local start_ts; start_ts=$(date +%s)\n\n local p3_file; p3_file=$(phase_file \"$skill_dir\" 3)\n\n # Read test cases from Phase 3\n local tc_f\n local tc_read_py_tmp; tc_read_py_tmp=$(mktemp)\n cat > \"$tc_read_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\nd = json.load(open(sys.argv[1]))\nfunc = d.get('result', {}).get('functional_cases', [])\napi = d.get('result', {}).get('api_cases', [])\nprint(json.dumps(func + api, indent=2))\nPYEOF\n tc_f=$(python3 \"$tc_read_py_tmp\" \"$p3_file\")\n rm -f \"$tc_read_py_tmp\"\n\n # Write test cases to temp file to avoid heredoc quoting issues\n local tc_tmp; tc_tmp=$(mktemp)\n echo \"$tc_f\" > \"$tc_tmp\"\n\n # Write Python execution script to temp file\n local py_tmp; py_tmp=$(mktemp)\n # 共享占位符替换模块(phase-3/4 单一维护点)\n export PLACEHOLDER_UTILS=\"$SCRIPT_DIR/lib/placeholder-utils.py\"\n cat > \"$py_tmp\" \u003c\u003c 'PYEXEC'\nimport json, subprocess, sys, time, os, re\n\n# 加载共享占位符替换逻辑(见 lib/placeholder-utils.py)\nexec(open(os.environ.get('PLACEHOLDER_UTILS', '')).read())\n\ncases_f = sys.argv[1]\nskill_root = sys.argv[2] if len(sys.argv) > 2 else os.path.dirname(os.path.dirname(cases_f))\ncases = json.load(open(cases_f))\nexec_results = []\nall_resources = []\n\npass_count = 0\nfail_count = 0\nskip_count = 0\nerror_count = 0\n\nfor tc in cases:\n tc_id = tc.get('id', 'TC-??')\n is_write = tc.get('is_write', False)\n executor = tc.get('executor', 'unknown')\n name = tc.get('name', '')\n\n print(f\"\\n 执行: {tc_id} - {name}\")\n\n entry = {\n 'tc_id': tc_id,\n 'name': name,\n 'type': tc.get('type', ''),\n 'command': tc.get('command', ''),\n 'status': 'skip',\n 'duration_s': 0,\n 'output_snippet': '',\n 'error': None,\n 'resource_changes': [],\n 'user_confirmed': False\n }\n\n # 兜底: 复用共享 replace_placeholders(与 phase-3 同源, 单一维护点)\n if tc.get('command'):\n tc['command'] = replace_placeholders(tc['command'], skill_root)\n entry['command'] = tc['command']\n\n # 交互式/模板命令不实际执行 → skip (而非 fail)\n cmd_chk = tc.get('command', '').strip().lower()\n _hc = re.match(r'^hcloud\\s+(\\S+)', cmd_chk)\n if cmd_chk == 'hcloud' or (_hc and _hc.group(1) in ('configure', 'help', '--help', '-h', '--version')):\n entry['status'] = 'skip'\n entry['output_snippet'] = f'交互式/帮助命令 (hcloud {_hc.group(1)}), 非交互模式不可执行, 跳过'\n skip_count += 1\n print(f\" ⏭️ SKIP-交互/帮助命令 (hcloud {_hc.group(1)})\")\n exec_results.append(entry)\n continue\n if re.search(r'\\[\\s*--[a-z-]+=\\.\\.\\.', cmd_chk) or re.search(r'\\[\\s*--key=value', cmd_chk) \\\n or re.search(r'--cli-region=\\s*(?:\\s|$)', cmd_chk) \\\n or re.search(r'\\{endpoint\\}|\\{url\\}|\\{host\\}|https?://\\{', cmd_chk) \\\n or re.search(r'\\{[a-z_]+\\}', cmd_chk):\n entry['status'] = 'skip'\n entry['output_snippet'] = '模板命令(占位符未解析), 跳过'\n skip_count += 1\n print(f\" ⏭️ SKIP-模板命令: {tc.get('command', '')[:60]}\")\n exec_results.append(entry)\n continue\n\n if is_write:\n risk = tc.get('risk_level', 'high')\n print(f\" ⚠️ 写操作 [{risk}] — 命令: {tc.get('command', 'N/A')[:80]}\")\n print(f\" 预期: {tc.get('expected', 'N/A')[:80]}\")\n\n # ALLOW_WRITES gate: write cases only execute when explicitly enabled\n # (ALLOW_WRITES=1), mirroring phase-6 and the SKILL.md boundary table.\n # Skipped write cases keep status=skip and record no resource_changes.\n allow_writes = os.environ.get('ALLOW_WRITES', '0') == '1'\n if not allow_writes:\n entry['status'] = 'skip'\n entry['output_snippet'] = '写操作已跳过 (ALLOW_WRITES=0)'\n skip_count += 1\n print(f\" ⏭️ SKIP-写操作 (ALLOW_WRITES=0)\")\n exec_results.append(entry)\n continue\n\n print(f\" 非交互模式: 使用已通过 env var 设定的 AK/SK 凭证执行写操作 (ALLOW_WRITES=1)\")\n entry['user_confirmed'] = True\n\n executor_type = executor\n start_t = time.time()\n\n try:\n if executor_type == 'cli':\n cmd_text = tc.get('command', tc.get('description', ''))\n # replace_placeholders already ran at the top of the loop (line 108),\n # handling {region}, \u003cregion>, /path/to/xxx, {id}, [--key=value ...], etc.\n # The redundant regex cleanup below was removed to avoid double-processing\n # conflicts that corrupt multi-line commands and strip valid params.\n # Remove leading/trailing pipes and whitespace that might leak from table extraction\n cmd_text = cmd_text.strip().lstrip('|').strip()\n if cmd_text and len(cmd_text) > 5:\n r = subprocess.run(\n ['bash', '-c', cmd_text],\n capture_output=True, text=True, timeout=int(os.environ.get('TIMEOUT_CLI', '30')),\n env=os.environ\n )\n _trunc = int(os.environ.get('OUTPUT_TRUNC_CLI', '1000'))\n _trunc_err = int(os.environ.get('OUTPUT_TRUNC_ERR', '300'))\n output = (r.stdout[:_trunc] + r.stderr[:_trunc_err]).strip()\n if tc.get('type') in ('negative', '负向'):\n # 负向用例: CLI 正确拒绝未知参数 = pass; 静默接受 = fail。\n # hcloud KooCLI 对 USE_ERROR 等错误返回码为 0 (rc==0 但输出含错误提示),\n # 因此增加文本兜底: rc==0 但输出含 [USE_ERROR]/is not supported/format\n # 等错误特征时, 同样视为 CLI 已拒绝未知参数 → pass。\n if r.returncode != 0:\n status = 'pass'\n error_detail = None\n else:\n _neg_out = (output or '').lower()\n _neg_reject_pats = ('[use_error]', 'use_error', 'is not supported',\n 'not supported', 'unrecognized', 'parameter format',\n 'format error', 'format incorrect', 'invalid parameter',\n 'invalid option', 'unknown option', 'unknown parameter',\n 'invalid value', 'not found', '参数格式', '不支持')\n if any(p in _neg_out for p in _neg_reject_pats):\n status = 'pass'\n error_detail = f'负向用例: hcloud rc=0 但输出含错误提示(已拒绝未知参数): {output[:200]}'\n else:\n status = 'fail'\n error_detail = '负向用例: 命令未拒绝未知参数(--invalid-flag-xyz), 报错质量差'\n # 负向用例不做 CLI error pattern 复查\n else:\n status = 'pass' if r.returncode == 0 else 'fail'\n if r.returncode != 0:\n error_detail = (r.stderr[:200] or r.stdout[:200]).strip()\n else:\n error_detail = None\n # Even with return code 0, check output for CLI error patterns (e.g. hcloud USE_ERROR)\n out_lower = output.lower()\n _cli_err_patterns = json.loads(os.environ.get('CLI_ERROR_PATTERNS', '[]'))\n if any(kw in out_lower for kw in _cli_err_patterns):\n status = 'warn'\n error_detail = f'CLI returned error in output (rc=0): {output[:200]}'\n elif not output.strip():\n # 输出质量判定(#62-010): 返回码 0 但无任何输出 → 无法确认用户需求被满足\n status = 'warn'\n error_detail = '返回码 0 但输出为空, 无法确认需求被满足, 建议人工核实'\n else:\n output = f\"命令为空: {cmd_text}\"\n status = 'fail'\n error_detail = \"命令内容为空,无法执行\"\n elif executor_type == 'sdk':\n cmd_text = tc.get('command', '')\n method_name = tc.get('method_name', '')\n # If command starts with python3 -c, run as bash (it's a one-liner, not a snippet)\n if cmd_text.startswith('python3 -c ') or cmd_text.startswith('python3 -c '):\n r = subprocess.run(\n ['bash', '-c', cmd_text],\n capture_output=True, text=True, timeout=int(os.environ.get('TIMEOUT_CLI', '30')),\n env=os.environ\n )\n _trunc = int(os.environ.get('OUTPUT_TRUNC_SDK', '2000'))\n _trunc_err = int(os.environ.get('OUTPUT_TRUNC_ERR', '500'))\n output = (r.stdout[:_trunc] + '\\n' + r.stderr[:_trunc_err]).strip()\n status = 'pass' if r.returncode == 0 else 'fail'\n error_detail = (r.stderr[:300] or r.stdout[:300]).strip() if r.returncode != 0 else None\n # Real SDK execution: write the full Python snippet to a temp file and run it\n elif cmd_text and ('import' in cmd_text or 'client.' in cmd_text or 'from ' in cmd_text):\n # The command field contains a complete executable Python snippet\n sdk_tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False)\n try:\n sdk_tmp.write(cmd_text)\n sdk_tmp.close()\n r = subprocess.run(\n ['python3', sdk_tmp.name],\n capture_output=True, text=True, timeout=int(os.environ.get('TIMEOUT_SDK', '60')),\n env=os.environ\n )\n output = (r.stdout[:2000] + '\\n' + r.stderr[:500]).strip()\n status = 'pass' if r.returncode == 0 else 'fail'\n if r.returncode != 0:\n error_detail = (r.stderr[:300] or r.stdout[:300]).strip()\n else:\n error_detail = None\n except Exception as e:\n output = f\"SDK执行异常: {str(e)[:300]}\"\n status = 'fail'\n error_detail = output\n finally:\n try:\n os.unlink(sdk_tmp.name)\n except OSError:\n pass\n else:\n # Fallback: try dynamic SDK call for known methods\n try:\n import importlib\n # Parse method_name from tc metadata or command text\n if not method_name:\n m = re.match(r'(\\w+)\\s*(?:\\((.*?)\\))?', cmd_text.strip())\n method_name = m.group(1) if m else ''\n\n # Canonical SDK method map is in scripts/lib/config.sh (SERVICES_SDK).\n # This inline dict is a fallback for dynamic SDK execution.\n # Keep in sync with config.sh when adding new services.\n svc_map = {\n 'list_sub_customer_coupons': ('bss', 'v2', 'ListSubCustomerCouponsRequest'),\n 'list_customer_coupon_change_records': ('bss', 'v2', 'ListCustomerCouponChangeRecordsRequest'),\n 'list_stored_value_cards': ('bss', 'v2', 'ListStoredValueCardsRequest'),\n 'list_order_coupons_by_order_id': ('bss', 'v2', 'ListOrderCouponsByOrderIdRequest'),\n 'list_coupon_quotas': ('bss', 'v2', 'ListCouponQuotasRequest'),\n 'reclaim_partner_coupons': ('bss', 'v2', 'ReclaimPartnerCouponsRequest'),\n 'create_partner_coupons': ('bss', 'v2', 'CreatePartnerCouponsRequest'),\n 'list_servers_details': ('ecs', 'v2', 'ListServersDetailsRequest'),\n 'show_server': ('ecs', 'v2', 'ShowServerRequest'),\n 'create_servers': ('ecs', 'v2', 'CreateServersRequest'),\n 'delete_servers': ('ecs', 'v2', 'DeleteServersRequest'),\n 'list_flavors': ('ecs', 'v2', 'ListFlavorsRequest'),\n }\n if method_name in svc_map:\n svc, ver, req_cls = svc_map[method_name]\n mod = importlib.import_module(f'huaweicloudsdk{svc}.{ver}')\n client_cls_name = svc[0].upper() + svc[1:] + 'Client'\n client_class = getattr(mod, client_cls_name)\n from huaweicloudsdkcore.auth.credentials import BasicCredentials, GlobalCredentials\n ak = (os.environ.get('HUAWEI_ACCESS_KEY') or os.environ.get('HW_ACCESS_KEY')\n or os.environ.get('HUAWEI_AK') or os.environ.get('HW_AK') or '')\n sk = (os.environ.get('HUAWEI_SECRET_KEY') or os.environ.get('HW_SECRET_KEY')\n or os.environ.get('HUAWEI_SK') or os.environ.get('HW_SK') or '')\n if svc == 'bss':\n domain_id = os.environ.get('HUAWEI_DOMAIN_ID', '')\n cred = GlobalCredentials().with_ak(ak).with_sk(sk).with_domain_id(domain_id)\n _region = os.environ.get('HUAWEI_REGION', 'cn-north-4')\n client = client_class.new_builder() \\\n .with_credentials(cred) \\\n .with_region(_region) \\\n .build()\n else:\n cred = BasicCredentials(ak, sk)\n _region = os.environ.get('HUAWEI_REGION', 'cn-north-4')\n region_kw = {f'{svc}_region': _region}\n client = client_class.new_builder() \\\n .with_credentials(cred) \\\n .with_region(**region_kw) \\\n .build()\n req_class = getattr(mod, req_cls)\n request = req_class()\n method = getattr(client, method_name)\n resp = method(request)\n resp_dict = resp.to_dict() if hasattr(resp, 'to_dict') else str(resp)\n output = json.dumps(resp_dict, indent=2, ensure_ascii=False)[:2000]\n status = 'pass'\n else:\n output = f\"未知SDK方法: {method_name},请添加映射或提供完整Python代码\"\n status = 'fail'\n except ImportError as e:\n output = f\"SDK导入失败: {str(e)[:200]}\"\n # ModuleNotFoundError → 环境依赖缺失(非 skill 逻辑错误),\n # 明确标注, 提示在 SKILL.md 声明依赖(#53-002)\n if 'No module named' in str(e):\n _m = re.search(r\"No module named '([\\w\\.]+)'\", str(e))\n _dep = _m.group(1) if _m else '未知'\n output = f\"环境依赖缺失: 需安装 {_dep} (pip install {_dep}) — 建议在 SKILL.md 依赖说明中声明\"\n status = 'fail'\n except Exception as e:\n output = f\"SDK执行失败: {str(e)[:300]}\"\n status = 'fail'\n elif executor_type == 'script':\n cmd_text = tc.get('command', '')\n # Handle both \"python3 scripts/coupon.py list ...\" and \"bash scripts/test-cli-commands.sh\"\n if cmd_text.startswith('python3 ') and 'scripts/' in cmd_text:\n script_part = cmd_text.replace('python3 ', '', 1).strip()\n script_path = os.path.join(skill_root, script_part.split()[0])\n script_args = ' '.join(script_part.split()[1:]) if len(script_part.split()) > 1 else ''\n if os.path.isfile(script_path):\n # _posix: converts Windows drive paths (e.g. C:/Users/x) to\n # POSIX form; the python shim converts back before execution.\n # 参数列表直传(subprocess.argv), 不拼 shell 字符串, 避免注入。\n import shlex as _shlex\n try:\n _argv = ['python3', _posix(script_path)] + _shlex.split(script_args)\n except ValueError:\n _argv = ['python3', _posix(script_path)]\n r = subprocess.run(_argv, capture_output=True, text=True, timeout=int(os.environ.get('TIMEOUT_SDK', '60')), env=os.environ)\n output = (r.stdout[:1000] + '\\n' + r.stderr[:500]).strip()\n status = 'pass' if r.returncode == 0 else 'fail'\n else:\n output = f\"脚本未找到: {script_path}\"\n status = 'fail'\n else:\n script_part = cmd_text.replace('Run script: bash ', '').replace(' [args]', '').strip()\n script_path = os.path.join(skill_root, script_part)\n if not os.path.isfile(script_path):\n script_path = os.path.join(skill_root, 'scripts', os.path.basename(script_part))\n if os.path.isfile(script_path):\n r = subprocess.run(['bash', script_path, skill_root], capture_output=True, text=True, timeout=int(os.environ.get('TIMEOUT_SDK', '60')))\n output = (r.stdout[:1000] + '\\n' + r.stderr[:500]).strip()\n status = 'pass' if r.returncode == 0 else 'fail'\n else:\n output = f\"脚本未找到: {script_path}\"\n status = 'fail'\n else:\n # Unknown executor: try running command text as shell command\n cmd_text = tc.get('command', '')\n if cmd_text.strip() and len(cmd_text) > 10:\n r = subprocess.run(\n ['bash', '-c', cmd_text],\n capture_output=True, text=True, timeout=int(os.environ.get('TIMEOUT_CLI', '30')),\n env={**__import__('os').environ}\n )\n output = (r.stdout[:500] + r.stderr[:200]).strip()\n status = 'pass' if r.returncode == 0 else 'fail'\n else:\n output = f\"未知执行器 {executor_type},命令为空\"\n status = 'fail'\n # Also fix empty CLI command\n if executor_type == 'cli' and '命令为空' in output:\n status = 'fail'\n\n elapsed = round(time.time() - start_t, 2)\n entry['duration_s'] = elapsed\n entry['output_snippet'] = output[:300]\n\n if status == 'pass':\n entry['status'] = 'pass'\n pass_count += 1\n print(f\" ✅ PASS ({elapsed}s)\")\n else:\n # Classify SDK/CLI errors: param validation → warn, auth → fail, other → fail\n err_text = output[:500].lower()\n _param_err_kw = json.loads(os.environ.get('PARAM_ERROR_KEYWORDS', '[]'))\n _auth_err_kw = json.loads(os.environ.get('AUTH_ERROR_KEYWORDS', '[]'))\n is_param_error = any(kw in err_text for kw in _param_err_kw)\n is_auth_error = any(kw in err_text for kw in _auth_err_kw)\n if is_param_error and not is_auth_error:\n entry['status'] = 'warn'\n # Extract missing param hints from error\n missing_params = []\n for m in re.finditer(r'(\\w+)\\s+cannot be none|(\\w+)\\s+cannot be empty|missing\\s+(\\w+)|field required.*?(\\w+)', err_text):\n missing_params.append(m.group(1) or m.group(2) or m.group(3) or m.group(4))\n # Also extract from request.PARAM = '' pattern in command\n cmd_text = tc.get('command', '')\n for m in re.finditer(r'request\\.(\\w+)\\s*=\\s*[\\'\\\"]{2}', cmd_text):\n missing_params.append(m.group(1))\n missing_params = list(dict.fromkeys(missing_params)) # dedupe\n entry['error'] = f'[参数校验] {output[:200]}'\n entry['missing_params'] = missing_params\n entry['manual_test_hint'] = f\"需手工测试: API 需要业务数据参数 {missing_params},请提供真实值后重试\"\n skip_count += 1\n print(f\" ⚠️ WARN-参数校验 ({elapsed}s): {output[:80]}\")\n if missing_params:\n print(f\" 📋 需手工测试: 缺少业务参数 {missing_params}\")\n else:\n entry['status'] = 'fail'\n entry['error'] = output[:300]\n fail_count += 1\n print(f\" ❌ FAIL ({elapsed}s): {output[:100]}\")\n\n if is_write and status == 'pass':\n resource_change = {\n 'resource_type': tc.get('endpoint', 'unknown').split('/')[-1] if tc.get('endpoint') else f'resource_{tc_id}',\n 'resource_id': f'demo-{tc_id.lower()}-{int(time.time())}',\n 'change_type': 'created' if 'create' in tc.get('name', '').lower() else ('deleted' if 'delete' in tc.get('name', '').lower() else 'modified'),\n 'cleanup_method': {\n 'type': executor_type,\n 'command': tc.get('command', '')[:100]\n },\n 'cleanup_required': True\n }\n entry['resource_changes'] = [resource_change]\n all_resources.append(resource_change)\n\n except subprocess.TimeoutExpired:\n entry['status'] = 'error'\n entry['error'] = '执行超时'\n entry['duration_s'] = 30\n error_count += 1\n print(f\" ⏰ 超时\")\n except Exception as e:\n entry['status'] = 'error'\n entry['error'] = str(e)[:200]\n error_count += 1\n print(f\" ❌ 异常: {str(e)[:100]}\")\n\n exec_results.append(entry)\n\ntotal = len(cases)\npass_rate = round(pass_count / total * 100, 1) if total > 0 else 0\n\n# Collect manual test hints\nmanual_test_items = []\nfor r in exec_results:\n if r.get('status') == 'warn' and r.get('manual_test_hint'):\n manual_test_items.append({\n 'tc_id': r['tc_id'],\n 'name': r.get('name', ''),\n 'missing_params': r.get('missing_params', []),\n 'hint': r['manual_test_hint'],\n 'command': r.get('command', '')[:200],\n })\n\nif manual_test_items:\n print(f\"\\n{'='*60}\")\n print(f\"📋 以下 {len(manual_test_items)} 个用例因缺少业务数据需手工测试:\")\n print(f\"{'='*60}\")\n for i, item in enumerate(manual_test_items, 1):\n params_str = ', '.join(item['missing_params']) if item['missing_params'] else '未知参数'\n print(f\" {i}. {item['tc_id']}: {item['name']}\")\n print(f\" 缺少参数: {params_str}\")\n print(f\" 命令: {item['command'][:120]}\")\n print(f\"{'='*60}\")\n print(f\"💡 提示: 请提供真实业务数据后手工执行上述命令,或在 templates/test-defaults.json 中配置 request_defaults\")\n\n# 文档缺口分析(#007): warn/skip 用例中, SKILL.md 声明了但命令实际不支持的参数\ndoc_gap_issues = []\ntry:\n with open(os.path.join(skill_root, 'SKILL.md'), encoding='utf-8') as _md_f:\n _md_text = _md_f.read()\nexcept Exception:\n _md_text = ''\nfor r in exec_results:\n if r.get('status') not in ('warn', 'skip'):\n continue\n err = str(r.get('error') or r.get('output_snippet') or '')\n m = re.search(r'Invalid parameter:?\\s*--?([\\w-]+)', err) or \\\n re.search(r'Unsupported (?:parameter|option)[^:]*:?\\s*--?([\\w-]+)', err)\n if m:\n flag = '--' + m.group(1).split('=')[0]\n if flag in _md_text and flag not in [g['param'] for g in doc_gap_issues]:\n doc_gap_issues.append({\n 'tc_id': r['tc_id'],\n 'param': flag,\n 'detail': f'{flag} 出现在 SKILL.md 中, 但命令执行不被支持 (用例命令: {r.get(\"command\", \"\")[:80]})'\n })\n print(f\" 📋 文档缺口: SKILL.md 声明 {flag} 但实际命令不支持 → 建议修正 SKILL.md\")\n\nresult = {\n 'execution_results': exec_results,\n 'statistics': {\n 'total': total,\n 'pass': pass_count,\n 'fail': fail_count,\n 'skip': skip_count,\n 'error': error_count,\n 'pass_rate': pass_rate\n },\n 'all_resources_changed': all_resources,\n 'manual_test_items': manual_test_items,\n 'doc_gap_issues': doc_gap_issues\n}\n\nprint(\"\\n\\n---JSON_START---\")\nprint(json.dumps(result, indent=2, ensure_ascii=False))\nprint(\"---JSON_END---\")\nPYEXEC\n\n local results\n results=$(python3 \"$py_tmp\" \"$tc_tmp\" \"$skill_dir\")\n rm -f \"$py_tmp\"\n rm -f \"$tc_tmp\"\n\n # Extract the JSON from stdout between markers\n local json_output\n json_output=$(echo \"$results\" | sed -n '/---JSON_START---/,/---JSON_END---/p' | grep -v 'JSON_START\\|JSON_END')\n\n local end_ts; end_ts=$(date +%s)\n local duration=$((end_ts - start_ts))\n\n # Parse statistics\n local pass_count; pass_count=$(echo \"$json_output\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['pass'])\")\n local fail_count; fail_count=$(echo \"$json_output\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['fail'])\")\n local skip_count; skip_count=$(echo \"$json_output\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['skip'])\")\n local total_count; total_count=$(echo \"$json_output\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['statistics']['total'])\")\n\n local verdict=\"pass\"\n [ \"$fail_count\" -gt 0 ] && verdict=\"partial\"\n [ \"$fail_count\" -eq \"$total_count\" ] && verdict=\"fail\"\n\n local tmp_json; tmp_json=$(mktemp)\n echo \"$json_output\" > \"$tmp_json\"\n local summary_py_tmp; summary_py_tmp=$(mktemp)\n cat > \"$summary_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\nwith open(sys.argv[1]) as f:\n exec_data = json.load(f)\nr = {\n \"phase\": int(sys.argv[2]),\n \"phase_name\": sys.argv[3],\n \"tier\": 1,\n \"target\": {\"type\": \"single_skill\", \"skills\": [sys.argv[4]]},\n \"timestamp\": sys.argv[5],\n \"execution_meta\": {\"duration_s\": int(sys.argv[6]), \"retry_count\": 0, \"user_confirmed\": True},\n \"result\": exec_data,\n \"summary\": {\"verdict\": sys.argv[7], \"pass_checks\": int(sys.argv[8]), \"fail_checks\": int(sys.argv[9]), \"warn_checks\": int(sys.argv[10])}\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nPYEOF\n ensure_test_files_dir \"$skill_dir\" > /dev/null\n python3 \"$summary_py_tmp\" \"$tmp_json\" \"$PHASE_NUM\" \"$PHASE_NAME\" \"$skill_name\" \"$ts\" \"$duration\" \"$verdict\" \"$pass_count\" \"$fail_count\" \"$skip_count\" > \"$(phase_file \"$skill_dir\" 4)\"\n rm -f \"$summary_py_tmp\"\n rm -f \"$tmp_json\"\n\n echo \"\"\n info \"执行统计: ${pass_count}P / ${fail_count}F / ${skip_count}S / 共${total_count}条\"\n\n # Print manual test hints from warn cases\n local manual_count; manual_count=$(echo \"$json_output\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(len(d.get('manual_test_items',[])))\" 2>/dev/null || echo 0)\n if [ \"$manual_count\" -gt 0 ]; then\n echo \"\"\n echo \"============================================================\"\n echo \"📋 以下 ${manual_count} 个用例因缺少业务数据需手工测试:\"\n echo \"============================================================\"\n local manual_tmp; manual_tmp=$(mktemp \"${TMPDIR:-/tmp}/manual_test_XXXXXX.json\")\n echo \"$json_output\" > \"$manual_tmp\"\n local manual_display_py_tmp; manual_display_py_tmp=$(mktemp)\n cat > \"$manual_display_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, sys\nwith open(sys.argv[1]) as f:\n d = json.load(f)\nfor i, item in enumerate(d.get('manual_test_items', []), 1):\n params = ', '.join(item.get('missing_params', [])) or '未知参数'\n tc = item.get('tc_id', '')\n name = item.get('name', '')\n cmd = item.get('command', '')[:120]\n print(f' {i}. {tc}: {name}')\n print(f' 缺少参数: {params}')\n print(f' 命令: {cmd}')\nPYEOF\n python3 \"$manual_display_py_tmp\" \"$manual_tmp\"\n rm -f \"$manual_display_py_tmp\"\n rm -f \"$manual_tmp\"\n echo \"============================================================\"\n echo \"💡 提示: 请提供真实业务数据后手工执行上述命令,或在 templates/test-defaults.json 中配置 request_defaults\"\n fi\n}\n\n# Parse args: getopts for -s, pre-filter --skill (getopts can't handle --long)\nSKILL_DIRS=()\n_rest=()\nwhile [ $# -gt 0 ]; do\n case \"$1\" in\n --skill) SKILL_DIRS+=(\"$2\"); shift 2 ;;\n --skill=*) SKILL_DIRS+=(\"${1#--skill=}\"); shift ;;\n --help|-h) echo \"用法: $(basename \"$0\") [-s \u003cdir>]... [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n *) _rest+=(\"$1\"); shift ;;\n esac\ndone\nset -- ${_rest[@]+\"${_rest[@]}\"}\nOPTIND=1\nwhile getopts \":s:h\" opt; do\n case $opt in\n s) SKILL_DIRS+=(\"$OPTARG\") ;;\n h) echo \"用法: $(basename \"$0\") [-s \u003cdir>]... [--skill \u003cdir>]... [\u003cdir>...]\"; exit 0 ;;\n \\?) ;;\n esac\ndone\nshift $((OPTIND-1))\nfor arg in \"$@\"; do SKILL_DIRS+=(\"$arg\"); done\n\nfor skill_dir in \"${SKILL_DIRS[@]}\"; do\n set +e\n run_phase4 \"$skill_dir\"\n rc=$?\n set -e\n if [ $rc -ne 0 ]; then\n exit $rc # propagate exit code (77 for cred request, 1 for other errors)\n fi\n echo \"\"\ndone\n\npass \"Phase 4: 执行全部完成\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":31128,"content_sha256":"7c9fe920b695fda269ac7faaa771a8a1de2d318e3282ea961a24a853011a6089"},{"filename":"scripts/tier2/phase-5-orchestration.sh","content":"#!/usr/bin/env bash\n\n\n# phase-5-orchestration.sh — 多Skill编排测试\n# 全量触发词冲突扫描 + 数据传递测试 + 并行加载验证\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\n\nPHASE_NUM=5\nPHASE_NAME=\"orchestration\"\n\n# Parse args: getopts for -s, pre-filter --skills (getopts can't handle --long)\nSKILLS_LIST=\"\"\nSKILL_PATHS=()\n_rest=()\nwhile [ $# -gt 0 ]; do\n case \"$1\" in\n --skills) SKILLS_LIST=\"$2\"; shift 2 ;;\n --skills=*) SKILLS_LIST=\"${1#--skills=}\"; shift ;;\n --help|-h) echo \"用法: $(basename \"$0\") [-s \u003clist>] [--skills \u003clist>] [\u003cdir>...]\"; exit 0 ;;\n *) _rest+=(\"$1\"); shift ;;\n esac\ndone\nset -- ${_rest[@]+\"${_rest[@]}\"}\nOPTIND=1\nwhile getopts \":s:h\" opt; do\n case $opt in\n s) SKILLS_LIST=\"$OPTARG\" ;;\n h) echo \"用法: $(basename \"$0\") [-s \u003clist>] [--skills \u003clist>] [\u003cdir>...]\"; exit 0 ;;\n \\?) ;;\n esac\ndone\nshift $((OPTIND-1))\nfor arg in \"$@\"; do\n if [[ \"$arg\" =~ ^/ || \"$arg\" =~ ^\\. || \"$arg\" =~ ^[A-Za-z]:[/\\\\] ]]; then\n SKILL_PATHS+=(\"$arg\")\n fi\ndone\n\nSKILL_COUNT=${#SKILL_PATHS[@]}\n\nheader \"Phase ${PHASE_NUM}: 多Skill编排测试\"\n\nts=$(timestamp)\nstart_ts=$(date +%s)\n\n# === Auto-discover sibling skills (default ON) ===\n# 用户要求: 默认从被测试 skill 的同级目录找其他 huawei-cloud-* skill 做编排。\n# 默认开启; opt-out: WITHOUT_SIBLINGS=1 或 SIBLING_LIMIT=0\nif [ \"$SKILL_COUNT\" -le 1 ]; then\n target_dir=\"${SKILL_PATHS[0]}\"\n parent_dir=$(dirname \"$target_dir\")\n target_name=$(basename \"$target_dir\")\n\n info \"扫描同级兄弟 skill (parent: $parent_dir)\"\n while IFS= read -r sibling; do\n [ -z \"$sibling\" ] && continue\n SKILL_PATHS+=(\"$sibling\")\n sname=$(basename \"$sibling\")\n info \" 发现兄弟 skill: $sname\"\n done \u003c \u003c(discover_siblings \"$target_dir\" \"${SKILL_PATHS[@]}\")\n SKILL_COUNT=${#SKILL_PATHS[@]}\n\n if [ \"$SKILL_COUNT\" -gt 1 ]; then\n info \"编排模式: ${SKILL_COUNT} skill 组合 (默认扫兄弟)\"\n fi\nfi\n\n# === Branch: downgrade for single skill ===\nif [ \"$SKILL_COUNT\" -le 1 ]; then\n info \"仅 ${SKILL_COUNT} 个 skill(无其他可组合 skill),降级为自检模式\"\n\n local_skill_dir=\"${SKILL_PATHS[0]}\"\n check_phase_deps \"$local_skill_dir\" 5 ${SKILLS_LIST} || exit 1\n\n # Read Phase 1 for the single skill's triggers\n p1_file=$(phase_file \"$local_skill_dir\" 1)\n \n # Read Phase 1 for the single skill's triggers\n p1_file=$(phase_file \"$local_skill_dir\" 1)\n \n # Write Python self-check script to temp file\n p5s_tmp=$(mktemp)\n cat > \"$p5s_tmp\" \u003c\u003c 'PYSELF'\nimport json, sys\n\np1f = sys.argv[1]\nskc = sys.argv[2]\n\nwith open(p1f) as f:\n p1 = json.load(f)\n\ntriggers = p1.get('result', {}).get('metadata', {}).get('triggers', [])\ncommands = p1.get('result', {}).get('commands', [])\n\nambiguities = []\nseen = set()\nfor t in triggers:\n t_lower = t.lower().strip()\n for t2 in triggers:\n t2_lower = t2.lower().strip()\n if t != t2 and t_lower in t2_lower:\n ambiguities.append({\n 'description': f\"触发词 '{t}' 是 '{t2}' 的子串,语义高度重叠\",\n 'risk': 'low'\n })\n\ncycle_warnings = []\nfor i, c1 in enumerate(commands):\n for j, c2 in enumerate(commands):\n if i != j and c1.get('is_write') and c2.get('is_write'):\n cycle_warnings.append(f\"写操作命令 {c1['id']} 和 {c2['id']} 可能需排序执行\")\n\nresult = {\n 'mode': 'downgraded_self_check',\n 'conflict_scan': {\n 'internal_ambiguities': ambiguities,\n 'cycle_warnings': cycle_warnings\n },\n 'data_flow_tests': [],\n 'parallel_load_test': {\n 'verdict': 'skipped',\n 'reason': f'仅{skc}个skill,无需并行测试'\n },\n 'cleanup': {'resources_cleaned': 0, 'resources_failed': 0}\n}\nprint(json.dumps(result, indent=2, ensure_ascii=False))\nPYSELF\n\n self_check=$(python3 \"$p5s_tmp\" \"$p1_file\" \"$SKILL_COUNT\")\n rm -f \"$p5s_tmp\"\n\n # Safety net: if Python crashed, generate fallback so phase-5-summary.json is valid (T-ISSUE-001)\n if ! printf '%s' \"$self_check\" | python3 -c \"import json,sys; json.load(sys.stdin)\" 2>/dev/null; then\n warn \"⚠️ Phase 5 自检 Python 脚本输出为空或非 JSON,生成降级结果\"\n self_check='{\"mode\":\"downgraded_self_check\",\"conflict_scan\":{\"internal_ambiguities\":[],\"cycle_warnings\":[]},\"data_flow_tests\":[],\"parallel_load_test\":{\"verdict\":\"skipped\",\"reason\":\"Python 脚本异常\"},\"cleanup\":{\"resources_cleaned\":0,\"resources_failed\":0}}'\n fi\n\n verdict=\"pass\"\n has_ambiguities=$(echo \"$self_check\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(len(d.get('conflict_scan',{}).get('internal_ambiguities',[]))\" 2>/dev/null || echo \"0\")\n has_cycles=$(echo \"$self_check\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(len(d.get('conflict_scan',{}).get('cycle_warnings',[])))\" 2>/dev/null || echo \"0\")\n [ \"$has_ambiguities\" -gt 0 ] || [ \"$has_cycles\" -gt 0 ] && verdict=\"partial\"\n\n output_file=\"$(phases_dir \"$local_skill_dir\")/phase-5-summary.json\"\n ensure_test_files_dir \"$local_skill_dir\" > /dev/null\nelse\n # === Full orchestration mode ===\n # Strict chain check on the target skill only (the one whose phase-4 ran).\n # Sibling skills (discovered via discover_siblings) often lack phase-1/4\n # JSONs because they haven't been individually tested. We only soft-warn\n # for siblings and let the orchestration logic fall back to SKILL.md parsing.\n check_phase_deps \"${SKILL_PATHS[0]}\" 5 || exit 1\n for ((i=1; i\u003cSKILL_COUNT; i++)); do\n sp=\"${SKILL_PATHS[$i]}\"\n for p in 1 4; do\n pf=\"$(phase_file \"$sp\" $p)\"\n if [ ! -f \"$pf\" ]; then\n warn \"兄弟 skill $(basename \"$sp\") 缺 phase $p: $pf (将 fallback 到 SKILL.md 实时解析)\"\n fi\n done\n done\n\n info \"全量编排模式: ${SKILL_COUNT} 个 skill\"\n\n # Read phase-1 data for all skills via Python\n # Write Python script to temp file to avoid quoting issues\n py_tmp_p5=$(mktemp)\n cat > \"$py_tmp_p5\" \u003c\u003c 'PYORCH5'\nimport json, os, re, sys\n\nsp_list = sys.argv[1:]\nskill_data = []\nparse_warnings = []\n\n# Phase JSONs live in \u003cskill>-test-files/phases/, not in the skill dir\ndef _phases_dir(skill_dir):\n parent = os.path.dirname(skill_dir)\n name = os.path.basename(skill_dir)\n return os.path.join(parent, f\"{name}-test-files\", \"phases\")\n\n# Fallback: parse SKILL.md frontmatter + body to extract triggers / resource types\n# Used when phase-1-summary.json doesn't exist (e.g. sibling skills not yet tested).\n# Supports three common trigger declaration patterns found in this repo:\n# 1. `triggers: [a, b, c]` — inline YAML list\n# 2. block YAML list: triggers on its own line, then a / b on following lines\n# 3. `description: |\\n ... Triggers include: \"x\",\"y\",\"z\"` — embedded in description\ndef parse_skill_md(skill_dir):\n smd = os.path.join(skill_dir, 'SKILL.md')\n if not os.path.isfile(smd):\n return None\n try:\n with open(smd, encoding='utf-8') as f:\n content = f.read()\n except Exception:\n return None\n\n # Parse YAML frontmatter block\n m = re.match(r'^---\\s*\\n(.*?)\\n---', content, re.DOTALL)\n fm = m.group(1) if m else ''\n\n triggers = []\n\n # Pattern 1+2: inline list [..] OR block list (triggers then items on next lines)\n m1 = re.search(r'^triggers\\s*:\\s*\\[([^\\]]*)\\]', fm, re.MULTILINE)\n if m1:\n triggers += [x.strip().strip('\"').strip(\"'\") for x in m1.group(1).split(',') if x.strip()]\n else:\n m2 = re.search(r'^triggers\\s*:\\s*

Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on real-environment functional testing and multi-skill orchestration scenarios . --- Overview This Skill provides a three-track, eight-phase standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report): | Tier | Phases | Goal | |------|--------|------| | Tier 1: Single-Skill Unit Testing | Phase 0 4 | Verify each skill item by i…

, fm, re.MULTILINE)\n if m2:\n # Block list — collect \"- xxx\" lines after it\n after = fm[m2.end():]\n for line in after.split('\\n'):\n stripped = line.strip()\n if stripped.startswith('- '):\n triggers.append(stripped[2:].strip().strip('\"').strip(\"'\"))\n elif stripped and not stripped.startswith('#') and ':' in stripped:\n break\n\n # Pattern 3: \"Triggers include: ...\" in description block\n if not triggers:\n m3 = re.search(r'Triggers\\s+include\\s*[::]\\s*([^\\n]+)', fm, re.IGNORECASE)\n if m3:\n tail = m3.group(1)\n # split on commas, quotes, or Chinese commas\n parts = re.split(r'[\"\\',,;;]+', tail)\n triggers += [p.strip() for p in parts if p.strip() and len(p.strip()) > 1]\n\n # Heuristic resource types from full content (tags + body)\n body = content.lower()\n rtypes = []\n for kw in ['rds', 'ecs', 'evs', 'vpc', 'eip', 'obs', 'cdn', 'iam', 'bss', 'cce', 'dms', 'dcs',\n 'functiongraph', 'modelarts', 'ocr', 'sms', 'swr', 'cbr', 'waf', 'hss', 'elb',\n 'nat', 'vpn', 'dws', 'mrs', 'css', 'kms', 'smn', 'apig']:\n if kw in body and kw not in rtypes:\n rtypes.append(kw)\n\n return {\n 'triggers': triggers,\n 'resource_types': rtypes,\n 'commands': [], # commands are too brittle to parse from SKILL.md; only phase-1 has them\n }\n\nfor sp in sp_list:\n sn = os.path.basename(sp)\n p1_file = os.path.join(_phases_dir(sp), 'phase-1-summary.json')\n if os.path.isfile(p1_file):\n try:\n with open(p1_file, encoding='utf-8') as f:\n p1 = json.load(f)\n triggers = p1.get('result', {}).get('metadata', {}).get('triggers', [])\n resource_types = p1.get('result', {}).get('resource_types', [])\n commands = p1.get('result', {}).get('commands', [])\n skill_data.append({\n 'name': sn,\n 'triggers': triggers,\n 'resource_types': resource_types,\n 'commands': commands,\n 'source': 'phase-1',\n })\n except Exception as e:\n parse_warnings.append(f\"{sn}: phase-1 JSON 解析失败 ({e})\")\n else:\n # Sibling skill without phase-1 JSON: parse SKILL.md on the fly\n parsed = parse_skill_md(sp)\n if parsed and (parsed['triggers'] or parsed['resource_types']):\n skill_data.append({\n 'name': sn,\n 'triggers': parsed['triggers'],\n 'resource_types': parsed['resource_types'],\n 'commands': parsed['commands'],\n 'source': 'skill-md-fallback',\n })\n else:\n parse_warnings.append(f\"{sn}: SKILL.md 解析失败 (无 triggers/resource_types) — 已跳过\")\n\n# === Conflict Scan ===\nconflicts = []\nno_conflict_pairs = 0\npairs_checked = 0\n\nfor i in range(len(skill_data)):\n for j in range(i+1, len(skill_data)):\n sa = skill_data[i]\n sb = skill_data[j]\n for ta in sa['triggers']:\n ta_clean = ta.strip().lower()\n for tb in sb['triggers']:\n tb_clean = tb.strip().lower()\n pairs_checked += 1\n if ta_clean == tb_clean:\n conflicts.append({\n 'severity': 'high', 'skill_a': sa['name'], 'skill_b': sb['name'],\n 'trigger': ta_clean[:50],\n 'recommendation': '触发词完全重叠,可能导致Agent路由混乱。建议为其中一个skill修改触发词。'\n })\n elif ta_clean in tb_clean or tb_clean in ta_clean:\n conflicts.append({\n 'severity': 'medium', 'skill_a': sa['name'], 'skill_b': sb['name'],\n 'trigger': f\"'{ta_clean[:30]}' \u003c-> '{tb_clean[:30]}'\",\n 'recommendation': '包含关系触发词,可能误触'\n })\n else:\n no_conflict_pairs += 1\n\n# === Data Flow Tests ===\ndata_flow_tests = []\nfor i in range(len(skill_data)):\n for j in range(len(skill_data)):\n if i == j: continue\n sa = skill_data[i]; sb = skill_data[j]\n for rt in sa.get('resource_types', []):\n for cmd in sb.get('commands', []):\n desc = cmd.get('description', '').lower()\n if rt.lower() in desc:\n data_flow_tests.append({\n 'test_id': f'DF-{len(data_flow_tests)+1:02d}',\n 'from_skill': sb['name'], 'to_skill': sa['name'],\n 'data_item': rt, 'status': 'identified',\n 'detail': f\"技能 {sb['name']} 的输出 '{rt}' 可能作为 {sa['name']} 的输入\"\n })\n\nparallel_result = {\n 'skills_loaded': [sd['name'] for sd in skill_data],\n 'verdict': 'pass',\n 'detail': f\"所有 {len(skill_data)} 个 skill 的 SKILL.md 均可正常解析。\"\n}\n# 真实检查:尝试解析每个 SKILL.md 的 YAML frontmatter\nfor sd in skill_data:\n sp = next((p for p in sp_list if sd['name'] in p), None)\n if sp:\n smd = os.path.join(sp, 'SKILL.md')\n if os.path.isfile(smd):\n with open(smd) as f:\n content = f.read()\n yaml_match = __import__('re').match(r'^---\\s*\\n(.*?)\\n---', content, __import__('re').DOTALL)\n if not yaml_match:\n parallel_result['verdict'] = 'fail'\n parallel_result['detail'] = f\"{sd['name']}: YAML frontmatter 解析失败\"\n\nresult = {\n 'mode': 'full',\n 'conflict_scan': {'pairs_checked': pairs_checked, 'conflicts': conflicts, 'no_conflict_pairs': no_conflict_pairs},\n 'data_flow_tests': data_flow_tests,\n 'parallel_load_test': parallel_result,\n 'cleanup': {'resources_cleaned': 0, 'resources_failed': 0}\n}\nprint(json.dumps(result, indent=2, ensure_ascii=False))\nPYORCH5\n\n orchestration_result=$(python3 \"$py_tmp_p5\" \"${SKILL_PATHS[@]}\")\n rm -f \"$py_tmp_p5\"\n\n # Safety net: if Python crashed, generate fallback so phase-5-summary.json is valid (T-ISSUE-001)\n if ! printf '%s' \"$orchestration_result\" | python3 -c \"import json,sys; json.load(sys.stdin)\" 2>/dev/null; then\n warn \"⚠️ Phase 5 编排 Python 脚本输出为空或非 JSON,生成降级结果\"\n orchestration_result='{\"mode\":\"full\",\"conflict_scan\":{\"pairs_checked\":0,\"conflicts\":[],\"no_conflict_pairs\":0},\"data_flow_tests\":[],\"parallel_load_test\":{\"verdict\":\"fail\",\"detail\":\"Python 脚本异常\"},\"cleanup\":{\"resources_cleaned\":0,\"resources_failed\":0}}'\n fi\n\n verdict=\"pass\"\n high_conflicts=$(echo \"$orchestration_result\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(sum(1 for c in d.get('conflict_scan',{}).get('conflicts',[]) if c.get('severity')=='high'))\" 2>/dev/null || echo \"0\")\n\n [ \"$high_conflicts\" -gt 0 ] && verdict=\"fail\"\n\n output_file=\"$(phases_dir \"${SKILL_PATHS[0]}\")/phase-5-summary.json\"\n ensure_test_files_dir \"${SKILL_PATHS[0]}\" > /dev/null\nfi\n\nend_ts=$(date +%s)\nduration=$((end_ts - start_ts))\n\nif [ \"$SKILL_COUNT\" -le 1 ]; then\n # Write self_check to temp file\n p5_tmp=$(mktemp)\n echo \"$self_check\" > \"$p5_tmp\"\n p5_py_tmp=$(mktemp)\n cat > \"$p5_py_tmp\" \u003c\u003c 'P5PY'\nimport json, os, sys\ndata = json.load(open(sys.argv[1]))\nPHASE_NUM = int(sys.argv[2])\nPHASE_NAME = sys.argv[3]\n# SKILL_PATHS is a newline-separated list of skill dirs (one per line)\nSKILL_PATHS = sys.argv[4].split('\\n') if sys.argv[4] else []\nSKILL_NAMES = [os.path.basename(p) for p in SKILL_PATHS if p.strip()]\nTS = sys.argv[5]\nDURATION = int(sys.argv[6])\nVERDICT = sys.argv[7]\nr = {\n 'phase': PHASE_NUM,\n 'phase_name': PHASE_NAME,\n 'tier': 2,\n 'target': {'type': 'multi_skill', 'skills': SKILL_NAMES},\n 'timestamp': TS,\n 'execution_meta': {'duration_s': DURATION, 'retry_count': 0, 'user_confirmed': False},\n 'result': data,\n 'summary': {'verdict': VERDICT, 'pass_checks': 1, 'fail_checks': 0, 'warn_checks': 0}\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nP5PY\n # Pass SKILL_PATHS as newline-separated string\n _paths_arg=$(printf '%s\\n' \"${SKILL_PATHS[@]}\")\n write_json \"$output_file\" \"$(python3 \"$p5_py_tmp\" \"$p5_tmp\" \"$PHASE_NUM\" \"$PHASE_NAME\" \"$_paths_arg\" \"$ts\" \"$duration\" \"$verdict\")\"\n rm -f \"$p5_py_tmp\"\n rm -f \"$p5_tmp\"\n echo \"$self_check\" | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\namb = d.get('conflict_scan', {}).get('internal_ambiguities', [])\ncyc = d.get('conflict_scan', {}).get('cycle_warnings', [])\nif amb:\n for a in amb:\n print(f\\\" ⚠️ {a['description']} [{a['risk']}]\\\")\nelse:\n print(' ✅ 无内部触发词歧义')\nif cyc:\n for c in cyc:\n print(f\\\" ⚠️ {c}\\\")\n\"\nelse\n # Write orchestration_result to temp file\n p5b_tmp=$(mktemp)\n echo \"$orchestration_result\" > \"$p5b_tmp\"\n p5b_py_tmp=$(mktemp)\n cat > \"$p5b_py_tmp\" \u003c\u003c 'P5BPY'\nimport json, os, sys\ndata = json.load(open(sys.argv[1]))\nPHASE_NUM = int(sys.argv[2])\nPHASE_NAME = sys.argv[3]\n# SKILL_PATHS is a newline-separated list of skill dirs (one per line)\nSKILL_PATHS = sys.argv[4].split('\\n') if sys.argv[4] else []\nSKILL_NAMES = [os.path.basename(p) for p in SKILL_PATHS if p.strip()]\nTS = sys.argv[5]\nDURATION = int(sys.argv[6])\nVERDICT = sys.argv[7]\nHIGH_CONFLICTS = int(sys.argv[8])\nr = {\n 'phase': PHASE_NUM,\n 'phase_name': PHASE_NAME,\n 'tier': 2,\n 'target': {'type': 'multi_skill', 'skills': SKILL_NAMES},\n 'timestamp': TS,\n 'execution_meta': {'duration_s': DURATION, 'retry_count': 0, 'user_confirmed': False},\n 'result': data,\n 'summary': {'verdict': VERDICT, 'pass_checks': 1, 'fail_checks': HIGH_CONFLICTS, 'warn_checks': 0}\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nP5BPY\n # Pass SKILL_PATHS as newline-separated string\n _paths_arg=$(printf '%s\\n' \"${SKILL_PATHS[@]}\")\n write_json \"$output_file\" \"$(python3 \"$p5b_py_tmp\" \"$p5b_tmp\" \"$PHASE_NUM\" \"$PHASE_NAME\" \"$_paths_arg\" \"$ts\" \"$duration\" \"$verdict\" \"$high_conflicts\")\"\n rm -f \"$p5b_py_tmp\"\n\n echo \"\"\n echo \"$orchestration_result\" | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\ncs = d.get('conflict_scan', {})\nprint(f\\\" 扫描对数: {cs.get('pairs_checked', 0)}\\\")\nprint(f\\\" 冲突 ({len(cs.get('conflicts', []))}):\\\")\nfor c in cs.get('conflicts', []):\n sev = {'high':'🔴','medium':'🟡','low':'🟢'}.get(c['severity'], '⚪')\n print(f\\\" {sev} [{c['severity']}] {c['skill_a']} ↔ {c['skill_b']}: {c['trigger'][:50]}\\\")\nprint(f\\\" 数据流: {len(d.get('data_flow_tests', []))} 条待验证\\\")\nprint(f\\\" 并行加载: {d.get('parallel_load_test', {}).get('verdict', 'N/A')}\\\")\n\"\nfi\n\npass \"Phase ${PHASE_NUM}: 编排测试完成\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":18332,"content_sha256":"3140814faaa0017515a60b09f726b00983e267be6c40a22a44b65dadbbef476a"},{"filename":"scripts/tier2/phase-6-full-flow.sh","content":"#!/usr/bin/env bash\n\n\n# phase-6-full-flow.sh — 全流程走通测试\n# 自动推导场景链 → 用户确认 → 端到端执行 → 状态验证 → 清理\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\n\nPHASE_NUM=6\nPHASE_NAME=\"full-flow\"\n\n# Export placeholder-utils path for the embedded Python executor (same as phase-3/4)\nexport PLACEHOLDER_UTILS=\"${PLACEHOLDER_UTILS:-$SCRIPT_DIR/lib/placeholder-utils.py}\"\n\n# Parse args: getopts for -s, pre-filter --skills (getopts can't handle --long)\nSKILLS_LIST=\"\"\nSKILL_PATHS=()\n_rest=()\nwhile [ $# -gt 0 ]; do\n case \"$1\" in\n --skills) SKILLS_LIST=\"$2\"; shift 2 ;;\n --skills=*) SKILLS_LIST=\"${1#--skills=}\"; shift ;;\n --help|-h) echo \"用法: $(basename \"$0\") [-s \u003clist>] [--skills \u003clist>] [\u003cdir>...]\"; exit 0 ;;\n *) _rest+=(\"$1\"); shift ;;\n esac\ndone\nset -- ${_rest[@]+\"${_rest[@]}\"}\nOPTIND=1\nwhile getopts \":s:h\" opt; do\n case $opt in\n s) SKILLS_LIST=\"$OPTARG\" ;;\n h) echo \"用法: $(basename \"$0\") [-s \u003clist>] [--skills \u003clist>] [\u003cdir>...]\"; exit 0 ;;\n \\?) ;;\n esac\ndone\nshift $((OPTIND-1))\nfor arg in \"$@\"; do\n if [[ \"$arg\" =~ ^/ || \"$arg\" =~ ^\\. || \"$arg\" =~ ^[A-Za-z]:[/\\\\] ]]; then\n SKILL_PATHS+=(\"$arg\")\n fi\ndone\n\nSKILL_COUNT=${#SKILL_PATHS[@]}\n\nheader \"Phase ${PHASE_NUM}: 全流程走通测试\"\n\nts=$(timestamp)\nstart_ts=$(date +%s)\n\n# === Auto-discover sibling skills (default ON) ===\n# 用户要求: 默认从被测试 skill 的同级目录找其他 huawei-cloud-* skill 做编排。\n# 默认开启; opt-out: WITHOUT_SIBLINGS=1 或 SIBLING_LIMIT=0\nif [ \"$SKILL_COUNT\" -le 1 ]; then\n target_dir=\"${SKILL_PATHS[0]}\"\n parent_dir=$(dirname \"$target_dir\")\n target_name=$(basename \"$target_dir\")\n\n info \"扫描同级兄弟 skill (parent: $parent_dir)\"\n while IFS= read -r sibling; do\n [ -z \"$sibling\" ] && continue\n SKILL_PATHS+=(\"$sibling\")\n sname=$(basename \"$sibling\")\n info \" 发现兄弟 skill: $sname\"\n done \u003c \u003c(discover_siblings \"$target_dir\" \"${SKILL_PATHS[@]}\")\n SKILL_COUNT=${#SKILL_PATHS[@]}\n\n if [ \"$SKILL_COUNT\" -gt 1 ]; then\n info \"E2E 模式: ${SKILL_COUNT} skill 组合 (默认扫兄弟)\"\n fi\nfi\n\nif [ \"$SKILL_COUNT\" -le 1 ]; then\n # === Single-skill full flow ===\n local_skill_dir=\"${SKILL_PATHS[0]}\"\n local_skill_name=$(basename \"$local_skill_dir\")\n \n info \"降级为单技能完整功能闭环(无其他可组合 skill): $local_skill_name\"\n\n check_phase_deps \"$local_skill_dir\" 6 || exit 1\n\n # Force AK/SK check before any SDK/CLI execution\n step \"检查 AK/SK 凭证...\"\n set +e\n ensure_ak_sk\n cred_rc=$?\n set -e\n if [ $cred_rc -ne 0 ]; then\n if [ $cred_rc -eq 77 ]; then\n fail \"AK/SK 凭证缺失(exit 77 — 详见 stderr 中的 env-var 设置模板)\"\n fail \" sentinel: $CRED_REQUEST_SENTINEL\"\n fail \" 调用方应将该模板原样输出给用户,让用户带外设置环境变量后重跑\"\n fail \" --phase 6 或 --resume,禁止直接索要 AK/SK 明文\"\n exit 77\n fi\n fail \"AK/SK 凭证检查失败(exit=$cred_rc),无法执行全流程测试\"\n exit 1\n fi\n\n p1_file=$(phase_file \"$local_skill_dir\" 1)\n \n p6_py_tmp=$(mktemp)\n cat > \"$p6_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, subprocess, os, sys\n\n_PU = os.environ.get('PLACEHOLDER_UTILS', '')\nif _PU and os.path.isfile(_PU):\n exec(open(_PU, encoding='utf-8').read())\n\np1_file = sys.argv[1]\nlocal_skill_name = sys.argv[2]\nlocal_skill_dir = sys.argv[3]\n\nwith open(p1_file) as f:\n p1 = json.load(f)\n\ncaps = p1.get('result', {}).get('capabilities', {})\ncommands = p1.get('result', {}).get('commands', [])\nhas_write = p1.get('result', {}).get('has_write_operations', False)\n\n# Build a full flow from capabilities\nsteps = []\nseq = 0\n\n# Order: query first, then write ops in logical order\n# For most skills: list → create (if exists) → query → update (if exists) → delete → verify\naction_order = ['list', 'create', 'update', 'delete']\n\nfor action in action_order:\n items = caps.get(action, [])\n for item in items:\n seq += 1\n steps.append({\n 'seq': seq,\n 'tc_id': f'FF-{seq:02d}',\n 'skill': local_skill_name,\n 'action': f\"{item}\",\n 'status': 'pending',\n 'resource_changes': []\n })\n\n# If no capabilities found, derive from commands\nif not steps:\n for cmd in commands:\n seq += 1\n steps.append({\n 'seq': seq,\n 'tc_id': f'FF-{seq:02d}',\n 'skill': local_skill_name,\n 'action': cmd.get('description', '')[:60],\n 'status': 'pending',\n 'resource_changes': []\n })\n\n# Execute each step with real commands\nfor step in steps:\n step['status'] = 'pending'\n step['output'] = ''\n step['error'] = None\n\n # Find matching command from phase-1\n step_action = step.get('action', '')\n matched_cmd = None\n for cmd in commands:\n if step_action in cmd.get('description', '') or cmd.get('description', '') in step_action:\n matched_cmd = cmd\n break\n\n if matched_cmd:\n executor = matched_cmd.get('executor', 'unknown')\n cmd_text = matched_cmd.get('command', '')\n method_name = matched_cmd.get('method_name', '')\n is_write = matched_cmd.get('is_write', False)\n allow_writes = os.environ.get('ALLOW_WRITES', '0') == '1'\n\n if is_write and not allow_writes:\n step['status'] = 'skip'\n step['output'] = '写操作已跳过 (ALLOW_WRITES=0)'\n continue\n\n if executor == 'sdk' and cmd_text and ('import' in cmd_text or 'client.' in cmd_text or 'from ' in cmd_text):\n # Write full Python snippet to temp file and execute\n import tempfile\n sdk_tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False)\n try:\n sdk_tmp.write(cmd_text)\n sdk_tmp.close()\n r = subprocess.run(\n ['python3', sdk_tmp.name],\n capture_output=True, text=True, timeout=60,\n env=os.environ\n )\n step['output'] = (r.stdout[:500] + r.stderr[:200]).strip()\n step['status'] = 'pass' if r.returncode == 0 else 'fail'\n except Exception as e:\n step['output'] = str(e)[:200]\n step['status'] = 'fail'\n finally:\n try:\n os.unlink(sdk_tmp.name)\n except OSError:\n pass\n elif executor == 'cli' and cmd_text:\n r = subprocess.run(\n ['bash', '-c', cmd_text],\n capture_output=True, text=True, timeout=30,\n env=os.environ\n )\n step['output'] = (r.stdout[:500] + r.stderr[:200]).strip()\n step['status'] = 'pass' if r.returncode == 0 else 'fail'\n elif executor == 'script' and cmd_text:\n cmd_text = replace_placeholders(cmd_text, local_skill_dir)\n skill_root = local_skill_dir\n if cmd_text.startswith('python3 ') and 'scripts/' in cmd_text:\n script_part = cmd_text.replace('python3 ', '', 1).strip()\n script_path = os.path.join(skill_root, script_part.split()[0])\n script_args = ' '.join(script_part.split()[1:]) if len(script_part.split()) > 1 else ''\n if os.path.isfile(script_path):\n import shlex as _shlex\n try:\n _argv = ['python3', _posix(script_path)] + _shlex.split(script_args)\n except ValueError:\n _argv = ['python3', _posix(script_path)]\n r = subprocess.run(_argv, capture_output=True, text=True, timeout=60, env=os.environ)\n step['output'] = (r.stdout[:500] + r.stderr[:200]).strip()\n step['status'] = 'pass' if r.returncode == 0 else 'fail'\n else:\n step['output'] = f\"脚本未找到: {script_path}\"\n step['status'] = 'fail'\n else:\n step['status'] = 'skip'\n step['output'] = '无可执行命令'\n else:\n step['status'] = 'skip'\n step['output'] = f'无可执行命令 (executor={executor})'\n else:\n step['status'] = 'skip'\n step['output'] = '未找到匹配命令'\n\n# Compute step statistics for verdict (ISSUE-004: all steps failing should\n# not result in pass verdict)\n_step_total = len(steps)\n_step_pass = sum(1 for s in steps if s.get('status') == 'pass')\n_step_fail = sum(1 for s in steps if s.get('status') == 'fail')\n_step_skip = sum(1 for s in steps if s.get('status') == 'skip')\n# state_consistency is True only if no steps failed (skips are acceptable)\n_state_ok = _step_fail == 0 and _step_total > 0\n\nresult = {\n 'mode': 'downgraded_single_skill_flow',\n 'scenario': {\n 'name': f\"单技能完整功能闭环 — {caps.get('metadata', {}).get('name', local_skill_name)}\",\n 'skills_involved': [local_skill_name],\n 'description': f\"串联 skill '{local_skill_name}' 的所有功能点\",\n 'derived_automatically': True,\n 'user_confirmed': True,\n 'steps': steps\n },\n 'state_consistency': {\n 'pass': _state_ok,\n 'detail': f'{_step_pass}/{_step_total} 步通过, {_step_fail} 步失败, {_step_skip} 步跳过',\n 'final_state_summary': '功能点全部走通' if _state_ok else f'{_step_fail} 步失败, 需排查'\n },\n 'cleanup': {\n 'verdict': 'pass',\n 'resources_cleaned': 0,\n 'resources_failed': 0,\n 'manual_required': []\n },\n 'step_stats': {\n 'total': _step_total,\n 'pass': _step_pass,\n 'fail': _step_fail,\n 'skip': _step_skip\n }\n}\nprint(json.dumps(result, indent=2, ensure_ascii=False))\nPYEOF\n\n flow_result=$(python3 \"$p6_py_tmp\" \"$p1_file\" \"$local_skill_name\" \"$local_skill_dir\")\n rm -f \"$p6_py_tmp\"\n\n output_file=\"$(phases_dir \"$local_skill_dir\")/phase-6-summary.json\"\n ensure_test_files_dir \"$local_skill_dir\" > /dev/null\n\nelse\n # === Multi-skill full flow ===\n info \"多skill全流程走通: ${SKILL_COUNT} 个 skill\"\n check_phase_deps \"${SKILL_PATHS[0]}\" 6 || exit 1\n\n # Multi-skill 模式当前只做\"派生计划\"(按 phase-1 派生 create→query→delete\n # 步骤,步骤 status=pass 是派生标记,不真跑 API)。所以 AK/SK 不是必填。\n # 兄弟 skill 大多没跑过 phase 4(无 phase 4 JSON),所以不调真 API。\n if [ \"${ALLOW_REAL_E2E:-0}\" = \"1\" ]; then\n step \"检查 AK/SK 凭证(ALLOW_REAL_E2E=1 模式: 真跑 E2E 步骤)...\"\n set +e\n ensure_ak_sk\n cred_rc=$?\n set -e\n if [ $cred_rc -ne 0 ]; then\n if [ $cred_rc -eq 77 ]; then\n fail \"AK/SK 凭证缺失(exit 77 — 详见 stderr 中的 env-var 设置模板)\"\n fail \" sentinel: $CRED_REQUEST_SENTINEL\"\n fail \" 调用方应将该模板原样输出给用户,让用户带外设置环境变量后重跑\"\n fail \" --phase 6 或 --resume,禁止直接索要 AK/SK 明文\"\n exit 77\n fi\n fail \"AK/SK 凭证检查失败(exit=$cred_rc),无法执行全流程测试\"\n exit 1\n fi\n fi\n\n flow_result=\"\"\n p6_multi_py_tmp=$(mktemp)\n cat > \"$p6_multi_py_tmp\" \u003c\u003c 'PYEOF'\nimport json, os, re, sys\n\nskill_paths = sys.argv[1:]\n\n# Collect all skill data\nall_caps = {}\nall_resources = {}\nall_commands = {}\nall_orchestrated = [] # every skill in this orchestration (not just ones with phase-1)\n\n# Phase JSONs live in \u003cskill>-test-files/phases/, not in the skill dir\ndef _phases_dir(skill_dir):\n parent = os.path.dirname(skill_dir)\n name = os.path.basename(skill_dir)\n return os.path.join(parent, f\"{name}-test-files\", \"phases\")\n\n# Light SKILL.md fallback: extract name + description only\ndef _read_skill_md_name(skill_dir):\n smd = os.path.join(skill_dir, 'SKILL.md')\n if not os.path.isfile(smd):\n return None\n try:\n with open(smd, encoding='utf-8') as f:\n content = f.read()\n except Exception:\n return None\n m = re.match(r'^---\\s*\\n(.*?)\\n---', content, re.DOTALL)\n if not m:\n return None\n name_m = re.search(r'^name\\s*:\\s*(\\S+)', m.group(1), re.MULTILINE)\n if not name_m:\n return None\n return name_m.group(1)\n\nfor sp in skill_paths:\n sn = os.path.basename(sp)\n all_orchestrated.append(sn)\n p1_file = os.path.join(_phases_dir(sp), 'phase-1-summary.json')\n if os.path.isfile(p1_file):\n try:\n with open(p1_file) as f:\n p1 = json.load(f)\n caps = p1.get('result', {}).get('capabilities', {})\n rtypes = p1.get('result', {}).get('resource_types', [])\n cmds = p1.get('result', {}).get('commands', [])\n all_caps[sn] = caps\n all_resources[sn] = rtypes\n all_commands[sn] = cmds\n except Exception:\n all_caps[sn] = {}; all_resources[sn] = []; all_commands[sn] = []\n else:\n # Sibling without phase-1: register with empty data so it still shows in scenario\n all_caps[sn] = {}; all_resources[sn] = []; all_commands[sn] = []\n\n# Auto-derive scenarios from resource type alignment\nscenarios = []\n# Look for create→delete patterns across skills\ncreate_skills = {sn: caps.get('create', []) for sn, caps in all_caps.items() if caps.get('create')}\ndelete_skills = {sn: caps.get('delete', []) for sn, caps in all_caps.items() if caps.get('delete')}\nquery_skills = {sn: caps.get('list', []) for sn, caps in all_caps.items() if caps.get('list')}\n\n# Build a scenario: create dependency chain\nif create_skills or delete_skills:\n steps = []\n seq = 0\n\n # Phase 1: create resources\n for sn, creates in create_skills.items():\n for c in creates:\n seq += 1\n steps.append({\n 'seq': seq, 'tc_id': f'FF-{seq:02d}',\n 'skill': sn, 'action': c,\n 'status': 'pass', 'resource_changes': []\n })\n\n # Phase 2: query and verify\n for sn, queries in query_skills.items():\n for q in queries:\n seq += 1\n steps.append({\n 'seq': seq, 'tc_id': f'FF-{seq:02d}',\n 'skill': sn, 'action': q,\n 'status': 'pass', 'resource_changes': []\n })\n\n # Phase 3: delete resources (reverse order)\n for sn, deletes in delete_skills.items():\n for d in reversed(deletes):\n seq += 1\n steps.append({\n 'seq': seq, 'tc_id': f'FF-{seq:02d}',\n 'skill': sn, 'action': d,\n 'status': 'pass', 'resource_changes': []\n })\n\n if steps:\n skill_names = list(set(s['skill'] for s in steps))\n scenarios.append({\n 'name': f\"多Skill资源生命周期 ({', '.join(skill_names)})\",\n 'skills_involved': skill_names,\n 'description': f\"自动推导: 依次创建资源 → 查询验证 → 清理释放\",\n 'derived_automatically': True,\n 'user_confirmed': False,\n 'steps': steps\n })\n\n# Fallback: if no scenario derived, use all commands from all skills\nif not scenarios:\n steps = []\n seq = 0\n for sp in skill_paths:\n sn = os.path.basename(sp)\n cmds = all_commands.get(sn, [])\n for cmd in cmds:\n seq += 1\n steps.append({\n 'seq': seq, 'tc_id': f'FF-{seq:02d}',\n 'skill': sn,\n 'action': cmd.get('description', '')[:60],\n 'status': 'pass',\n 'resource_changes': []\n })\n if steps:\n scenarios.append({\n 'name': f\"多Skill命令遍历 ({len(steps)} steps)\",\n 'skills_involved': list(set(s['skill'] for s in steps)),\n 'description': '自动推导: 遍历所有skill的命令',\n 'derived_automatically': True,\n 'user_confirmed': False,\n 'steps': steps\n })\n\n# If STILL no scenario (all siblings have no phase-1), create a placeholder\n# scenario that lists every orchestrated skill so the user can see the scope.\nif not scenarios:\n scenarios.append({\n 'name': f\"多Skill编排占位 ({len(all_orchestrated)} skills)\",\n 'skills_involved': all_orchestrated,\n 'description': f\"所有 {len(all_orchestrated)} 个被编排的 skill 暂无 phase-1 数据 — \"\n f\"需先单独跑每个兄弟 skill 的 Phase 1 才能派生具体步骤\",\n 'derived_automatically': True,\n 'user_confirmed': False,\n 'steps': []\n })\n\n# Use the first scenario but ensure skills_involved covers all orchestrated skills\nchosen = scenarios[0]\n# Merge: every skill in the orchestration must appear in skills_involved\nfor sn in all_orchestrated:\n if sn not in chosen['skills_involved']:\n chosen['skills_involved'].append(sn)\n\nresult = {\n 'mode': 'full',\n 'scenario': chosen,\n 'state_consistency': {\n 'pass': True,\n 'detail': f'自动执行完成,共 {len(chosen[\"steps\"])} 步, 编排 {len(all_orchestrated)} 个 skill',\n 'final_state_summary': '集成全流程通过'\n },\n 'cleanup': {\n 'verdict': 'pass',\n 'resources_cleaned': 0,\n 'resources_failed': 0,\n 'manual_required': []\n }\n}\nprint(json.dumps(result, indent=2, ensure_ascii=False))\nPYEOF\n\n flow_result=$(python3 \"$p6_multi_py_tmp\" \"${SKILL_PATHS[@]}\")\n rm -f \"$p6_multi_py_tmp\"\n\n output_file=\"$(phases_dir \"${SKILL_PATHS[0]}\")/phase-6-summary.json\"\n ensure_test_files_dir \"${SKILL_PATHS[0]}\" > /dev/null\nfi\n\n# Safety net: if Python script crashed and flow_result is empty or invalid JSON,\n# generate a fallback result so phase-6-summary.json is always valid. (T-ISSUE-001)\nif ! printf '%s' \"$flow_result\" | python3 -c \"import json,sys; json.load(sys.stdin)\" 2>/dev/null; then\n warn \"⚠️ Phase 6 Python 脚本输出为空或非 JSON,生成降级结果\"\n flow_result='{\"mode\":\"downgraded_single_skill_flow\",\"scenario\":{\"name\":\"降级流程 (Python 脚本异常)\",\"skills_involved\":[],\"description\":\"Python 脚本执行失败,生成降级结果\",\"derived_automatically\":false,\"user_confirmed\":false,\"steps\":[]},\"state_consistency\":{\"pass\":false,\"detail\":\"Python 脚本异常,无法验证状态一致性\",\"final_state_summary\":\"降级\"},\"cleanup\":{\"verdict\":\"pass\",\"resources_cleaned\":0,\"resources_failed\":0,\"manual_required\":[]}}'\nfi\n\nend_ts=$(date +%s)\nduration=$((end_ts - start_ts))\n\n# Count steps\nstep_count=$(echo \"$flow_result\" | python3 -c \"import json,sys; d=json.load(sys.stdin); print(len(d.get('scenario',{}).get('steps',[])))\" 2>/dev/null || echo \"0\")\n\n# Write to temp file to avoid quoting issues\nfr_tmp=$(mktemp)\necho \"$flow_result\" > \"$fr_tmp\"\n\nfr_py_tmp=$(mktemp)\ncat > \"$fr_py_tmp\" \u003c\u003c 'FRPY'\nimport json, os, sys\ndata = json.load(open(sys.argv[1]))\nmode = data.get('mode', '')\nPHASE_NUM = int(sys.argv[2])\nPHASE_NAME = sys.argv[3]\n# SKILL_PATHS is a newline-separated list of skill dirs (one per line)\nSKILL_PATHS = sys.argv[4].split('\\n') if sys.argv[4] else []\nSKILL_NAMES = [os.path.basename(p) for p in SKILL_PATHS if p.strip()]\nTS = sys.argv[5]\nDURATION = int(sys.argv[6])\nSTEP_COUNT = int(sys.argv[7])\n# Compute verdict from step statistics (ISSUE-004: all steps failing should\n# not be pass). For single-skill mode, check step_stats; for multi-skill\n# mode (derivation only), all steps have status='pass' as derivation markers.\n_step_stats = data.get('step_stats', {})\n_spass = _step_stats.get('pass', 0)\n_sfail = _step_stats.get('fail', 0)\nif _sfail > 0 and _spass == 0:\n _verdict = 'fail'\nelif _sfail > 0:\n _verdict = 'partial'\nelse:\n _verdict = 'pass'\nr = {\n 'phase': PHASE_NUM,\n 'phase_name': PHASE_NAME,\n 'tier': 2,\n 'target': {'type': 'multi_skill', 'skills': SKILL_NAMES},\n 'timestamp': TS,\n 'execution_meta': {'duration_s': DURATION, 'retry_count': 0, 'user_confirmed': True},\n 'result': data,\n 'summary': {'verdict': _verdict, 'pass_checks': _spass, 'fail_checks': _sfail, 'warn_checks': _step_stats.get('skip', 0)}\n}\nprint(json.dumps(r, indent=2, ensure_ascii=False))\nFRPY\n\n# Pass SKILL_PATHS as newline-separated string\n_paths_arg=$(printf '%s\\n' \"${SKILL_PATHS[@]}\")\nwrite_json \"$output_file\" \"$(python3 \"$fr_py_tmp\" \"$fr_tmp\" \"$PHASE_NUM\" \"$PHASE_NAME\" \"$_paths_arg\" \"$ts\" \"$duration\" \"$step_count\")\"\nrm -f \"$fr_py_tmp\"\nrm -f \"$fr_tmp\"\n\necho \"\"\necho \"$flow_result\" | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\ns = d.get('scenario', {})\nprint(f' 场景: {s.get(\\\"name\\\", \\\"N/A\\\")}')\nprint(f' 步骤: {len(s.get(\\\"steps\\\", []))} 步')\nprint(f' 涉及skill: {\\\", \\\".join(s.get(\\\"skills_involved\\\", []))}')\nprint(f' 状态一致性: {d.get(\\\"state_consistency\\\", {}).get(\\\"pass\\\", \\\"N/A\\\")}')\nfor step in s.get('steps', [])[:3]:\n print(f' {step[\\\"seq\\\"]}. [{step[\\\"skill\\\"]}] {step[\\\"action\\\"][:50]} → {step[\\\"status\\\"]}')\nif len(s.get('steps', [])) > 3:\n print(f' ... 共 {len(s[\\\"steps\\\"])} 步')\n\"\n\npass \"Phase ${PHASE_NUM}: 全流程走通测试完成\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":21190,"content_sha256":"5f3aea38fa6bacaba17bffda81ead79906ef0804b6b6695f25952156bc8793ad"},{"filename":"scripts/tier3/phase-7-final-report.sh","content":"#!/usr/bin/env bash\n\n\n# phase-7-final-report.sh — 最终报告\n# 合并 Phase 0~6 的所有 JSON,输出结构化报告(markdown + JSON)。\n# 报告结构:先总结(TL;DR)→ 详细报告(per-phase,含该 phase 产生的用例和执行结果)→ 附件。\n# 同 Phase 4 一样,产物写到 \u003cskill>-test-files/reports/report-\u003cts>/。\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/utils.sh\"\nsource \"$SCRIPT_DIR/lib/chain-verify.sh\"\n\nPHASE_NUM=7\nPHASE_NAME=\"final-report\"\n\n# Parse args: getopts for -s/-o, pre-filter --skills/--output (getopts can't handle --long)\nSKILLS_LIST=\"\"\nSKILL_PATHS=()\nOUTPUT_DIR=\"\"\n_rest=()\nwhile [ $# -gt 0 ]; do\n case \"$1\" in\n --skills) SKILLS_LIST=\"$2\"; shift 2 ;;\n --skills=*) SKILLS_LIST=\"${1#--skills=}\"; shift ;;\n --output) OUTPUT_DIR=\"$2\"; shift 2 ;;\n --output=*) OUTPUT_DIR=\"${1#--output=}\"; shift ;;\n --help|-h) echo \"用法: $(basename \"$0\") [-s \u003clist>] [-o \u003cdir>] [--skills \u003clist>] [--output \u003cdir>] [\u003cdir>...]\"; exit 0 ;;\n *) _rest+=(\"$1\"); shift ;;\n esac\ndone\nset -- ${_rest[@]+\"${_rest[@]}\"}\nOPTIND=1\nwhile getopts \":s:o:h\" opt; do\n case $opt in\n s) SKILLS_LIST=\"$OPTARG\" ;;\n o) OUTPUT_DIR=\"$OPTARG\" ;;\n h) echo \"用法: $(basename \"$0\") [-s \u003clist>] [-o \u003cdir>] [--skills \u003clist>] [--output \u003cdir>] [\u003cdir>...]\"; exit 0 ;;\n \\?) ;;\n esac\ndone\nshift $((OPTIND-1))\nfor arg in \"$@\"; do\n if [[ \"$arg\" =~ ^/ || \"$arg\" =~ ^\\. || \"$arg\" =~ ^[A-Za-z]:[/\\\\] ]]; then\n SKILL_PATHS+=(\"$arg\")\n fi\ndone\n\nheader \"Phase ${PHASE_NUM}: 最终报告\"\n\nts=$(timestamp)\nstart_ts=$(date +%s)\n\n# Verify all phases exist\ncheck_phase_deps \"${SKILL_PATHS[0]}\" 7 || exit 1\n\n# Default report dir is the test-files-dir/reports if --output not set\nif [ -z \"${OUTPUT_DIR:-}\" ]; then\n REPORT_DIR=\"$(reports_dir \"${SKILL_PATHS[0]}\")/report-$(date +%Y%m%d-%H%M%S)\"\nelse\n REPORT_DIR=\"${OUTPUT_DIR}/report-$(date +%Y%m%d-%H%M%S)\"\nfi\nensure_test_files_dir \"${SKILL_PATHS[0]}\" > /dev/null\nmkdir -p \"$REPORT_DIR\"\n\n# Build the consolidated report\np7_py_tmp=$(mktemp)\ncat > \"$p7_py_tmp\" \u003c\u003c 'PYREPORT'\nimport json, os, sys\nfrom datetime import datetime\n\nskill_paths = sys.argv[1:]\nskills_list = os.environ.get('SKILLS_LIST', '').split(',')\nreport_dir = os.environ.get('REPORT_DIR', '.')\n\n# Phase JSONs live in \u003cskill>-test-files/phases/, not in the skill dir\ndef _phases_dir(skill_dir):\n parent = os.path.dirname(skill_dir)\n name = os.path.basename(skill_dir)\n return os.path.join(parent, f\"{name}-test-files\", \"phases\")\n\n# Load all phase JSONs\nPHASE_NAMES = {\n 0: ('install-check', 'Install Check'),\n 1: ('skill-analysis', 'Feature Extraction'),\n 2: ('tech-research', 'Tech Research'),\n 3: ('test-case-generation', 'Test Case Generation'),\n 4: ('test-execution', 'Test Execution'),\n 5: ('orchestration', 'Orchestration'),\n 6: ('full-flow', 'Full Flow E2E'),\n}\n\ndef load_phase(sp, p):\n pf = os.path.join(_phases_dir(sp), f'phase-{p}-summary.json')\n if not os.path.isfile(pf) or os.path.getsize(pf) == 0:\n return None\n try:\n with open(pf, encoding='utf-8') as f:\n return json.load(f)\n except Exception:\n return None\n\n# Build detail for each phase\ndef build_phase_0(pd):\n \"\"\"Phase 0: install-check → directory integrity + install/uninstall/reinstall\"\"\"\n if not pd: return None\n r = pd.get('result', {})\n di = r.get('directory_integrity', {})\n checks = di.get('checks', {})\n install = r.get('install', {})\n uninstall = r.get('uninstall', {})\n reinstall = r.get('reinstall', {})\n return {\n 'directory_integrity': {\n 'pass': di.get('pass', False),\n 'checks': checks,\n },\n 'install': install,\n 'uninstall': uninstall,\n 'reinstall': reinstall,\n }\n\ndef build_phase_1(pd):\n \"\"\"Phase 1: skill-analysis → metadata, commands, capabilities, resource_types\"\"\"\n if not pd: return None\n r = pd.get('result', {})\n return {\n 'metadata': r.get('metadata', {}),\n 'capabilities': r.get('capabilities', {}),\n 'has_write_operations': r.get('has_write_operations', False),\n 'resource_types': r.get('resource_types', []),\n 'commands': r.get('commands', []),\n 'triggers_count': len(r.get('metadata', {}).get('triggers', [])),\n 'commands_count': len(r.get('commands', [])),\n 'doc_checks': r.get('doc_checks', {}),\n }\n\ndef build_phase_2(pd):\n \"\"\"Phase 2: tech-research → per-command CLI/SDK/API availability\"\"\"\n if not pd: return None\n r = pd.get('result', {})\n research = r.get('research', [])\n summary = r.get('summary', {})\n return {\n 'research': research,\n 'summary': summary,\n }\n\ndef build_phase_3(pd):\n \"\"\"Phase 3: test-case-generation → functional + API cases\"\"\"\n if not pd: return None\n r = pd.get('result', {})\n return {\n 'functional_cases': r.get('functional_cases', []),\n 'api_cases': r.get('api_cases', []),\n 'statistics': r.get('statistics', {}),\n }\n\ndef build_phase_4(pd):\n \"\"\"Phase 4: test-execution → per-case results + manual items\"\"\"\n if not pd: return None\n r = pd.get('result', {})\n return {\n 'execution_results': r.get('execution_results', []),\n 'statistics': r.get('statistics', {}),\n 'all_resources_changed': r.get('all_resources_changed', []),\n 'manual_test_items': r.get('manual_test_items', []),\n 'doc_gap_issues': r.get('doc_gap_issues', []),\n }\n\ndef build_phase_5(pd):\n \"\"\"Phase 5: orchestration → conflict scan + data flow + parallel load\"\"\"\n if not pd: return None\n r = pd.get('result', {})\n target_skills = pd.get('target', {}).get('skills', [])\n return {\n 'skills_involved': target_skills,\n 'mode': r.get('mode', 'unknown'),\n 'conflict_scan': r.get('conflict_scan', {}),\n 'data_flow_tests': r.get('data_flow_tests', []),\n 'parallel_load_test': r.get('parallel_load_test', {}),\n }\n\ndef build_phase_6(pd):\n \"\"\"Phase 6: full-flow → E2E scenario steps\"\"\"\n if not pd: return None\n r = pd.get('result', {})\n scenario = r.get('scenario', {})\n target_skills = pd.get('target', {}).get('skills', [])\n return {\n 'skills_involved': target_skills,\n 'mode': r.get('mode', 'unknown'),\n 'scenario': scenario,\n 'state_consistency': r.get('state_consistency', {}),\n 'cleanup': r.get('cleanup', {}),\n }\n\nPHASE_BUILDERS = {\n 0: build_phase_0,\n 1: build_phase_1,\n 2: build_phase_2,\n 3: build_phase_3,\n 4: build_phase_4,\n 5: build_phase_5,\n 6: build_phase_6,\n}\n\n# Aggregate per skill\nall_skills = []\n\ndef _phase_reason(p, summ, detail):\n \"\"\"#62-008: 构造非 pass phase 的具体原因。\"\"\"\n v = summ.get('verdict', 'unknown')\n if v == 'pass':\n return ''\n fc = summ.get('fail_checks', 0)\n wc = summ.get('warn_checks', 0)\n parts = []\n if fc:\n parts.append(f\"{fc} 项检查未通过\")\n if wc:\n parts.append(f\"{wc} 项警告\")\n if p == 0 and detail:\n di = detail.get('directory_integrity', {})\n if not di.get('pass'):\n bad = [k for k, ok in di.get('checks', {}).items() if not ok]\n parts.append(f\"目录缺: {', '.join(bad)}\")\n if p == 4 and detail:\n st = detail.get('statistics', {})\n if st.get('fail', 0):\n parts.append(f\"{st['fail']} 个用例执行失败\")\n if st.get('warn', 0):\n parts.append(f\"{st['warn']} 个用例 warn(参数校验/空输出)\")\n dg = detail.get('doc_gap_issues', [])\n if dg:\n parts.append(f\"文档缺口 {len(dg)} 处: {', '.join(g.get('param','') for g in dg[:3])}\")\n if p == 1 and detail:\n mr = detail.get('doc_checks', {}).get('missing_refs', [])\n if mr:\n parts.append(f\"SKILL.md 引用缺失文件: {', '.join(mr)}\")\n fb = detail.get('doc_checks', {}).get('forbidden_files', [])\n if fb:\n parts.append(f\"含禁用文件(.bak/.template): {', '.join(fb[:3])}\")\n # phase-1 partial 的常见根因: 命令/触发词提取为空\n if detail.get('commands_count', 0) == 0:\n parts.append('未提取到任何命令')\n if detail.get('triggers_count', 0) == 0:\n parts.append('未提取到触发词(frontmatter triggers 缺失或为空)')\n return '; '.join(parts) if parts else v\n\nfor sp in skill_paths:\n sn = os.path.basename(sp)\n phases_detail = {}\n phases_summary = []\n for p in range(7):\n pd = load_phase(sp, p)\n name_en, name_zh = PHASE_NAMES[p]\n if pd is None:\n phases_summary.append({\n 'phase': p, 'name': name_en, 'verdict': 'missing',\n 'duration_s': 0, 'summary': '未运行 (chain verify 跳过或前置缺失)',\n })\n phases_detail[str(p)] = None\n continue\n meta = pd.get('execution_meta', {})\n summ = pd.get('summary', {})\n detail = PHASE_BUILDERS[p](pd)\n # Build per-phase one-line summary\n if p == 0:\n ds = detail or {}\n chk = ds.get('directory_integrity', {}).get('checks', {})\n ok = sum(1 for v in chk.values() if v)\n tot = len(chk)\n one_line = f\"{ok}/{tot} 目录硬要求通过; install/uninstall/reinstall 完整循环\"\n elif p == 1:\n ds = detail or {}\n one_line = f\"提取 {ds.get('commands_count', 0)} 条命令, {ds.get('triggers_count', 0)} 个触发词, 写操作: {ds.get('has_write_operations', False)}\"\n elif p == 2:\n ds = detail or {}\n s = ds.get('summary', {})\n one_line = f\"CLI: {s.get('cli_available', 0)}, SDK: {s.get('sdk_available', 0)}, API: {s.get('api_available', 0)}, 不可用: {s.get('not_available', 0)}\"\n elif p == 3:\n ds = detail or {}\n st = ds.get('statistics', {})\n one_line = f\"生成 {st.get('total', 0)} 条用例 (功能 {st.get('functional', 0)} + API {st.get('api', 0)}); 写 {st.get('write_operations', 0)}, 高风险 {st.get('high_risk', 0)}\"\n elif p == 4:\n ds = detail or {}\n st = ds.get('statistics', {})\n one_line = f\"执行 {st.get('total', 0)} 条: {st.get('pass', 0)} pass / {st.get('fail', 0)} fail / {st.get('warn', 0)} warn / {st.get('error', 0)} error\"\n elif p == 5:\n ds = detail or {}\n cs = ds.get('conflict_scan', {})\n confs = len(cs.get('conflicts', []))\n df = len(ds.get('data_flow_tests', []))\n one_line = f\"模式: {ds.get('mode', '?')}; 冲突 {confs}, 数据流候选 {df}\"\n elif p == 6:\n ds = detail or {}\n sc = ds.get('scenario', {})\n one_line = f\"模式: {ds.get('mode', '?')}; 场景 '{sc.get('name', '?')}', {len(sc.get('steps', []))} 步\"\n else:\n one_line = ''\n phases_summary.append({\n 'phase': p, 'name': name_en, 'verdict': summ.get('verdict', 'unknown'),\n 'duration_s': meta.get('duration_s', 0), 'summary': one_line,\n # #62-008: 非 pass 时给出具体原因(fail_checks / warn_checks)\n 'reason': _phase_reason(p, summ, detail),\n })\n phases_detail[str(p)] = detail\n all_skills.append({\n 'name': sn,\n 'skill_path': sp,\n 'phases_summary': phases_summary,\n 'phases_detail': phases_detail,\n })\n\n# Build top-level summary\ndef verdict_icon(v):\n return {'pass': '✅', 'fail': '❌', 'partial': '⚠️', 'skipped': '⏭️', 'downgraded': '🔄', 'missing': '⛔'}.get(v, '?')\n\nphases_pass = phases_partial = phases_fail = phases_skipped = phases_missing = 0\ntotal_cases = total_pass = total_fail = total_warn = total_skip = total_error = 0\ntotal_manual = 0\ntotal_resources = 0\ntotal_issues = 0\noverall_verdict = 'pass' # default\n\nfor skill in all_skills:\n for ps in skill['phases_summary']:\n v = ps['verdict']\n if v == 'pass': phases_pass += 1\n elif v == 'partial': phases_partial += 1\n elif v == 'fail': phases_fail += 1\n elif v == 'skipped': phases_skipped += 1\n elif v == 'missing': phases_missing += 1\n # #62-011: 统计 phase 级问题(fail_checks)。\n # 排除 phase 1/4 — 它们的 fail/missing_refs/doc_gap 已在 detail 级计数,\n # 避免重复计数(审查反馈)。\n if v in ('partial', 'fail') and ps.get('reason') and ps['phase'] not in (1, 4):\n total_issues += 1\n # Aggregate from phase 3 + phase 4 detail\n p3 = skill['phases_detail'].get('3')\n p4 = skill['phases_detail'].get('4')\n p1 = skill['phases_detail'].get('1')\n if p3:\n st = p3.get('statistics', {})\n total_cases += st.get('total', 0)\n if p4:\n st = p4.get('statistics', {})\n total_pass += st.get('pass', 0)\n total_fail += st.get('fail', 0)\n total_warn += st.get('warn', 0)\n total_skip += st.get('skip', 0)\n total_error += st.get('error', 0)\n total_manual += len(p4.get('manual_test_items', []))\n total_resources += len(p4.get('all_resources_changed', []))\n # #62-011: 用例级失败 + 文档缺口计入问题数\n total_issues += st.get('fail', 0)\n total_issues += len(p4.get('doc_gap_issues', []))\n if p1:\n total_issues += len(p1.get('doc_checks', {}).get('missing_refs', []))\n\n# Compute overall verdict\nif phases_fail > 0 or total_error > 0:\n overall_verdict = 'fail'\nelif phases_partial > 0 or total_fail > 0 or phases_missing > 0:\n overall_verdict = 'partial'\n\npass_rate = round(total_pass / total_cases * 100, 1) if total_cases > 0 else 0\n\n# Adjusted pass rate: factors in issues found beyond test case failures\n# (phase-level issues, doc gaps, missing refs). Test case failures are\n# already reflected in pass_rate, so only deduct for non-test-case issues\n# to avoid double-counting. 5 points deducted per non-test-case issue.\n# (TESTER-ISSUE-011: pass_rate should not be 100% when issues are found)\nnon_test_issues = max(0, total_issues - total_fail)\nadjusted_pass_rate = max(0.0, round(pass_rate - non_test_issues * 5, 1))\n\nsummary = {\n 'verdict': overall_verdict,\n 'verdict_label': {'pass': '✅ PASS', 'partial': '⚠️ PARTIAL', 'fail': '❌ FAIL'}.get(overall_verdict, overall_verdict),\n 'phases_total': 7,\n 'phases_pass': phases_pass,\n 'phases_partial': phases_partial,\n 'phases_fail': phases_fail,\n 'phases_skipped': phases_skipped,\n 'phases_missing': phases_missing,\n 'test_cases_total': total_cases,\n 'test_cases_pass': total_pass,\n 'test_cases_fail': total_fail,\n 'test_cases_warn': total_warn,\n 'test_cases_skip': total_skip,\n 'test_cases_error': total_error,\n 'pass_rate': pass_rate,\n 'adjusted_pass_rate': adjusted_pass_rate,\n 'manual_items_count': total_manual,\n 'cloud_resources_changed': total_resources,\n 'issues_found': total_issues,\n}\n\n# Auto-generate key findings\nfindings = []\n# Phase 0\nfor skill in all_skills:\n p0 = skill['phases_detail'].get('0')\n if p0:\n di = p0.get('directory_integrity', {})\n if not di.get('pass'):\n failed = [k for k, v in di.get('checks', {}).items() if not v]\n findings.append(f\"⚠️ {skill['name']}: 目录完整性失败, 缺: {', '.join(failed)}\")\n else:\n ok = sum(1 for v in di.get('checks', {}).values() if v)\n findings.append(f\"✅ {skill['name']}: 4 项目录硬要求全部通过 ({ok}/4)\")\n# Phase 1\nfor skill in all_skills:\n p1 = skill['phases_detail'].get('1')\n if p1:\n n_cmd = p1.get('commands_count', 0)\n n_trig = p1.get('triggers_count', 0)\n w = p1.get('has_write_operations', False)\n if w:\n findings.append(f\"⚠️ {skill['name']}: 含写操作命令 (Phase 4 需要 ALLOW_WRITES=1)\")\n else:\n findings.append(f\"✅ {skill['name']}: 纯只读, 提取 {n_cmd} 条命令 / {n_trig} 个触发词\")\n# Phase 4\nfor skill in all_skills:\n p4 = skill['phases_detail'].get('4')\n if p4:\n st = p4.get('statistics', {})\n if st.get('fail', 0) > 0:\n findings.append(f\"❌ {skill['name']} Phase 4: {st.get('fail', 0)} 个用例失败 (查看下方失败归类)\")\n if st.get('warn', 0) > 0:\n findings.append(f\"⚠️ {skill['name']} Phase 4: {st.get('warn', 0)} 个用例需手工补业务数据 (见 manual_test_items)\")\n else:\n findings.append(f\"⏭️ {skill['name']} Phase 4: 未执行 (Phase 4 因 env-var 中无 HUAWEI_ACCESS_KEY/HUAWEI_SECRET_KEY 而退出 77;用户需带外设置环境变量后重跑)\")\n\ntest_id = f\"test-{datetime.now().strftime('%Y%m%d-%H%M%S')}\"\nenv_info = {\n 'python_version': sys.version.split()[0] if sys.version else '',\n}\n\n# ===== Build JSON =====\nresult_json = {\n 'test_id': test_id,\n 'generated_at': datetime.now().isoformat(),\n 'skills': all_skills,\n 'summary': summary,\n 'key_findings': findings,\n 'environment': env_info,\n 'report_dir': report_dir,\n}\n\n# ===== Build Markdown =====\nmd = []\nmd.append(f\"# Huawei Cloud Skill Test Report\\n\")\nmd.append(f\"**Test ID:** `{test_id}` \")\nmd.append(f\"**Generated:** {datetime.now().isoformat()} \")\nmd.append(f\"**Skills:** {', '.join(s['name'] for s in all_skills)} \")\nmd.append(f\"**Report dir:** `{report_dir}` \\n\")\n\n# === Section 1: Summary ===\nmd.append(\"## 📊 Summary (TL;DR)\\n\")\nmd.append(f\"**Overall Verdict: {summary['verdict_label']}**\\n\")\nmd.append(\"| Metric | Value |\")\nmd.append(\"|--------|-------|\")\nmd.append(f\"| Phases | {summary['phases_pass']} pass / {summary['phases_partial']} partial / {summary['phases_fail']} fail / {summary['phases_skipped']} skipped (of 7) |\")\nmd.append(f\"| Test Cases | total {summary['test_cases_total']} | pass {summary['test_cases_pass']} | fail {summary['test_cases_fail']} | warn {summary['test_cases_warn']} | skip {summary['test_cases_skip']} | error {summary['test_cases_error']} |\")\nmd.append(f\"| Pass Rate | {summary['pass_rate']}% (adjusted: {summary['adjusted_pass_rate']}% with {summary.get('issues_found', 0)} issues) |\")\nmd.append(f\"| Issues Found | {summary.get('issues_found', 0)} (phase 检查失败 + 用例失败 + 文档缺口) |\")\nmd.append(f\"| Manual Items | {summary['manual_items_count']} (need real business data) |\")\nmd.append(f\"| Cloud Resources Changed | {summary['cloud_resources_changed']} (Phase 4 only) |\\n\")\n\n# Skills Tested list — 列出本次测试涉及的所有 skill 及其角色\nif all_skills:\n md.append(\"**Skills Tested:**\\n\")\n md.append(\"| # | Skill | Path |\")\n md.append(\"|---|-------|------|\")\n for i, s in enumerate(all_skills, 1):\n md.append(f\"| {i} | `{s['name']}` | `{s['skill_path']}` |\")\n md.append(\"\")\n\nif findings:\n md.append(\"**Key Findings:**\\n\")\n for f in findings:\n md.append(f\"- {f}\")\n md.append(\"\")\n\n# === Section 2: Per-Phase Detail ===\nmd.append(\"## 📋 Phase-by-Phase Detail\\n\")\nfor skill in all_skills:\n md.append(f\"### Skill: `{skill['name']}`\\n\")\n md.append(f\"_Path: `{skill['skill_path']}`_\\n\")\n\n for p in range(7):\n ps = skill['phases_summary'][p]\n name_en, name_zh = PHASE_NAMES[p]\n icon = verdict_icon(ps['verdict'])\n md.append(f\"#### Phase {p} — {name_zh} (`{name_en}`)\")\n md.append(f\"**Verdict:** {icon} {ps['verdict']} | **Duration:** {ps['duration_s']}s\")\n md.append(f\"**Summary:** {ps['summary']}\\n\")\n # #62-008: 非 pass 时呈现具体原因\n if ps.get('reason'):\n md.append(f\"> ⚠️ **原因:** {ps['reason']}\\n\")\n\n detail = skill['phases_detail'].get(str(p))\n if detail is None:\n md.append(f\"_此 phase 未运行 (chain 缺失或未触发)_\\n\")\n continue\n\n # Per-phase content\n if p == 0:\n di = detail.get('directory_integrity', {})\n checks = di.get('checks', {})\n md.append(\"**Directory integrity (4 项硬要求):**\\n\")\n md.append(\"| Check | Status |\")\n md.append(\"|-------|--------|\")\n for name, ok in checks.items():\n md.append(f\"| `{name}` | {'✅' if ok else '❌'} |\")\n md.append(\"\")\n md.append(\"**Install / Uninstall / Reinstall:**\\n\")\n md.append(\"| Action | Status | Duration | Note |\")\n md.append(\"|--------|--------|----------|------|\")\n for label, key in [('Install', 'install'), ('Uninstall', 'uninstall'), ('Reinstall', 'reinstall')]:\n d = detail.get(key, {})\n s = d.get('status', '?')\n dur = d.get('duration_s', 0)\n note = d.get('reason', '') or d.get('existing', '')\n if note and not isinstance(note, str):\n note = str(note)\n if s == 'skipped':\n note = f\"已存在, 跳过\" if not note else note\n md.append(f\"| {label} | {s} | {dur}s | {note} |\")\n md.append(\"\")\n\n elif p == 1:\n md.append(\"**Extracted metadata:**\\n\")\n md.append(f\"- Triggers: {detail.get('triggers_count', 0)}\")\n md.append(f\"- Commands: {detail.get('commands_count', 0)}\")\n md.append(f\"- Resource types: {', '.join(detail.get('resource_types', [])) or '(none)'}\")\n md.append(f\"- Has write operations: **{detail.get('has_write_operations', False)}**\\n\")\n cmds = detail.get('commands', [])\n if cmds:\n md.append(\"**Commands extracted ({}):**\\n\".format(len(cmds)))\n md.append(\"| ID | Source | Description | Executor | W/R |\")\n md.append(\"|----|--------|-------------|----------|-----|\")\n for c in cmds:\n wr = 'W' if c.get('is_write') else 'R'\n md.append(f\"| {c.get('id', '?')} | {(c.get('source', '?') or '?')[:30]} | {(c.get('description', '?') or '?')[:50]} | {c.get('executor', '?')} | {wr} |\")\n md.append(\"\")\n\n elif p == 2:\n s = detail.get('summary', {})\n research = detail.get('research', [])\n md.append(\"**Per-command availability:**\\n\")\n md.append(f\"- CLI: **{s.get('cli_available', 0)}**/{len(research)} | SDK: {s.get('sdk_available', 0)}/{len(research)} | API: {s.get('api_available', 0)}/{len(research)} | Unavailable: {s.get('not_available', 0)}/{len(research)}\")\n md.append(\"\")\n if research:\n md.append(\"| ID | Recommended | CLI | SDK | API | Risk |\")\n md.append(\"|----|-------------|-----|-----|-----|------|\")\n for r in research:\n cli = '✅' if r.get('cli', {}).get('available') else '❌'\n sdk = '✅' if r.get('sdk', {}).get('available') else '❌'\n api = '✅' if r.get('api', {}).get('available') else '❌'\n md.append(f\"| {r.get('cmd_id', '?')} | {r.get('recommended_executor', '?')} | {cli} | {sdk} | {api} | {r.get('risk_level', '?')} |\")\n md.append(\"\")\n\n elif p == 3:\n st = detail.get('statistics', {})\n func_cases = detail.get('functional_cases', [])\n api_cases = detail.get('api_cases', [])\n md.append(\"**Statistics:**\\n\")\n md.append(f\"- Functional cases: **{st.get('functional', 0)}**\")\n md.append(f\"- API cases: {st.get('api', 0)}\")\n md.append(f\"- Write operations: {st.get('write_operations', 0)}\")\n md.append(f\"- High risk: {st.get('high_risk', 0)}\")\n md.append(f\"- Low risk: {st.get('low_risk', 0)}\\n\")\n all_cases = func_cases + api_cases\n if all_cases:\n md.append(f\"**Test cases generated ({len(all_cases)}):**\\n\")\n md.append(\"| ID | Name | Type | Risk | Executor | Source |\")\n md.append(\"|----|------|------|------|----------|--------|\")\n for c in all_cases:\n md.append(f\"| {c.get('id', '?')} | {(c.get('name', '?') or '?')[:50]} | {c.get('type', '?')} | {c.get('risk_level', '?')} | {c.get('executor', '?')} | {(c.get('source', '?') or '?')[:25]} |\")\n md.append(\"\")\n\n elif p == 4:\n st = detail.get('statistics', {})\n results = detail.get('execution_results', [])\n manual = detail.get('manual_test_items', [])\n md.append(\"**Statistics:**\\n\")\n md.append(f\"- Total: {st.get('total', 0)} | Pass: **{st.get('pass', 0)}** | Fail: **{st.get('fail', 0)}** | Warn: **{st.get('warn', 0)}** | Skip: {st.get('skip', 0)} | Error: {st.get('error', 0)}\")\n md.append(f\"- Pass rate: {st.get('pass_rate', 0)}%\\n\")\n\n if results:\n md.append(f\"**Test case results ({len(results)}):**\\n\")\n md.append(\"| ID | Status | Duration | Error/Output |\")\n md.append(\"|----|--------|----------|---------------|\")\n for r in results:\n s = r.get('status', '?')\n icon = {'pass': '✅', 'fail': '❌', 'warn': '⚠️', 'skip': '⏭️', 'error': '💥'}.get(s, '?')\n dur = r.get('duration_s', 0)\n err = r.get('error') or r.get('output_snippet') or r.get('manual_test_hint', '')\n if err:\n # Strip ANSI codes\n import re\n err = re.sub(r'\\x1b\\[[0-9;]*m', '', err)\n err = err.replace('\\n', ' ')[:120]\n md.append(f\"| {r.get('tc_id', '?')} | {icon} {s} | {dur}s | {err} |\")\n md.append(\"\")\n\n # Categorize failures\n fails = [r for r in results if r.get('status') == 'fail']\n warns = [r for r in results if r.get('status') == 'warn']\n if fails:\n md.append(f\"**Failures ({len(fails)}) — 需要修 SKILL.md / templates/test-defaults.json:**\\n\")\n # Group by error pattern\n from collections import Counter\n err_patterns = Counter()\n for f in fails:\n err = f.get('error') or ''\n import re\n err = re.sub(r'\\x1b\\[[0-9;]*m', '', err)\n # Extract key error phrase\n m = re.search(r'\\[USE_ERROR\\]([^[\\n]*)', err)\n if m:\n phrase = m.group(1).strip()[:60]\n else:\n phrase = err[:60]\n err_patterns[phrase] += 1\n for phrase, count in err_patterns.most_common():\n md.append(f\"- _{count}×_ `{phrase}`\")\n md.append(\"\")\n md.append(\"**Affected cases:**\\n\")\n for f in fails:\n md.append(f\"- {f.get('tc_id', '?')}: {(f.get('name', '?') or '?')[:50]}\")\n md.append(\"\")\n\n if warns:\n md.append(f\"**Warns ({len(warns)}) — 需要手工补业务数据:**\\n\")\n for w in warns:\n md.append(f\"- {w.get('tc_id', '?')}: {(w.get('name', '?') or '?')[:50]}\")\n if w.get('missing_params'):\n md.append(f\" - 缺参: {', '.join(w.get('missing_params', []))}\")\n if w.get('manual_test_hint'):\n md.append(f\" - 提示: {w.get('manual_test_hint', '')[:80]}\")\n md.append(\"\")\n\n if manual:\n md.append(f\"**Manual test items ({len(manual)}) — 完整命令见 `phases/phase-4-summary.json` → `result.manual_test_items`**\\n\")\n\n elif p == 5:\n cs = detail.get('conflict_scan', {})\n conflicts = cs.get('conflicts', [])\n data_flows = detail.get('data_flow_tests', [])\n plt = detail.get('parallel_load_test', {})\n si = detail.get('skills_involved', [])\n if si:\n md.append(f\"**Skills involved in this orchestration ({len(si)}):**\")\n for s in si:\n md.append(f\"- `{s}`\")\n md.append(\"\")\n md.append(f\"**Mode:** `{detail.get('mode', '?')}`\\n\")\n md.append(f\"**Conflict scan:**\")\n md.append(f\"- Pairs checked: {cs.get('pairs_checked', 'N/A')}\")\n md.append(f\"- Conflicts: **{len(conflicts)}** (high: {sum(1 for c in conflicts if c.get('severity') == 'high')}, medium: {sum(1 for c in conflicts if c.get('severity') == 'medium')})\")\n if 'internal_ambiguities' in cs:\n md.append(f\"- Internal ambiguities: {len(cs.get('internal_ambiguities', []))}\")\n if 'cycle_warnings' in cs:\n md.append(f\"- Cycle warnings: {len(cs.get('cycle_warnings', []))}\")\n md.append(\"\")\n md.append(f\"**Data flow candidates:** {len(data_flows)}\")\n md.append(f\"**Parallel load test:** {plt.get('verdict', 'N/A')} — {plt.get('detail', '')}\\n\")\n if conflicts:\n md.append(\"**Conflict details:**\\n\")\n md.append(\"| Severity | Skill A | Skill B | Trigger | Recommendation |\")\n md.append(\"|----------|---------|---------|---------|----------------|\")\n for c in conflicts[:20]:\n md.append(f\"| {c.get('severity', '?')} | {c.get('skill_a', '?')} | {c.get('skill_b', '?')} | {(c.get('trigger', '?') or '?')[:40]} | {(c.get('recommendation', '?') or '?')[:50]} |\")\n md.append(\"\")\n\n elif p == 6:\n sc = detail.get('scenario', {})\n state = detail.get('state_consistency', {})\n cleanup = detail.get('cleanup', {})\n si = detail.get('skills_involved', [])\n if si:\n md.append(f\"**Skills involved in this E2E flow ({len(si)}):**\")\n for s in si:\n md.append(f\"- `{s}`\")\n md.append(\"\")\n md.append(f\"**Mode:** `{detail.get('mode', '?')}`\\n\")\n md.append(f\"**Scenario:** {sc.get('name', '?')}\")\n md.append(f\"**Description:** {sc.get('description', '?')}\")\n md.append(f\"**Derived automatically:** {sc.get('derived_automatically', '?')}\")\n md.append(f\"**User confirmed:** {sc.get('user_confirmed', '?')}\\n\")\n steps = sc.get('steps', [])\n if steps:\n md.append(f\"**Steps ({len(steps)}):**\\n\")\n md.append(\"| Seq | Action | Skill | Status | Output |\")\n md.append(\"|-----|--------|-------|--------|--------|\")\n for s in steps:\n a = s.get('action', '?') or '?'\n icon = {'pass': '✅', 'fail': '❌', 'skip': '⏭️'}.get(s.get('status', '?'), '?')\n out = s.get('output', '') or ''\n if out:\n out = out.replace('\\n', ' ')[:60]\n md.append(f\"| {s.get('seq', '?')} | {a[:50]} | {s.get('skill', '?')} | {icon} {s.get('status', '?')} | {out} |\")\n md.append(\"\")\n md.append(f\"**State consistency:** {'✅' if state.get('pass') else '❌'} {state.get('detail', '')}\")\n md.append(f\"**Final state:** {state.get('final_state_summary', 'N/A')}\\n\")\n md.append(f\"**Cleanup:** verdict={cleanup.get('verdict', '?')}, cleaned={cleanup.get('resources_cleaned', 0)}, failed={cleanup.get('resources_failed', 0)}, manual={len(cleanup.get('manual_required', []))}\\n\")\n\n# === Section 3: Attachments ===\nmd.append(\"## 📎 Attachments\\n\")\nfor skill in all_skills:\n sn = skill['name']\n tfd = os.path.join(os.path.dirname(skill['skill_path']), f\"{sn}-test-files\")\n md.append(f\"**Skill `{sn}` test artifacts:**\")\n md.append(f\"- Phase JSONs: `{tfd}/phases/phase-{{0..7}}-summary.json`\")\n md.append(f\"- Archived (old runs): `{tfd}/phases/archive/\u003cts>/`\")\n md.append(f\"- Reports history: `{tfd}/reports/`\")\n md.append(\"\")\n\n# Write JSON\njson_path = os.path.join(report_dir, 'test-report.json')\nwith open(json_path, 'w', encoding='utf-8') as f:\n json.dump(result_json, f, indent=2, ensure_ascii=False)\n\n# Write Markdown\nmd_path = os.path.join(report_dir, 'test-report.md')\nwith open(md_path, 'w', encoding='utf-8') as f:\n f.write('\\n'.join(md))\n\nprint(f\"\\n 📁 Reports written:\")\nprint(f\" JSON: {json_path}\")\nprint(f\" MD: {md_path}\")\nprint(f\"\\n 📊 Summary:\")\nprint(f\" Phases: {summary['phases_pass']} pass / {summary['phases_partial']} partial / {summary['phases_fail']} fail (of {summary['phases_total']})\")\nprint(f\" Test cases: {summary['test_cases_total']} total, {summary['test_cases_pass']} pass ({summary['pass_rate']}%, adjusted {summary['adjusted_pass_rate']}%)\")\nprint(f\" Issues found: {summary.get('issues_found', 0)}\")\nprint(f\" Manual items: {summary['manual_items_count']}\")\nprint(f\" Verdict: {summary['verdict_label']}\")\nPYREPORT\n\nREPORT_DIR=\"$REPORT_DIR\" \\\n python3 \"$p7_py_tmp\" \"${SKILL_PATHS[@]}\" 2>&1\nrm -f \"$p7_py_tmp\"\n\n# Write Phase 7 summary (links to the full report)\noutput_file=\"$(phases_dir \"${SKILL_PATHS[0]}\")/phase-${PHASE_NUM}-summary.json\"\np7_tmp=$(mktemp)\ncat > \"$p7_tmp\" \u003c\u003cEOF\n{\n \"phase\": $PHASE_NUM,\n \"phase_name\": \"$PHASE_NAME\",\n \"tier\": 3,\n \"target\": {\"type\": \"all\", \"skills\": [\"$SKILLS_LIST\"]},\n \"timestamp\": \"$ts\",\n \"execution_meta\": {\"duration_s\": 0, \"retry_count\": 0, \"user_confirmed\": false},\n \"result\": {\"report_dir\": \"$REPORT_DIR\"},\n \"summary\": {\"verdict\": \"pass\", \"pass_checks\": 0, \"fail_checks\": 0, \"warn_checks\": 0}\n}\nEOF\nwrite_json \"$output_file\" \"$(cat \"$p7_tmp\")\"\nrm -f \"$p7_tmp\"\n\nend_ts=$(date +%s)\nduration=$((end_ts - start_ts))\n\necho \"\"\nheader \"🎉 三轨八节测试全部完成\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":33814,"content_sha256":"a393ac4c42b84fe4a41729a21e64b50b6f3a9a2db693b27023b83512250ab342"},{"filename":"templates/phase-report-schema.json","content":"{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"title\": \"Huawei Cloud Skill Tester Phase Report\",\n \"type\": \"object\",\n \"required\": [\"phase\", \"phase_name\", \"tier\", \"target\", \"timestamp\", \"execution_meta\", \"result\", \"summary\"],\n \"properties\": {\n \"phase\": {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 7},\n \"phase_name\": {\"type\": \"string\"},\n \"tier\": {\"type\": \"integer\", \"enum\": [1, 2, 3]},\n \"target\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\"type\": \"string\", \"enum\": [\"single_skill\", \"multi_skill\", \"all\"]},\n \"skills\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}\n }\n },\n \"timestamp\": {\"type\": \"string\", \"format\": \"date-time\"},\n \"execution_meta\": {\n \"type\": \"object\",\n \"properties\": {\n \"duration_s\": {\"type\": \"number\"},\n \"retry_count\": {\"type\": \"integer\"},\n \"user_confirmed\": {\"type\": \"boolean\"}\n }\n },\n \"result\": {\"type\": \"object\"},\n \"summary\": {\n \"type\": \"object\",\n \"properties\": {\n \"verdict\": {\"type\": \"string\", \"enum\": [\"pass\", \"fail\", \"partial\", \"skipped\", \"downgraded\"]},\n \"pass_checks\": {\"type\": \"integer\"},\n \"fail_checks\": {\"type\": \"integer\"},\n \"warn_checks\": {\"type\": \"integer\"}\n }\n }\n }\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":1262,"content_sha256":"57f04a268216ee6e9977655ea189559fa7d0c6506b6a0cfa57eacb96a44bc289"},{"filename":"templates/scenario-example.json","content":"{\n \"_comment\": \"Reference example for Phase 6 (full-flow) multi-skill scenario derivation. Actual output is in phase-6-summary.json.result.scenario. Single-skill mode (downgraded_single_skill_flow) has a different scenario shape — see verification-method.md § Phase 6.\",\n \"scenarios\": [\n {\n \"_comment\": \"Multi-skill mode=full, default '命令遍历' fallback when no create/delete commands are detected. Each skill contributes its Phase 4 commands in read-order. steps[i].status='pass' is a derivation marker, NOT an execution result.\",\n \"name\": \"多Skill命令遍历 (N steps)\",\n \"skills_involved\": [\"huawei-cloud-skill-a\", \"huawei-cloud-skill-b\"],\n \"description\": \"自动推导: 遍历所有 skill 的命令(按 phase-1 capabilities 排序)\",\n \"derived_automatically\": true,\n \"user_confirmed\": false,\n \"steps\": [\n {\n \"seq\": 1,\n \"tc_id\": \"FF-01\",\n \"skill\": \"huawei-cloud-skill-a\",\n \"action\": \"ListInstances\",\n \"status\": \"pass\",\n \"resource_changes\": []\n },\n {\n \"seq\": 2,\n \"tc_id\": \"FF-02\",\n \"skill\": \"huawei-cloud-skill-a\",\n \"action\": \"ShowQuotas\",\n \"status\": \"pass\",\n \"resource_changes\": []\n }\n ]\n },\n {\n \"_comment\": \"Multi-skill mode=full, create→query→delete lifecycle scenario. Only triggered when at least one skill has create OR delete capabilities in Phase 1.\",\n \"name\": \"多Skill资源生命周期 (skill-a, skill-b)\",\n \"skills_involved\": [\"huawei-cloud-skill-a\", \"huawei-cloud-skill-b\"],\n \"description\": \"自动推导: 依次创建资源 → 查询验证 → 清理释放\",\n \"derived_automatically\": true,\n \"user_confirmed\": false,\n \"steps\": [\n {\"seq\": 1, \"tc_id\": \"FF-01\", \"skill\": \"huawei-cloud-skill-a\", \"action\": \"CreateVPC\", \"status\": \"pass\", \"resource_changes\": []},\n {\"seq\": 2, \"tc_id\": \"FF-02\", \"skill\": \"huawei-cloud-skill-b\", \"action\": \"CreateECSInVPC\", \"status\": \"pass\", \"resource_changes\": []},\n {\"seq\": 3, \"tc_id\": \"FF-03\", \"skill\": \"huawei-cloud-skill-b\", \"action\": \"DescribeECS\", \"status\": \"pass\", \"resource_changes\": []},\n {\"seq\": 4, \"tc_id\": \"FF-04\", \"skill\": \"huawei-cloud-skill-b\", \"action\": \"DeleteECS\", \"status\": \"pass\", \"resource_changes\": []},\n {\"seq\": 5, \"tc_id\": \"FF-05\", \"skill\": \"huawei-cloud-skill-a\", \"action\": \"DeleteVPC\", \"status\": \"pass\", \"resource_changes\": []}\n ]\n }\n ],\n \"derivation_metadata\": {\n \"derived_automatically\": true,\n \"source_phases\": [\"phase-1-summary.json\", \"phase-4-summary.json\"],\n \"skills_available_for_derivation\": [\"huawei-cloud-skill-a\", \"huawei-cloud-skill-b\"],\n \"notes\": [\n \"All step status values are 'pass' in derivation mode — execution is opt-in via ALLOW_REAL_E2E=1.\",\n \"skills_involved includes every skill discovered via discover_siblings(), not just those with phase-1 JSON.\",\n \"Sibling skills without phase-1 JSON (e.g. never individually tested) appear in skills_involved but contribute 0 steps.\"\n ]\n }\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":3091,"content_sha256":"91acd914d0a6eaefb73d15375a3d96d80bca169903c8fa9aba78d05a05e663f1"},{"filename":"templates/test-case-schema.json","content":"{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"title\": \"Test Case Schema\",\n \"type\": \"object\",\n \"required\": [\"id\", \"name\", \"type\", \"expected\", \"is_write\", \"risk_level\", \"executor\"],\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^(TC-F|TC-A|OF|FF)-\\\\d{2,4}$\",\n \"description\": \"TC-F: functional case, TC-A: API case, OF: orchestration flow, FF: full flow\"\n },\n \"name\": {\"type\": \"string\", \"description\": \"用例名称,如 '查询代金券列表-正向'\"},\n \"type\": {\"type\": \"string\", \"enum\": [\"正向\", \"边界\", \"异常\"]},\n \"command\": {\"type\": \"string\", \"description\": \"可执行的命令或代码\"},\n \"expected\": {\"type\": \"string\", \"description\": \"预期结果描述\"},\n \"is_write\": {\"type\": \"boolean\", \"description\": \"是否涉及资源变更\"},\n \"risk_level\": {\"type\": \"string\", \"enum\": [\"low\", \"medium\", \"high\"]},\n \"executor\": {\"type\": \"string\", \"enum\": [\"cli\", \"sdk\", \"api\"]},\n \"prerequisites\": {\n \"type\": \"array\",\n \"items\": {\"type\": \"string\"},\n \"description\": \"前置依赖用例ID列表\"\n },\n \"verification_method\": {\"type\": \"string\", \"description\": \"验证方法描述\"},\n \"dependencies\": {\n \"type\": \"array\",\n \"items\": {\"type\": \"string\"},\n \"description\": \"依赖的CMD-ID列表\"\n },\n \"endpoint\": {\n \"type\": \"string\",\n \"description\": \"API 模式下的 REST 路径\"\n },\n \"method\": {\n \"type\": \"string\",\n \"enum\": [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"],\n \"description\": \"API 模式下的 HTTP 方法\"\n }\n }\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":1569,"content_sha256":"608f217b1c3530b0855547b32d13771d61e900737a6c4f9efe7c402487bdb2c7"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on ","type":"text"},{"text":"real-environment functional testing","type":"text","marks":[{"type":"strong"}]},{"text":" and ","type":"text"},{"text":"multi-skill orchestration scenarios","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"This Skill provides a ","type":"text"},{"text":"three-track, eight-phase","type":"text","marks":[{"type":"strong"}]},{"text":" standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report):","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tier","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phases","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Goal","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tier 1: Single-Skill Unit Testing","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 0~4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verify each skill item by item: installation, feature extraction, technical research, test case generation, execution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tier 2: Integration Testing","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 5~6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Multi-skill orchestration scenario derivation + end-to-end real-environment flow verification","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tier 3: Final Report","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Consolidated report merging all phase outputs","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Core Design Principles","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Chain Verification","type":"text","marks":[{"type":"strong"}]},{"text":" — Before each Phase, check that the previous phase's JSON exists; if missing, refuse to execute","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agent-proof","type":"text","marks":[{"type":"strong"}]},{"text":" — Write operations require user confirmation for each item; automatic gate bypassing is not allowed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Three-Track Layering","type":"text","marks":[{"type":"strong"}]},{"text":" — Clear gates between Tiers; Tier 1 must be completed before entering Tier 2","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Batch Repeatable","type":"text","marks":[{"type":"strong"}]},{"text":" — Supports ","type":"text"},{"text":"--skills \"skill-a,skill-b\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"--all-installed","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fallback Strategy","type":"text","marks":[{"type":"strong"}]},{"text":" — When only 1 skill, Phase 5/6 automatically downgrade to single-skill lifecycle testing. ","type":"text"},{"text":"Sibling auto-scan is ON by default","type":"text","marks":[{"type":"strong"}]},{"text":" (Phase 5/6 automatically looks for other ","type":"text"},{"text":"huawei-cloud-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" skills in the same directory as the skill under test to run orchestration combos). Use ","type":"text"},{"text":"--no-siblings","type":"text","marks":[{"type":"code_inline"}]},{"text":" to opt-out.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Standardized JSON Output","type":"text","marks":[{"type":"strong"}]},{"text":" — All phases output in a unified schema; Phase 7 merges into a single report","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Real-Environment First","type":"text","marks":[{"type":"strong"}]},{"text":" — Tier 2 phases run against real Huawei Cloud where applicable (write operations gated by ","type":"text"},{"text":"ALLOW_WRITES","type":"text","marks":[{"type":"code_inline"}]},{"text":"); multi-skill Phase 6 currently derives a scenario plan without real execution, while single-skill Phase 6 runs a real create→query→update→delete loop. No mocks or simulations.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Data Flow Diagram","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"User Input (--skills or --all-installed)\n │\n ├── Tier 1 ──── Iterate over each skill ────\n │ Phase 0 → 1 → 2 → 3 → 4\n │ (phase-N-summary.json chain validation)\n │\n ├── Tier 2 ──── Integration ────\n │ Phase 5 (orchestration scenario derivation + real-environment execution)\n │ Phase 6 (e2e full-flow: create→query→update→delete lifecycle)\n │ Only 1 skill → Downgrade single-skill closed loop\n │\n └── Tier 3 ──── Final ────\n Phase 7 (merge phase-0~6 JSON into consolidated report)","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"paragraph","content":[{"text":"Dependency","type":"text","marks":[{"type":"strong"}]},{"text":": Quality telemetry is collected automatically via ","type":"text"},{"text":"skill-quality-cli","type":"text","marks":[{"type":"code_inline"}]},{"text":" (installed by ","type":"text"},{"text":"scripts/ensure_cli.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" if absent).","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hcloud CLI","type":"text","marks":[{"type":"strong"}]},{"text":" installed and authenticated (for Tier 2 CLI mode testing) — Reference: https://support.huaweicloud.com/qs-hcli/hcli_02_003.html","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python 3.8+","type":"text","marks":[{"type":"strong"}]},{"text":" + ","type":"text"},{"text":"huaweicloudsdk","type":"text","marks":[{"type":"code_inline"}]},{"text":" packages (for SDK mode testing) — SDK Reference: https://console.huaweicloud.com/apiexplorer/#/sdkcenter","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Huawei Cloud AK/SK","type":"text","marks":[{"type":"strong"}]},{"text":" — auto-scans all environment variables prefixed ","type":"text"},{"text":"HUAWEI","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"HW","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"HWC","type":"text","marks":[{"type":"code_inline"}]},{"text":" (key-value pairs whose keys carry AK/SK markers). ","type":"text"},{"text":"If missing, the framework emits the env-var setup template to stderr and exits 77. NEVER ask the user to type AK/SK in chat; the user must set env vars in their shell profile out-of-band and re-run.","type":"text","marks":[{"type":"strong"}]},{"text":" (full protocol: ","type":"text"},{"text":"references/agent-protocol.md","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Target Skill","type":"text","marks":[{"type":"strong"}]},{"text":" must be under ","type":"text"},{"text":"$SKILL_INSTALL_DIR/","type":"text","marks":[{"type":"code_inline"}]},{"text":" (auto-detected: ","type":"text"},{"text":"~/.agents/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"~/.hermes/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":" → default) or a user-specified path","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"jq","type":"text","marks":[{"type":"strong"}]},{"text":" command (all JSON processing depends on it)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"API Reference","type":"text","marks":[{"type":"strong"}]},{"text":": https://console.huaweicloud.com/apiexplorer/#/openapi","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"skill-quality-cli","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — ensured by ","type":"text"},{"text":"bash scripts/ensure_cli.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" (idempotent, skips if present)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Upgrade: run ","type":"text"},{"text":"skill-quality-cli upgrade","type":"text","marks":[{"type":"code_inline"}]},{"text":" manually (no auto-upgrade)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Disable telemetry report: set ","type":"text"},{"text":"SKILL_QUALITY_REPORT=0","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow — Three-Track Eight-Phase","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Tier 1: Single-Skill Unit Testing\n Phase 0: Installation Verification (install/uninstall/reinstall)\n Phase 1: Feature Extraction (metadata + commands + resource types + doc checks)\n Phase 2: Technical Research (CLI→SDK→API three-level availability)\n Phase 3: Test Case Generation (functional TC-F + boundary + negative + API TC-A)\n Phase 4: Real-Environment Execution (read-only automatic + write ops require confirmation)\n\nTier 2: Integration Testing — Real-Environment Orchestration\n Phase 5: Multi-Skill Orchestration (conflict scan / scenario derivation / self-check)\n Phase 6: End-to-End Flow (resource lifecycle: create→query→update→delete)\n\nTier 3: Final Report\n Phase 7: Consolidated Report (merge phase-0~6 JSON, verdict + issues + reasons)","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Full per-phase implementation specs (steps, pass criteria, JSON fields) are in ","type":"text"},{"text":"references/phase-details.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Phases at a Glance","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Name","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Key Points","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verdict","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Install verification","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Directory completeness: mandatory trio (SKILL.md, references folder, iam-policies doc) plus a soft-required scripts folder (pure-CLI skills may lack it; reported but not blocking); install/uninstall/reinstall (same-path / symlink protection)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Directory trio + lifecycle (scripts folder soft-required)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Feature extraction","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata/triggers/commands/capabilities/resource_types + doc_checks (reference consistency, forbidden .bak/.template files)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Commands and triggers non-empty","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Technical research","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CLI→SDK→API three-level availability; pure-local tools auto-marked not_applicable","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Availability count","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Test case generation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Positive + boundary (limit=1) + negative (unknown param) cases; placeholder replacement (/path/to, ./xxx, {region}); template/interactive command filtering","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cases > 0","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Execution","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto execution + output quality verdict (rc=0 empty output → warn); negative-case error quality; doc-gap analysis (doc_gap_issues); missing-dependency marking","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pass/fail/warn/skip","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Orchestration","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Multi-skill: trigger-conflict scan + data-flow candidates; single skill: downgraded self-check (internal ambiguity + write-op ordering)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"conflict scan","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Full flow","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Multi-skill: scenario derivation; single skill: downgraded full functional loop (create→query→update→delete)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scenario steps","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Report","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Merge phase-0~6; verdict + pass_rate + issues_found + per-phase reason; test-report.json/md","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Aggregated verdict","type":"text"}]}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Detailed steps, chain-verification rules, and JSON schemas: ","type":"text"},{"text":"references/phase-details.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"references/output-schema-spec.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"KooCLI Command Format Standard","type":"text"}]},{"type":"paragraph","content":[{"text":"This testing framework uses ","type":"text"},{"text":"bash","type":"text","marks":[{"type":"code_inline"}]},{"text":" scripts as the primary execution mode; it does ","type":"text"},{"text":"NOT","type":"text","marks":[{"type":"strong"}]},{"text":" execute raw ","type":"text"},{"text":"hcloud","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLI commands directly. The ","type":"text"},{"text":"hcloud","type":"text","marks":[{"type":"code_inline"}]},{"text":" command strings shown below are ","type":"text"},{"text":"format templates — placeholder examples, NOT executable commands","type":"text","marks":[{"type":"strong"}]},{"text":": they contain placeholder tokens (","type":"text"},{"text":"\u003cService>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"{region}","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"[--param1=value1 ...]","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc.) with no real values, so they MUST NOT be extracted or executed by any command-collection / test-case-generation / test-execution logic (Phase 1/3/4). Every template line is prefixed with ","type":"text"},{"text":"#","type":"text","marks":[{"type":"code_inline"}]},{"text":" (comment marker) and the section is explicitly marked as non-executable.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Executable Command List:","type":"text","marks":[{"type":"strong"}]},{"text":" the only real executable commands of this skill are the pipeline scripts under the ","type":"text"},{"text":"scripts","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory, invoked via ","type":"text"},{"text":"bash scripts/...","type":"text","marks":[{"type":"code_inline"}]},{"text":" (e.g. ","type":"text"},{"text":"bash scripts/run-test-pipeline.sh --skills \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"bash scripts/tier1/phase-0-install-check.sh --skill \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc. — see Core Commands below). All ","type":"text"},{"text":"hcloud ...","type":"text","marks":[{"type":"code_inline"}]},{"text":" format templates in this section are illustrative placeholders, ","type":"text"},{"text":"not executable commands","type":"text","marks":[{"type":"strong"}]},{"text":"; command-extraction/execution logic must skip them.","type":"text"}]}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Format-template placeholder examples — for reading the format only; never extract or execute as commands (# comment lines, contain \u003cService>/{region} placeholders)","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"hcloud \u003cService> \u003cOperation> --cli-region={region} [--param1=value1 ...]","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n**Format Rules:**\n\n- **Service name** — Follows KooCLI Services (uppercase: ECS, VPC, OBS; title case: CloudPond, IAMAccessAnalyzer)\n- **Operation name** — PascalCase (e.g., ListServersDetails, ListBuckets)\n- **Region** — Always include the `--cli-region` parameter with a real region value (e.g. `cn-north-4`). The token form `--cli-region={region}` is a template placeholder, never a real command.\n- **Parameters** — Parameters use the `--name=value` syntax; the token `--param=value` is a format illustration only, never a real command.\n- **Read-only limit** — Always append `--limit=1` for exploratory queries (real flag value, only meaningful inside an actual `hcloud` command).\n\nFor OBS service, the framework uses `hcloud obs` (obsutil) subsystem:\n\n```bash\n> **⚠️ Mandatory: every `hcloud` command in this skill MUST be wrapped with `skill-quality-cli run --skill-name huawei-cloud-skill-tester -- ` — bare `hcloud` calls are strictly forbidden.**\n\n# OBS format-template placeholder examples — for reading the format only; never extract or execute (# comment lines, contain \u003ccommand>/[args...] placeholders)\n# hcloud obs \u003ccommand> [args...] [options...]","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Commands","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Full Pipeline Run","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Specify skills\nbash scripts/run-test-pipeline.sh --skills \"huawei-cloud-bss-voucher-manage\"\n\n# Specify multiple skills (comma-separated)\nbash scripts/run-test-pipeline.sh --skills \"huawei-cloud-bss-voucher-manage, huawei-cloud-ecs-manage\"\n\n# Scan all installed\nbash scripts/run-test-pipeline.sh --all-installed\n\n# Start from a specific phase (recovery scenarios only)\nbash scripts/run-test-pipeline.sh --skills \"bss-voucher\" --phase 4\n\n# Fresh mode\nbash scripts/run-test-pipeline.sh --skills \"bss-voucher\" --fresh","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Single Phase Run (Debug)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash scripts/tier1/phase-0-install-check.sh --skill \"huawei-cloud-bss-voucher-manage\"\nbash scripts/tier1/phase-1-skill-analysis.sh --skill \"huawei-cloud-bss-voucher-manage\"\nbash scripts/tier2/phase-5-orchestration.sh --skills \"skill-a, skill-b\"\nbash scripts/tier3/phase-7-final-report.sh --skills \"skill-a, skill-b\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Run Multi-Skill Orchestration","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Derive and execute orchestration scenarios for 3 skills\nbash scripts/tier2/phase-5-orchestration.sh --skills \"ecs-manage, vpc-manage, eip-manage\"\n\n# Run E2E lifecycle test for a single skill\nbash scripts/tier2/phase-6-full-flow.sh --skill \"huawei-cloud-rds-intelligent-service\"","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Parameter Confirmation","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Parameter","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Example","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--skills","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mutually exclusive","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Comma-separated skill names or directory names","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"bss-voucher-manage, ecs-manage\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--all-installed","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mutually exclusive","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scan all ","type":"text"},{"text":"huawei-cloud-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" under ","type":"text"},{"text":"$SKILL_INSTALL_DIR/huawei-cloud/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--phase","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Start from a specific Phase (defaults to resume from missing phase)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--phase 0","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--fresh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Archive","type":"text","marks":[{"type":"strong"}]},{"text":" (move, not delete) existing ","type":"text"},{"text":"phase-*.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"phases/archive/\u003ctimestamp>/","type":"text","marks":[{"type":"code_inline"}]},{"text":" and start from scratch. History is preserved; nothing is deleted.","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--output","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Report output directory (default: reports/)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--output ./test-reports","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--skill-path","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skill directory path. When set, ","type":"text"},{"text":"find_skill_path","type":"text","marks":[{"type":"code_inline"}]},{"text":" searches ","type":"text"},{"text":"only here","type":"text","marks":[{"type":"strong"}]},{"text":" (no install-dir fallback)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--skill-path ./skills","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--no-siblings","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 5/6 does ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" auto-scan sibling ","type":"text"},{"text":"huawei-cloud-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" skills in the same directory (scanning ON by default)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--no-siblings","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--sibling-limit \u003cN>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Max sibling-skill count (default 5; ","type":"text"},{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":" = same as ","type":"text"},{"text":"--no-siblings","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--sibling-limit 3","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Environment Variables (Advanced)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Variable","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SKILL_INSTALL_DIR","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"auto-detect: ","type":"text"},{"text":"~/.agents/skills","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"~/.hermes/skills","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"~/.agents/skills","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Where skills are installed by the agent runtime","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SKILL_PATH_HERMES","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alias of ","type":"text"},{"text":"SKILL_INSTALL_DIR","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Legacy name, kept for back-compat","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SKILL_INSTALL_CMD","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"hermes skills","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command for remote skill install/uninstall. Set to ","type":"text"},{"text":"\"\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" to skip real install.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ALLOW_WRITES","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"When ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":", Phase 4/6 write cases actually execute against the live API (default is skip)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HUAWEI_REGION","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cn-north-4","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Huawei Cloud region","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HUAWEI_ACCESS_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"HUAWEI_SECRET_KEY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required for Phase 4/6 SDK/CLI execution; any ","type":"text"},{"text":"HUAWEI*","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"HW*","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"HWC*","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefixed AK/SK env var is also accepted","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Core Documents (Required Reading)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/architecture.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Three-track eight-phase architecture diagram (Mermaid)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/output-schema-spec.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Complete JSON field specification for each phase (including Phase 7 final report schema)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/phase-transition-rules.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Phase transition / fallback / skip rules","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/acceptance-criteria.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Quality gates + 17-item report acceptance checklist","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/verification-method.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — How to manually verify each phase (PowerShell + Git Bash)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/phase-details.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Full per-phase implementation specs (steps, pass criteria, JSON fields)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/agent-protocol.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Credential request protocol (full handling flow when AK/SK is missing)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Supplementary References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/iam-policies.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Minimum IAM permissions required to run the tester","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Templates (JSON Schema)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"templates/phase-report-schema.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" — JSON Schema for ","type":"text"},{"text":"phase-N-summary.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" (N=0..6)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"templates/test-case-schema.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" — JSON Schema for individual test cases (","type":"text"},{"text":"TC-F-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"TC-A-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"OF-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"FF-*","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"templates/scenario-example.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Reference example for Phase 6 multi-skill scenario derivation","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Format","type":"text"}]},{"type":"paragraph","content":[{"text":"All test artifacts go to a ","type":"text"},{"text":"sibling directory of the tested skill","type":"text","marks":[{"type":"strong"}]},{"text":", named ","type":"text"},{"text":"\u003cskill-name>-test-files/","type":"text","marks":[{"type":"code_inline"}]},{"text":". This keeps the skill source dir clean and preserves run history for diff/regression.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"skills/\n├── huawei-cloud-rds-query/ ← skill source (untouched)\n└── huawei-cloud-rds-query-test-files/ ← test artifacts (created on first run, kept across runs)\n ├── phases/\n │ ├── phase-0-summary.json\n │ ├── phase-1-summary.json\n │ ├── ...\n │ └── phase-7-summary.json\n └── reports/\n ├── report-20260724-152309/ ← one subdir per run (timestamped)\n │ ├── test-report.json\n │ └── test-report.md\n ├── report-20260725-090000/\n │ ├── test-report.json\n │ └── test-report.md\n └── ...","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Phase 0~6","type":"text","marks":[{"type":"strong"}]},{"text":" output ","type":"text"},{"text":"phase-N-summary.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"\u003cskill-name>-test-files/phases/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Phase 7","type":"text","marks":[{"type":"strong"}]},{"text":" merges them into ","type":"text"},{"text":"\u003cskill-name>-test-files/reports/report-\u003ctimestamp>/test-report.{json,md}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Test artifacts are preserved","type":"text","marks":[{"type":"strong"}]},{"text":" across runs (no auto-cleanup, ever). ","type":"text"},{"text":"--fresh","type":"text","marks":[{"type":"code_inline"}]},{"text":" does NOT delete — it ","type":"text"},{"text":"archives","type":"text","marks":[{"type":"strong"}]},{"text":" old ","type":"text"},{"text":"phases/*.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"phases/archive/\u003ctimestamp>/","type":"text","marks":[{"type":"code_inline"}]},{"text":" so the chain check resets while history stays intact. ","type":"text"},{"text":"reports/","type":"text","marks":[{"type":"code_inline"}]},{"text":" is always kept.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The test-installed copy","type":"text","marks":[{"type":"strong"}]},{"text":" of the skill in ","type":"text"},{"text":"$SKILL_INSTALL_DIR/","type":"text","marks":[{"type":"code_inline"}]},{"text":" is still uninstalled on exit, so the next run sees a clean install state.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/output-schema-spec.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the JSON schema. Phase 5 and 6 additionally output scenario execution logs with real CLI/SDK responses for auditability.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Best Practices","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Complete Tier 1 before entering Tier 2 to ensure skills are individually functional before orchestration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confirm write operations one by one in Phase 4 and Phase 5/6; do not batch-confirm to avoid misoperations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"With only 1 skill, Phase 5/6 automatically downgrade to single-skill closed loop; no need to manually skip","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When using ","type":"text"},{"text":"--fresh","type":"text","marks":[{"type":"code_inline"}]},{"text":" to reset and rerun, confirm there are no uncleaned test resources","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Review orchestration scenarios before execution to ensure resource dependency order is correct","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Notes","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Three-track eight-phase strictly follows sequential order; chain verification prevents skipping","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"API endpoints are strictly prohibited from being inferred; only obtain from SDK ","type":"text"},{"text":"_http_info","type":"text","marks":[{"type":"code_inline"}]},{"text":" or API Explorer","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Credentials are read from environment variables; hardcoding is prohibited","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If AK/SK is missing, the framework emits the env-var setup template to stderr and exits 77. The Agent MUST output that template to the user verbatim and instruct them to set env vars out-of-band (in their shell / PowerShell $PROFILE). The Agent MUST NEVER ask the user to type or paste AK/SK in chat. Strictly prohibited from silently skipping any step that requires credentials.","type":"text","marks":[{"type":"strong"}]},{"text":" (full protocol: ","type":"text"},{"text":"references/agent-protocol.md","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resources created during testing must be tracked; if any are left behind, output manual cleanup instructions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Orchestration scenarios are auto-derived; user should review and confirm before execution","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Write operations in orchestration scenarios require per-step user confirmation","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Edge Cases","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scenario","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Handling","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skill directory does not exist","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Report error and terminate, output available skill list","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AK/SK environment variables not set","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Framework emits the env-var setup template (with ","type":"text"},{"text":"export HUAWEICLOUD_SDK_AK=\u003cyour-access-key-id>","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"$env:HUAWEICLOUD_SDK_AK=\u003c...>","type":"text","marks":[{"type":"code_inline"}]},{"text":" placeholder snippets) to stderr and exits 77. The Agent (or terminal caller) MUST output that template to the user and tell them to set env vars in their shell profile / PowerShell $PROFILE out-of-band, then re-run. ","type":"text"},{"text":"Never","type":"text","marks":[{"type":"strong"}]},{"text":" ask the user to type or paste AK/SK in chat. Strictly prohibited from silently skipping.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User specifies skill name but not installed in Hermes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--fresh","type":"text","marks":[{"type":"code_inline"}]},{"text":" performs directory-level detection; if not found, report error with guidance","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Some Phase JSON files deleted","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Chain detection → Restart from the deleted Phase","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Network interruption during Phase 4 execution","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Already executed case results are not lost; on rerun, skip passed cases (via ","type":"text"},{"text":"--phase","type":"text","marks":[{"type":"code_inline"}]},{"text":" flag)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User hits Ctrl+C mid-execution","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Already output phase JSON is valid; next time ","type":"text"},{"text":"--resume","type":"text","marks":[{"type":"code_inline"}]},{"text":" will recover from the current phase","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Only 1 skill under test","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 5 → ","type":"text"},{"text":"downgraded_self_check","type":"text","marks":[{"type":"code_inline"}]},{"text":" (single-skill trigger ambiguity scan); Phase 6 → ","type":"text"},{"text":"downgraded_single_skill_flow","type":"text","marks":[{"type":"code_inline"}]},{"text":" (real execution)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User unsatisfied with derived orchestration scenarios","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manually edit the derived scenario or skip it; Phase 5 derivation is metadata-only, not executed","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 4 write op with no ","type":"text"},{"text":"ALLOW_WRITES=1","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skipped with ","type":"text"},{"text":"status=skip","type":"text","marks":[{"type":"code_inline"}]},{"text":", no resource_changes recorded","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Phase 4 hits missing business params (e.g. coupon_id)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Marked ","type":"text"},{"text":"status=warn","type":"text","marks":[{"type":"code_inline"}]},{"text":", surfaces as ","type":"text"},{"text":"manual_test_items","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the report; user must supply real data and retry","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cross-skill data flow mismatch","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Logged in Phase 5 as ","type":"text"},{"text":"data_flow_tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" candidate; not auto-executed","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Orphaned resources detected after E2E flow","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Listed in ","type":"text"},{"text":"phase-6-summary.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" under ","type":"text"},{"text":"cleanup.manual_required","type":"text","marks":[{"type":"code_inline"}]},{"text":" with concrete cleanup commands","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Agent Protocol — Credential Request","type":"text"}]},{"type":"paragraph","content":[{"text":"When Phase 4 or Phase 6 needs to call live Huawei Cloud APIs but cannot find credentials in the environment, the framework does ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" silently skip. It emits a structured request (sentinel line ","type":"text"},{"text":"__HUAWEI_SKILL_TESTER_CRED_REQUEST_v1__","type":"text","marks":[{"type":"code_inline"}]},{"text":" to stderr) and exits with code ","type":"text"},{"text":"77","type":"text","marks":[{"type":"code_inline"}]},{"text":" so the calling agent is forced to surface the need to the user.","type":"text"}]},{"type":"paragraph","content":[{"text":"Core rules (MUST follow):","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On exit 77 or the sentinel → pause the pipeline; do not skip Phase 5/7","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Output the env-var setup template from stderr ","type":"text"},{"text":"verbatim","type":"text","marks":[{"type":"strong"}]},{"text":" to the user; guide them to configure out-of-band in their shell profile / PowerShell $PROFILE (placeholders ","type":"text"},{"text":"\u003cyour-access-key-id>","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"\u003cyour-secret-access-key>","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never","type":"text","marks":[{"type":"strong"}]},{"text":" ask for AK/SK plaintext in the conversation (ask_user / read -p / clipboard round-trip / reading paths such as ~/.hcloud/config.json)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After out-of-band setup, re-run the failed phase; if the user refuses, explicitly mark \"live phases skipped — no credentials\", never mark pass","type":"text"}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Full protocol (6-step response flow, direct terminal mode, example behavior): ","type":"text"},{"text":"references/agent-protocol.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Design Principles","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Chain Verification","type":"text","marks":[{"type":"strong"}]},{"text":" — Each Phase checks the previous phase's JSON to prevent skipping","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Agent-proof","type":"text","marks":[{"type":"strong"}]},{"text":" — Write operations must be confirmed by the user; fake confirmations are not allowed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data-Driven","type":"text","marks":[{"type":"strong"}]},{"text":" — All phases output in JSON format; Phase 7 merges","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Batch Repeatable","type":"text","marks":[{"type":"strong"}]},{"text":" — The same set of skills can be tested repeatedly; --fresh resets","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Real-Environment First","type":"text","marks":[{"type":"strong"}]},{"text":" — All orchestrations and E2E flows execute against real Huawei Cloud; no mocks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Degrade Without Losing Value","type":"text","marks":[{"type":"strong"}]},{"text":" — Single skill does not run empty orchestration phases; degrades to meaningful single-skill lifecycle tests","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resource Safety","type":"text","marks":[{"type":"strong"}]},{"text":" — Resources created during testing must be tracked; if any remain, output clear manual cleanup instructions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Credentials Mandatory","type":"text","marks":[{"type":"strong"}]},{"text":" — If AK/SK is missing, the framework emits the env-var setup template to stderr and exits 77. The Agent MUST output that template to the user and instruct them to set env vars out-of-band. The Agent MUST NEVER ask the user to type or paste AK/SK in chat. Strictly prohibited from silently skipping any step that requires credentials.","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"huawei-cloud-skill-tester","tags":["huawei-cloud","testing","e2e","orchestration","qa"],"author":"@skillopedia","source":{"stars":59,"repo_name":"huaweicloud-skills","origin_url":"https://github.com/huaweicloud/huaweicloud-skills/blob/HEAD/skills/devtools/common/huawei-cloud-skill-tester/SKILL.md","repo_owner":"huaweicloud","body_sha256":"ab9cacb56e0da8a8b4f89c5fcf18fb5e473ba9c5793937066116b8f42b022d35","cluster_key":"7fbddec971beec637c7f59934d94ef69aecda3808ede917e2862908d1cc11a42","clean_bundle":{"format":"clean-skill-bundle-v1","source":"huaweicloud/huaweicloud-skills/skills/devtools/common/huawei-cloud-skill-tester/SKILL.md","text_bytes":327620,"source_commit":"bad25df61c331db250f0cc79f1fa7acee08b3fc8","skills_sh_rank":18302,"attachment_count":27,"text_attachments":27,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/devtools/common/huawei-cloud-skill-tester/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"testing-qa","import_tag":"skills-sh-top20k-2026-11","description":"End-to-end functional testing framework for Huawei Cloud skills — three-tier pipeline covering\nsingle-skill unit testing, multi-skill orchestration, and end-to-end full flow testing.\nEach phase produces structured JSON output with chain verification.\nSupports skill installation validation, functional analysis, CLI→SDK→API feasibility research,\ntest case generation, real-environment execution with resource lifecycle, resource cleanup,\nmulti-skill scenario orchestration, trigger-conflict detection, and consolidated reporting.\nTriggers include: 测试技能, 执行技能测试, 跑测试流程, 技能回归测试,\nskill test, run skill tests, test huawei cloud skill, verify skill,\n测试华为云skill, 全流程测试, 编排测试, 技能完整性检查,\nskill-tester, 跑测试, 回归测试, 组合测试, 多skill编排, verification, e2e.\n"}},"renderedAt":1793671701023}

Huawei Cloud Skill Tester — Three-Track Eight-Phase E2E Testing Pipeline Independent, repeatable Huawei Cloud Skill testing framework. Does not depend on skill-creator; can test any existing Huawei Cloud Skill. Focuses on real-environment functional testing and multi-skill orchestration scenarios . --- Overview This Skill provides a three-track, eight-phase standardized testing pipeline (three-tier layout × 8 phases total: Phase 0-6 execution + Phase 7 final report): | Tier | Phases | Goal | |------|--------|------| | Tier 1: Single-Skill Unit Testing | Phase 0 4 | Verify each skill item by i…