diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 21b3291..8b53428 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.5.0 + placeholder: ipcheck 0.6.0 validations: required: true - type: dropdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb9221..2c56ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to ipcheck are documented here. The project follows [Semantic Versioning](https://semver.org/). +## [0.6.0] - 2026-07-22 + +### Added + +- A visible `Ctrl+C` cancellation hint and graceful interrupt handling with + localized feedback, temporary-file cleanup, and exit status 130. +- A transparent 0–100 developer-readiness score in human, Markdown, and JSON + reports. The score is explicitly rule-based rather than a user percentile. + ## [0.5.0] - 2026-07-22 ### Added @@ -71,6 +80,7 @@ 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`. +[0.6.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.2.0...v0.3.0 diff --git a/README.md b/README.md index 1dfcab2..aa67c05 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,17 @@ Know whether your AI coding CLI is slow, blocked, or using the wrong gateway. non-secret routing configuration, then reports reachability, median/P95 time-to-first-byte (TTFB), jitter, reference bandwidth, and a clear `GOOD`, `FAIR`, `POOR`, or `BLOCKED` result. Interactive runs show live, -color-coded progress and finish with a direct answer to “Ready to code?”. +color-coded progress, can be cancelled cleanly with `Ctrl+C`, and finish with +a direct answer plus a rule-based readiness score. ```text -$ ipcheck --quick -ipcheck v0.5.0 — AI coding network diagnostics +$ ipcheck +ipcheck v0.6.0 — AI coding network diagnostics Developer verdict - Ready to code? YES, WITH CAUTION - Development is possible, but responses may feel slower than ideal. + Ready to code? YES + Readiness score: 95/100 · COMFORTABLE + This network is ready for AI-assisted development. Detected clients Codex model=gpt-5.6-sol, route=https://chatgpt.com + https://api.openai.com @@ -72,7 +74,7 @@ brew install ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.5.0/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.6.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -123,6 +125,10 @@ and diagnostic reasons remain stable English for automation. Progress is written to stderr only for human output and can be controlled with `IPCHECK_PROGRESS=auto|always|never`. +Interactive progress starts with `Press Ctrl+C to cancel at any time.` An +interrupt clears the live progress line, prints `Cancelled.`, removes temporary +files, and exits with the conventional status `130`. + The service table measures time to first byte and jitter; it is not a transfer speed test. The separate bandwidth section downloads up to 2 MB and uploads up to 1 MB of zero-filled data through the reported proxy/network path. It rates throughput @@ -136,6 +142,13 @@ higher and `ADEQUATE` from 2 Mbps. Lower measurements are `SLOW`. The optional `--system` test delegates to macOS `networkQuality`, which can transfer substantially more data than ipcheck's capped Cloudflare checks. +The readiness score is a transparent heuristic, not a claim about user +percentiles. Rule v1 starts from the service-path verdict (`90` healthy, `70` +usable but delayed, `45` reachable but temporarily unavailable, `35` poor, or +`0` blocked). Completed bandwidth ratings can then add up to 5 points or deduct +up to 10; partial samples deduct 2 points each. JSON exposes `score`, +`score_label`, and `score_method` for automation. + Run `ipcheck --help` for the complete option and environment-variable list. ## Supported clients and routes diff --git a/README.zh-CN.md b/README.zh-CN.md index e0a747b..f8bb890 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,15 +12,17 @@ **Claude Code** 的真实网络路径。它会自动识别本机客户端以及不含密钥的路由 配置,测量可达性、首字节延迟(TTFB)中位数/P95、抖动和参考带宽,并给出 明确的 `GOOD`、`FAIR`、`POOR` 或 `BLOCKED` 结论。交互运行时会显示带颜色的 -实时进度,并直接告诉你“现在是否适合开发”。 +实时进度,支持通过 `Ctrl+C` 友好取消,并直接告诉你“现在是否适合开发”以及 +规则化的开发适配分。 ```text -$ ipcheck --quick -ipcheck v0.5.0 — AI 编程网络诊断 +$ ipcheck +ipcheck v0.6.0 — AI 编程网络诊断 开发建议 - 现在适合开发吗?可以,但会有些慢 - 当前可以开发,但响应速度可能不够理想。 + 现在适合开发吗?适合 + 开发适配分:95/100 · 舒适 + 当前网络适合进行 AI 辅助开发。 Detected clients Codex model=gpt-5.6-sol, route=https://chatgpt.com + https://api.openai.com @@ -66,7 +68,7 @@ brew install ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.5.0/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.6.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -113,6 +115,9 @@ ipcheck --no-upload 枚举值和诊断原因始终保持英文,方便自动化脚本稳定解析。实时进度只会在普通 终端报告中写入 stderr,可通过 `IPCHECK_PROGRESS=auto|always|never` 控制。 +交互运行开始时会提示“按 Ctrl+C 可随时取消”。收到中断后,ipcheck 会清除 +动态进度行、显示“已取消”、删除临时文件,并使用标准退出码 `130` 结束。 + “服务链路”测量的是首字节延迟和抖动,并不是下载速度。独立的“网络带宽”模块 会通过报告中注明的代理/网络路径下载最多 2 MB,并上传最多 1 MB 全零测试数据,然后按 日常开发需求分别评级。带宽不会掩盖 AI 服务本身的高延迟或不稳定。使用 @@ -123,6 +128,11 @@ ipcheck --no-upload “慢”。可选的 `--system` 会交给 macOS `networkQuality` 测试,消耗的数据量 可能明显高于 ipcheck 自带的限量 Cloudflare 测试。 +开发适配分是透明的规则评分,不代表“超过多少用户”的百分位。规则 v1 根据 +服务链路结论从 90(健康)、70(可用但偏慢)、45(可达但服务暂不可用)、 +35(较差)或 0(阻断)起算;完整的上下行测速最多加 5 分或扣 10 分,部分 +样本每项扣 2 分。JSON 会输出 `score`、`score_label` 和 `score_method`。 + 运行 `ipcheck --help` 可以查看完整参数。 ## 支持的客户端与配置 diff --git a/bin/ipcheck b/bin/ipcheck index 52135e1..c4f21a2 100755 --- a/bin/ipcheck +++ b/bin/ipcheck @@ -10,7 +10,7 @@ unset LC_ALL LC_NUMERIC=C export LC_NUMERIC -VERSION="0.5.0" +VERSION="0.6.0" SCHEMA_VERSION=1 SAMPLES=3 TIMEOUT=15 @@ -308,7 +308,62 @@ command -v sed >/dev/null 2>&1 || die "sed is required" command -v sort >/dev/null 2>&1 || die "sort is required" WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ipcheck.XXXXXX") || die "cannot create a temporary directory" -trap 'rm -rf "$WORK_DIR"' EXIT INT TERM +# Preserve the caller's stderr so signal feedback is not swallowed by a +# per-command `2>/dev/null` redirection active when the trap runs. +exec 3>&2 +# shellcheck disable=SC2317 # Invoked indirectly by the EXIT trap. +cleanup() { + rm -rf "$WORK_DIR" +} + +ACTIVE_CHILD_PID="" +# shellcheck disable=SC2317 # Invoked indirectly by signal handlers. +stop_active_child() { + stop_signal="$1" + if [ -n "$ACTIVE_CHILD_PID" ]; then + kill -"$stop_signal" "$ACTIVE_CHILD_PID" 2>/dev/null || true + wait "$ACTIVE_CHILD_PID" 2>/dev/null || true + ACTIVE_CHILD_PID="" + fi +} + +# shellcheck disable=SC2317 # Invoked indirectly by the INT trap. +cancel_run() { + cancel_status="$1" + trap - INT + # Bash starts managed commands asynchronously, where SIGINT may be ignored. + # TERM reliably stops that child after the parent receives Ctrl+C. + stop_active_child TERM + if [ "$PROGRESS_TTY" -eq 1 ]; then + printf '\r\033[2K' >&3 + fi + if [ "$UI_LANG" = "zh" ]; then + printf '%s已取消。%s\n' "$C_YELLOW" "$C_RESET" >&3 + else + printf '%sCancelled.%s\n' "$C_YELLOW" "$C_RESET" >&3 + fi + exit "$cancel_status" +} + +# shellcheck disable=SC2317 # Invoked indirectly by the TERM trap. +terminate_run() { + trap - TERM + stop_active_child TERM + exit 143 +} + +run_managed() { + (trap - INT TERM; exec "$@") & + ACTIVE_CHILD_PID=$! + wait "$ACTIVE_CHILD_PID" + managed_status=$? + ACTIVE_CHILD_PID="" + return "$managed_status" +} + +trap cleanup EXIT +trap 'cancel_run 130' INT +trap terminate_run TERM SERVICES_FILE="$WORK_DIR/services.tsv" PROBES_FILE="$WORK_DIR/probes.tsv" @@ -677,14 +732,14 @@ curl_for_service() { if [ "$https_proxy_env_found" -eq 0 ] && [ -n "$claude_http_proxy" ]; then HTTPS_PROXY="$claude_http_proxy" https_proxy="$claude_http_proxy" \ HTTP_PROXY="$claude_http_proxy" http_proxy="$claude_http_proxy" \ - ALL_PROXY='' all_proxy='' command curl -q "$@" + ALL_PROXY='' all_proxy='' run_managed curl -q "$@" else - ALL_PROXY='' all_proxy='' command curl -q "$@" + ALL_PROXY='' all_proxy='' run_managed curl -q "$@" fi elif [ -n "$system_curl_proxy" ] && [ "$service" != "claude" ]; then - command curl -q --proxy "$system_curl_proxy" "$@" + run_managed curl -q --proxy "$system_curl_proxy" "$@" else - command curl -q "$@" + run_managed curl -q "$@" fi } @@ -783,8 +838,10 @@ localized_bandwidth_rating() { endpoint_count=0 TAB=$(printf '\t') if [ "$UI_LANG" = "zh" ]; then + progress_note "⌨" "按 Ctrl+C 可随时取消。" progress_note "●" "开始检查 AI 编程服务链路…" else + progress_note "⌨" "Press Ctrl+C to cancel at any time." progress_note "●" "Checking AI coding service paths…" fi while IFS="$TAB" read -r service label endpoint probe_type priority; do @@ -796,13 +853,15 @@ while IFS="$TAB" read -r service label endpoint probe_type priority; do sample=1 while [ "$sample" -le "$SAMPLES" ]; do progress_sample "$service" "$label" "$sample" "$SAMPLES" - metrics=$(run_probe "$service" "$probe_type" "$endpoint" \ + metrics_file="$WORK_DIR/metrics_${endpoint_count}_${sample}.txt" + run_probe "$service" "$probe_type" "$endpoint" \ --proto '=http,https' --http1.1 -sS -o /dev/null \ -A "ipcheck/$VERSION" \ --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \ -w '%{http_code}\t%{time_namelookup}\t%{time_connect}\t%{time_appconnect}\t%{time_starttransfer}\t%{time_total}\t%{size_download}\t%{speed_download}' \ - 2>/dev/null) + > "$metrics_file" 2>/dev/null curl_exit=$? + metrics=$(< "$metrics_file") http_code=000 dns=0 connect=0 @@ -862,12 +921,14 @@ if [ "$RUN_BANDWIDTH" -eq 1 ]; then else progress_note "●" "Testing reference download bandwidth (2 MB)…" fi - bandwidth_metrics=$(curl_for_service "custom" --proto '=https' --http1.1 -sS -o /dev/null \ + 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" 2>/dev/null) + --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 < "$upload_file" 2>/dev/null; then - upload_metrics=$(curl_for_service "custom" --proto '=https' --http1.1 -sS -o /dev/null \ + 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" 2>/dev/null) + --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 </dev/null 2>&1; then system_file="$WORK_DIR/networkQuality.json" - networkQuality -c -s -M "$TIMEOUT" > "$system_file" 2>/dev/null || true + run_managed networkQuality -c -s -M "$TIMEOUT" > "$system_file" 2>/dev/null || true system_interface=$(sed -n 's/.*"interface_name" *: *"\([^"]*\)".*/\1/p' "$system_file" | head -n 1) system_rtt=$(sed -n 's/.*"base_rtt" *: *\([0-9.]*\).*/\1/p' "$system_file" | head -n 1) system_throughput=$(sed -n 's/.*"dl_throughput" *: *\([0-9.]*\).*/\1/p' "$system_file" | head -n 1) @@ -1210,8 +1273,47 @@ case "$overall" in ;; esac +# A transparent heuristic, not a user percentile. Service-path quality carries +# most of the score; completed bandwidth samples can move it by at most 10. +case "$overall" in + good) readiness_score=90 ;; + fair) + if [ "$usable_services" -gt 0 ]; then readiness_score=70; else readiness_score=45; fi + ;; + poor) readiness_score=35 ;; + *) readiness_score=0 ;; +esac +if [ "$readiness_score" -gt 0 ]; then + case "$download_rating" in + fast) readiness_score=$((readiness_score + 2)) ;; + slow) readiness_score=$((readiness_score - 5)) ;; + partial) readiness_score=$((readiness_score - 2)) ;; + esac + case "$upload_rating" in + fast) readiness_score=$((readiness_score + 3)) ;; + slow) readiness_score=$((readiness_score - 5)) ;; + partial) readiness_score=$((readiness_score - 2)) ;; + esac +fi +[ "$readiness_score" -ge 0 ] || readiness_score=0 +[ "$readiness_score" -le 100 ] || readiness_score=100 +if [ "$readiness_score" -ge 90 ]; then + readiness_score_label_en="COMFORTABLE" + readiness_score_label_zh="舒适" +elif [ "$readiness_score" -ge 75 ]; then + readiness_score_label_en="GOOD" + readiness_score_label_zh="良好" +elif [ "$readiness_score" -ge 60 ]; then + readiness_score_label_en="USABLE" + readiness_score_label_zh="可用" +else + readiness_score_label_en="LIMITED" + readiness_score_label_zh="受限" +fi + if [ "$UI_LANG" = "zh" ]; then readiness_answer="$readiness_answer_zh" + readiness_score_label="$readiness_score_label_zh" readiness_summary="$readiness_summary_zh" readiness_recommendation="$readiness_recommendation_zh" if [ "$service_count" -eq 1 ]; then @@ -1221,6 +1323,7 @@ if [ "$UI_LANG" = "zh" ]; then fi else readiness_answer="$readiness_answer_en" + readiness_score_label="$readiness_score_label_en" readiness_summary="$readiness_summary_en" readiness_recommendation="$readiness_recommendation_en" localized_overall_reason="$overall_reason" @@ -1239,11 +1342,13 @@ render_human() { printf '时间(UTC):%s\n网络路径:%s\n' "$timestamp" "$proxy_text" printf '\n%s开发建议%s\n' "$C_BOLD" "$C_RESET" printf ' 现在适合开发吗?%s%s%s\n' "$C_BOLD" "$readiness_answer" "$C_RESET" + printf ' 开发适配分:%s%s/100 · %s%s\n' "$C_BOLD" "$readiness_score" "$readiness_score_label" "$C_RESET" else printf '%sipcheck v%s — AI coding network diagnostics%s\n' "$C_BOLD$C_CYAN" "$VERSION" "$C_RESET" printf 'Time (UTC): %s\nNetwork path: %s\n' "$timestamp" "$proxy_text" printf '\n%sDeveloper verdict%s\n' "$C_BOLD" "$C_RESET" printf ' Ready to code? %s%s%s\n' "$C_BOLD" "$readiness_answer" "$C_RESET" + printf ' Readiness score: %s%s/100 · %s%s\n' "$C_BOLD" "$readiness_score" "$readiness_score_label" "$C_RESET" fi case "$overall" in good) verdict_color="$C_GREEN" ;; @@ -1343,6 +1448,7 @@ render_markdown() { printf -- "- 网络路径:\`%s\`\n" "$proxy_text" printf -- '- 结果:**%s** — %s\n' "$(printf '%s' "$overall" | tr '[:lower:]' '[:upper:]')" "$localized_overall_reason" printf -- '- 现在适合开发吗?**%s** — %s\n\n' "$readiness_answer" "$readiness_summary" + printf -- '- 开发适配分:**%s/100 · %s**(规则评分,不是用户百分位)\n\n' "$readiness_score" "$readiness_score_label" printf '## 检测到的客户端\n\n' printf '| 客户端 | 模型 | 路由 | 配置来源 | 结果 |\n|---|---|---|---|---|\n' else @@ -1351,6 +1457,7 @@ render_markdown() { printf -- "- Network path: \`%s\`\n" "$proxy_text" printf -- '- Result: **%s** — %s\n' "$(printf '%s' "$overall" | tr '[:lower:]' '[:upper:]')" "$localized_overall_reason" printf -- '- Ready to code? **%s** — %s\n\n' "$readiness_answer" "$readiness_summary" + printf -- '- Readiness score: **%s/100 · %s** (rule-based, not a user percentile)\n\n' "$readiness_score" "$readiness_score_label" printf '## Detected clients\n\n' printf '| Client | Model | Route | Source | Result |\n|---|---|---|---|---|\n' fi @@ -1427,9 +1534,9 @@ 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","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","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_v1","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","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' \ "$SCHEMA_VERSION" "$VERSION" "$timestamp" "$(json_escape "$proxy_text")" "$overall" "$(json_escape "$overall_reason")" \ - "$readiness_ready" "$readiness_level" "$(json_escape "$readiness_summary_en")" "$(json_escape "$readiness_recommendation_en")" \ + "$readiness_ready" "$readiness_level" "$readiness_score" "$(json_escape "$readiness_score_label_en")" "$(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" \ diff --git a/package.json b/package.json index ed81b72..086a569 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@jacklv-coder/ipcheck", "private": true, - "version": "0.5.0", + "version": "0.6.0", "description": "Diagnose proxy, latency, and API routes used by Codex and Claude Code.", "license": "Apache-2.0", "author": "Jintao", diff --git a/test/smoke.sh b/test/smoke.sh index 23dba4f..e2a0f87 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -18,6 +18,7 @@ mkdir -p "$CODEX_FIXTURE" "$CLAUDE_FIXTURE" cat > "$STUB_DIR/curl" <<'EOF' #!/usr/bin/env bash +[ -z "${IPCHECK_TEST_CURL_SLEEP:-}" ] || exec sleep "$IPCHECK_TEST_CURL_SLEEP" is_bandwidth=0 is_upload=0 is_blocked=0 @@ -172,7 +173,7 @@ 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.5.0" ] +[ "$("$PROJECT_DIR/bin/ipcheck" --version)" = "ipcheck 0.6.0" ] : > "$CURL_LOG" report=$(ANTHROPIC_AUTH_TOKEN="runtime-secret-must-never-appear" run_ipcheck --samples 3 --no-bandwidth --json) @@ -188,6 +189,7 @@ 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":90,"score_label":"COMFORTABLE","score_method":"rule_v1"' if printf '%s\n' "$report" | grep -Eq 'fixture-secret|runtime-secret'; then printf 'Claude credential leaked into JSON report\n' >&2 exit 1 @@ -266,6 +268,11 @@ printf '%s\n' "$decimal_report" | grep -q '"successful_samples":8' fair_report=$(IPCHECK_TEST_TTFB=1.000 run_ipcheck --samples 1 --no-bandwidth --endpoint https://fair.invalid --json) printf '%s\n' "$fair_report" | grep -q '"result":"fair"' printf '%s\n' "$fair_report" | grep -q '"level":"with_caution"' +printf '%s\n' "$fair_report" | grep -q '"score":70,"score_label":"USABLE"' +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":75,"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":60,"score_label":"USABLE"' 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' @@ -304,6 +311,7 @@ printf '%s\n' "$flaky_report" | grep -q '"successful_samples":1' not_found_report=$(run_ipcheck --samples 1 --no-bandwidth --endpoint https://not-found.invalid/v1/messages --json) printf '%s\n' "$not_found_report" | grep -q '"result":"poor"' +printf '%s\n' "$not_found_report" | grep -q '"score":35,"score_label":"LIMITED"' printf '%s\n' "$not_found_report" | grep -q 'configured API route returned HTTP 404' rate_limit_report=$(run_ipcheck --samples 1 --no-bandwidth --endpoint https://rate-limit.invalid --json) @@ -312,6 +320,7 @@ printf '%s\n' "$rate_limit_report" | grep -q '"ready":false,"level":"temporarily server_error_report=$(run_ipcheck --samples 1 --no-bandwidth --endpoint https://server-error.invalid --json) printf '%s\n' "$server_error_report" | grep -q '"result":"fair"' printf '%s\n' "$server_error_report" | grep -q '"ready":false,"level":"temporarily_unavailable"' +printf '%s\n' "$server_error_report" | grep -q '"score":45,"score_label":"LIMITED"' server_error_progress="$STUB_DIR/server-error-progress.log" IPCHECK_PROGRESS=always run_ipcheck --samples 1 --no-bandwidth --endpoint https://server-error.invalid >/dev/null 2>"$server_error_progress" @@ -438,18 +447,21 @@ 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":95,"score_label":"COMFORTABLE","score_method":"rule_v1"' printf '%s\n' "$bandwidth_report" | grep -q '"bandwidth":{"enabled":true,"available":true,"http_code":"200"' 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 '"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 '开发适配分:95/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 '路径 HTTPS_PROXY=http://127.0.0.1:1080' slow_upload_human=$(IPCHECK_TEST_UPLOAD_SPEED=100000 IPCHECK_LANG=en run_ipcheck --samples 1) printf '%s\n' "$slow_upload_human" | grep -q 'Upload.*0.8 Mbps.*SLOW' +printf '%s\n' "$slow_upload_human" | grep -q 'Readiness score: 87/100 · GOOD' printf '%s\n' "$slow_upload_human" | grep -q 'Upload is slow; sending large code contexts may take longer.' 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,' @@ -461,6 +473,7 @@ printf '%s\n' "$no_upload_report" | grep -q '"upload":{"enabled":false,"availabl 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":90,"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,' @@ -473,6 +486,67 @@ partial_markdown=$(IPCHECK_TEST_BANDWIDTH_BYTES=500000 IPCHECK_TEST_BANDWIDTH_EX printf '%s\n' "$partial_markdown" | grep -q '| Download | 80.0 Mbps | ESTIMATE |' printf '%s\n' "$partial_markdown" | grep -q 'partial estimates and do not prove bandwidth is sufficient' +progress_report=$(IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --progress 2>&1 >/dev/null) +printf '%s\n' "$progress_report" | grep -q 'Press Ctrl+C to cancel at any time.' +if command -v python3 >/dev/null 2>&1; then + PROJECT_DIR="$PROJECT_DIR" STUB_DIR="$STUB_DIR" FIXTURE_HOME="$FIXTURE_HOME" \ + CODEX_FIXTURE="$CODEX_FIXTURE" CLAUDE_FIXTURE="$CLAUDE_FIXTURE" CURL_LOG="$CURL_LOG" \ + python3 <<'PY' +import os +import glob +import signal +import subprocess +import time + +env = os.environ.copy() +for key in ( + "ANTHROPIC_BASE_URL", "ANTHROPIC_MODEL", "IPCHECK_SERVICES", "IPCHECK_ENDPOINTS", + "CODEX_NETWORK_ENDPOINTS", "CLAUDE_NETWORK_ENDPOINTS", "HTTP_PROXY", "http_proxy", + "ALL_PROXY", "all_proxy", +): + env.pop(key, None) +env.update({ + "PATH": os.environ["STUB_DIR"] + os.pathsep + env["PATH"], + "HOME": os.environ["FIXTURE_HOME"], + "CODEX_HOME": os.environ["CODEX_FIXTURE"], + "CLAUDE_CONFIG_DIR": os.environ["CLAUDE_FIXTURE"], + "HTTPS_PROXY": "http://127.0.0.1:1080", + "IPCHECK_LANG": "en", + "IPCHECK_PROGRESS": "always", + "IPCHECK_TEST_CURL_LOG": os.environ["CURL_LOG"], + "IPCHECK_TEST_CURL_SLEEP": "5", +}) +command = [os.path.join(os.environ["PROJECT_DIR"], "bin", "ipcheck"), "--samples", "1", "--no-bandwidth"] + +interrupt_tmp = os.path.join(os.environ["STUB_DIR"], "interrupt-tmp") +os.mkdir(interrupt_tmp) +interrupt_env = env.copy() +interrupt_env["TMPDIR"] = interrupt_tmp +process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=interrupt_env) +time.sleep(0.5) +started = time.monotonic() +process.send_signal(signal.SIGINT) +_, stderr = process.communicate(timeout=2) +assert process.returncode == 130, process.returncode +assert time.monotonic() - started < 2 +assert "Cancelled." in stderr, stderr +assert not glob.glob(os.path.join(interrupt_tmp, "ipcheck.*")) + +term_tmp = os.path.join(os.environ["STUB_DIR"], "term-tmp") +os.mkdir(term_tmp) +term_env = env.copy() +term_env["TMPDIR"] = term_tmp +process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=term_env) +time.sleep(0.5) +started = time.monotonic() +process.terminate() +process.communicate(timeout=2) +assert process.returncode == 143, process.returncode +assert time.monotonic() - started < 2 +assert not glob.glob(os.path.join(term_tmp, "ipcheck.*")) +PY +fi + grep -Fq -- "--max-time \"\$TIMEOUT\"" "$PROJECT_DIR/bin/ipcheck" grep -Fq "networkQuality -c -s -M \"\$TIMEOUT\"" "$PROJECT_DIR/bin/ipcheck" grep -q 'download_bits_per_second' "$PROJECT_DIR/bin/ipcheck" @@ -491,6 +565,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_v1"' markdown=$(run_ipcheck --samples 1 --no-bandwidth --markdown) printf '%s\n' "$markdown" | grep -q '^# ipcheck: AI Coding Network Report'