File size: 12,767 Bytes
4abeb9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#!/usr/bin/env bash
#
# validate-submission.sh — OpenEnv Submission Validator for Viraltest
#
# Checks that your HF Space is live, Docker image builds, and openenv validate passes.
#
# Prerequisites:
#   - Docker:       https://docs.docker.com/get-docker/
#   - openenv validate: uv sync (uses .venv/bin/openenv), or pip install openenv-core, or uv on PATH
#   - curl (usually pre-installed)
#
# Run:
#     chmod +x validate-submission.sh
#     ./validate-submission.sh <ping_url> [repo_dir]
#
# Optional: create repo-local .env (gitignored) with HF_TOKEN=... — sourced automatically.
#     cp .env.example .env   # then edit .env
#
# Skip Docker build (Step 2) — faster local checks; run full build before submit:
#     SKIP_DOCKER=1 ./validate-submission.sh https://your-space.hf.space
#
# Step 5 — Hugging Face Inference Router LLM smoke test (runs by default if HF_TOKEN is set):
#     export HF_TOKEN=hf_...   # required for Step 5; never commit; use Space Secrets for deploys
#     # Optional overrides (defaults match inference.py / HF router):
#     export MODEL_NAME=gemma-4-E4B-it-IQ4_XS
#     export API_BASE_URL=https://router.huggingface.co/v1
#     SKIP_LLM_SMOKE=1         # only if you must skip Step 5 (e.g. CI without secrets)
#
# HF token permissions (403 = insufficient permissions):
#   - Create or edit at https://huggingface.co/settings/tokens
#   - For https://router.huggingface.co/v1 the token must be allowed to call
#     Inference Providers / serverless inference for your account (UI labels vary).
#   - If 403 persists, confirm billing/access for Inference Providers in HF account settings.
#   - LLM_SMOKE_OPTIONAL=1 — still pass Steps 1,3–5 when Step 5 auth fails (not for production).
#
# Arguments:
#   ping_url   Your HuggingFace Space URL (e.g. https://your-space.hf.space)
#   repo_dir   Path to your repo (default: current directory)
#
# Examples:
#   ./validate-submission.sh https://my-team.hf.space
#   ./validate-submission.sh https://my-team.hf.space ./viraltest

set -uo pipefail

DOCKER_BUILD_TIMEOUT=600
if [ -t 1 ]; then
  RED='\033[0;31m'
  GREEN='\033[0;32m'
  YELLOW='\033[1;33m'
  BOLD='\033[1m'
  NC='\033[0m'
else
  RED='' GREEN='' YELLOW='' BOLD='' NC=''
fi

run_with_timeout() {
  local secs="$1"; shift
  if command -v timeout &>/dev/null; then
    timeout "$secs" "$@"
  elif command -v gtimeout &>/dev/null; then
    gtimeout "$secs" "$@"
  else
    "$@" &
    local pid=$!
    ( sleep "$secs" && kill "$pid" 2>/dev/null ) &
    local watcher=$!
    wait "$pid" 2>/dev/null
    local rc=$?
    kill "$watcher" 2>/dev/null
    wait "$watcher" 2>/dev/null
    return $rc
  fi
}

portable_mktemp() {
  local prefix="${1:-validate}"
  mktemp "${TMPDIR:-/tmp}/${prefix}-XXXXXX" 2>/dev/null || mktemp
}

CLEANUP_FILES=()
cleanup() { rm -f "${CLEANUP_FILES[@]+"${CLEANUP_FILES[@]}"}"; }
trap cleanup EXIT

PING_URL="${1:-}"
REPO_DIR="${2:-.}"

if [ -z "$PING_URL" ]; then
  printf "Usage: %s <ping_url> [repo_dir]\n" "$0"
  printf "\n"
  printf "  ping_url   Your HuggingFace Space URL (e.g. https://your-space.hf.space)\n"
  printf "  repo_dir   Path to your repo (default: current directory)\n"
  exit 1
fi

if ! REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)"; then
  printf "Error: directory '%s' not found\n" "${2:-.}"
  exit 1
fi
PING_URL="${PING_URL%/}"
export PING_URL
PASS=0

log()  { printf "[%s] %b\n" "$(date -u +%H:%M:%S)" "$*"; }
pass() { log "${GREEN}PASSED${NC} -- $1"; PASS=$((PASS + 1)); }
fail() { log "${RED}FAILED${NC} -- $1"; }
hint() { printf "  ${YELLOW}Hint:${NC} %b\n" "$1"; }
stop_at() {
  printf "\n"
  printf "${RED}${BOLD}Validation stopped at %s.${NC} Fix the above before continuing.\n" "$1"
  exit 1
}

if [ -f "$REPO_DIR/.env" ]; then
  set -a
  # shellcheck disable=SC1091
  . "$REPO_DIR/.env"
  set +a
fi

printf "\n"
printf "${BOLD}========================================${NC}\n"
printf "${BOLD}  Viraltest Submission Validator${NC}\n"
printf "${BOLD}========================================${NC}\n"
log "Repo:     $REPO_DIR"
log "Ping URL: $PING_URL"
if [ "${SKIP_DOCKER:-}" = "1" ]; then
  log "${YELLOW}SKIP_DOCKER=1 — Docker build will be skipped${NC}"
fi
printf "\n"

# ──────────────────────────────────────
# Step 1: Ping HF Space
# ──────────────────────────────────────
log "${BOLD}Step 1/5: Pinging HF Space${NC} ($PING_URL/reset) ..."

CURL_OUTPUT=$(portable_mktemp "validate-curl")
CLEANUP_FILES+=("$CURL_OUTPUT")
HTTP_CODE=$(curl -s -o "$CURL_OUTPUT" -w "%{http_code}" -X POST \
  -H "Content-Type: application/json" -d '{}' \
  "$PING_URL/reset" --max-time 30 2>"$CURL_OUTPUT" || printf "000")

if [ "$HTTP_CODE" = "200" ]; then
  pass "HF Space is live and responds to /reset"
elif [ "$HTTP_CODE" = "000" ]; then
  fail "HF Space not reachable (connection failed or timed out)"
  hint "Check your network and that the Space is running."
  stop_at "Step 1"
else
  fail "HF Space /reset returned HTTP $HTTP_CODE (expected 200)"
  hint "Make sure your Space is running. Try: curl -X POST $PING_URL/reset"
  stop_at "Step 1"
fi

# ──────────────────────────────────────
# Step 2: Docker build
# ──────────────────────────────────────
if [ "${SKIP_DOCKER:-}" = "1" ]; then
  log "${BOLD}Step 2/5: Docker build${NC} ${YELLOW}SKIPPED${NC} (SKIP_DOCKER=1)"
  hint "Run without SKIP_DOCKER=1 before submission to confirm docker build still succeeds."
else
  log "${BOLD}Step 2/5: Running docker build${NC} ..."

  if ! command -v docker &>/dev/null; then
    fail "docker command not found"
    hint "Install Docker: https://docs.docker.com/get-docker/"
    stop_at "Step 2"
  fi

  if [ -f "$REPO_DIR/Dockerfile" ]; then
    DOCKER_CONTEXT="$REPO_DIR"
  elif [ -f "$REPO_DIR/server/Dockerfile" ]; then
    DOCKER_CONTEXT="$REPO_DIR/server"
  else
    fail "No Dockerfile found in repo root or server/ directory"
    stop_at "Step 2"
  fi

  log "  Found Dockerfile in $DOCKER_CONTEXT"

  BUILD_OK=false
  BUILD_OUTPUT=$(run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$DOCKER_CONTEXT" 2>&1) && BUILD_OK=true

  if [ "$BUILD_OK" = true ]; then
    pass "Docker build succeeded"
  else
    fail "Docker build failed (timeout=${DOCKER_BUILD_TIMEOUT}s)"
    printf "%s\n" "$BUILD_OUTPUT" | tail -20
    stop_at "Step 2"
  fi
fi

# ──────────────────────────────────────
# Step 3: openenv validate
# ──────────────────────────────────────
log "${BOLD}Step 3/5: Running openenv validate${NC} ..."

VALIDATE_OK=false
VALIDATE_OUTPUT=""
VENV_OPENENV="$REPO_DIR/.venv/bin/openenv"
if command -v uv &>/dev/null && [ -f "$REPO_DIR/pyproject.toml" ]; then
  log "  Using: uv run openenv validate (avoids global CLI / Python mismatch)"
  VALIDATE_OUTPUT=$(cd "$REPO_DIR" && uv run openenv validate 2>&1) && VALIDATE_OK=true
elif command -v openenv &>/dev/null; then
  VALIDATE_OUTPUT=$(cd "$REPO_DIR" && openenv validate 2>&1) && VALIDATE_OK=true
elif [ -x "$VENV_OPENENV" ]; then
  log "  Using: .venv/bin/openenv (repo virtualenv; run: uv sync)"
  VALIDATE_OUTPUT=$(cd "$REPO_DIR" && "$VENV_OPENENV" validate 2>&1) && VALIDATE_OK=true
else
  fail "openenv not found (no uv, no openenv on PATH, no .venv/bin/openenv)"
  hint "From the repo: uv sync  # then re-run; or: pip install openenv-core"
  stop_at "Step 3"
fi

if [ "$VALIDATE_OK" = true ]; then
  pass "openenv validate passed"
  [ -n "$VALIDATE_OUTPUT" ] && log "  $VALIDATE_OUTPUT"
else
  fail "openenv validate failed"
  printf "%s\n" "$VALIDATE_OUTPUT"
  stop_at "Step 3"
fi

# ──────────────────────────────────────
# Step 4: Viraltest-specific checks
# ──────────────────────────────────────
log "${BOLD}Step 4/5: Viraltest environment checks${NC} ..."

STEP_OUTPUT=$(portable_mktemp "validate-step")
CLEANUP_FILES+=("$STEP_OUTPUT")

# Test all 3 tasks respond to reset
for TASK in weekly_engage weekly_strategic weekly_competitive; do
  TASK_CODE=$(curl -s -o "$STEP_OUTPUT" -w "%{http_code}" -X POST \
    -H "Content-Type: application/json" \
    -d "{\"task\": \"$TASK\"}" \
    "$PING_URL/reset" --max-time 15 2>/dev/null || printf "000")

  if [ "$TASK_CODE" = "200" ]; then
    log "  ${GREEN}OK${NC} task=$TASK reset responds"
  else
    fail "Task $TASK reset returned HTTP $TASK_CODE"
    stop_at "Step 4"
  fi
done

# Test step endpoint with a daily plan action (sparse: one post at hour 12)
STEP_CODE=$(curl -s -o "$STEP_OUTPUT" -w "%{http_code}" -X POST \
  -H "Content-Type: application/json" \
  -d '{"action":{"scheduled_actions":[{"hour":12,"action_type":"post","content_type":"reel","topic":"AI trends","tags":["ai","ml"]}]}}' \
  "$PING_URL/step" --max-time 15 2>/dev/null || printf "000")

if [ "$STEP_CODE" = "200" ]; then
  pass "Step endpoint responds correctly"
else
  fail "Step endpoint returned HTTP $STEP_CODE"
  stop_at "Step 4"
fi

# Check inference.py exists
if [ -f "$REPO_DIR/inference.py" ]; then
  pass "inference.py found in project root"
else
  fail "inference.py not found in $REPO_DIR"
  stop_at "Step 4"
fi

# ──────────────────────────────────────
# Step 5: HF Inference Router — one chat completion
# ──────────────────────────────────────
DEFAULT_SMOKE_MODEL="gemma-4-E4B-it-IQ4_XS"
DEFAULT_SMOKE_API="https://router.huggingface.co/v1"
SMOKE_MODEL="${MODEL_NAME:-$DEFAULT_SMOKE_MODEL}"
SMOKE_API="${API_BASE_URL:-$DEFAULT_SMOKE_API}"

if [ "${SKIP_LLM_SMOKE:-}" = "1" ]; then
  log "${BOLD}Step 5/5: LLM router smoke test${NC} ${YELLOW}SKIPPED${NC} (SKIP_LLM_SMOKE=1)"
elif [ -z "${HF_TOKEN:-}" ]; then
  fail "Step 5 requires HF_TOKEN (Inference router). Export it from https://huggingface.co/settings/tokens"
  hint "Override model/URL: MODEL_NAME and API_BASE_URL (defaults: $DEFAULT_SMOKE_MODEL, $DEFAULT_SMOKE_API). To skip Step 5: SKIP_LLM_SMOKE=1"
  stop_at "Step 5"
else
  log "${BOLD}Step 5/5: LLM router smoke test${NC} (model=$SMOKE_MODEL) ..."
  LLM_OK=false
  LLM_OUT=""
  if [ ! -f "$REPO_DIR/pyproject.toml" ]; then
    fail "No pyproject.toml in repo — cannot run LLM smoke test"
    stop_at "Step 5"
  fi
  RUN_PYTHON=()
  if command -v uv &>/dev/null; then
    RUN_PYTHON=(uv run python)
  elif [ -x "$REPO_DIR/.venv/bin/python" ]; then
    RUN_PYTHON=("$REPO_DIR/.venv/bin/python")
  else
    fail "Need uv on PATH or .venv/bin/python (run: uv sync)"
    stop_at "Step 5"
  fi
  if [ "${#RUN_PYTHON[@]}" -gt 0 ]; then
    LLM_OUT=$(cd "$REPO_DIR" && \
      MODEL_NAME="$SMOKE_MODEL" API_BASE_URL="$SMOKE_API" HF_TOKEN="$HF_TOKEN" \
      "${RUN_PYTHON[@]}" - <<'PY' 2>&1
import os, sys
from openai import OpenAI

def main() -> None:
    client = OpenAI(
        base_url=os.environ["API_BASE_URL"].rstrip("/"),
        api_key=os.environ["HF_TOKEN"],
    )
    r = client.chat.completions.create(
        model=os.environ["MODEL_NAME"],
        messages=[{"role": "user", "content": "Reply with exactly: OK"}],
        max_tokens=32,
        temperature=0.0,
    )
    text = (r.choices[0].message.content or "").strip()
    if not text:
        print("empty completion", file=sys.stderr)
        sys.exit(1)
    print(text[:500])

if __name__ == "__main__":
    main()
PY
    ) && LLM_OK=true
  fi

  if [ "$LLM_OK" = true ]; then
    pass "LLM router responded"
    if [ -n "$LLM_OUT" ]; then
      preview="${LLM_OUT:0:120}"
      [ "${#LLM_OUT}" -gt 120 ] && preview="${preview}..."
      log "  completion: $preview"
    fi
  else
    fail "LLM router smoke test failed"
    printf "%s\n" "$LLM_OUT"
    if [ "${LLM_SMOKE_OPTIONAL:-}" = "1" ]; then
      hint "LLM_SMOKE_OPTIONAL=1 set — continuing (fix HF token / Inference Providers access for real inference runs)."
    else
      hint "403 often means the token cannot use Inference Providers for this account. See HF token settings or set LLM_SMOKE_OPTIONAL=1 to still pass Steps 1–4."
      stop_at "Step 5"
    fi
  fi
fi

printf "\n"
printf "${BOLD}========================================${NC}\n"
printf "${GREEN}${BOLD}  All checks passed!${NC}\n"
printf "${GREEN}${BOLD}  Your submission is ready to submit.${NC}\n"
printf "${BOLD}========================================${NC}\n"
printf "\n"

exit 0