diff --git a/.github/workflows/cost-check.yml b/.github/workflows/cost-check.yml index 5e4248e..c095cf2 100644 --- a/.github/workflows/cost-check.yml +++ b/.github/workflows/cost-check.yml @@ -21,7 +21,7 @@ on: required: false default: "" llm_tokens_used_override: - description: "LLM token 用量覆盖(pending 数据源的注入测试通道)" + description: "LLM token 用量覆盖(优先于计量账本的注入测试通道)" required: false default: "" llm_tokens_quota_override: @@ -49,6 +49,15 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # 归账引擎 sparse checkout(W2-C3 .github#216,ADR-0062):CI-Workflows 是公开仓且 + # 自身有 CI+org-gate(C1),跟 main 消费;升级若破坏 aggregate 会让本检查显式 + # INFRA 红(fail-closed 可见),不会被静默吞掉 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: Cloudbird-Software/CI-Workflows + sparse-checkout: pipeline/metering + path: ciw-metering + persist-credentials: false - name: 用量检查与熔断(exit 1=触发告警/熔断 2=基础设施故障) env: GH_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} @@ -57,5 +66,6 @@ jobs: COST_QUOTA_MINUTES_OVERRIDE: ${{ inputs.quota_minutes_override }} COST_LLM_TOKENS_USED_OVERRIDE: ${{ inputs.llm_tokens_used_override }} COST_LLM_TOKENS_QUOTA_OVERRIDE: ${{ inputs.llm_tokens_quota_override }} + COST_LLM_METERING_PY: ciw-metering/pipeline/metering/metering.py # 归账引擎(ADR-0062 通道) COST_DRY_RUN: ${{ inputs.dry_run }} run: bash governance/cost-check.sh diff --git a/governance/cost-check.sh b/governance/cost-check.sh index 2223d5d..66ce3e7 100644 --- a/governance/cost-check.sh +++ b/governance/cost-check.sh @@ -6,6 +6,9 @@ # ≥ warn_pct(80) → 告警 issue(.github 仓,label cost-budget-warning,同日去重,不硬停) # ≥ hardstop_pct(100)→ org Actions 变量 AUTO_MERGE_DISABLED=true + 撤全部 open PR 的 # auto-merge + P0 issue(label cost-circuit-breaker) +# LLM token 通道(W2-C3 .github#216,ADR-0062):data_source=ciw-metering 时按角色档归账 +# (CI-Workflows metering-ledger 分支 → metering.py aggregate,先验链后归账); +# 链断/拉取失败 = INFRA fail-closed(exit 2),不静默归零不盲熔断。 # 熔断消费点:agent 派发/automerge 前置检查(AGENTS.md 行为契约)+ auto-fix-limit.sh # 每轮机器执法撤 auto-merge。复位仅人工(owner PATCH/DELETE 变量 + P0 issue 留评论); # 本脚本观察到"变量已复位且用量 <100%"后自动关闭 P0 issue(复位留痕=issue 评论历史)。 @@ -67,6 +70,10 @@ try: rows = [("AM_QUOTA", am["quota_per_month"]), ("AM_WARN", am["warn_pct"]), ("AM_STOP", am["hardstop_pct"]), ("LT_QUOTA", lt["quota_per_month"]), ("LT_WARN", lt["warn_pct"]), ("LT_STOP", lt["hardstop_pct"]), ("LT_SOURCE", lt["data_source"]), ("CB_VARIABLE", cb["variable"]), ("CB_RESET_BY", cb["reset_by"])] + # W2-C3(ADR-0062):ciw-metering 数据源定位(data_source 非 ciw-metering 时可为空) + m = lt.get("metering") or {} + rows += [("LT_M_REPO", m.get("repo", "")), ("LT_M_BRANCH", m.get("branch", "")), + ("LT_M_CODE", m.get("code_path", ""))] for kk, vv in rows: vv = str(vv) assert "=" not in vv and "\n" not in vv, f"policy 值含非法字符: {kk}" @@ -79,6 +86,12 @@ while IFS='=' read -r key val; do declare "$key=$val"; done <<< "$POLICY_ENV" for v in AM_QUOTA AM_WARN AM_STOP LT_QUOTA LT_WARN LT_STOP LT_SOURCE CB_VARIABLE CB_RESET_BY; do [[ -n "${!v:-}" ]] || { echo "FATAL: policy 缺 $v" >&2; exit 2; } done +# ciw-metering 数据源须完整声明定位三件套(W2-C3,ADR-0062)——缺一 = 配置面残缺,fail-closed +if [[ "$LT_SOURCE" == "ciw-metering" ]]; then + for v in LT_M_REPO LT_M_BRANCH LT_M_CODE; do + [[ -n "${!v:-}" ]] || { echo "FATAL: data_source=ciw-metering 但 policy llm_tokens.metering 缺 $v" >&2; exit 2; } + done +fi # 环境注入优先(T2 注入式测试通道) AM_QUOTA="${COST_QUOTA_MINUTES_OVERRIDE:-$AM_QUOTA}" LT_QUOTA="${COST_LLM_TOKENS_QUOTA_OVERRIDE:-$LT_QUOTA}" @@ -127,21 +140,93 @@ PCT_MIN=$(pct "$USED_MIN" "$AM_QUOTA") check_num PCT_MIN "Actions 用量百分比" ok "Actions 分钟($YEAR-$MONTH): $USED_MIN / $AM_QUOTA = ${PCT_MIN}%($SRC_MIN)" -# LLM token:data_source=pending 时仅注入通道可测,不触发真实告警(ADR-0040 决策 6) +# ---------- LLM token 用量(W2-C3 .github#216,ADR-0062:ciw-metering 数据源按角色档归账) ---------- +# @w2c3-llm-channel-begin(governance/tests/test-cost-llm-channel.sh 按标记对提取本函数体 +# 离线单测——标记对缺失=测试红,防"测试测影子") +llm_channel_account() { + # → stdout 单行 "DATA当月token角色档json" | "ZERO说明" | "INFRA说明"。 + # 本函数经命令替换调用(子 shell),不直接调 infra/ok(计数会丢)——标签由调用方 + # 在父 shell 落账。env 注入通道(T2):COST_LLM_METERING_DIR=本地账本目录、 + # COST_LLM_METERING_PY=归账引擎(metering.py)路径;缺省走真实数据源 + # (metering-ledger 分支 tarball → metering.py aggregate,先验链后归账)。 + local led="${COST_LLM_METERING_DIR:-}" mpy="${COST_LLM_METERING_PY:-}" out rc=0 + if [[ -z "$led" ]]; then + led=$(mktemp -d) || { printf 'INFRA\tmetering 账本临时目录创建失败\n'; return 0; } + if ! "$GH" api "repos/$LT_M_REPO/tarball/$LT_M_BRANCH" >"$led.tar.gz" 2>/dev/null; then + if "$GH" api "repos/$LT_M_REPO/branches/$LT_M_BRANCH" >/dev/null 2>&1; then + printf 'INFRA\tmetering 账本分支存在但 tarball 拉取失败(%s@%s)\n' "$LT_M_REPO" "$LT_M_BRANCH" + else + printf 'ZERO\t计量账本分支 %s@%s 未建(尚无经 wrapper 的 LLM 调用落账)——当月用量记 0\n' "$LT_M_REPO" "$LT_M_BRANCH" + fi + return 0 + fi + if ! tar -xzf "$led.tar.gz" -C "$led" --strip-components=1 --wildcards \ + "*/$LT_M_CODE/records-*.jsonl" 2>/dev/null; then + printf 'INFRA\tmetering 账本 tar 解包失败(strip-components=1 + %s/records-*.jsonl)\n' "$LT_M_CODE" + return 0 + fi + fi + if [[ -z "$mpy" || ! -f "$mpy" ]]; then + printf 'INFRA\t归账引擎不可用(COST_LLM_METERING_PY=%s——cost-check.yml 须 sparse checkout %s 的 %s)\n' "$mpy" "$LT_M_REPO" "$LT_M_CODE" + return 0 + fi + out=$(python3 "$mpy" aggregate --dir "$led" --since "$(date -u +%Y-%m-01)" --json 2>&1) || rc=$? + if [[ $rc -eq 2 ]]; then + printf 'ZERO\t账本无周片(aggregate: %.200s)——当月用量记 0\n' "$out" + return 0 + fi + if [[ $rc -ne 0 ]]; then + printf 'INFRA\tmetering 归账失败(账本验链不过——不可信数据不入账):%.300s\n' "$out" + return 0 + fi + printf 'DATA\t%s\t%s\n' \ + "$(python3 -c 'import json,sys;print(json.load(sys.stdin)["totals"]["total_tokens"])' <<<"$out")" \ + "$(python3 -c 'import json,sys;print(json.dumps(json.load(sys.stdin)["roles"],ensure_ascii=False,sort_keys=True))' <<<"$out")" +} +# @w2c3-llm-channel-end + PCT_TOK="" +USED_TOK="" +LLM_ROLES="" +LLM_SUMMARY="" if [[ -n "${COST_LLM_TOKENS_USED_OVERRIDE:-}" ]]; then PCT_TOK=$(pct "$COST_LLM_TOKENS_USED_OVERRIDE" "$LT_QUOTA") check_num PCT_TOK "LLM 用量百分比" - ok "LLM token: $COST_LLM_TOKENS_USED_OVERRIDE / $LT_QUOTA = ${PCT_TOK}%(注入——pending 数据源的注入测试通道)" + USED_TOK="$COST_LLM_TOKENS_USED_OVERRIDE"; LLM_ROLES="注入通道" + LLM_SUMMARY="LLM token: $USED_TOK / $LT_QUOTA = ${PCT_TOK}%(注入通道——优先于计量账本)" + ok "$LLM_SUMMARY" elif [[ "$LT_SOURCE" == "pending" ]]; then - ok "LLM token: 数据源 pending(llm-gateway usage 端点未就绪)——仅声明,不参与告警" + LLM_SUMMARY="LLM token: 数据源 pending(回滚形态,ADR-0040 决策 6)——仅声明,不参与告警" + ok "$LLM_SUMMARY" +elif [[ "$LT_SOURCE" == "ciw-metering" ]]; then + LLINE=$(llm_channel_account) || true + IFS=$'\t' read -r LTAG LVAL LREST <<<"$LLINE" + case "$LTAG" in + DATA) + USED_TOK="$LVAL"; LLM_ROLES="$LREST" + check_num USED_TOK "LLM 当月 token(归账)" + PCT_TOK=$(pct "$USED_TOK" "$LT_QUOTA") + check_num PCT_TOK "LLM 用量百分比" + LLM_SUMMARY="LLM token(当月归账): $USED_TOK / $LT_QUOTA = ${PCT_TOK}%(角色档 ${LLM_ROLES:-∅})" + ok "$LLM_SUMMARY" + ;; + ZERO) LLM_SUMMARY="LLM token: $LVAL"; ok "$LLM_SUMMARY" ;; + INFRA) infra "$LVAL" ;; + *) infra "LLM 通道输出不可解析(期望 DATA/ZERO/INFRA 标签):$LLINE" ;; + esac +else + infra "LLM token 数据源未知:$LT_SOURCE(policy cost.llm_tokens.data_source 无此形态)" fi # ---------- 熔断当前状态 ---------- BREAKER_SET=0 -VERR=$("$GH" api "orgs/$ORG/actions/variables/$CB_VARIABLE" --jq .value 2>&1) || true -if [[ "$VERR" == *"true"* && "$VERR" != *"Not Found"* ]]; then - BREAKER_SET=1 +# 读取 rc 单独捕获(W2-C3 顺带修复的潜在缺陷):变量存在且 value=false(人工复位后 +# 的常态)曾被当作"读取失败非 404"落 INFRA——复位确认路径每小时误红。现按 rc 判: +# 0=读到值(true→置位,false/其他→未置位);404=未建;其余=infra fail-closed。 +VCB_RC=0 +VERR=$("$GH" api "orgs/$ORG/actions/variables/$CB_VARIABLE" --jq .value 2>&1) || VCB_RC=$? +if [[ $VCB_RC -eq 0 ]]; then + [[ "$VERR" == *"true"* ]] && BREAKER_SET=1 # 读到值:true=置位;false/其他=未置位(读取成功即终态) elif grep -q "Not Found" <<<"$VERR"; then : else @@ -205,7 +290,7 @@ if [[ "$STOP_MIN" == "True" || "$STOP_TOK" == "True" ]]; then P0_BODY="P0:额度/成本熔断已置位(ADR-0040,运行 $(date -u +%FT%TZ))。 - Actions 分钟($YEAR-$MONTH): $USED_MIN / $AM_QUOTA = ${PCT_MIN}%(阈值 $AM_STOP%)${PCT_TOK:+ -- LLM token: $COST_LLM_TOKENS_USED_OVERRIDE / $LT_QUOTA = ${PCT_TOK}%(阈值 $LT_STOP%,注入通道)} +- ${LLM_SUMMARY:-LLM token: $USED_TOK}(阈值 $LT_STOP%,ADR-0062 归账通道)} - 已执行:org 变量 \`$CB_VARIABLE\`=true;全部 open PR 的 auto-merge 已撤销。 - 效果:agent 派发与 automerge 前置检查将拒绝启动(AGENTS.md);auto-fix-limit 每轮机器执法撤销新 enable。 @@ -231,9 +316,11 @@ if [[ "$STOP_MIN" == "True" || "$STOP_TOK" == "True" ]]; then elif [[ "$WARN_MIN" == "True" || "$WARN_TOK" == "True" ]]; then TRIPPED=1 label_ensure "$GOV_REPO" cost-budget-warning fbca04 - W_TITLE="成本告警:Actions 分钟 $YEAR-$MONTH 用量 ${PCT_MIN}%(阈值 $AM_WARN%)" + W_TITLE="成本告警($YEAR-$MONTH):Actions 分钟 ${PCT_MIN}% / LLM token ${PCT_TOK:--}%(阈值 ${AM_WARN}%)" W_EXISTING=$(gov_open_issues cost-budget-warning | grep -m1 "$YEAR-$MONTH" | cut -f1) - W_BODY="额度告警(ADR-0040,$(date -u +%FT%TZ)):Actions 分钟($YEAR-$MONTH)$USED_MIN / $AM_QUOTA = ${PCT_MIN}%,达 ${AM_WARN}% 告警档——未硬停;达 ${AM_STOP}% 将置 \`$CB_VARIABLE\` 熔断并撤全部 auto-merge。" + W_BODY="额度告警(ADR-0040,$(date -u +%FT%TZ)):Actions 分钟($YEAR-$MONTH)$USED_MIN / $AM_QUOTA = ${PCT_MIN}%,达 ${AM_WARN}% 告警档——未硬停;达 ${AM_STOP}% 将置 \`$CB_VARIABLE\` 熔断并撤全部 auto-merge。 +${LLM_SUMMARY:+$LLM_SUMMARY +}(LLM 阈值 ${LT_WARN}%/${LT_STOP}%,ADR-0062 归账通道)" if [[ -n "$W_EXISTING" ]]; then if ! issue_silent_today "$W_EXISTING"; then mutate "$GH" issue comment "$W_EXISTING" --repo "$GOV_REPO" --body "$W_BODY" >/dev/null 2>&1 || true @@ -242,7 +329,7 @@ elif [[ "$WARN_MIN" == "True" || "$WARN_TOK" == "True" ]]; then mutate "$GH" issue create --repo "$GOV_REPO" --title "$W_TITLE" \ --body "$W_BODY" --label cost-budget-warning >/dev/null 2>&1 || infra "告警 issue 开立失败" fi - act "告警档触发: Actions ${PCT_MIN}% ≥ ${AM_WARN}%(issue 已开/更新)" + act "告警档触发: Actions ${PCT_MIN}% / LLM token ${PCT_TOK:--}%(阈值 ${AM_WARN}%,issue 已开/更新)" else # 用量回落:关闭过期的告警 issue(月度滚动或已回落) for row in $(gov_open_issues cost-budget-warning | cut -f1); do diff --git a/governance/policy/automation-limits.yaml b/governance/policy/automation-limits.yaml index bcdd701..749ebd5 100644 --- a/governance/policy/automation-limits.yaml +++ b/governance/policy/automation-limits.yaml @@ -36,13 +36,20 @@ cost: quota_per_month: 200000000 warn_pct: 80 # ≥80% 开告警 issue(同日去重,不硬停) hardstop_pct: 100 # ≥100% 置熔断 + 撤全部 open PR auto-merge + P0 issue - # LLM token 月预算(数据源 pending:llm-gateway usage 端点就绪后接入,ADR-0040 决策 6; - # pending 期间仅声明 + 注入通道(LLM_TOKENS_USED)可测,不触发真实告警) + # LLM token 月预算(W2-C3 .github#216,ADR-0062:数据源接入——CI-Workflows + # pipeline/metering 计量账本,按 invoke 聚合的 JSONL hash 链;cost-check 拉取 + # metering-ledger 分支经 metering.py aggregate 按角色档归账。链断/归账失败= + # infra fail-closed(exit 2),不静默归零不盲熔断。pending 形态保留为回滚值 + # (ADR-0040 决策 6);注入通道 COST_LLM_TOKENS_USED 仍优先于真实数据源) llm_tokens: quota_per_month: 200000000 warn_pct: 80 hardstop_pct: 100 - data_source: pending + data_source: ciw-metering + metering: # data_source=ciw-metering 时消费(复用本段现有结构,不新建配置面) + repo: Cloudbird-Software/CI-Workflows + branch: metering-ledger + code_path: pipeline/metering # 归账引擎 metering.py 所在(cost-check.yml sparse checkout) circuit_breaker: # 熔断标志:org Actions 变量(visibility=all,全仓 workflow/agent 可读) diff --git a/governance/tests/test-cost-llm-channel.sh b/governance/tests/test-cost-llm-channel.sh new file mode 100644 index 0000000..98c376a --- /dev/null +++ b/governance/tests/test-cost-llm-channel.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# test-cost-llm-channel.sh —— cost-check LLM 预算通道单测(W2-C3 .github#216,ADR-0062) +# +# 从 cost-check.sh 按 @w2c3-llm-channel 标记对提取 llm_channel_account 函数体 +# (不复制实现——防"测试测影子",同 test-ir0002.sh 模式;标记对缺失=fail-closed 红), +# 用桩 metering.py(行为受 STUB_MODE 控制)与桩 gh 断言通道契约: +# DATA 归账成功 → "DATA当月token角色档json"(调用方据此算 PCT_TOK) +# 空账本 aggregate rc=2 → ZERO(账本分支已建但无周片 = 零用量,不是 infra) +# 链断 aggregate rc=3 → INFRA(不可信数据不入账,fail-closed) +# 引擎缺失 / tarball 拉取失败(分支在)/ 分支未建 → INFRA / ZERO +# 真实归账数值(角色档聚合、验链、hash 链)由 CI-Workflows 仓 +# pipeline/metering/selftest T1/T3/T5 覆盖;本测试锁 cost-check 侧消费契约与 +# fail-closed 方向。用法:bash governance/tests/test-cost-llm-channel.sh(零网络零真实 gh) +set -uo pipefail +DIR="$(cd "$(dirname "$0")/.." && pwd)" +FAILS=0 +pass() { echo "PASS $1"; } +fail() { echo "FAIL $1"; FAILS=$((FAILS+1)); } + +# --- 提取被测函数(标记对缺失=fail-closed:测试与实现脱钩即红) --- +SRC="$DIR/cost-check.sh" +[[ -f "$SRC" ]] || { echo "FATAL: cost-check.sh 不存在"; exit 2; } +EXTRACTED=$(awk '/@w2c3-llm-channel-begin/{f=1} f{print} /@w2c3-llm-channel-end/{exit}' "$SRC") +if ! grep -q '^llm_channel_account()' <<<"$EXTRACTED"; then + echo "FATAL: 标记对内未找到 llm_channel_account 定义(提取失效——实现与测试脱钩)"; exit 2 +fi + +# --- 桩环境(函数在命令替换子 shell 外定义;ok/infra 桩防实现漂移误伤主脚本计数) --- +INFRAS=0 +infra() { echo "INFRA $1"; INFRAS=$((INFRAS+1)); } +ok() { echo "OK $1"; } +GH=gh +LT_M_REPO="Cloudbird-Software/CI-Workflows" +LT_M_BRANCH="metering-ledger" +LT_M_CODE="pipeline/metering" +eval "$EXTRACTED" + +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/bin" "$TMP/ledger" +# python3 本地垫片(Windows 商店 stub 环境);CI(ubuntu)python3 直用不受影响 +if ! python3 -c 'import sys' >/dev/null 2>&1; then + printf '#!/usr/bin/env bash\nexec python "$@"\n' >"$TMP/bin/python3"; chmod +x "$TMP/bin/python3" + export PATH="$TMP/bin:$PATH" +fi +# 桩 metering.py:与真实 metering.py aggregate 的退出码契约一致(0=数据 2=无账本 3=链断) +STUB_MODE_PATH="$TMP/mode" +cat >"$TMP/bin/metering.py" <<'EOF' +import os, sys +mode = open(os.environ["STUB_MODE_PATH"], encoding="utf-8").read().strip() +if mode == "data": + print('{"roles": {"probe": {"invokes": 2, "total_tokens": 165}, ' + '"spec-author": {"invokes": 1, "total_tokens": 42}}, ' + '"totals": {"invokes": 3, "total_tokens": 207}}') + sys.exit(0) +if mode == "empty": + print("账本目录无 records-*.jsonl", file=sys.stderr); sys.exit(2) +if mode == "broken": + print("CHAIN records-2026-W34.jsonl:1: record_sha256 重算不符", file=sys.stderr); sys.exit(3) +print(f"未知桩模式 {mode}", file=sys.stderr); sys.exit(9) +EOF + +run_chan() { # 捕获函数输出(真实数据源路径由 gh 桩 + 已 unset 的注入变量控制) + LLINE=$(llm_channel_account) +} +export STUB_MODE_PATH +export COST_LLM_METERING_DIR="$TMP/ledger" + +# 1) DATA:归账成功 → 标签+当月 token+角色档 json +echo data >"$STUB_MODE_PATH" +COST_LLM_METERING_PY="$TMP/bin/metering.py" +run_chan +if [[ "${LLINE%%$'\t'*}" == "DATA" ]] && grep -q '207' <<<"$LLINE" && grep -q '"probe"' <<<"$LLINE"; then + pass "DATA 形态:当月 token=207 + 角色档 json 透传($LLINE)" +else fail "DATA 形态不符:$LLINE"; fi + +# 2) 空账本(分支已建、尚无周片)→ ZERO(零用量不是 infra) +echo empty >"$STUB_MODE_PATH" +run_chan +if [[ "${LLINE%%$'\t'*}" == "ZERO" ]] && grep -q "用量记 0" <<<"$LLINE"; then + pass "空账本 → ZERO(零用量声明)" +else fail "空账本应 ZERO,得到:$LLINE"; fi + +# 3) 链断 → INFRA(不可信数据不入账——fail-closed 方向) +echo broken >"$STUB_MODE_PATH" +run_chan +if [[ "${LLINE%%$'\t'*}" == "INFRA" ]] && grep -q "不可信数据不入账" <<<"$LLINE"; then + pass "链断 → INFRA(归账拒绝,fail-closed)" +else fail "链断应 INFRA,得到:$LLINE"; fi + +# 4) 归账引擎缺失 → INFRA(配置面残缺可见) +COST_LLM_METERING_PY="$TMP/nope.py" +run_chan +if [[ "${LLINE%%$'\t'*}" == "INFRA" ]] && grep -q "归账引擎不可用" <<<"$LLINE"; then + pass "引擎缺失 → INFRA(sparse checkout 缺失可见)" +else fail "引擎缺失应 INFRA,得到:$LLINE"; fi + +# 5) 分支未建(tarball 与 branches 查询都 404)→ ZERO +unset COST_LLM_METERING_DIR COST_LLM_METERING_PY +gh() { return 1; } +run_chan +unset -f gh +if [[ "${LLINE%%$'\t'*}" == "ZERO" ]] && grep -q "未建" <<<"$LLINE"; then + pass "账本分支未建 → ZERO(尚无 LLM 调用落账)" +else fail "分支未建应 ZERO,得到:$LLINE"; fi + +# 6) tarball 拉取失败但分支存在 → INFRA(用量不可知,不盲熔断不静默归零) +gh() { case "$*" in *tarball*) return 1 ;; *) return 0 ;; esac; } +run_chan +unset -f gh +if [[ "${LLINE%%$'\t'*}" == "INFRA" ]] && grep -q "tarball 拉取失败" <<<"$LLINE"; then + pass "tarball 失败+分支在 → INFRA(fail-closed 出口 2 的判定输入)" +else fail "tarball 失败应 INFRA,得到:$LLINE"; fi + +echo "----------------------------------------" +if [[ $FAILS -eq 0 ]]; then echo "test-cost-llm-channel PASS(6/6)"; exit 0; fi +echo "test-cost-llm-channel FAIL:$FAILS 处"; exit 1