diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1664662..833506e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,7 +12,7 @@ body: id: version attributes: label: ipcheck version - placeholder: ipcheck 0.9.0 + placeholder: ipcheck 0.10.0 validations: required: true - type: dropdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b9165..834e375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to ipcheck are documented here. The project follows ## [Unreleased] +## [0.10.0] - 2026-07-24 + +### Changed + +- Replace readiness-score rule v3 with rule v4: AI interaction now contributes + 80 points and engineering transfer contributes 20, with repeated constrained + transfer samples applying conservative score caps. +- Replace the single Cloudflare transfer in each direction with two capped + sizes, average valid rates, cap each request at eight seconds, and expose + confidence plus sample counts in human, Markdown, and JSON reports. +- Bump JSON `schema_version` to 3 for explicit AI-interaction and + engineering-transfer dimensions and transfer-cap metadata. + ## [0.9.0] - 2026-07-23 ### Changed @@ -171,7 +184,8 @@ All notable changes to ipcheck are documented here. The project follows - Homebrew and direct-download packaging. - Median/P95 TTFB, jitter, reference bandwidth, and macOS `networkQuality`. -[Unreleased]: https://github.com/jacklv-coder/ipcheck/compare/v0.9.0...HEAD +[Unreleased]: https://github.com/jacklv-coder/ipcheck/compare/v0.10.0...HEAD +[0.10.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.9.0...v0.10.0 [0.9.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.8.2...v0.9.0 [0.8.2]: https://github.com/jacklv-coder/ipcheck/compare/v0.8.1...v0.8.2 [0.8.1]: https://github.com/jacklv-coder/ipcheck/compare/v0.8.0...v0.8.1 diff --git a/README.md b/README.md index 30d8579..933b310 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ brew upgrade ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.9.0/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.10.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -119,9 +119,9 @@ The Cloudflare samples describe small transfers from the current proxy/network path to Cloudflare. They are not peak-bandwidth tests and do not represent OpenAI, Anthropic, GitHub, or npm throughput. -The 0–100 score is a transparent rule, not a user percentile. The measured AI -service path supplies all positive points; low Cloudflare samples can only make -a small deduction. See +The 0–100 score is a transparent rule, not a user percentile. AI interaction +contributes 80 points and engineering transfer contributes 20. Repeated +constrained transfer samples can also cap an otherwise high score. See [Scoring and result rules](docs/scoring.md) for the exact calculation. ## Claude Code gateway example diff --git a/README.zh-CN.md b/README.zh-CN.md index db6298e..59f75e7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -34,7 +34,7 @@ brew upgrade ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.9.0/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.10.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -112,8 +112,8 @@ TTFB 来自无凭据协议请求,反映 DNS、代理、TLS、网络与网关 Cloudflare 样本反映的是当前代理/网络到 Cloudflare 的小文件传输表现,不是 峰值宽带测速,也不代表 OpenAI、Anthropic、GitHub 或 npm 的吞吐。 -0–100 分是透明的规则评分,不代表用户百分位。实际 AI 服务路径提供全部正向 -分数,Cloudflare 样本偏低时只会小幅扣分。精确计算方式见 +0–100 分是透明的规则评分,不代表用户百分位。AI 交互占 80 分,工程传输占 +20 分;重复出现明显受限的传输样本还会限制原本较高的总分。精确计算方式见 [评分与结论规则](docs/scoring.zh-CN.md)。 ## Claude Code 网关示例 diff --git a/assets/ipcheck-demo-zh.gif b/assets/ipcheck-demo-zh.gif index 28c48c6..e2f00f8 100644 Binary files a/assets/ipcheck-demo-zh.gif and b/assets/ipcheck-demo-zh.gif differ diff --git a/assets/ipcheck-demo.gif b/assets/ipcheck-demo.gif index 3f0c678..cf0194f 100644 Binary files a/assets/ipcheck-demo.gif and b/assets/ipcheck-demo.gif differ diff --git a/assets/ipcheck-preview-zh.png b/assets/ipcheck-preview-zh.png index a844b5b..9a48d79 100644 Binary files a/assets/ipcheck-preview-zh.png and b/assets/ipcheck-preview-zh.png differ diff --git a/assets/ipcheck-preview.png b/assets/ipcheck-preview.png index f4aecba..38d79e0 100644 Binary files a/assets/ipcheck-preview.png and b/assets/ipcheck-preview.png differ diff --git a/bin/ipcheck b/bin/ipcheck index 6980488..a364888 100755 --- a/bin/ipcheck +++ b/bin/ipcheck @@ -10,16 +10,19 @@ unset LC_ALL LC_NUMERIC=C export LC_NUMERIC -VERSION="0.9.0" -SCHEMA_VERSION=2 +VERSION="0.10.0" +SCHEMA_VERSION=3 SAMPLES=3 TIMEOUT=15 SERVICE_MODE="auto" CLI_ENDPOINTS="" CLI_ENDPOINTS_SET=0 -BANDWIDTH_URL="https://speed.cloudflare.com/__down?bytes=2000000" +BANDWIDTH_URL_BASE="https://speed.cloudflare.com/__down?bytes=" +DOWNLOAD_SAMPLE_BYTES="262144 2000000" UPLOAD_URL="https://speed.cloudflare.com/__up" -UPLOAD_BYTES=1000000 +UPLOAD_SAMPLE_BYTES="131072 1000000" +TRANSFER_SAMPLE_TOTAL=2 +TRANSFER_TIMEOUT_MAX=8 MODE="human" RUN_BANDWIDTH=1 RUN_UPLOAD=1 @@ -44,8 +47,8 @@ Options: --samples N Requests per endpoint (default: 3). --timeout SECONDS Per-request timeout (default: 15). --quick Use 1 sample, an 8s timeout, and skip reference transfers. - --no-bandwidth Skip capped Cloudflare reference transfers. - --no-upload Skip only the 1 MB reference upload test. + --no-bandwidth Skip multi-size Cloudflare reference transfers. + --no-upload Skip only the reference upload samples. --system Also run macOS networkQuality when available. --color Always use terminal colors in human output. --no-color Disable terminal colors. @@ -178,6 +181,9 @@ case "$LANG_MODE" in *) die "language must be auto, en, or zh" ;; esac +TRANSFER_TIMEOUT="$TIMEOUT" +[ "$TRANSFER_TIMEOUT" -le "$TRANSFER_TIMEOUT_MAX" ] || TRANSFER_TIMEOUT="$TRANSFER_TIMEOUT_MAX" + UI_LANG="$LANG_MODE" if [ "$UI_LANG" = "auto" ]; then locale_hint="$IPCHECK_SYSTEM_LOCALE" @@ -1009,42 +1015,59 @@ bandwidth_rating() { if [ "$direction" = "download" ]; then awk -v speed="$speed" 'BEGIN { mbps = sprintf("%.1f", speed * 8 / 1000000) + 0 - if (mbps >= 25) print "fast" - else if (mbps >= 5) print "adequate" - else print "slow" + if (mbps >= 10) print "fast" + else if (mbps >= 3) print "adequate" + else if (mbps >= 1) print "slow" + else print "severe" }' else awk -v speed="$speed" 'BEGIN { mbps = sprintf("%.1f", speed * 8 / 1000000) + 0 - if (mbps >= 10) print "fast" - else if (mbps >= 2) print "adequate" - else print "slow" + if (mbps >= 5) print "fast" + else if (mbps >= 1) print "adequate" + else if (mbps >= 0.3) print "slow" + else print "severe" }' fi } localized_bandwidth_rating() { case "$UI_LANG:$1" in - zh:fast) printf '样本较高' ;; - zh:adequate) printf '样本一般' ;; - zh:slow) printf '样本偏低' ;; + zh:fast) printf '传输舒适' ;; + zh:adequate) printf '轻度受限' ;; + zh:slow) printf '明显受限' ;; + zh:severe) printf '严重受限' ;; zh:partial) printf '未完成估算' ;; zh:unavailable) printf '不可用' ;; - *:fast) printf 'HIGHER SAMPLE' ;; - *:adequate) printf 'MODERATE SAMPLE' ;; - *:slow) printf 'LOW SAMPLE' ;; + *:fast) printf 'COMFORTABLE' ;; + *:adequate) printf 'MILDLY LIMITED' ;; + *:slow) printf 'CONSTRAINED' ;; + *:severe) printf 'SEVERELY LIMITED' ;; *:partial) printf 'INCOMPLETE ESTIMATE' ;; *) printf 'UNAVAILABLE' ;; esac } +localized_transfer_confidence() { + case "$UI_LANG:$1" in + zh:high) printf '高' ;; + zh:medium) printf '中' ;; + zh:low) printf '低' ;; + zh:unavailable|zh:unmeasured) printf '未测量' ;; + *:high) printf 'HIGH' ;; + *:medium) printf 'MEDIUM' ;; + *:low) printf 'LOW' ;; + *) printf 'UNMEASURED' ;; + esac +} + partial_sample_note() { [ "$1" = "partial" ] || return 0 case "$2:$UI_LANG" in - human:zh) printf ' · 未完成样本 -1' ;; - human:*) printf ' · incomplete sample -1' ;; - markdown:zh) printf ',未完成样本 -1' ;; - markdown:*) printf ', incomplete sample -1' ;; + human:zh) printf ' · 可信度较低' ;; + human:*) printf ' · lower confidence' ;; + markdown:zh) printf ',可信度较低' ;; + markdown:*) printf ', lower confidence' ;; esac } @@ -1126,50 +1149,106 @@ bandwidth_code=000 bandwidth_valid=0 bandwidth_bytes=0 bandwidth_complete=false -bandwidth_exit=0 +bandwidth_sample_count=0 +bandwidth_complete_count=0 +bandwidth_timeout_count=0 +download_confirmed_severe=0 +download_confirmed_constrained=0 +download_sample_state="unavailable" download_rating="unavailable" download_speed_rating="unavailable" if [ "$RUN_BANDWIDTH" -eq 1 ]; then if [ "$UI_LANG" = "zh" ]; then - progress_note "●" "正在测试代理链路下行样本(2 MB)…" + progress_note "●" "正在测试代理链路下行(2 个不同大小的样本,最大 2 MB)…" else - progress_note "●" "Testing proxy-path download sample (2 MB)…" - fi - bandwidth_metrics_file="$WORK_DIR/bandwidth-metrics.txt" - curl_for_service "custom" --proto '=https' --http1.1 -sS -o /dev/null \ - -A "ipcheck/$VERSION" \ - --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \ - -w '%{http_code}\t%{size_download}\t%{speed_download}' \ - --url "$BANDWIDTH_URL" > "$bandwidth_metrics_file" 2>/dev/null - bandwidth_exit=$? - bandwidth_metrics=$(< "$bandwidth_metrics_file") - if [ -n "$bandwidth_metrics" ]; then - IFS="$TAB" read -r bandwidth_code bandwidth_bytes bandwidth_speed < "$bandwidth_samples_file" + download_index=0 + for expected_bytes in $DOWNLOAD_SAMPLE_BYTES; do + download_index=$((download_index + 1)) + bandwidth_metrics_file="$WORK_DIR/bandwidth-metrics-$download_index.txt" + curl_for_service "custom" --proto '=https' --http1.1 -sS -o /dev/null \ + -A "ipcheck/$VERSION" \ + --connect-timeout "$TRANSFER_TIMEOUT" --max-time "$TRANSFER_TIMEOUT" \ + -w '%{http_code}\t%{size_download}\t%{speed_download}' \ + --url "${BANDWIDTH_URL_BASE}${expected_bytes}" > "$bandwidth_metrics_file" 2>/dev/null + sample_exit=$? + sample_code=000 + sample_bytes=0 + sample_speed=0 + sample_valid=0 + sample_complete=0 + sample_rating="unavailable" + bandwidth_metrics=$(< "$bandwidth_metrics_file") + if [ -n "$bandwidth_metrics" ]; then + IFS="$TAB" read -r sample_code sample_bytes sample_speed <= 65536 && speed > 0) }'; then - bandwidth_valid=1 - [ "$bandwidth_exit" -ne 0 ] || bandwidth_complete=true - fi - ;; - esac + case "$sample_code" in + 2[0-9][0-9]) + if awk -v bytes="$sample_bytes" -v speed="$sample_speed" 'BEGIN { exit !(bytes >= 32768 && speed > 0) }'; then + sample_valid=1 + sample_rating=$(bandwidth_rating download "$sample_speed") + if [ "$sample_exit" -eq 0 ] && awk -v bytes="$sample_bytes" -v expected="$expected_bytes" 'BEGIN { exit !(bytes >= expected) }'; then + sample_complete=1 + fi + fi + ;; + esac + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$expected_bytes" "$sample_exit" "$sample_code" "$sample_bytes" "$sample_speed" \ + "$sample_valid" "$sample_complete" "$sample_rating" >> "$bandwidth_samples_file" + done + bandwidth_summary=$(awk -F '\t' ' + $3 != "000" && observed_code == "" { observed_code = $3 } + $6 == 1 { + if (code == "") code = $3 + valid++ + bytes += $4 + speed += $5 + if ($8 == "severe") severe++ + if ($8 == "severe" || $8 == "slow") constrained++ + } + $7 == 1 { complete++ } + $2 == 28 { timed_out++ } + END { + if (code == "") code = observed_code + if (code == "") code = "000" + average = valid ? speed / valid : 0 + print code "\t" bytes + 0 "\t" average + 0 "\t" valid + 0 "\t" complete + 0 \ + "\t" (valid >= 2 && severe == valid ? 1 : 0) \ + "\t" (valid >= 2 && constrained == valid ? 1 : 0) \ + "\t" timed_out + 0 + }' "$bandwidth_samples_file") + IFS="$TAB" read -r bandwidth_code bandwidth_bytes bandwidth_speed bandwidth_sample_count \ + bandwidth_complete_count download_confirmed_severe download_confirmed_constrained \ + bandwidth_timeout_count < "$upload_file" 2>/dev/null; then - upload_metrics_file="$WORK_DIR/upload-metrics.txt" - curl_for_service "custom" --proto '=https' --http1.1 -sS -o /dev/null \ - -A "ipcheck/$VERSION" -X POST -H 'content-type: application/octet-stream' -H 'Expect:' \ - --data-binary "@$upload_file" \ - --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \ - -w '%{http_code}\t%{size_upload}\t%{speed_upload}' \ - --url "$UPLOAD_URL" > "$upload_metrics_file" 2>/dev/null - upload_exit=$? - upload_metrics=$(< "$upload_metrics_file") - if [ -n "$upload_metrics" ]; then - IFS="$TAB" read -r upload_code upload_bytes upload_speed < "$upload_samples_file" + upload_index=0 + for expected_bytes in $UPLOAD_SAMPLE_BYTES; do + upload_index=$((upload_index + 1)) + upload_file="$WORK_DIR/upload-${expected_bytes}.bin" + sample_exit=1 + sample_code=000 + sample_bytes=0 + sample_speed=0 + sample_valid=0 + sample_complete=0 + sample_rating="unavailable" + if head -c "$expected_bytes" /dev/zero > "$upload_file" 2>/dev/null; then + upload_metrics_file="$WORK_DIR/upload-metrics-$upload_index.txt" + curl_for_service "custom" --proto '=https' --http1.1 -sS -o /dev/null \ + -A "ipcheck/$VERSION" -X POST -H 'content-type: application/octet-stream' -H 'Expect:' \ + --data-binary "@$upload_file" \ + --connect-timeout "$TRANSFER_TIMEOUT" --max-time "$TRANSFER_TIMEOUT" \ + -w '%{http_code}\t%{size_upload}\t%{speed_upload}' \ + --url "$UPLOAD_URL" > "$upload_metrics_file" 2>/dev/null + sample_exit=$? + upload_metrics=$(< "$upload_metrics_file") + if [ -n "$upload_metrics" ]; then + IFS="$TAB" read -r sample_code sample_bytes sample_speed <= 65536 && speed > 0) }'; then - upload_valid=1 - [ "$upload_exit" -ne 0 ] || upload_complete=true - fi - ;; - esac + case "$sample_code" in + 2[0-9][0-9]) + if awk -v bytes="$sample_bytes" -v speed="$sample_speed" 'BEGIN { exit !(bytes >= 32768 && speed > 0) }'; then + sample_valid=1 + sample_rating=$(bandwidth_rating upload "$sample_speed") + if [ "$sample_exit" -eq 0 ] && awk -v bytes="$sample_bytes" -v expected="$expected_bytes" 'BEGIN { exit !(bytes >= expected) }'; then + sample_complete=1 + fi + fi + ;; + esac + fi + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$expected_bytes" "$sample_exit" "$sample_code" "$sample_bytes" "$sample_speed" \ + "$sample_valid" "$sample_complete" "$sample_rating" >> "$upload_samples_file" + done + upload_summary=$(awk -F '\t' ' + $3 != "000" && observed_code == "" { observed_code = $3 } + $6 == 1 { + if (code == "") code = $3 + valid++ + bytes += $4 + speed += $5 + if ($8 == "severe") severe++ + if ($8 == "severe" || $8 == "slow") constrained++ + } + $7 == 1 { complete++ } + $2 == 28 { timed_out++ } + END { + if (code == "") code = observed_code + if (code == "") code = "000" + average = valid ? speed / valid : 0 + print code "\t" bytes + 0 "\t" average + 0 "\t" valid + 0 "\t" complete + 0 \ + "\t" (valid >= 2 && severe == valid ? 1 : 0) \ + "\t" (valid >= 2 && constrained == valid ? 1 : 0) \ + "\t" timed_out + 0 + }' "$upload_samples_file") + IFS="$TAB" read -r upload_code upload_bytes upload_speed upload_sample_count \ + upload_complete_count upload_confirmed_severe upload_confirmed_constrained \ + upload_timeout_count < 当前代理到 Cloudflare 的小文件样本;不是峰值宽带测速,也不代表 AI API、GitHub 或 npm 的吞吐。\n\n' - printf '| 方向 | 样本速率 | 样本判断 | 测试数据 |\n|---|---:|---|---|\n' - if [ "$bandwidth_valid" -eq 1 ]; then printf '| 下行 | %s | %s | Cloudflare,最多 2 MB |\n' "$(format_mbps "$bandwidth_speed")" "$(localized_bandwidth_rating "$download_rating")"; else printf '| 下行 | — | 不可用或已跳过 | — |\n'; fi - if [ "$upload_valid" -eq 1 ]; then printf '| 上行 | %s | %s | Cloudflare,最多 1 MB 零字节 |\n' "$(format_mbps "$upload_speed")" "$(localized_bandwidth_rating "$upload_rating")"; else printf '| 上行 | — | 不可用或已跳过 | — |\n'; fi - printf '\n- 参考路径:%s%s%s\n- 提示:%s\n' '`' "$proxy_text" '`' "$bandwidth_advice" + printf '| 方向 | 样本速率 | 工程传输分 | 判断 | 测试数据 |\n|---|---:|---:|---|---|\n' + if [ "$bandwidth_valid" -eq 1 ]; then printf '| 下行 | %s | %s/10 | %s | Cloudflare,%s/%s 有效,最大 2 MB |\n' "$(format_mbps "$bandwidth_speed")" "$download_score" "$(localized_bandwidth_rating "$download_rating")" "$bandwidth_sample_count" "$TRANSFER_SAMPLE_TOTAL"; else printf '| 下行 | — | 10/10(中性) | 不可用或已跳过 | — |\n'; fi + if [ "$upload_valid" -eq 1 ]; then printf '| 上行 | %s | %s/10 | %s | Cloudflare,%s/%s 有效,最大 1 MB 零字节 |\n' "$(format_mbps "$upload_speed")" "$upload_score" "$(localized_bandwidth_rating "$upload_rating")" "$upload_sample_count" "$TRANSFER_SAMPLE_TOTAL"; else printf '| 上行 | — | 10/10(中性) | 不可用或已跳过 | — |\n'; fi + printf '\n- 传输可信度:**%s**\n- 参考路径:%s%s%s\n- 提示:%s\n' "$(localized_transfer_confidence "$transfer_confidence")" '`' "$proxy_text" '`' "$bandwidth_advice" else printf '\n## Proxy-path reference transfer\n\n' printf '> Small-transfer samples from the current proxy to Cloudflare; not a peak bandwidth test and not AI API, GitHub, or npm throughput.\n\n' - printf '| Direction | Sample rate | Sample assessment | Test data |\n|---|---:|---|---|\n' - if [ "$bandwidth_valid" -eq 1 ]; then printf '| Down | %s | %s | Cloudflare, up to 2 MB |\n' "$(format_mbps "$bandwidth_speed")" "$(localized_bandwidth_rating "$download_rating")"; else printf '| Down | — | unavailable or skipped | — |\n'; fi - if [ "$upload_valid" -eq 1 ]; then printf '| Up | %s | %s | Cloudflare, up to 1 MB zero-filled |\n' "$(format_mbps "$upload_speed")" "$(localized_bandwidth_rating "$upload_rating")"; else printf '| Up | — | unavailable or skipped | — |\n'; fi - printf '\n- Reference path: %s%s%s\n- Note: %s\n' '`' "$proxy_text" '`' "$bandwidth_advice" + printf '| Direction | Sample rate | Engineering points | Assessment | Test data |\n|---|---:|---:|---|---|\n' + if [ "$bandwidth_valid" -eq 1 ]; then printf '| Down | %s | %s/10 | %s | Cloudflare, %s/%s valid, up to 2 MB |\n' "$(format_mbps "$bandwidth_speed")" "$download_score" "$(localized_bandwidth_rating "$download_rating")" "$bandwidth_sample_count" "$TRANSFER_SAMPLE_TOTAL"; else printf '| Down | — | 10/10 (neutral) | unavailable or skipped | — |\n'; fi + if [ "$upload_valid" -eq 1 ]; then printf '| Up | %s | %s/10 | %s | Cloudflare, %s/%s valid, up to 1 MB zero-filled |\n' "$(format_mbps "$upload_speed")" "$upload_score" "$(localized_bandwidth_rating "$upload_rating")" "$upload_sample_count" "$TRANSFER_SAMPLE_TOTAL"; else printf '| Up | — | 10/10 (neutral) | unavailable or skipped | — |\n'; fi + printf '\n- Transfer confidence: **%s**\n- Reference path: %s%s%s\n- Note: %s\n' "$(localized_transfer_confidence "$transfer_confidence")" '`' "$proxy_text" '`' "$bandwidth_advice" fi if [ -n "$system_throughput" ]; then printf "macOS networkQuality: download **%s**" "$(format_bits_mbps "$system_throughput")" @@ -2111,16 +2376,18 @@ render_json() { warnings_json="$warnings_json$separator\"$(json_escape "$warning")\"" separator="," done < "$WARNINGS_FILE" - printf '{"schema_version":%s,"version":"%s","timestamp":"%s","proxy":"%s","result":"%s","reason":"%s","developer_readiness":{"ready":%s,"level":"%s","score":%s,"score_label":"%s","score_method":"rule_v3","score_breakdown":{"scored_service":"%s","service_path":%s,"availability":%s,"median_ttfb":%s,"p95":%s,"jitter":%s,"download":%s,"download_speed":%s,"download_partial_penalty":%s,"upload":%s,"upload_speed":%s,"upload_partial_penalty":%s,"pre_cap_total":%s,"verdict_cap":%s},"summary":"%s","recommendation":"%s"},"reachable_endpoints":%s,"primary_success_rate_pct":%s,"primary_ttfb_median_ms":%s,"primary_jitter_ms":%s,"privacy":{"credentials_used":false,"billable_requests":false,"upload_payload":"zero-filled"},"warnings":[%s],"bandwidth":{"enabled":%s,"available":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"path":"%s","scope":"cloudflare_reference_path","method":"single_capped_transfer","represents_api_path":false,"represents_peak_bandwidth":false,"download":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"},"upload":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"}},"system":{"enabled":%s,"interface":"%s","base_rtt_ms":%s,"download_bits_per_second":%s,"upload_bits_per_second":%s,"proxy_state":"%s"},"services":[%s],"endpoints":[%s]}\n' \ + printf '{"schema_version":%s,"version":"%s","timestamp":"%s","proxy":"%s","result":"%s","reason":"%s","developer_readiness":{"ready":%s,"level":"%s","score":%s,"score_label":"%s","score_method":"rule_v4","dimensions":{"ai_interaction":{"score":%s,"max_score":80,"label":"%s"},"engineering_transfer":{"score":%s,"max_score":20,"label":"%s","confidence":"%s"}},"score_breakdown":{"scored_service":"%s","service_path":%s,"service_path_max":80,"availability":%s,"median_ttfb":%s,"p95":%s,"jitter":%s,"download":%s,"download_speed":%s,"download_partial_penalty":%s,"upload":%s,"upload_speed":%s,"upload_partial_penalty":%s,"transfer_score":%s,"pre_cap_total":%s,"service_verdict_cap":%s,"transfer_verdict_cap":%s,"verdict_cap":%s},"summary":"%s","recommendation":"%s"},"reachable_endpoints":%s,"primary_success_rate_pct":%s,"primary_ttfb_median_ms":%s,"primary_jitter_ms":%s,"privacy":{"credentials_used":false,"billable_requests":false,"upload_payload":"zero-filled"},"warnings":[%s],"bandwidth":{"enabled":%s,"available":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"path":"%s","scope":"cloudflare_reference_path","method":"multi_size_capped_transfer","sample_strategy":"two_sizes_per_direction","confidence":"%s","represents_api_path":false,"represents_peak_bandwidth":false,"download":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s","samples_total":%s,"samples_valid":%s,"samples_complete":%s,"samples_timed_out":%s},"upload":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s","samples_total":%s,"samples_valid":%s,"samples_complete":%s,"samples_timed_out":%s}},"system":{"enabled":%s,"interface":"%s","base_rtt_ms":%s,"download_bits_per_second":%s,"upload_bits_per_second":%s,"proxy_state":"%s"},"services":[%s],"endpoints":[%s]}\n' \ "$SCHEMA_VERSION" "$VERSION" "$timestamp" "$(json_escape "$proxy_text")" "$overall" "$(json_escape "$overall_reason")" \ "$readiness_ready" "$readiness_level" "$readiness_score" "$(json_escape "$readiness_score_label_en")" \ + "$score_service_path" "$(json_escape "$interaction_score_label_en")" "$transfer_score" "$(json_escape "$transfer_score_label_en")" "$transfer_confidence" \ "$(json_escape "$score_service")" "$score_service_path" "$score_availability" "$score_median_ttfb" "$score_p95" "$score_jitter" \ - "$download_score" "$download_speed_points" "$download_partial_penalty" "$upload_score" "$upload_speed_points" "$upload_partial_penalty" "$readiness_score_before_cap" "$readiness_score_cap" \ + "$download_score" "$download_speed_points" "$download_partial_penalty" "$upload_score" "$upload_speed_points" "$upload_partial_penalty" "$transfer_score" "$readiness_score_before_cap" "$service_verdict_cap" "$transfer_verdict_cap" "$readiness_score_cap" \ "$(json_escape "$readiness_summary_en")" "$(json_escape "$readiness_recommendation_en")" \ "$reachable_count" "$primary_success_rate" "$primary_ttfb" "$primary_jitter" "$warnings_json" \ "$([ "$RUN_BANDWIDTH" -eq 1 ] && printf true || printf false)" "$legacy_bandwidth_available" "$legacy_bandwidth_code" "$legacy_bandwidth_bytes" "$legacy_bandwidth_speed" "$(json_escape "$proxy_text")" \ - "$([ "$RUN_BANDWIDTH" -eq 1 ] && printf true || printf false)" "$([ "$bandwidth_valid" -eq 1 ] && printf true || printf false)" "$bandwidth_complete" "$bandwidth_code" "$bandwidth_bytes" "$bandwidth_speed" "$download_mbps" "$download_rating" \ - "$([ "$RUN_UPLOAD" -eq 1 ] && printf true || printf false)" "$([ "$upload_valid" -eq 1 ] && printf true || printf false)" "$upload_complete" "$upload_code" "$upload_bytes" "$upload_speed" "$upload_mbps" "$upload_rating" \ + "$transfer_confidence" \ + "$([ "$RUN_BANDWIDTH" -eq 1 ] && printf true || printf false)" "$([ "$bandwidth_valid" -eq 1 ] && printf true || printf false)" "$bandwidth_complete" "$bandwidth_code" "$bandwidth_bytes" "$bandwidth_speed" "$download_mbps" "$download_rating" "$download_samples_total" "$bandwidth_sample_count" "$bandwidth_complete_count" "$bandwidth_timeout_count" \ + "$([ "$RUN_UPLOAD" -eq 1 ] && printf true || printf false)" "$([ "$upload_valid" -eq 1 ] && printf true || printf false)" "$upload_complete" "$upload_code" "$upload_bytes" "$upload_speed" "$upload_mbps" "$upload_rating" "$upload_samples_total" "$upload_sample_count" "$upload_complete_count" "$upload_timeout_count" \ "$([ -n "${system_throughput}${system_upload_throughput}" ] && printf true || printf false)" "$(json_escape "$system_interface")" "${system_rtt:-0}" "${system_throughput:-0}" "${system_upload_throughput:-0}" "$(json_escape "$system_proxy_state")" \ "$services_json" "$endpoints_json" } diff --git a/demo/render_demo.py b/demo/render_demo.py index 80dd172..53d9839 100644 --- a/demo/render_demo.py +++ b/demo/render_demo.py @@ -178,10 +178,11 @@ def demo_output(language: str, scenario: str) -> list[str]: " 1", " 0/100", " █", + " AI 交互", " 评分构成", - " 服务链路", - " 下行参考样本", - " 上行参考样本", + " AI 交互", + " 下行工程传输", + " 上行工程传输", " 当前网络", " 可以正常开发", " 建议先", @@ -220,10 +221,11 @@ def demo_output(language: str, scenario: str) -> list[str]: " 1", " 0/100", " █", + " AI interaction", " Score breakdown", - " Service path", - " Download reference", - " Upload reference", + " AI interaction", + " Download engineering", + " Upload engineering", " This network", " You can work normally", " Switch proxy routes", @@ -340,7 +342,11 @@ def frame(command: str, lines: list[str], language: str) -> Image.Image: draw.text((76, 98), command, font=body_font, fill=TEXT) max_lines = 28 - visible = wrap_lines(draw, lines, body_font)[-max_lines:] + wrapped = wrap_lines(draw, lines, body_font) + if len(wrapped) > max_lines: + visible = wrapped[:10] + [" ⋯"] + wrapped[-17:] + else: + visible = wrapped score_color = GREEN for candidate in lines: score_match = re.search(r"\b(\d+)/100\b", candidate) diff --git a/docs/network.md b/docs/network.md index 3395a54..184058e 100644 --- a/docs/network.md +++ b/docs/network.md @@ -72,16 +72,19 @@ unauthenticated protocol request. It never reads `ANTHROPIC_AUTH_TOKEN`. The service table measures time to first byte and jitter; it is not a transfer speed test. The separate reference-transfer section: -- downloads one capped sample of at most 2 MB from Cloudflare; -- uploads one capped sample of at most 1 MB of zero-filled data; +- downloads two capped samples (256 KiB and 2 MB) from Cloudflare; +- uploads two capped zero-filled samples (128 KiB and 1 MB); - uses the reported proxy/network path; -- treats incomplete transfers as estimates; -- never adds readiness points for a high result. +- limits each transfer request to at most eight seconds; +- averages valid sample rates and reports sample confidence; +- contributes 20 points to the readiness score, with repeated low results + applying a score cap. `https://speed.cloudflare.com/__down` is the download API used by [Cloudflare's official speed-test engine](https://github.com/cloudflare/speedtest). -The official engine combines repeated transfers at multiple sizes; ipcheck's -single capped transfer deliberately does not reproduce that methodology. +The official engine performs a broader adaptive benchmark. ipcheck uses only +two small sizes per direction to reduce runtime and data usage; it deliberately +does not reproduce Cloudflare's full speed-test methodology. The sample answers a narrow question: how a small transfer to Cloudflare behaved on the current proxy/network path at that moment. It is not: @@ -90,10 +93,11 @@ behaved on the current proxy/network path at that moment. It is not: - a measurement of OpenAI, Anthropic, GitHub, or npm throughput; - a substitute for the service-specific reachability and TTFB probes. -A complete low sample can deduct two readiness points per direction. A high or -moderate sample adds no points, an unavailable/skipped sample has no effect, -and an incomplete sample deducts one point per direction. This keeps the -reference signal secondary to the actual AI service path. +The two directions form the 20-point engineering-transfer dimension. A +repeated constrained result can cap an otherwise high total; unavailable or +skipped directions remain neutral and are labelled unmeasured. This gives +transfer performance meaningful weight while keeping the actual AI service +path as the primary signal. See [Scoring and result rules](scoring.md). Use `--no-upload` to skip upload only, or `--no-bandwidth` to skip both. The optional `--system` flag runs macOS `networkQuality`, which may transfer diff --git a/docs/network.zh-CN.md b/docs/network.zh-CN.md index 0f8a892..91ef443 100644 --- a/docs/network.zh-CN.md +++ b/docs/network.zh-CN.md @@ -67,16 +67,17 @@ provider 给出结论。该 provider 会显示为“已跳过”;没有其他 服务表测量的是首字节延迟和抖动,并不是传输测速。独立的参考传输模块会: -- 从 Cloudflare 下载一个最多 2 MB 的限量样本; -- 上传一个最多 1 MB 的全零限量样本; +- 从 Cloudflare 下载两个限量样本(256 KiB 和 2 MB); +- 上传两个全零限量样本(128 KiB 和 1 MB); - 使用报告中显示的代理/网络路径; -- 将未完成传输明确标记为估算; -- 样本较高时不增加开发适配分。 +- 将每个传输请求的时间限制在最多 8 秒; +- 对有效样本速度取平均值,并显示样本可信度; +- 为开发适配分贡献 20 分,重复出现低速结果时还会限制总分上限。 `https://speed.cloudflare.com/__down` 是 [Cloudflare 官方测速引擎](https://github.com/cloudflare/speedtest)使用的下载 -API。官方引擎会组合多个尺寸并重复传输;ipcheck 的单个限量样本并未复刻这套 -完整测速方法。 +API。官方引擎会执行范围更广的自适应测速。ipcheck 每个方向只使用两个小 +尺寸,以控制耗时和流量;它不会复刻 Cloudflare 的完整测速流程。 这个样本只回答一个范围有限的问题:当前时刻,通过当前代理/网络路径向 Cloudflare 传输小文件时表现如何。它不是: @@ -85,9 +86,10 @@ Cloudflare 传输小文件时表现如何。它不是: - OpenAI、Anthropic、GitHub 或 npm 的吞吐测量; - AI 服务专用可达性和 TTFB 探测的替代品。 -完整的偏低样本每个方向扣 2 分;较高或一般的样本不加分,不可用或跳过不影响 -分数,未完成样本每个方向扣 1 分。这样可以让参考信号始终从属于真实 AI -服务路径。 +上下行共同构成 20 分的“工程传输”维度。重复出现明显受限的结果会限制原本 +较高的总分;不可用或跳过的方向保持中性,并标记为未测量。这样既让传输表现 +具有实质权重,也让真实 AI 服务路径继续作为主要信号。详见 +[评分与结论规则](scoring.zh-CN.md)。 使用 `--no-upload` 只跳过上传,使用 `--no-bandwidth` 跳过上下行。可选的 `--system` 会运行 macOS `networkQuality`,消耗的数据量可能明显高于 ipcheck diff --git a/docs/reporting.md b/docs/reporting.md index 1ef3a44..a6e4ebc 100644 --- a/docs/reporting.md +++ b/docs/reporting.md @@ -37,13 +37,16 @@ automation. Output includes: - overall result and developer readiness; - per-service and per-endpoint measurements; - score method and component breakdown; +- separate AI-interaction and engineering-transfer dimensions; - warnings and redacted network path; - capped Cloudflare reference transfers and optional macOS system measurements; - explicit privacy guarantees. -The `bandwidth` object name is retained for JSON compatibility. Its -`scope`, `method`, `represents_api_path`, and `represents_peak_bandwidth` -fields define the narrower Cloudflare reference-transfer meaning. +Schema 3 adds `developer_readiness.dimensions`, transfer caps, confidence, and +per-direction sample counts. The `bandwidth` object name is retained for JSON +compatibility. Its `scope`, `method`, `sample_strategy`, +`represents_api_path`, and `represents_peak_bandwidth` fields define the +narrower Cloudflare reference-transfer meaning. Additive fields may appear without changing `schema_version`; breaking field changes increment it. Progress is disabled for JSON and Markdown modes. diff --git a/docs/reporting.zh-CN.md b/docs/reporting.zh-CN.md index 34ac4fc..a805370 100644 --- a/docs/reporting.zh-CN.md +++ b/docs/reporting.zh-CN.md @@ -34,13 +34,15 @@ ipcheck --json > ipcheck-report.json - 总体结论和开发适配结果; - 每个服务、每个端点的测量数据; - 评分方法和逐项分数; +- 独立的 AI 交互与工程传输维度; - 警告及脱敏后的网络路径; - 限量 Cloudflare 参考传输和可选的 macOS 系统数据; - 明确的隐私保证。 -JSON 为兼容已有使用方继续保留 `bandwidth` 对象名;其中的 `scope`、`method`、 -`represents_api_path` 和 `represents_peak_bandwidth` 字段明确限定其含义是 -Cloudflare 参考传输。 +Schema 3 增加了 `developer_readiness.dimensions`、传输限幅、可信度和各方向 +样本数量。JSON 为兼容已有使用方继续保留 `bandwidth` 对象名;其中的 `scope`、 +`method`、`sample_strategy`、`represents_api_path` 和 +`represents_peak_bandwidth` 字段明确限定其含义是 Cloudflare 参考传输。 增加兼容字段不会修改 `schema_version`,破坏性变更才会升级。JSON 和 Markdown 模式不会输出动态进度。 diff --git a/docs/scoring.md b/docs/scoring.md index c9f8af4..e670fab 100644 --- a/docs/scoring.md +++ b/docs/scoring.md @@ -6,7 +6,7 @@ - a measured service result: `GOOD`, `FAIR`, `POOR`, or `BLOCKED`, plus `SKIPPED` when a provider-specific route cannot be probed safely; -- a 0–100 development-readiness score calculated with `rule_v3`. +- a 0–100 development-readiness score calculated with `rule_v4`. The score is a transparent heuristic, not a user percentile or a benchmark of model intelligence. @@ -37,47 +37,74 @@ Skipped clients do not affect another measured client's score. If every selected client is skipped, the overall result is `UNAVAILABLE`, the score is 0, and the command exits with status 1. -## Rule v3 service-path points +## Rule v4 dimensions -The selected service path contributes up to 100 points. +The score has two visible dimensions: + +- **AI interaction: 80 points.** The lowest-scoring selected service path + measures reachability, credential-free protocol TTFB, P95, and jitter. +- **Engineering transfer: 20 points.** Two small download samples and two + zero-filled upload samples measure the current proxy path to Cloudflare. + +This weighting makes sustained transfer limitations materially affect the +result without letting a CDN path outweigh the actual Codex or Claude protocol +path. Editing files is local; network transfer matters mainly when sending +context, receiving responses, cloning repositories, and installing +dependencies. + +### AI interaction: 80 points | Component | Maximum | Rule | | --- | ---: | --- | -| Reachability | 40 | 40 at 100% success, 25 at 60–99%, 11 above 0%, otherwise 0 | -| Median TTFB | 40 | 40 below 800 ms, 34 below 1,500, 25 below 3,000, 16 below 5,000, otherwise 7 | +| Reachability | 30 | 30 at 100% success, 19 at 60–99%, 8 above 0%, otherwise 0 | +| Median TTFB | 30 | 30 below 800 ms, 26 below 1,500, 19 below 3,000, 12 below 5,000, otherwise 5 | | P95 TTFB | 10 | 10 below 2,000 ms, 7 below 4,000, 4 below 6,000, otherwise 0 | | Jitter | 10 | 10 below 200 ms, 7 below 500, 4 below 1,000, otherwise 0 | Blocked paths receive zero latency and stability points. -## Cloudflare reference-transfer adjustments +### Engineering transfer: 20 points + +Download and upload each contribute up to 10 points. The displayed speed is the +mean of valid samples in that direction. -The capped Cloudflare samples are scored independently, but never add points. -They measure the current path to Cloudflare, not peak bandwidth or AI API -throughput. +| Direction | 10 points | 7 points | 4 points | 0 points | +| --- | ---: | ---: | ---: | ---: | +| Download | at least 10 Mbps | at least 3 Mbps | at least 1 Mbps | below 1 Mbps | +| Upload | at least 5 Mbps | at least 1 Mbps | at least 0.3 Mbps | below 0.3 Mbps | -| Rating | Points per direction | -| --- | ---: | -| `FAST` | 0 | -| `ADEQUATE` | 0 | -| `SLOW` | -2 | -| Unavailable or skipped | 0 | -| Incomplete sample | -1 instead of the speed rating | +The ratings are `COMFORTABLE`, `MILDLY LIMITED`, `CONSTRAINED`, and +`SEVERELY LIMITED`. They apply only to these capped Cloudflare samples and are +not general-purpose ISP ratings. -Download is `FAST` from 25 Mbps and `ADEQUATE` from 5 Mbps. Upload is `FAST` -from 10 Mbps and `ADEQUATE` from 2 Mbps. These thresholds only classify the -capped Cloudflare sample; they are not general-purpose ISP quality ratings. -Across both directions, reference transfers can reduce the score by at most -four points. +Skipped or unavailable transfer directions receive a neutral 10 points so +firewalls, offline reporting, or `--no-bandwidth` do not create a false +penalty. The dimension is labelled `UNMEASURED`, and JSON reports its +confidence. A partial sample can still supply an estimate, but lowers +confidence and cannot by itself establish a confirmed bottleneck. ## Caps and labels -Service health still limits the final score: +The raw dimension total is limited by both service and transfer evidence. +Service caps are: - usable `FAIR` paths are capped at 89; - temporarily unavailable `FAIR` and `POOR` paths are capped at 64; - `BLOCKED` paths are capped at 0. +Transfer caps require repeat evidence: + +- when both valid samples in either direction are `CONSTRAINED` or worse, the + total is capped at 79; +- when both valid samples in either direction are `SEVERELY LIMITED`, the + total is capped at 74; +- one valid `SEVERELY LIMITED` estimate caps the total at 89, but is not treated + as confirmed. + +The final cap is the lower of the service and transfer caps. This prevents an +excellent TTFB from hiding a repeatedly unusable transfer path while avoiding +a strong conclusion from one partial sample. + | Score | Label | | ---: | --- | | 90–100 | `COMFORTABLE` | @@ -85,5 +112,6 @@ Service health still limits the final score: | 65–74 | `USABLE` | | 0–64 | `LIMITED` | -Run `ipcheck --explain-score` to see every component. JSON reports expose the -same calculation under `developer_readiness.score_breakdown`. +Run `ipcheck --explain-score` to see both dimensions, their components, and all +caps. JSON schema 3 exposes the same calculation under +`developer_readiness.dimensions` and `developer_readiness.score_breakdown`. diff --git a/docs/scoring.zh-CN.md b/docs/scoring.zh-CN.md index 82254af..578a76f 100644 --- a/docs/scoring.zh-CN.md +++ b/docs/scoring.zh-CN.md @@ -6,7 +6,7 @@ - 已测服务结论:`GOOD`、`FAIR`、`POOR` 或 `BLOCKED`;provider 专用路径无法 安全自动探测时为 `SKIPPED`; -- 使用 `rule_v3` 计算的 0–100 开发适配分。 +- 使用 `rule_v4` 计算的 0–100 开发适配分。 分数是透明的启发式规则,不代表用户百分位,也不评价模型智能水平。 @@ -31,45 +31,63 @@ TTFB 来自无凭据协议探测,包含 DNS、代理、TLS、网络与网关 已跳过的客户端不会拉低其他已测客户端的分数。所有选中客户端均被跳过时, 总体结论为 `UNAVAILABLE`、分数为 0,命令返回状态码 1。 -## 规则 v3 的服务链路分 +## 规则 v4 的两个维度 -选中的最弱服务链路最多贡献 100 分。 +总分明确拆成两个维度: + +- **AI 交互:80 分。** 使用得分最低的已选服务链路,衡量可达性、无凭据协议 + TTFB、P95 和抖动。 +- **工程传输:20 分。** 使用两个小型下载样本和两个全零上传样本,衡量当前 + 代理到 Cloudflare 的传输表现。 + +这个权重会让持续的传输瓶颈明显影响结论,同时不会让 CDN 路径盖过真实的 +Codex 或 Claude 协议链路。代码编辑本身发生在本地;网络传输主要影响发送 +上下文、接收响应、克隆仓库和安装依赖。 + +### AI 交互:80 分 | 项目 | 最高分 | 规则 | | --- | ---: | --- | -| 可达性 | 40 | 成功率 100% 得 40,60–99% 得 25,大于 0% 得 11,否则 0 | -| TTFB 中位数 | 40 | 低于 800 ms 得 40,低于 1,500 得 34,低于 3,000 得 25,低于 5,000 得 16,否则 7 | +| 可达性 | 30 | 成功率 100% 得 30,60–99% 得 19,大于 0% 得 8,否则 0 | +| TTFB 中位数 | 30 | 低于 800 ms 得 30,低于 1,500 得 26,低于 3,000 得 19,低于 5,000 得 12,否则 5 | | TTFB P95 | 10 | 低于 2,000 ms 得 10,低于 4,000 得 7,低于 6,000 得 4,否则 0 | | 抖动 | 10 | 低于 200 ms 得 10,低于 500 得 7,低于 1,000 得 4,否则 0 | 阻断链路不会获得延迟和稳定性分。 -## Cloudflare 参考传输调整 +### 工程传输:20 分 + +下行和上行各占 10 分。界面显示的速度是该方向有效样本速度的平均值。 -限量 Cloudflare 样本的上下行分别计分,但永远不会加分。它测量当前路径到 -Cloudflare 的表现,不代表峰值宽带或 AI API 吞吐。 +| 方向 | 10 分 | 7 分 | 4 分 | 0 分 | +| --- | ---: | ---: | ---: | ---: | +| 下行 | 至少 10 Mbps | 至少 3 Mbps | 至少 1 Mbps | 低于 1 Mbps | +| 上行 | 至少 5 Mbps | 至少 1 Mbps | 至少 0.3 Mbps | 低于 0.3 Mbps | -| 评级 | 每个方向的分数 | -| --- | ---: | -| 样本较高 | 0 | -| 样本一般 | 0 | -| 样本偏低 | -2 | -| 不可用或跳过 | 0 | -| 未完成样本 | 不采用速率评级,改为 -1 | +对应评级为“传输舒适”“轻度受限”“明显受限”和“严重受限”。这些阈值 +只描述限量 Cloudflare 样本,不是通用的运营商网络评级。 -下载达到 25 Mbps 为“样本较高”,达到 5 Mbps 为“样本一般”;上传达到 -10 Mbps 为“样本较高”, -达到 2 Mbps 为“样本一般”。这些阈值只用于描述限量 Cloudflare 样本,不是 -通用的运营商网络评级。两个方向合计最多让总分降低 4 分。 +跳过或无法取得样本时,该方向使用中性的 10 分,避免防火墙、离线报告或 +`--no-bandwidth` 产生虚假扣分;维度会标记为“未测量”,JSON 同时给出可信度。 +未完成样本仍可提供估算值,但会降低可信度,而且不能单独认定为已确认瓶颈。 ## 结论限幅与标签 -服务健康度仍会限制最终得分: +原始维度总分同时受到服务证据与传输证据的限幅。服务限幅为: - 可用的 `FAIR` 链路最高 89 分; - 临时不可用的 `FAIR` 和 `POOR` 链路最高 64 分; - `BLOCKED` 链路为 0 分。 +传输限幅必须有重复证据: + +- 任一方向的两个有效样本均为“明显受限”或更差时,总分最高 79; +- 任一方向的两个有效样本均为“严重受限”时,总分最高 74; +- 只有一个有效的“严重受限”估算值时,总分最高 89,但不视为已确认。 + +最终采用服务限幅和传输限幅中更低的一个。这样既不会让优秀 TTFB 掩盖持续 +不可用的传输路径,也不会凭一个部分样本就下过强结论。 + | 分数 | 标签 | | ---: | --- | | 90–100 | 舒适 | @@ -77,5 +95,6 @@ Cloudflare 的表现,不代表峰值宽带或 AI API 吞吐。 | 65–74 | 可用 | | 0–64 | 受限 | -运行 `ipcheck --explain-score` 可以查看每个项目。JSON 会在 +运行 `ipcheck --explain-score` 可以查看两个维度、各个项目和全部限幅。 +JSON schema 3 会在 `developer_readiness.dimensions` 和 `developer_readiness.score_breakdown` 中提供相同的计算数据。 diff --git a/package.json b/package.json index 4f5e6b7..68fb760 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@jacklv-coder/ipcheck", "private": true, - "version": "0.9.0", + "version": "0.10.0", "description": "Diagnose proxy, latency, and API routes used by Codex and Claude Code.", "license": "MIT", "author": "Jintao", diff --git a/test/smoke.sh b/test/smoke.sh index 8b9deba..6e033a5 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -34,6 +34,8 @@ is_server_error=0 is_mixed_status=0 is_anthropic=0 is_score_scenario=0 +download_requested=2000000 +upload_requested=1000000 [ "${1-}" = "-q" ] || { printf 'curl was not invoked with -q first\n' >&2 exit 64 @@ -47,6 +49,11 @@ for argument in "$@"; do printf '%s\n' "$argument" >> "${IPCHECK_TEST_CURL_LOG:?}" case "$argument" in *__up*) is_upload=1 ;; + *__down?bytes=*) is_bandwidth=1; download_requested=${argument##*bytes=} ;; + @*/upload-*.bin) + upload_requested=${argument##*/upload-} + upload_requested=${upload_requested%.bin} + ;; *speed.cloudflare.com*) is_bandwidth=1 ;; *blocked.invalid*) is_blocked=1 ;; *flaky.invalid*) is_flaky=1 ;; @@ -72,18 +79,32 @@ elif [ "$is_proxy_auth" -eq 1 ]; then elif [ "$is_blocked" -eq 1 ]; then exit 28 elif [ "$is_upload" -eq 1 ]; then - upload_code=${IPCHECK_TEST_UPLOAD_CODE:-200} - upload_speed=${IPCHECK_TEST_UPLOAD_SPEED:-2000000} - upload_bytes=${IPCHECK_TEST_UPLOAD_BYTES:-1000000} + if [ "$upload_requested" -eq 131072 ]; then + upload_code=${IPCHECK_TEST_UPLOAD_SMALL_CODE:-${IPCHECK_TEST_UPLOAD_CODE:-200}} + upload_speed=${IPCHECK_TEST_UPLOAD_SMALL_SPEED:-${IPCHECK_TEST_UPLOAD_SPEED:-2000000}} + upload_bytes=${IPCHECK_TEST_UPLOAD_SMALL_BYTES:-${IPCHECK_TEST_UPLOAD_BYTES:-$upload_requested}} + upload_exit=${IPCHECK_TEST_UPLOAD_SMALL_EXIT:-${IPCHECK_TEST_UPLOAD_EXIT:-0}} + else + upload_code=${IPCHECK_TEST_UPLOAD_LARGE_CODE:-${IPCHECK_TEST_UPLOAD_CODE:-200}} + upload_speed=${IPCHECK_TEST_UPLOAD_LARGE_SPEED:-${IPCHECK_TEST_UPLOAD_SPEED:-2000000}} + upload_bytes=${IPCHECK_TEST_UPLOAD_LARGE_BYTES:-${IPCHECK_TEST_UPLOAD_BYTES:-$upload_requested}} + upload_exit=${IPCHECK_TEST_UPLOAD_LARGE_EXIT:-${IPCHECK_TEST_UPLOAD_EXIT:-0}} + fi printf '%s\t%s\t%s' "$upload_code" "$upload_bytes" "$upload_speed" - upload_exit=${IPCHECK_TEST_UPLOAD_EXIT:-0} [ "$upload_exit" -eq 0 ] || exit "$upload_exit" elif [ "$is_bandwidth" -eq 1 ]; then - bandwidth_code=${IPCHECK_TEST_BANDWIDTH_CODE:-200} - bandwidth_speed=${IPCHECK_TEST_BANDWIDTH_SPEED:-10000000} - bandwidth_bytes=${IPCHECK_TEST_BANDWIDTH_BYTES:-2000000} + if [ "$download_requested" -eq 262144 ]; then + bandwidth_code=${IPCHECK_TEST_BANDWIDTH_SMALL_CODE:-${IPCHECK_TEST_BANDWIDTH_CODE:-200}} + bandwidth_speed=${IPCHECK_TEST_BANDWIDTH_SMALL_SPEED:-${IPCHECK_TEST_BANDWIDTH_SPEED:-10000000}} + bandwidth_bytes=${IPCHECK_TEST_BANDWIDTH_SMALL_BYTES:-${IPCHECK_TEST_BANDWIDTH_BYTES:-$download_requested}} + bandwidth_exit=${IPCHECK_TEST_BANDWIDTH_SMALL_EXIT:-${IPCHECK_TEST_BANDWIDTH_EXIT:-0}} + else + bandwidth_code=${IPCHECK_TEST_BANDWIDTH_LARGE_CODE:-${IPCHECK_TEST_BANDWIDTH_CODE:-200}} + bandwidth_speed=${IPCHECK_TEST_BANDWIDTH_LARGE_SPEED:-${IPCHECK_TEST_BANDWIDTH_SPEED:-10000000}} + bandwidth_bytes=${IPCHECK_TEST_BANDWIDTH_LARGE_BYTES:-${IPCHECK_TEST_BANDWIDTH_BYTES:-$download_requested}} + bandwidth_exit=${IPCHECK_TEST_BANDWIDTH_LARGE_EXIT:-${IPCHECK_TEST_BANDWIDTH_EXIT:-0}} + fi printf '%s\t%s\t%s' "$bandwidth_code" "$bandwidth_bytes" "$bandwidth_speed" - bandwidth_exit=${IPCHECK_TEST_BANDWIDTH_EXIT:-0} [ "$bandwidth_exit" -eq 0 ] || exit "$bandwidth_exit" elif [ "$is_not_found" -eq 1 ]; then printf '404\t0.001\t0.002\t0.003\t0.050\t0.050\t100\t1000' @@ -230,12 +251,12 @@ run_ipcheck_direct() { bash -n "$PROJECT_DIR/bin/ipcheck" "$PROJECT_DIR/bin/ipcheck" --help | grep -q '^ipcheck - diagnose Codex and Claude Code' -[ "$("$PROJECT_DIR/bin/ipcheck" --version)" = "ipcheck 0.9.0" ] +[ "$("$PROJECT_DIR/bin/ipcheck" --version)" = "ipcheck 0.10.0" ] "$PROJECT_DIR/bin/ipcheck" --help | grep -q -- '--explain-score' : > "$CURL_LOG" report=$(ANTHROPIC_AUTH_TOKEN="runtime-secret-must-never-appear" run_ipcheck --samples 3 --no-bandwidth --json) -printf '%s\n' "$report" | grep -q '"schema_version":2' +printf '%s\n' "$report" | grep -q '"schema_version":3' printf '%s\n' "$report" | grep -q '"result":"good"' printf '%s\n' "$report" | grep -q '"reachable_endpoints":3' printf '%s\n' "$report" | grep -q '"id":"codex"' @@ -302,8 +323,8 @@ printf '%s\n' "$report" | grep -q 'https://dashscope.aliyuncs.com/apps/anthropic printf '%s\n' "$report" | grep -q '"credentials_used":false' printf '%s\n' "$report" | grep -q '"billable_requests":false' printf '%s\n' "$report" | grep -q '"developer_readiness":{"ready":true,"level":"ready"' -printf '%s\n' "$report" | grep -q '"score":100,"score_label":"COMFORTABLE","score_method":"rule_v3"' -printf '%s\n' "$report" | grep -q '"score_breakdown":{"scored_service":"codex","service_path":100,"availability":40,"median_ttfb":40,"p95":10,"jitter":10' +printf '%s\n' "$report" | grep -q '"score":100,"score_label":"COMFORTABLE","score_method":"rule_v4"' +printf '%s\n' "$report" | grep -q '"score_breakdown":{"scored_service":"codex","service_path":80,"service_path_max":80,"availability":30,"median_ttfb":30,"p95":10,"jitter":10' if printf '%s\n' "$report" | grep -Eq 'fixture-secret|runtime-secret'; then printf 'Claude credential leaked into JSON report\n' >&2 exit 1 @@ -339,30 +360,30 @@ fi mixed_client_quality=$(IPCHECK_TEST_ANTHROPIC_TTFB=4.000 run_ipcheck --samples 1 --no-bandwidth --json) printf '%s\n' "$mixed_client_quality" | grep -q '"result":"poor"' -printf '%s\n' "$mixed_client_quality" | grep -q '"score":64,"score_label":"LIMITED","score_method":"rule_v3"' -printf '%s\n' "$mixed_client_quality" | grep -q '"scored_service":"claude","service_path":70' +printf '%s\n' "$mixed_client_quality" | grep -q '"score":64,"score_label":"LIMITED","score_method":"rule_v4"' +printf '%s\n' "$mixed_client_quality" | grep -q '"scored_service":"claude","service_path":56' for anthropic_status in 429 503; do mixed_service_error=$(IPCHECK_TEST_ANTHROPIC_CODE="$anthropic_status" run_ipcheck --samples 1 --no-bandwidth --json) printf '%s\n' "$mixed_service_error" | grep -q '"result":"fair"' - printf '%s\n' "$mixed_service_error" | grep -q '"score":64,"score_label":"LIMITED","score_method":"rule_v3"' - printf '%s\n' "$mixed_service_error" | grep -q '"scored_service":"claude","service_path":100' - printf '%s\n' "$mixed_service_error" | grep -q '"pre_cap_total":100,"verdict_cap":64' + printf '%s\n' "$mixed_service_error" | grep -q '"score":64,"score_label":"LIMITED","score_method":"rule_v4"' + printf '%s\n' "$mixed_service_error" | grep -q '"scored_service":"claude","service_path":80' + printf '%s\n' "$mixed_service_error" | grep -q '"pre_cap_total":100,"service_verdict_cap":64,"transfer_verdict_cap":100,"verdict_cap":64' done for anthropic_status in 429 503; do mixed_fair_unavailable=$(IPCHECK_TEST_TTFB=1.000 IPCHECK_TEST_ANTHROPIC_CODE="$anthropic_status" run_ipcheck --samples 1 --no-bandwidth --json) printf '%s\n' "$mixed_fair_unavailable" | grep -q '"result":"fair"' - printf '%s\n' "$mixed_fair_unavailable" | grep -q '"score":64,"score_label":"LIMITED","score_method":"rule_v3"' - printf '%s\n' "$mixed_fair_unavailable" | grep -q '"scored_service":"claude","service_path":94' - printf '%s\n' "$mixed_fair_unavailable" | grep -q '"pre_cap_total":94,"verdict_cap":64' + printf '%s\n' "$mixed_fair_unavailable" | grep -q '"score":64,"score_label":"LIMITED","score_method":"rule_v4"' + printf '%s\n' "$mixed_fair_unavailable" | grep -q '"scored_service":"claude","service_path":76' + printf '%s\n' "$mixed_fair_unavailable" | grep -q '"pre_cap_total":96,"service_verdict_cap":64,"transfer_verdict_cap":100,"verdict_cap":64' done mixed_blocked=$(IPCHECK_TEST_ANTHROPIC_BLOCKED=1 run_ipcheck --samples 1 --json) printf '%s\n' "$mixed_blocked" | grep -q '"result":"poor"' -printf '%s\n' "$mixed_blocked" | grep -q '"score":0,"score_label":"LIMITED","score_method":"rule_v3"' +printf '%s\n' "$mixed_blocked" | grep -q '"score":0,"score_label":"LIMITED","score_method":"rule_v4"' printf '%s\n' "$mixed_blocked" | grep -q '"scored_service":"claude","service_path":0' -printf '%s\n' "$mixed_blocked" | grep -q '"pre_cap_total":0,"verdict_cap":0' +printf '%s\n' "$mixed_blocked" | grep -q '"pre_cap_total":20,"service_verdict_cap":0,"transfer_verdict_cap":100,"verdict_cap":0' AUTO_HOME="$STUB_DIR/auto-home" mkdir -p "$AUTO_HOME/codex" "$AUTO_HOME/claude" @@ -443,6 +464,7 @@ set -e printf '%s\n' "$bedrock_report" | grep -q 'configured for Amazon Bedrock' printf '%s\n' "$bedrock_report" | grep -q '"result":"unavailable"' printf '%s\n' "$bedrock_report" | grep -q '"result":"skipped"' +printf '%s\n' "$bedrock_report" | grep -q '"ai_interaction":{"score":0,"max_score":80,"label":"UNMEASURED"}' set +e foundry_report=$(CLAUDE_CODE_USE_FOUNDRY=1 run_ipcheck claude --samples 1 --no-bandwidth --json) @@ -581,10 +603,10 @@ printf '%s\n' "$fair_report" | grep -q '"score":89,"score_label":"GOOD"' fair_fast_bandwidth_report=$(IPCHECK_TEST_TTFB=1.000 run_ipcheck --samples 1 --endpoint https://fair.invalid --json) printf '%s\n' "$fair_fast_bandwidth_report" | grep -q '"score":89,"score_label":"GOOD"' fair_slow_bandwidth_report=$(IPCHECK_TEST_TTFB=1.000 IPCHECK_TEST_BANDWIDTH_SPEED=100000 IPCHECK_TEST_UPLOAD_SPEED=100000 run_ipcheck --samples 1 --endpoint https://fair.invalid --json) -printf '%s\n' "$fair_slow_bandwidth_report" | grep -q '"score":89,"score_label":"GOOD"' +printf '%s\n' "$fair_slow_bandwidth_report" | grep -q '"score":74,"score_label":"USABLE"' just_below_poor=$(IPCHECK_TEST_TTFB=2.999 run_ipcheck --samples 1 --no-bandwidth --endpoint https://boundary.invalid --json) -printf '%s\n' "$just_below_poor" | grep -q '"score":82,"score_label":"GOOD"' +printf '%s\n' "$just_below_poor" | grep -q '"score":86,"score_label":"GOOD"' at_poor_boundary=$(IPCHECK_TEST_TTFB=3.000 run_ipcheck --samples 1 --no-bandwidth --endpoint https://boundary.invalid --json) printf '%s\n' "$at_poor_boundary" | grep -q '"score":64,"score_label":"LIMITED"' @@ -592,8 +614,8 @@ SCORE_SCENARIO_FILE="$STUB_DIR/score-scenario-attempt" : > "$SCORE_SCENARIO_FILE" score_scenario=$(IPCHECK_TEST_SCORE_FILE="$SCORE_SCENARIO_FILE" IPCHECK_TEST_BANDWIDTH_SPEED=400000 IPCHECK_TEST_UPLOAD_SPEED=425000 run_ipcheck --samples 3 --endpoint https://score-scenario.invalid --json) printf '%s\n' "$score_scenario" | grep -q '"result":"poor"' -printf '%s\n' "$score_scenario" | grep -q '"score":54,"score_label":"LIMITED","score_method":"rule_v3"' -printf '%s\n' "$score_scenario" | grep -q '"service_path":56,"availability":40,"median_ttfb":16,"p95":0,"jitter":0,"download":-2' +printf '%s\n' "$score_scenario" | grep -q '"score":56,"score_label":"LIMITED","score_method":"rule_v4"' +printf '%s\n' "$score_scenario" | grep -q '"service_path":42,"service_path_max":80,"availability":30,"median_ttfb":12,"p95":0,"jitter":0,"download":7' fair_human=$(IPCHECK_TEST_TTFB=1.000 IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --no-progress --endpoint https://fair.invalid) printf '%s\n' "$fair_human" | grep -q 'Ready to code? YES, WITH CAUTION' @@ -632,7 +654,11 @@ grep -q 'reachable 2/2' "$progress_log" json_progress_log="$STUB_DIR/json-progress.log" IPCHECK_PROGRESS=always run_ipcheck --samples 1 --no-bandwidth --json > /dev/null 2>"$json_progress_log" -[ ! -s "$json_progress_log" ] +if [ -s "$json_progress_log" ]; then + printf 'JSON mode unexpectedly wrote to stderr:\n' >&2 + sed -n '1,20p' "$json_progress_log" >&2 + exit 1 +fi colored_human=$(IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --color --endpoint https://color.invalid) case "$colored_human" in @@ -816,64 +842,87 @@ printf '%s\n' "$socks_report" | grep -q 'Claude Code does not document ALL_PROXY grep -q '^env:ALL_PROXY=$' "$CURL_LOG" bandwidth_report=$(run_ipcheck --samples 1 --json) -printf '%s\n' "$bandwidth_report" | grep -q '"score":100,"score_label":"COMFORTABLE","score_method":"rule_v3"' -printf '%s\n' "$bandwidth_report" | grep -q '"score_breakdown":{"scored_service":"codex","service_path":100,"availability":40,"median_ttfb":40,"p95":10,"jitter":10,"download":0,"download_speed":0,"download_partial_penalty":0,"upload":0' +printf '%s\n' "$bandwidth_report" | grep -q '"score":100,"score_label":"COMFORTABLE","score_method":"rule_v4"' +printf '%s\n' "$bandwidth_report" | grep -q '"score_breakdown":{"scored_service":"codex","service_path":80,"service_path_max":80,"availability":30,"median_ttfb":30,"p95":10,"jitter":10,"download":10,"download_speed":10,"download_partial_penalty":0,"upload":10' printf '%s\n' "$bandwidth_report" | grep -q '"bandwidth":{"enabled":true,"available":true,"http_code":"200"' -printf '%s\n' "$bandwidth_report" | grep -q '"scope":"cloudflare_reference_path","method":"single_capped_transfer","represents_api_path":false,"represents_peak_bandwidth":false' -printf '%s\n' "$bandwidth_report" | grep -q '"download":{"enabled":true,"available":true,"complete":true,"http_code":"200","bytes":2000000,"bytes_per_second":10000000,"mbps":80.0,"rating":"fast"}' -printf '%s\n' "$bandwidth_report" | grep -q '"upload":{"enabled":true,"available":true,"complete":true,"http_code":"200","bytes":1000000,"bytes_per_second":2000000,"mbps":16.0,"rating":"fast"}' +printf '%s\n' "$bandwidth_report" | grep -q '"scope":"cloudflare_reference_path","method":"multi_size_capped_transfer","sample_strategy":"two_sizes_per_direction","confidence":"high","represents_api_path":false,"represents_peak_bandwidth":false' +printf '%s\n' "$bandwidth_report" | grep -q '"download":{"enabled":true,"available":true,"complete":true,"http_code":"200","bytes":2262144,"bytes_per_second":10000000,"mbps":80.0,"rating":"fast","samples_total":2,"samples_valid":2,"samples_complete":2,"samples_timed_out":0}' +printf '%s\n' "$bandwidth_report" | grep -q '"upload":{"enabled":true,"available":true,"complete":true,"http_code":"200","bytes":1131072,"bytes_per_second":2000000,"mbps":16.0,"rating":"fast","samples_total":2,"samples_valid":2,"samples_complete":2,"samples_timed_out":0}' printf '%s\n' "$bandwidth_report" | grep -q '"upload_payload":"zero-filled"' bandwidth_human=$(IPCHECK_LANG=zh run_ipcheck --samples 1) printf '%s\n' "$bandwidth_human" | grep -q '◆ 代理链路参考传输' printf '%s\n' "$bandwidth_human" | grep -q '100/100.*舒适' -printf '%s\n' "$bandwidth_human" | grep -q '下行样本 80.0 Mbps.*样本较高.*Cloudflare,最多 2 MB' -printf '%s\n' "$bandwidth_human" | grep -q '上行样本 16.0 Mbps.*样本较高.*Cloudflare,最多 1 MB 零字节' +printf '%s\n' "$bandwidth_human" | grep -q 'AI 交互.*80/80.*舒适.*工程传输.*20/20.*舒适.*可信度高' +printf '%s\n' "$bandwidth_human" | grep -q '下行样本 80.0 Mbps.*传输舒适.*Cloudflare,2/2 有效,最大 2 MB' +printf '%s\n' "$bandwidth_human" | grep -q '上行样本 16.0 Mbps.*传输舒适.*Cloudflare,2/2 有效,最大 1 MB 零字节' printf '%s\n' "$bandwidth_human" | grep -q '范围.*当前代理 → Cloudflare;小文件样本,非峰值宽带,不代表 AI API 吞吐' score_explanation=$(IPCHECK_LANG=en run_ipcheck --samples 1 --explain-score) printf '%s\n' "$score_explanation" | grep -q '^ Score breakdown$' -printf '%s\n' "$score_explanation" | grep -q 'Service path.*100/100.*reachability 40.*TTFB 40.*P95 10.*jitter 10' -printf '%s\n' "$score_explanation" | grep -q 'Download reference.*0.*80.0 Mbps.*HIGHER SAMPLE' -printf '%s\n' "$score_explanation" | grep -q 'Upload reference.*0.*16.0 Mbps.*HIGHER SAMPLE' +printf '%s\n' "$score_explanation" | grep -q 'AI interaction.*80/80.*reachability 30.*TTFB 30.*P95 10.*jitter 10' +printf '%s\n' "$score_explanation" | grep -q 'Download engineering.*10/10.*80.0 Mbps.*COMFORTABLE' +printf '%s\n' "$score_explanation" | grep -q 'Upload engineering.*10/10.*16.0 Mbps.*COMFORTABLE' score_explanation_zh=$(IPCHECK_LANG=zh run_ipcheck --samples 1 --markdown --explain-score) -printf '%s\n' "$score_explanation_zh" | grep -q '评分依据:服务链路 100/100(可达性 40 + TTFB 40 + P95 10 + 抖动 10);下行参考样本 0(样本较高);上行参考样本 0(样本较高);限幅前 100;结论上限 100;总分 100。' +printf '%s\n' "$score_explanation_zh" | grep -q '评分依据:AI 交互 80/80(可达性 30 + TTFB 30 + P95 10 + 抖动 10);下行工程传输 10/10(传输舒适);上行工程传输 10/10(传输舒适);限幅前 100;服务上限 100;传输上限 100;总分 100。' score_explanation_without_bandwidth=$(IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --explain-score) -printf '%s\n' "$score_explanation_without_bandwidth" | grep -q 'Download reference.*0.*0.0 Mbps.*UNAVAILABLE' -printf '%s\n' "$score_explanation_without_bandwidth" | grep -q 'Upload reference.*0.*0.0 Mbps.*UNAVAILABLE' +printf '%s\n' "$score_explanation_without_bandwidth" | grep -q 'Download engineering.*10/10.*0.0 Mbps.*UNAVAILABLE' +printf '%s\n' "$score_explanation_without_bandwidth" | grep -q 'Upload engineering.*10/10.*0.0 Mbps.*UNAVAILABLE' slow_upload_human=$(IPCHECK_TEST_UPLOAD_SPEED=100000 IPCHECK_LANG=en run_ipcheck --samples 1 --explain-score) -printf '%s\n' "$slow_upload_human" | grep -q 'Up sample.*0.8 Mbps.*LOW SAMPLE' -printf '%s\n' "$slow_upload_human" | grep -q '98/100.*COMFORTABLE' -printf '%s\n' "$slow_upload_human" | grep -q 'Upload reference.*-2.*0.8 Mbps.*LOW SAMPLE' -printf '%s\n' "$slow_upload_human" | grep -q 'Cloudflare upload sample is low' +printf '%s\n' "$slow_upload_human" | grep -q 'Up sample.*0.8 Mbps.*CONSTRAINED' +printf '%s\n' "$slow_upload_human" | grep -q '79/100.*GOOD' +printf '%s\n' "$slow_upload_human" | grep -q 'Upload engineering.*4/10.*0.8 Mbps.*CONSTRAINED' +printf '%s\n' "$slow_upload_human" | grep -q 'Multiple samples show constrained proxy transfers' invalid_bandwidth_report=$(IPCHECK_TEST_BANDWIDTH_CODE=407 run_ipcheck --samples 1 --json) printf '%s\n' "$invalid_bandwidth_report" | grep -q '"bandwidth":{"enabled":true,"available":false,"http_code":"407","bytes":0,"bytes_per_second":0,' invalid_upload_report=$(IPCHECK_TEST_UPLOAD_CODE=503 run_ipcheck --samples 1 --json) -printf '%s\n' "$invalid_upload_report" | grep -q '"upload":{"enabled":true,"available":false,"complete":false,"http_code":"503","bytes":0,"bytes_per_second":0,"mbps":0.0,"rating":"unavailable"}' +printf '%s\n' "$invalid_upload_report" | grep -q '"upload":{"enabled":true,"available":false,"complete":false,"http_code":"503","bytes":0,"bytes_per_second":0,"mbps":0.0,"rating":"unavailable","samples_total":2,"samples_valid":0,"samples_complete":0,"samples_timed_out":0}' + +mixed_failure_report=$(IPCHECK_TEST_BANDWIDTH_SMALL_CODE=503 run_ipcheck --samples 1 --json) +printf '%s\n' "$mixed_failure_report" | grep -q '"bandwidth":{"enabled":true,"available":false,"http_code":"200","bytes":0,"bytes_per_second":0,' +printf '%s\n' "$mixed_failure_report" | grep -q '"download":{"enabled":true,"available":true,"complete":false,"http_code":"200"' +mixed_failure_human=$(IPCHECK_TEST_BANDWIDTH_SMALL_CODE=503 IPCHECK_LANG=en run_ipcheck --samples 1) +printf '%s\n' "$mixed_failure_human" | grep -q 'At least one transfer sample failed or was invalid' +if printf '%s\n' "$mixed_failure_human" | grep -q 'did not finish before its time limit'; then + printf 'non-timeout transfer failure was reported as a timeout\n' >&2 + exit 1 +fi + +mixed_speed_human=$(IPCHECK_TEST_BANDWIDTH_SMALL_SPEED=125000 IPCHECK_TEST_BANDWIDTH_LARGE_SPEED=500000 IPCHECK_LANG=en run_ipcheck --samples 1) +printf '%s\n' "$mixed_speed_human" | grep -q 'Down sample.*2.5 Mbps.*CONSTRAINED' +printf '%s\n' "$mixed_speed_human" | grep -q 'Valid samples average constrained, but the evidence is incomplete or individual results are mixed' +if printf '%s\n' "$mixed_speed_human" | grep -q 'Multiple samples show constrained proxy transfers'; then + printf 'mixed transfer samples were reported as repeated constrained evidence\n' >&2 + exit 1 +fi no_upload_report=$(run_ipcheck --samples 1 --no-upload --json) -printf '%s\n' "$no_upload_report" | grep -q '"upload":{"enabled":false,"available":false' +printf '%s\n' "$no_upload_report" | grep -q '"upload":{"enabled":false,"available":false,"complete":false,"http_code":"000","bytes":0,"bytes_per_second":0,"mbps":0.0,"rating":"unavailable","samples_total":0' + +bottleneck_with_missing_upload=$(IPCHECK_TEST_BANDWIDTH_SPEED=12500 IPCHECK_TEST_UPLOAD_CODE=503 IPCHECK_LANG=en run_ipcheck --samples 1) +printf '%s\n' "$bottleneck_with_missing_upload" | grep -q 'Multiple samples show severely constrained proxy transfers' +printf '%s\n' "$bottleneck_with_missing_upload" | grep -q 'Some directions are unavailable or incomplete' partial_upload_report=$(IPCHECK_TEST_UPLOAD_BYTES=500000 IPCHECK_TEST_UPLOAD_EXIT=28 run_ipcheck --samples 1 --json) -printf '%s\n' "$partial_upload_report" | grep -q '"upload":{"enabled":true,"available":true,"complete":false,"http_code":"200","bytes":500000,"bytes_per_second":2000000,"mbps":16.0,"rating":"partial"}' -printf '%s\n' "$partial_upload_report" | grep -q '"score":99,"score_label":"COMFORTABLE"' +printf '%s\n' "$partial_upload_report" | grep -q '"upload":{"enabled":true,"available":true,"complete":false,"http_code":"200","bytes":1000000,"bytes_per_second":2000000,"mbps":16.0,"rating":"fast","samples_total":2,"samples_valid":2,"samples_complete":0,"samples_timed_out":2}' +printf '%s\n' "$partial_upload_report" | grep -q '"score":100,"score_label":"COMFORTABLE"' partial_download_report=$(IPCHECK_TEST_BANDWIDTH_BYTES=500000 IPCHECK_TEST_BANDWIDTH_EXIT=28 run_ipcheck --samples 1 --json) -printf '%s\n' "$partial_download_report" | grep -q '"bandwidth":{"enabled":true,"available":false,"http_code":"000","bytes":0,"bytes_per_second":0,' -printf '%s\n' "$partial_download_report" | grep -q '"download":{"enabled":true,"available":true,"complete":false,"http_code":"200","bytes":500000,"bytes_per_second":10000000,"mbps":80.0,"rating":"partial"}' +printf '%s\n' "$partial_download_report" | grep -q '"bandwidth":{"enabled":true,"available":false,"http_code":"200","bytes":0,"bytes_per_second":0,' +printf '%s\n' "$partial_download_report" | grep -q '"download":{"enabled":true,"available":true,"complete":false,"http_code":"200","bytes":1000000,"bytes_per_second":10000000,"mbps":80.0,"rating":"fast","samples_total":2,"samples_valid":2,"samples_complete":0,"samples_timed_out":2}' complete_slow_download=$(IPCHECK_TEST_BANDWIDTH_SPEED=12500 run_ipcheck --samples 1 --json) -printf '%s\n' "$complete_slow_download" | grep -q '"score":98,"score_label":"COMFORTABLE"' +printf '%s\n' "$complete_slow_download" | grep -q '"score":74,"score_label":"USABLE"' rounded_threshold_report=$(IPCHECK_TEST_UPLOAD_SPEED=245000 run_ipcheck --samples 1 --json) printf '%s\n' "$rounded_threshold_report" | grep -q '"mbps":2.0,"rating":"adequate"' partial_slow_download=$(IPCHECK_TEST_BANDWIDTH_SPEED=12500 IPCHECK_TEST_BANDWIDTH_BYTES=500000 IPCHECK_TEST_BANDWIDTH_EXIT=28 run_ipcheck --samples 1 --json) -printf '%s\n' "$partial_slow_download" | grep -q '"score":99,"score_label":"COMFORTABLE"' -printf '%s\n' "$partial_slow_download" | grep -q '"download":-1,"download_speed":0,"download_partial_penalty":-1' +printf '%s\n' "$partial_slow_download" | grep -q '"score":74,"score_label":"USABLE"' +printf '%s\n' "$partial_slow_download" | grep -q '"download":0,"download_speed":0,"download_partial_penalty":0' partial_human=$(IPCHECK_TEST_UPLOAD_BYTES=500000 IPCHECK_TEST_UPLOAD_EXIT=28 IPCHECK_LANG=en run_ipcheck --samples 1) -printf '%s\n' "$partial_human" | grep -q 'Up sample.*16.0 Mbps.*INCOMPLETE ESTIMATE' +printf '%s\n' "$partial_human" | grep -q 'Up sample.*16.0 Mbps.*COMFORTABLE' printf '%s\n' "$partial_human" | grep -q 'partial estimates and do not prove that path throughput is sufficient' partial_markdown=$(IPCHECK_TEST_BANDWIDTH_BYTES=500000 IPCHECK_TEST_BANDWIDTH_EXIT=28 IPCHECK_LANG=en run_ipcheck --samples 1 --markdown) -printf '%s\n' "$partial_markdown" | grep -q '| Down | 80.0 Mbps | INCOMPLETE ESTIMATE |' +printf '%s\n' "$partial_markdown" | grep -q '| Down | 80.0 Mbps | 10/10 | COMFORTABLE |' printf '%s\n' "$partial_markdown" | grep -q 'partial estimates and do not prove that path throughput is sufficient' progress_report=$(IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --progress 2>&1 >/dev/null) @@ -955,7 +1004,7 @@ set -e [ "$blocked_exit" -eq 1 ] printf '%s\n' "$blocked_report" | grep -q '"http_code":"000"' printf '%s\n' "$blocked_report" | grep -q '"bandwidth":{"enabled":false,"available":false,"http_code":"000"' -printf '%s\n' "$blocked_report" | grep -q '"score":0,"score_label":"LIMITED","score_method":"rule_v3"' +printf '%s\n' "$blocked_report" | grep -q '"score":0,"score_label":"LIMITED","score_method":"rule_v4"' markdown=$(run_ipcheck --samples 1 --no-bandwidth --markdown) printf '%s\n' "$markdown" | grep -q '^# ipcheck: AI Coding Network Report' @@ -970,7 +1019,8 @@ printf '%s\n' "$chinese_markdown" | grep -q '^## AI 服务延迟(TTFB)$' quick_report=$(run_ipcheck --quick --json) printf '%s\n' "$quick_report" | grep -q '"samples":1' printf '%s\n' "$quick_report" | grep -q '"bandwidth":{"enabled":false' -printf '%s\n' "$quick_report" | grep -q '"upload":{"enabled":false' +printf '%s\n' "$quick_report" | grep -q '"download":{"enabled":false,"available":false,"complete":false,"http_code":"000","bytes":0,"bytes_per_second":0,"mbps":0.0,"rating":"unavailable","samples_total":0' +printf '%s\n' "$quick_report" | grep -q '"upload":{"enabled":false,"available":false,"complete":false,"http_code":"000","bytes":0,"bytes_per_second":0,"mbps":0.0,"rating":"unavailable","samples_total":0' set +e run_ipcheck --service invalid >/dev/null 2>&1