#!/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" < "$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 ]