#!/bin/bash
# test_lab_start.sh — lab_start.sh のつなぎ直し (依頼 44) を, Pod を借りずに確かめる
#
#   ./test_lab_start.sh          全部流す (最後に「OK n / NG m」. NG があれば終了コード 1)
#
# 一時フォルダに lab_start.sh・lab_stop.sh を写し, 次を差し替えて --dry-run で最後まで流す:
#   - lab_lib.sh: 本物を読んだあとで, 置き場所を一時フォルダへ, runpod_api / pod_ssh / pod_scp を偽物にする
#     (偽物の API は api/ のファイルで答えを決め, 呼ばれた中身を calls.log・ssh.log・scp.log に残す)
#   - PATH の先頭に ssh (トンネル)・lsof・python3 (run_policy_client.py) の偽物
#   - lab_start.sh の写しの /Volumes/KIOXIA/r2r2r-data を一時フォルダに置き換える (SSD には触らない)
# Pod の中で流す確かめのコマンド (lab_probe_cmd) は, この Mac の bash で実際に流して書き方を確かめる

set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
OK=0; NG=0
ok() { OK=$((OK + 1)); echo "  ok   $1"; }
ng() { NG=$((NG + 1)); echo "  NG   $1"; }
check() { if eval "$2"; then ok "$1"; else ng "$1"; fi; }

ID_OLD=oldpod0000001
ID_NEW=newpod0000002

setup() {                   # 1 つの場合ごとに新しい一時フォルダを作る
    T=$(mktemp -d)
    mkdir -p "$T/bin" "$T/api" "$T/keys" "$T/pi05" \
             "$T/data/lab/Log11/pod" "$T/data/lab/LogT/pod" \
             "$T/data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_194818"
    echo "x" > "$T/data/lab/Log11/pod/add_trim_config.py"
    echo "rk" > "$T/keys/runpod"; echo "hk" > "$T/keys/hf"; echo "k" > "$T/keys/id"; echo "k.pub" > "$T/keys/id.pub"
    sed -e "s#/Volumes/KIOXIA/r2r2r-data#$T/data#g" -e 's#^LISTEN_WAIT=30; LISTEN_POLL=5#LISTEN_WAIT=2; LISTEN_POLL=1#' \
        "$HERE/lab_start.sh" > "$T/lab_start.sh"
    cp "$HERE/lab_stop.sh" "$T/lab_stop.sh"
    chmod +x "$T/lab_start.sh" "$T/lab_stop.sh"
    cat > "$T/lab_lib.sh" <<EOF
. "$HERE/lab_lib.sh"
T="$T"
LAB_ROOT="\$T/data/lab"; LAB_CURRENT="\$LAB_ROOT/.lab_current"
RUNPOD_KEY_FILE="\$T/keys/runpod"; HF_TOKEN_FILE="\$T/keys/hf"; POD_SSH_KEY="\$T/keys/id"
. "\$T/fakes.sh"
EOF
    cat > "$T/fakes.sh" <<'EOF'
runpod_api() {              # api/ の答えを返す. DELETE のあとは, その Pod はないことにする
    local m="$1" p="$2" id
    echo "$m $p" >> "$T/calls.log"
    case "$m $p" in
        "GET /pods")
            [ -f "$T/api/list_delay" ] && . "$T/api/list_delay"
            if [ -f "$T/api/deleted" ] && [ -f "$T/api/list_after.txt" ]; then
                cat "$T/api/list_after.txt"
            elif [ -f "$T/api/deleted" ]; then
                jq --arg id "$(cat "$T/api/deleted")" 'map(select(.id != $id))' "$T/api/list.json"
            else cat "$T/api/list.json"; fi
            RUNPOD_HTTP=$(cat "$T/api/list.code" 2>/dev/null || echo 200) ;;
        "GET /pods/"*)
            id=${p#/pods/}
            if [ -f "$T/api/deleted" ] && [ "$(cat "$T/api/deleted")" = "$id" ]; then
                echo '{"error":"pod not found"}'; RUNPOD_HTTP=404
            elif [ "$id" = "$ID_NEW" ]; then
                echo "{\"id\":\"$ID_NEW\",\"desiredStatus\":\"RUNNING\",\"publicIp\":\"10.9.8.7\",\"portMappings\":{\"22\":40123}}"
                RUNPOD_HTTP=200
            else
                cat "$T/api/pod.json"; RUNPOD_HTTP=$(cat "$T/api/pod.code")
            fi ;;
        "POST /pods")
            echo "{\"id\":\"$ID_NEW\",\"costPerHr\":0.59}"; RUNPOD_HTTP=200 ;;
        "DELETE /pods/"*)
            RUNPOD_HTTP=$(cat "$T/api/delete.code" 2>/dev/null || echo 200)
            case "$RUNPOD_HTTP" in 2??) echo "${p#/pods/}" > "$T/api/deleted" ;; esac
            echo '{}' ;;
    esac
    case "$RUNPOD_HTTP" in 2??) return 0 ;; *) return 1 ;; esac
}
pod_ssh() {                 # 呼ばれたコマンドを ssh.log に. 確かめのコマンドには api/probe.txt (2 回目からは api/probe2.txt があればそれ) を返す
    printf '%s\n----\n' "$*" >> "$T/ssh.log"
    cat > /dev/null
    case "$*" in
        *"echo \"PROBE serve="*)
            echo x >> "$T/probe.n"
            if [ -f "$T/api/probe2.txt" ] && [ $(wc -l < "$T/probe.n") -ge 2 ]; then cat "$T/api/probe2.txt"; else cat "$T/api/probe.txt"; fi ;;
        *"grep 'server listening'"*) echo "INFO:websockets.server:server listening on 0.0.0.0:8000" ;;
        *"pkill -9 -f"*) [ -f "$T/api/stop_fail" ] && return 1 ;;
    esac
    return 0
}
pod_scp() {
    echo "$*" >> "$T/scp.log"
    case "$1" in *:/workspace/logs/serve.log) echo "old serve line" > "$2" ;; esac
    return 0
}
lab_tunnel_pids() { :; }
EOF
    echo "ID_NEW=$ID_NEW" >> "$T/fakes.sh"
    printf '#!/bin/bash\nsleep 1\n' > "$T/bin/ssh"
    printf '#!/bin/bash\nn=$(cat "%s/lsof.n" 2>/dev/null || echo 0); echo $((n+1)) > "%s/lsof.n"; [ "$n" -gt 0 ]\n' "$T" "$T" > "$T/bin/lsof"
    printf '#!/bin/bash\necho "python3 $*" >> "%s/py.log"\n' "$T" > "$T/bin/python3"
    chmod +x "$T/bin/"*
    echo '[]' > "$T/api/list.json"
    : > "$T/calls.log"; : > "$T/ssh.log"; : > "$T/scp.log"
}

state() {                   # 記録の lab_pod.json を置く
    jq -n --arg id "$1" '{id:$id, gpu:"NVIDIA A40", log:"LogT", costPerHr:0.59, created_epoch:1793438135,
                          created_at:"2026-10-31 18:15:35"}' > "$T/data/lab/LogT/pod/lab_pod.json"
}
pod() { echo "$2" > "$T/api/pod.json"; echo "$1" > "$T/api/pod.code"; }
probe() { echo "PROBE serve=$1 listen=$2 ckpt=$3 mark=${6:-$3} openpi=$4 venv=$5" > "$T/api/probe.txt"; }   # 6 番目は済んだ印 (既定は ckpt と同じ)
probe2() { echo "PROBE serve=$1 listen=$2 ckpt=$3 mark=$3 openpi=$4 venv=$5" > "$T/api/probe2.txt"; }
run() { (cd "$T" && PATH="$T/bin:$PATH" ./lab_start.sh --log LogT --dry-run < /dev/null > "$T/out.txt" 2>&1); RC=$?; }
has() { grep -qF -- "$2" "$T/$1"; }
n_call() { grep -c -- "^$1" "$T/calls.log"; }
archived() { ls "$T/data/lab/LogT/pod/" | grep -cE '^lab_pod_[0-9]{8}_[0-9]{6}(_[0-9]+)?\.json$'; }
RUNNING="{\"id\":\"$ID_OLD\",\"desiredStatus\":\"RUNNING\",\"publicIp\":\"10.1.2.3\",\"portMappings\":{\"22\":40999}}"

echo "== 1. RUNNING・サーバーが待っている → つなぎ直し, 3b〜4 を飛ばす"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1; run
check "終了コード 0" '[ $RC = 0 ]'
check "「つなぎ直します」と出る" 'has out.txt "LogT の Pod (id $ID_OLD) につなぎ直します"'
check "POST・DELETE を呼ばない" '[ $(n_call POST) = 0 ] && [ $(n_call DELETE) = 0 ]'
check "IP とポートを API から取り直す (GET /pods/id)" '[ $(n_call "GET /pods/$ID_OLD") -ge 2 ] && has out.txt "API から取り直した"'
check "スクリプトは送る (3a)" 'has scp.log "/pi05 root@10.1.2.3:/workspace/r2r2r/"'
check "hf download・git clone・uv sync・tmux new を流さない" '! has ssh.log "hf download" && ! has ssh.log "git clone" && ! has ssh.log "uv sync" && ! has ssh.log "tmux new"'
check "一覧に「つなぎ直し」の行" 'has out.txt "1. Pod につなぎ直す" && has out.txt "かかった時間 (LogT の Pod (id $ID_OLD) につなぎ直し)" && has out.txt "3b〜4. 準備とサーバーの起動 (飛ばした)"'
check "記録はそのまま (名前を変えない)" '[ "$(jq -r .id "$T/data/lab/LogT/pod/lab_pod.json")" = $ID_OLD ] && [ $(archived) = 0 ]'
check "推論のクライアントまで進む" 'has py.log "run_policy_client.py"'
check "ターミナルの記録 lab_start_*.txt が残る" 'ls "$T/data/lab/LogT"/lab_start_*.txt >/dev/null 2>&1 && grep -qF "つなぎ直し)" "$T/data/lab/LogT"/lab_start_*.txt'
check "IP とポートを画面に出さない" '! has out.txt "10.1.2.3" && ! has out.txt "40999"'
sed -n '/=== 1. Pod/,/=== 3a/p' "$T/out.txt" > /tmp/lab_reattach_sample.txt; sed -n '/=== かかった時間/,$p' "$T/out.txt" >> /tmp/lab_reattach_sample.txt
rm -rf "$T"

echo "== 2. RUNNING・サーバーが落ちている・モデルと openpi はある・.venv がない"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 0 0 1 1 0; run
check "終了コード 0" '[ $RC = 0 ]'
check "hf download・git clone を流さない" '! has ssh.log "hf download" && ! has ssh.log "git clone"'
check "uv sync・3f・サーバーの起動を流す" 'has ssh.log "uv sync" && has ssh.log "install_addon.py" && has ssh.log "tmux new -d -s serve"'
check "前の serve.log を持ち帰る" 'ls "$T/data/lab/LogT"/serve_before_restart_*.log >/dev/null 2>&1'
check "一覧に飛ばした行" 'has out.txt "3b・3c. huggingface_hub・モデル (飛ばした)" && has out.txt "3d. openpi を取る (215abfb) (飛ばした)"'
check "POST しない" '[ $(n_call POST) = 0 ]'
rm -rf "$T"

echo "== 3. RUNNING・サーバーが落ちている・モデルも openpi もない (.venv はある)"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 0 0 0 0 1; run
check "終了コード 0" '[ $RC = 0 ]'
check "hf download・openpi・(openpi を取り直したので) uv sync を流す" 'has ssh.log "hf download" && has ssh.log "touch /workspace/ckpt/3999/.lab_download_ok" && has ssh.log "git checkout -q 215abfb" && has ssh.log "uv sync"'
rm -rf "$T"

echo "== 4. RUNNING・プロセスはあるが 8000 番で待っていない → 待っても待たない → 起動し直す (依頼 47. テストでは 30 秒を 2 秒に)"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 0 1 1 1; run
check "終了コード 0" '[ $RC = 0 ]'
check "待つと出し, 待つ間に確かめ直す" 'has out.txt "読み込み中かもしれないので 2 秒まで待ちます" && [ $(wc -l < "$T/probe.n") -ge 2 ] && has out.txt "2 秒待っても 8000 番で待っていない. 起動し直します"'
check "サーバーを起動し直す (前のプロセスを止めてから)・ほかの準備は飛ばす" 'has ssh.log "tmux new -d -s serve" && has ssh.log "pkill -f '"'"'[s]cripts/serve_policy" && [ $(grep -n "^tmux kill-session" "$T/ssh.log" | head -1 | cut -d: -f1) -lt $(grep -n "tmux new -d -s serve" "$T/ssh.log" | cut -d: -f1) ] && ! has ssh.log "hf download" && ! has ssh.log "uv sync"'
check "前の serve.log を持ち帰る" 'ls "$T/data/lab/LogT"/serve_before_restart_*.log >/dev/null 2>&1'
rm -rf "$T"

echo "== 4b. プロセスはあるが 8000 番で待っていない → 待つ間に待ち始めた → 起動し直さない"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 0 1 1 1; probe2 1 1 1 1 1; run
check "終了コード 0・待ち始めたと出す" '[ $RC = 0 ] && has out.txt "秒で 8000 番で待ち始めました" && has out.txt "3b〜4 を飛ばします"'
check "起動し直さない・前の serve.log も持ち帰らない" '! has ssh.log "tmux new" && ! has ssh.log "pkill" && ! ls "$T/data/lab/LogT"/serve_before_restart_*.log >/dev/null 2>&1'
rm -rf "$T"

echo "== 4c. プロセスはあるが 8000 番で待っていない → 待つ間にプロセスが終わった → 待つのをやめて起動し直す"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 0 1 1 1; probe2 0 0 1 1 1; run
check "終了コード 0・終わったと出して起動し直す" '[ $RC = 0 ] && has out.txt "(その間にプロセスが終わった). 起動し直します" && has ssh.log "tmux new -d -s serve"'
check "1 回確かめ直したところで待つのをやめる" '[ $(wc -l < "$T/probe.n") = 2 ]'
rm -rf "$T"

echo "== 4e. 前の serve_policy.py を止められない → 起動せずに止まる"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 0 0 1 1 1; touch "$T/api/stop_fail"; run
check "終了コード 1・起動しない・止められないと出す" '[ $RC = 1 ] && ! has ssh.log "tmux new" && has out.txt "前の serve_policy.py を止められない"'
rm -rf "$T"

echo "== 4d. 待っていれば, 待たずに進む (確かめは 1 回)"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1; run
check "確かめ直さない・待つと出さない" '[ $(wc -l < "$T/probe.n") = 1 ] && ! has out.txt "秒まで待ちます"'
rm -rf "$T"

echo "== 27. 依頼 44 より前に作った Pod (モデルの中身はあるが済んだ印がない)・サーバーは待っている (2026-10-31 18:50 の実機)"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1 0; run
check "終了コード 0・モデルを「ない」と出さない" '[ $RC = 0 ] && ! has out.txt "モデル (/workspace/ckpt/3999): ない" && has out.txt "モデル (/workspace/ckpt/3999): ある (取り終えた印 .lab_download_ok はない"'
check "3b〜4 を飛ばす" 'has out.txt "3b〜4 を飛ばします" && ! has ssh.log "hf download"'
rm -rf "$T"

echo "== 28. モデルの中身はあるが済んだ印がない・サーバーが落ちている → 3b・3c を流し直す (openpi・.venv は飛ばす)"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 0 0 1 1 1 0; run
check "終了コード 0・hf download を流し, 印を付ける" '[ $RC = 0 ] && has ssh.log "hf download" && has ssh.log "touch /workspace/ckpt/3999/.lab_download_ok"'
check "openpi・uv sync は飛ばす" '! has ssh.log "git clone" && ! has ssh.log "uv sync"'
rm -rf "$T"

echo "== 29. モデルの中身も印もある・サーバーが落ちている → 3b・3c を飛ばす"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 0 0 1 1 1 1; run
check "終了コード 0・hf download を流さない・起動はする" '[ $RC = 0 ] && ! has ssh.log "hf download" && has ssh.log "tmux new -d -s serve" && has out.txt "モデル (/workspace/ckpt/3999): ある  "'
rm -rf "$T"

echo "== 30. 動いている途中で lab_start.sh が書き換わっても, その回は最後まで流れる (依頼 47)"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1
# 推論のクライアント (偽物) が, 動いている lab_start.sh をその場で書き換える (同じファイルに上書き)
printf '#!/bin/bash\necho "python3 $*" >> "%s/py.log"\n{ cat "%s/lab_start.sh.new"; echo "echo REWRITTEN-RAN; )"; } > "%s/lab_start.sh"\n' "$T" "$T" "$T" > "$T/bin/python3"
{ echo '#!/bin/bash'; for i in $(seq 1 600); do echo "# 書き換えたあとの行 $i ......................................"; done; } > "$T/lab_start.sh.new"
run
check "終了コード 0・最後まで流れる・syntax error が出ない" '[ $RC = 0 ] && has out.txt "Pod は残しています" && ! has out.txt "syntax error" && ! has out.txt "REWRITTEN-RAN"'
check "書き換わっている (テストの前提)" 'grep -q "REWRITTEN-RAN" "$T/lab_start.sh"'
rm -rf "$T"

echo "== 5. 記録の Pod が止まっている (EXITED) → lab_stop.sh と同じ消し方で消してから作る"
setup; state $ID_OLD; pod 200 "{\"id\":\"$ID_OLD\",\"desiredStatus\":\"EXITED\"}"
echo "[{\"id\":\"$ID_OLD\",\"name\":\"p2m-LogT\",\"desiredStatus\":\"EXITED\",\"costPerHr\":0.59}]" > "$T/api/list.json"
probe 1 1 1 1 1; run
check "終了コード 0" '[ $RC = 0 ]'
check "止まっていると出す" 'has out.txt "(id $ID_OLD) は止まっています (状態 EXITED)"'
check "DELETE してから POST" '[ $(n_call DELETE) = 1 ] && [ $(n_call POST) = 1 ] && [ $(grep -n "^DELETE" "$T/calls.log" | cut -d: -f1) -lt $(grep -n "^POST" "$T/calls.log" | cut -d: -f1) ]'
check "lab_stop の記録 (lab_stop_*.txt) が残る" 'ls "$T/data/lab/LogT"/lab_stop_*.txt >/dev/null 2>&1'
check "古い記録は時刻付きで残り, 消した時刻が入っている" '[ $(archived) = 1 ] && jq -e ".id == \"$ID_OLD\" and (.terminated_epoch | type) == \"number\"" "$T"/data/lab/LogT/pod/lab_pod_2*.json >/dev/null'
check "新しい記録は新しい Pod" '[ "$(jq -r .id "$T/data/lab/LogT/pod/lab_pod.json")" = $ID_NEW ]'
check "新しい Pod は全部の準備を流す" 'has ssh.log "hf download" && has ssh.log "uv sync" && has ssh.log "tmux new -d -s serve"'
rm -rf "$T"

echo "== 6. 記録の Pod が API にない (404・一覧にもない) → 作る"
setup; state $ID_OLD; pod 404 '{"error":"pod not found"}'; run
check "終了コード 0・DELETE なし・POST 1 回" '[ $RC = 0 ] && [ $(n_call DELETE) = 0 ] && [ $(n_call POST) = 1 ]'
check "古い記録は時刻付きで残る" '[ $(archived) = 1 ] && has out.txt "は API で見ると残っていない"'
rm -rf "$T"

echo "== 7. 記録の Pod が TERMINATED (200 で返る) → 作る"
setup; state $ID_OLD; pod 200 "{\"id\":\"$ID_OLD\",\"desiredStatus\":\"TERMINATED\"}"; run
check "終了コード 0・DELETE なし・POST 1 回・古い記録を残す" '[ $RC = 0 ] && [ $(n_call DELETE) = 0 ] && [ $(n_call POST) = 1 ] && [ $(archived) = 1 ]'
rm -rf "$T"

echo "== 8. GET が 500 で, 一覧にはまだある → 分からないので止まる (作らない)"
setup; state $ID_OLD; pod 500 '{"error":"internal"}'
echo "[{\"id\":\"$ID_OLD\",\"name\":\"p2m-LogT\",\"desiredStatus\":\"RUNNING\",\"costPerHr\":0.59}]" > "$T/api/list.json"; run
check "終了コード 1・POST・DELETE なし" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && [ $(n_call DELETE) = 0 ]'
check "二重に借りないよう止まると出す・記録はそのまま" 'has out.txt "二重に借りないよう止まります" && [ $(archived) = 0 ]'
rm -rf "$T"

echo "== 9. API につながらない (000) → 止まる"
setup; state $ID_OLD; pod 000 ''; run
check "終了コード 1・POST なし・一覧も見ない" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && [ $(n_call "GET /pods$") = 0 ]'
rm -rf "$T"

echo "== 10. 200 なのに状態がない → 止まる"
setup; state $ID_OLD; pod 200 "{\"id\":\"$ID_OLD\"}"; run
check "終了コード 1・POST・DELETE なし" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && [ $(n_call DELETE) = 0 ]'
rm -rf "$T"

echo "== 11. 記録はないが p2m-LogT が残っている → 止まる"
setup; echo "[{\"id\":\"$ID_OLD\",\"name\":\"p2m-LogT\",\"desiredStatus\":\"RUNNING\",\"costPerHr\":0.59}]" > "$T/api/list.json"; run
check "終了コード 1・POST なし" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && has out.txt "記録にない LogT の Pod"'
rm -rf "$T"

echo "== 12. 記録がなく, ほかの Log の Pod だけが残っている → 今までどおり作る"
setup; echo "[{\"id\":\"other01\",\"name\":\"p2m-Log16\",\"desiredStatus\":\"RUNNING\",\"costPerHr\":0.59}]" > "$T/api/list.json"; run
check "終了コード 0・POST 1 回・ほかの Pod の WARN" '[ $RC = 0 ] && [ $(n_call POST) = 1 ] && has out.txt "ほかに Pod が 1 個あります"'
check "準備を全部流す・つなぎ直しの行はない" 'has ssh.log "hf download" && has ssh.log "git clone" && has ssh.log "uv sync" && ! has out.txt "つなぎ直し"'
rm -rf "$T"

echo "== 13. 記録に id がない → 時刻付きで残して作る"
setup; echo '{}' > "$T/data/lab/LogT/pod/lab_pod.json"; run
check "終了コード 0・POST 1 回・古い記録を残す" '[ $RC = 0 ] && [ $(n_call POST) = 1 ] && [ $(archived) = 1 ]'
rm -rf "$T"

echo "== 14. Pod の中を確かめる答えが読めない → 止まる"
setup; state $ID_OLD; pod 200 "$RUNNING"; echo "garbage" > "$T/api/probe.txt"; run
check "終了コード 1・サーバーを起動しない" '[ $RC = 1 ] && ! has ssh.log "tmux new" && has out.txt "Pod の中を確かめられない"'
rm -rf "$T"

echo "== 16. 記録がなく, 一覧の答えが読めない (null・{}・壊れた JSON・空・id のない要素) → 止まる"
for bad in 'null' '{}' 'garbage' '' '[{"name":"p2m-LogT"}]'; do
    setup; printf '%s' "$bad" > "$T/api/list.json"; run
    check "一覧 '$bad': 終了コード 1・POST なし" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && has out.txt "一覧の答えが読めない"'
    rm -rf "$T"
done

echo "== 17. 記録がなく, 一覧が取れない (500) → 止まる"
setup; echo 500 > "$T/api/list.code"; run
check "終了コード 1・POST なし" '[ $RC = 1 ] && [ $(n_call POST) = 0 ]'
rm -rf "$T"

echo "== 18. 200 だが id が記録と違う / {} → 止まる"
for body in '{"id":"someoneelse","desiredStatus":"RUNNING"}' '{}' '[]'; do
    setup; state $ID_OLD; pod 200 "$body"; run
    check "答え '$body': 終了コード 1・POST・DELETE なし・記録はそのまま" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && [ $(n_call DELETE) = 0 ] && [ $(archived) = 0 ]'
    rm -rf "$T"
done

echo "== 19. 知らない状態 (CREATED) → 消さずに止まる"
setup; state $ID_OLD; pod 200 "{\"id\":\"$ID_OLD\",\"desiredStatus\":\"CREATED\"}"; run
check "終了コード 1・POST・DELETE なし" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && [ $(n_call DELETE) = 0 ]'
rm -rf "$T"

echo "== 20. GET が 401 → 一覧も見ずに止まる"
setup; state $ID_OLD; pod 401 '{"error":"unauthorized"}'; run
check "終了コード 1・POST なし・一覧を見ない" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && [ $(n_call "GET /pods$") = 0 ]'
rm -rf "$T"

echo "== 21. EXITED を消そうとして DELETE が失敗 → 作らずに止まる"
setup; state $ID_OLD; pod 200 "{\"id\":\"$ID_OLD\",\"desiredStatus\":\"EXITED\"}"; echo 500 > "$T/api/delete.code"; run
check "終了コード 1・POST なし・記録はそのまま" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && [ $(archived) = 0 ] && has out.txt "止まっている Pod (id $ID_OLD) を消せない"'
rm -rf "$T"

echo "== 22. EXITED を DELETE したあと, 一覧の答えが読めない → 消えたと見なさず止まる (lab_stop.sh が 2 分半待つ)"
setup; state $ID_OLD; pod 200 "{\"id\":\"$ID_OLD\",\"desiredStatus\":\"EXITED\"}"; echo 'garbage' > "$T/api/list_after.txt"; run
check "終了コード 1・POST なし・記録は残る" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && [ -f "$T/data/lab/LogT/pod/lab_pod.json" ] && [ $(archived) = 0 ]'
rm -rf "$T"

echo "== 23. 同じ Log の lab_start.sh が動いている (印あり・pid が生きている) → 止まる"
setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1
mkdir "$T/data/lab/LogT/pod/.lab_start.lock"; sleep 30 & live=$!; echo $live > "$T/data/lab/LogT/pod/.lab_start.lock/pid"; run
check "終了コード 1・API を呼ばない・印は残す" '[ $RC = 1 ] && [ ! -s "$T/calls.log" ] && has out.txt "lab_start.sh がもう動いています" && [ -d "$T/data/lab/LogT/pod/.lab_start.lock" ]'
kill $live 2>/dev/null; wait $live 2>/dev/null
echo "== 24. 印が残っているが pid はもういない・pid がない → 取り直さずに止まる (消し方を出す)"
run
check "死んだ pid: 終了コード 1・API を呼ばない・印は残す・消し方を出す" '[ $RC = 1 ] && [ ! -s "$T/calls.log" ] && has out.txt "印が残っています (pid ${live}・" && has out.txt "rm -r " && [ -d "$T/data/lab/LogT/pod/.lab_start.lock" ]'
rm -f "$T/data/lab/LogT/pod/.lab_start.lock/pid"; run
check "pid がない (mkdir の直後): 終了コード 1・API を呼ばない・印は残す" '[ $RC = 1 ] && [ ! -s "$T/calls.log" ] && [ -d "$T/data/lab/LogT/pod/.lab_start.lock" ]'
rm -r "$T/data/lab/LogT/pod/.lab_start.lock"; run
check "印を消せば進む・終わったら印は外れる" '[ $RC = 0 ] && has out.txt "につなぎ直します" && [ ! -e "$T/data/lab/LogT/pod/.lab_start.lock" ]'
rm -rf "$T"

echo "== 26. 同じ Log の lab_start.sh を 4 つ同時に打つ (一覧の答えを 2 秒遅らせる) → 作るのは 1 回だけ"
setup; echo "sleep 2" > "$T/api/list_delay"
for i in 1 2 3 4; do
    (cd "$T" && PATH="$T/bin:$PATH" ./lab_start.sh --log LogT --dry-run < /dev/null > "$T/out$i.txt" 2>&1; echo $? > "$T/rc$i") &
done
wait
check "POST は 1 回・終了コード 0 は 1 つ・ほかは「もう動いています」で止まる" '[ $(n_call POST) = 1 ] && [ "$(cat "$T"/rc? | sort | tr -d "\n")" = "0111" ] && [ $(grep -l "lab_start.sh がもう動いています\|印が残っています" "$T"/out?.txt | wc -l) = 3 ]'
rm -rf "$T"

echo "== 25. ふつうに終わったら・止まったら印を外す"
setup; run
check "作る道で終わったあと印がない" '[ $RC = 0 ] && [ ! -e "$T/data/lab/LogT/pod/.lab_start.lock" ]'
rm -rf "$T"
setup; state $ID_OLD; pod 000 ''; run
check "止まったあとも印がない" '[ $RC = 1 ] && [ ! -e "$T/data/lab/LogT/pod/.lab_start.lock" ]'
rm -rf "$T"

echo "== 15. lab_probe_cmd をこの Mac の bash で流す"
. "$HERE/lab_lib.sh"
NS=assets/local/mecharm_redblock_box_teach_v1_trim2/norm_stats.json
cmd=$(lab_probe_cmd 215abfb /nonexistent/ckpt "$NS")
check "bash で書き方の誤りがない" 'bash -n <<<"$cmd"'
out=$(bash -c "$cmd" 2>&1)
check "PROBE の1行だけを出し, 自分の bash -c をサーバーと取り違えない" '[ "$out" = "PROBE serve=0 listen=0 ckpt=0 mark=0 openpi=0 venv=0" ]'
fd=$(mktemp -d); mkdir -p "$fd/scripts"; echo 'sleep 5' > "$fd/scripts/serve_policy.py"
bash "$fd/scripts/serve_policy.py" &
fake=$!; sleep 0.3
check "serve_policy.py のプロセスがあれば serve=1" '[[ "$(bash -c "$cmd")" == "PROBE serve=1 "* ]]'
kill $fake 2>/dev/null; wait $fake 2>/dev/null; rm -rf "$fd"
sample=$(mktemp)
printf '  sl  local_address rem_address   st\n   0: 00000000:1F40 00000000:0000 0A 0\n' > "$sample"
lcmd=$(grep '^l=0' <<<"$cmd" | sed "s#cat /proc/net/tcp /proc/net/tcp6 2>/dev/null#cat $sample#")
check "8000 番 (1F40) が LISTEN (0A) なら listen=1" '[ "$(bash -c "$lcmd; echo \$l")" = 1 ]'
printf '  sl  local_address rem_address   st\n   0: 00000000:1F40 0100007F:1234 01 0\n   1: 00000000:1F41 00000000:0000 0A 0\n' > "$sample"
check "つながっているだけ (01)・別の番号なら listen=0" '[ "$(bash -c "$lcmd; echo \$l")" = 0 ]'
rm -f "$sample"
stopc=$(lab_serve_stop_cmd)
check "止めるコマンドに scripts/serve_policy.py をそのまま書かない (自分の bash -c に当たらない)" '! grep -q "[^[]scripts/serve_policy" <<<"$stopc"'
fd=$(mktemp -d); mkdir -p "$fd/scripts"; echo 'sleep 30' > "$fd/scripts/serve_policy.py"
bash "$fd/scripts/serve_policy.py" &
fake=$!; sleep 0.3
out=$(bash -c "$stopc; echo STOP-RAN-TO-END:\$?" 2>&1)
check "止めるコマンドを流すと, serve_policy.py は終わり, 自分は最後まで流れて 0 を返す" '! kill -0 $fake 2>/dev/null && [[ "$out" == *"STOP-RAN-TO-END:0"* ]]'
wait $fake 2>/dev/null; rm -rf "$fd"
# hf download が /workspace/ckpt/3999 に置くもの (Hugging Face の一覧の 33 ファイルのうち確かめに使うもの) を作る
d=$(mktemp -d)
pr() { bash -c "$(lab_probe_cmd 215abfb "$d" "$NS")"; }
mkdir -p "$d/params/ocdbt.process_0/d" "$d/assets/local/mecharm_redblock_box_teach_v1_trim2" "$d/.cache/huggingface/download/params"
touch "$d/_CHECKPOINT_METADATA" "$d/params/_METADATA" "$d/params/manifest.ocdbt" "$d/params/ocdbt.process_0/manifest.ocdbt" "$d/$NS"
check "中身がそろっていて済んだ印がない (依頼 44 より前の Pod) → ckpt=1 mark=0" '[[ "$(pr)" == *" ckpt=1 mark=0 "* ]]'
touch "$d/.lab_download_ok"
check "中身と済んだ印 → ckpt=1 mark=1" '[[ "$(pr)" == *" ckpt=1 mark=1 "* ]]'
touch "$d/.cache/huggingface/download/params/x.incomplete"
check "取りかけ (*.incomplete) が残っていれば ckpt=0" '[[ "$(pr)" == *" ckpt=0 mark=1 "* ]]'
rm "$d/.cache/huggingface/download/params/x.incomplete"
chmod 000 "$d/.cache/huggingface/download"
check "取りかけを探せない (読めない) ときは ckpt=0" '[[ "$(pr)" == *" ckpt=0 "* ]]'
chmod 755 "$d/.cache/huggingface/download"
rm -rf "$d/.cache"
check ".cache/huggingface がなければ (取りかけはない) ckpt=1" '[[ "$(pr)" == *" ckpt=1 "* ]]'
for f in _CHECKPOINT_METADATA params/_METADATA params/manifest.ocdbt params/ocdbt.process_0/manifest.ocdbt "$NS"; do
    mv "$d/$f" "$d/$f.bak"
    check "$f がなければ ckpt=0" '[[ "$(pr)" == *" ckpt=0 "* ]]'
    mv "$d/$f.bak" "$d/$f"
done
rm -rf "$d/params" "$d/_CHECKPOINT_METADATA"; mkdir -p "$d/assets/local"
check "済んだ印と assets だけ (前の確かめなら ckpt=1 だった形) → ckpt=0" '[[ "$(pr)" == *" ckpt=0 mark=1 "* ]]'
rm -rf "$d"

echo
echo "OK $OK / NG $NG"
[ $NG = 0 ]
