mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
feat(qa): telegram soak test on digitalocean + fix bun -e (#2547)
- soak.sh: SOAK_CLOUD env var makes cloud configurable (default: sprite) - qa.sh: load TELEGRAM_BOT_TOKEN, TELEGRAM_TEST_CHAT_ID, SOAK_CLOUD from /etc/spawn-qa-auth.env in soak mode - qa.yml: add weekly Monday 3am UTC scheduled soak trigger - fix: bun eval → bun -e across soak.sh, key-request.sh, github-auth.sh (bun eval is not a valid subcommand in bun 1.3.9) - fix: export _TOKEN via env prefix so process.env._TOKEN works in bun -e - docs: update shell-scripts.md rule to say bun -e (not bun eval) Verified: 3/4 Telegram tests pass in smoke test on DigitalOcean (120s wait) getMe ✓ sendMessage ✓ getWebhookInfo ✓; cron test needs full 55-min window. Co-authored-by: spawn-qa-bot <qa@openrouter.ai> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2b83a8106d
commit
6081c0a17f
6 changed files with 50 additions and 19 deletions
|
|
@ -25,10 +25,10 @@ macOS ships bash 3.2. All scripts MUST work on it:
|
|||
|
||||
## Use Bun + TypeScript for Inline Scripting — NEVER python/python3
|
||||
When shell scripts need JSON processing, HTTP calls, crypto, or any non-trivial logic:
|
||||
- **ALWAYS** use `bun eval '...'` or write a temp `.ts` file and `bun run` it
|
||||
- **ALWAYS** use `bun -e '...'` or write a temp `.ts` file and `bun run` it
|
||||
- **NEVER** use `python3 -c` or `python -c` for inline scripting — python is not a project dependency
|
||||
- Prefer `jq` for simple JSON extraction; fall back to `bun eval` when jq is unavailable
|
||||
- Pass data to bun via environment variables (e.g., `_DATA="${var}" bun eval "..."`) or temp files — never interpolate untrusted values into JS strings
|
||||
- Prefer `jq` for simple JSON extraction; fall back to `bun -e` when jq is unavailable
|
||||
- Pass data to bun via environment variables (e.g., `_DATA="${var}" bun -e "..."`) or temp files — never interpolate untrusted values into JS strings
|
||||
- For complex operations (SigV4 signing, API calls with retries), write a heredoc `.ts` file and `bun run` it
|
||||
|
||||
## ESM Only — NEVER use require() or CommonJS
|
||||
|
|
|
|||
|
|
@ -226,6 +226,29 @@ if [[ "${RUN_MODE}" == "e2e" ]]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# --- Load Telegram credentials for soak mode ---
|
||||
if [[ "${RUN_MODE}" == "soak" ]]; then
|
||||
if [[ -f /etc/spawn-qa-auth.env ]]; then
|
||||
while IFS='=' read -r _tkey _tval || [[ -n "${_tkey}" ]]; do
|
||||
_tkey="${_tkey#"${_tkey%%[! ]*}"}"
|
||||
_tkey="${_tkey%"${_tkey##*[! ]}"}"
|
||||
[[ -z "${_tkey}" || "${_tkey}" == \#* ]] && continue
|
||||
case "${_tkey}" in
|
||||
TELEGRAM_BOT_TOKEN|TELEGRAM_TEST_CHAT_ID|SOAK_CLOUD)
|
||||
export "${_tkey}=${_tval}"
|
||||
;;
|
||||
esac
|
||||
done < /etc/spawn-qa-auth.env
|
||||
if [[ -n "${TELEGRAM_BOT_TOKEN:-}" ]] && [[ -n "${TELEGRAM_TEST_CHAT_ID:-}" ]]; then
|
||||
log "Telegram credentials loaded for soak test (cloud: ${SOAK_CLOUD:-sprite})"
|
||||
else
|
||||
log "WARNING: TELEGRAM_BOT_TOKEN or TELEGRAM_TEST_CHAT_ID missing from /etc/spawn-qa-auth.env — soak test will fail"
|
||||
fi
|
||||
else
|
||||
log "WARNING: /etc/spawn-qa-auth.env not found — soak test requires TELEGRAM_BOT_TOKEN and TELEGRAM_TEST_CHAT_ID"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Launch Claude Code with mode-specific prompt
|
||||
# Enable agent teams (required for team-based workflows)
|
||||
export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
|
||||
|
|
|
|||
9
.github/workflows/qa.yml
vendored
9
.github/workflows/qa.yml
vendored
|
|
@ -1,7 +1,8 @@
|
|||
name: QA
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */4 * * *'
|
||||
- cron: '0 */4 * * *' # Every 4 hours — quality sweep
|
||||
- cron: '0 3 * * 1' # Every Monday 3am UTC — Telegram soak test (OpenClaw on DigitalOcean)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
|
|
@ -24,7 +25,11 @@ jobs:
|
|||
SPRITE_URL: ${{ secrets.QA_SPRITE_URL }}
|
||||
TRIGGER_SECRET: ${{ secrets.QA_TRIGGER_SECRET }}
|
||||
run: |
|
||||
REASON="${{ github.event.inputs.reason || 'schedule' }}"
|
||||
if [ "${{ github.event_name }}" = "schedule" ] && [ "${{ github.event.schedule }}" = "0 3 * * 1" ]; then
|
||||
REASON="soak"
|
||||
else
|
||||
REASON="${{ github.event.inputs.reason || 'schedule' }}"
|
||||
fi
|
||||
curl -sS --fail-with-body -X POST \
|
||||
"${SPRITE_URL}/trigger?reason=${REASON}" \
|
||||
-H "Authorization: Bearer ${TRIGGER_SECRET}"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ set -eo pipefail
|
|||
# ---------------------------------------------------------------------------
|
||||
SOAK_WAIT_SECONDS="${SOAK_WAIT_SECONDS:-3600}"
|
||||
SOAK_CRON_DELAY_SECONDS="${SOAK_CRON_DELAY_SECONDS:-3300}"
|
||||
SOAK_CLOUD="${SOAK_CLOUD:-sprite}"
|
||||
SOAK_HEARTBEAT_INTERVAL=300 # 5 minutes
|
||||
SOAK_GATEWAY_PORT=18789
|
||||
TELEGRAM_API_BASE="https://api.telegram.org"
|
||||
|
|
@ -146,14 +147,15 @@ soak_inject_telegram_config() {
|
|||
|
||||
log_step "Patching ~/.openclaw/openclaw.json with Telegram bot token..."
|
||||
|
||||
# Use bun eval on the remote to JSON-patch the config file
|
||||
# Use bun -e on the remote to JSON-patch the config file.
|
||||
# _TOKEN is passed via env var prefix so process.env._TOKEN is available in bun.
|
||||
cloud_exec "${app}" "source ~/.spawnrc 2>/dev/null; \
|
||||
export PATH=\$HOME/.npm-global/bin:\$HOME/.bun/bin:\$HOME/.local/bin:\$PATH; \
|
||||
_TOKEN=\$(printf '%s' '${encoded_token}' | base64 -d); \
|
||||
bun eval ' \
|
||||
_TOKEN=\${_TOKEN} bun -e ' \
|
||||
import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\"; \
|
||||
import { dirname } from \"node:path\"; \
|
||||
const configPath = process.env.HOME + \"/.openclaw/openclaw.json\"; \
|
||||
const configPath = (process.env.HOME ?? \"\") + \"/.openclaw/openclaw.json\"; \
|
||||
let config = {}; \
|
||||
try { config = JSON.parse(readFileSync(configPath, \"utf-8\")); } catch {} \
|
||||
if (!config.channels) config.channels = {}; \
|
||||
|
|
@ -162,7 +164,7 @@ soak_inject_telegram_config() {
|
|||
mkdirSync(dirname(configPath), { recursive: true }); \
|
||||
writeFileSync(configPath, JSON.stringify(config, null, 2)); \
|
||||
console.log(\"Telegram config injected\"); \
|
||||
'" >/dev/null 2>&1
|
||||
'" 2>&1
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
log_err "Failed to inject Telegram config"
|
||||
|
|
@ -477,7 +479,7 @@ soak_run_telegram_tests() {
|
|||
# ---------------------------------------------------------------------------
|
||||
# run_soak_test [LOG_DIR]
|
||||
#
|
||||
# Orchestrator: validate env → load sprite driver → provision openclaw →
|
||||
# Orchestrator: validate env → load cloud driver (SOAK_CLOUD) → provision openclaw →
|
||||
# verify → inject telegram config → schedule openclaw cron reminder →
|
||||
# soak wait → run tests (including openclaw cron verification) → teardown.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -488,6 +490,7 @@ run_soak_test() {
|
|||
fi
|
||||
|
||||
log_header "Spawn Soak Test: OpenClaw + Telegram (with cron reminder)"
|
||||
log_info "Cloud: ${SOAK_CLOUD}"
|
||||
log_info "Soak wait: ${SOAK_WAIT_SECONDS}s"
|
||||
log_info "Cron delay: ${SOAK_CRON_DELAY_SECONDS}s"
|
||||
|
||||
|
|
@ -497,8 +500,8 @@ run_soak_test() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# Load sprite cloud driver
|
||||
load_cloud_driver "sprite"
|
||||
# Load cloud driver (configurable via SOAK_CLOUD, default: sprite)
|
||||
load_cloud_driver "${SOAK_CLOUD}"
|
||||
|
||||
# Validate cloud environment
|
||||
if ! require_env; then
|
||||
|
|
|
|||
|
|
@ -136,11 +136,11 @@ _fetch_gh_latest_version() {
|
|||
}
|
||||
|
||||
local latest_version=""
|
||||
# Prefer jq for safe JSON parsing; fall back to bun eval (never python)
|
||||
# Prefer jq for safe JSON parsing; fall back to bun -e (never python)
|
||||
if command -v jq &>/dev/null; then
|
||||
latest_version=$(printf '%s' "${api_response}" | jq -r '.tag_name // empty' 2>/dev/null) || true
|
||||
elif command -v bun &>/dev/null; then
|
||||
latest_version=$(_GH_API_RESPONSE="${api_response}" bun eval "
|
||||
latest_version=$(_GH_API_RESPONSE="${api_response}" bun -e "
|
||||
const data = JSON.parse(process.env._GH_API_RESPONSE || '{}');
|
||||
const tag = typeof data.tag_name === 'string' ? data.tag_name : '';
|
||||
process.stdout.write(tag);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ _check_cli_auth_clouds() {
|
|||
if command -v jq &>/dev/null; then
|
||||
cli_clouds=$(jq -r '.clouds | to_entries[] | select(.value.auth != null) | select(.value.auth | test("\\b(login|configure|setup)\\b"; "i")) | "\(.key)|\(.value.auth)"' "${manifest_path}" 2>/dev/null)
|
||||
else
|
||||
cli_clouds=$(_MANIFEST="${manifest_path}" bun eval "
|
||||
cli_clouds=$(_MANIFEST="${manifest_path}" bun -e "
|
||||
import fs from 'fs';
|
||||
const m = JSON.parse(fs.readFileSync(process.env._MANIFEST, 'utf8'));
|
||||
for (const [key, cloud] of Object.entries(m.clouds || {})) {
|
||||
|
|
@ -58,7 +58,7 @@ for (const [key, cloud] of Object.entries(m.clouds || {})) {
|
|||
if command -v jq &>/dev/null; then
|
||||
project=$(jq -r '.GCP_PROJECT // .project // "" | select(. != null)' "${gcp_config}" 2>/dev/null)
|
||||
else
|
||||
project=$(_FILE="${gcp_config}" bun eval "
|
||||
project=$(_FILE="${gcp_config}" bun -e "
|
||||
import fs from 'fs';
|
||||
const d = JSON.parse(fs.readFileSync(process.env._FILE, 'utf8'));
|
||||
process.stdout.write(d.GCP_PROJECT || d.project || '');
|
||||
|
|
@ -95,7 +95,7 @@ _parse_cloud_auths() {
|
|||
if command -v jq &>/dev/null; then
|
||||
jq -r '.clouds | to_entries[] | select(.value.auth != null and .value.auth != "") | select(.value.key_request != false) | select(.value.auth | test("\\b(login|configure|setup)\\b"; "i") | not) | "\(.key)|\(.value.auth)"' "${manifest_path}" 2>/dev/null
|
||||
else
|
||||
_MANIFEST="${manifest_path}" bun eval "
|
||||
_MANIFEST="${manifest_path}" bun -e "
|
||||
import fs from 'fs';
|
||||
const m = JSON.parse(fs.readFileSync(process.env._MANIFEST, 'utf8'));
|
||||
for (const [key, cloud] of Object.entries(m.clouds || {})) {
|
||||
|
|
@ -134,7 +134,7 @@ _try_load_env_var() {
|
|||
if command -v jq &>/dev/null; then
|
||||
val=$(jq -r --arg v "${var_name}" '(.[$v] // .api_key // .token) // "" | select(. != null)' "${config_file}" 2>/dev/null)
|
||||
else
|
||||
val=$(_FILE="${config_file}" _VAR="${var_name}" bun eval "
|
||||
val=$(_FILE="${config_file}" _VAR="${var_name}" bun -e "
|
||||
import fs from 'fs';
|
||||
const d = JSON.parse(fs.readFileSync(process.env._FILE, 'utf8'));
|
||||
process.stdout.write(d[process.env._VAR] || d.api_key || d.token || '');
|
||||
|
|
@ -268,7 +268,7 @@ request_missing_cloud_keys() {
|
|||
if command -v jq &>/dev/null; then
|
||||
providers_json=$(printf '%s\n' ${MISSING_KEY_PROVIDERS} | jq -Rn '[inputs | select(. != "")]' 2>/dev/null) || return 0
|
||||
elif command -v bun &>/dev/null; then
|
||||
providers_json=$(_PROVIDERS="${MISSING_KEY_PROVIDERS}" bun eval "
|
||||
providers_json=$(_PROVIDERS="${MISSING_KEY_PROVIDERS}" bun -e "
|
||||
const providers = process.env._PROVIDERS.trim().split(/\s+/).filter(Boolean);
|
||||
process.stdout.write(JSON.stringify(providers));
|
||||
" 2>/dev/null) || return 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue