run.sh 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. #!/usr/bin/env bash
  2. # Self-test for the fleet-worker skill scripts.
  3. #
  4. # Offline + deterministic (no network). Mocks `claude` and `keyring` on a
  5. # controlled PATH, then asserts the launcher's auth isolation, key-resolution
  6. # chain, no-key-leak, and arg forwarding; fleet-collect's success/failure gating;
  7. # and fleet-doctor's --offline / --help / ASCII purity. Resolves paths relative to
  8. # itself so it runs both in the repo and once installed to ~/.claude/skills/.
  9. #
  10. # Usage: bash tests/run.sh
  11. # Exit: 0 all pass, 1 one or more failures (SKIP+exit 0 if jq is unavailable)
  12. set -uo pipefail
  13. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  14. SKILL="$(dirname "$HERE")"
  15. SCRIPTS="$SKILL/scripts"
  16. WORKER="$SCRIPTS/fleet-worker"
  17. COLLECT="$SCRIPTS/fleet-collect.sh"
  18. DOCTOR="$SCRIPTS/fleet-doctor.sh"
  19. command -v jq >/dev/null 2>&1 || { echo "SKIP: jq not available"; exit 0; }
  20. SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
  21. PASS=0; FAIL=0
  22. ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$1"; }
  23. no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; }
  24. ee() { [ "$2" = "$3" ] && ok "$1 (exit $3)" || no "$1 (want $2 got $3)"; }
  25. eh() { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
  26. echo "=== fleet-worker self-test ==="
  27. # ── Mock bin: a fake `claude` that records env+args to a probe file and prints
  28. # only a marker, plus a fake `keyring`. A controlled PATH keeps the real
  29. # claude out so launcher behaviour is deterministic. ──────────────────────
  30. MB="$SB/bin"; mkdir -p "$MB"
  31. PROBE="$SB/probe.txt"
  32. cat > "$MB/claude" <<EOF
  33. #!/usr/bin/env bash
  34. {
  35. echo "CONFIG=\$CLAUDE_CONFIG_DIR"
  36. echo "BASE=\$ANTHROPIC_BASE_URL"
  37. echo "OPUS=\$ANTHROPIC_DEFAULT_OPUS_MODEL"
  38. echo "SONNET=\$ANTHROPIC_DEFAULT_SONNET_MODEL"
  39. echo "HAIKU=\$ANTHROPIC_DEFAULT_HAIKU_MODEL"
  40. echo "TOKEN=\$ANTHROPIC_AUTH_TOKEN"
  41. echo "ARGS=\$*"
  42. } > "$PROBE"
  43. echo "MOCK_CLAUDE_RAN"
  44. EOF
  45. chmod +x "$MB/claude"
  46. cat > "$MB/keyring" <<'EOF'
  47. #!/usr/bin/env bash
  48. # keyring get <service> <key>
  49. [ "${1:-}" = "get" ] && echo "KEYRING-TOKEN-${3:-}"
  50. EOF
  51. chmod +x "$MB/keyring"
  52. PC="$MB:/usr/bin:/bin" # controlled PATH: mock first, no real claude
  53. echo "-- launcher --"
  54. "$WORKER" --help >/dev/null 2>&1; ee "worker --help" 0 $?
  55. # Success: env isolation + model mapping + arg forwarding + key never printed
  56. CFG="$SB/cfg-a"
  57. out="$(PATH="$PC" FLEET_WORKER_CONFIG_DIR="$CFG" \
  58. ANTHROPIC_AUTH_TOKEN="" ZHIPU_API_KEY="SEKRET-AAA" GLM_API_KEY="" \
  59. "$WORKER" --output-format json "do a thing" 2>&1)"; rc=$?
  60. ee "worker runs with key" 0 "$rc"
  61. eh "mock claude executed" "MOCK_CLAUDE_RAN" "$out"
  62. case "$out" in *SEKRET-AAA*) no "worker leaked key to its own output";; *) ok "worker never prints the key";; esac
  63. P="$(cat "$PROBE")"
  64. eh "isolated CLAUDE_CONFIG_DIR set" "CONFIG=$CFG" "$P"
  65. eh "z.ai base url set" "BASE=https://api.z.ai/api/anthropic" "$P"
  66. eh "sonnet maps to GLM-5.2" "SONNET=GLM-5.2" "$P"
  67. eh "haiku maps to GLM-4.5-Air" "HAIKU=GLM-4.5-Air" "$P"
  68. eh "key reached claude via env" "TOKEN=SEKRET-AAA" "$P"
  69. eh "bakes flags + forwards args" "ARGS=-p --model sonnet --permission-mode bypassPermissions --output-format json do a thing" "$P"
  70. [ -f "$CFG/settings.json" ] && ok "seeds settings.json" || no "settings.json not seeded"
  71. eh "settings carries effortLevel" "effortLevel" "$(cat "$CFG/settings.json" 2>/dev/null)"
  72. # Custom endpoint/model override
  73. : > "$PROBE"
  74. PATH="$PC" FLEET_WORKER_CONFIG_DIR="$SB/cfg-x" \
  75. ANTHROPIC_AUTH_TOKEN="T" FLEET_WORKER_BASE_URL="https://example.test/anthropic" \
  76. FLEET_WORKER_MODEL="GLM-9" FLEET_WORKER_SMALL_MODEL="GLM-9-mini" \
  77. "$WORKER" "hi" >/dev/null 2>&1
  78. P="$(cat "$PROBE")"
  79. eh "custom base url honoured" "BASE=https://example.test/anthropic" "$P"
  80. eh "custom model honoured" "SONNET=GLM-9" "$P"
  81. # Key chain: keyring
  82. : > "$PROBE"
  83. PATH="$PC" FLEET_WORKER_CONFIG_DIR="$SB/cfg-b" \
  84. ANTHROPIC_AUTH_TOKEN="" ZHIPU_API_KEY="" GLM_API_KEY="" \
  85. FLEET_WORKER_KEYRING_SERVICE="svc" FLEET_WORKER_KEYRING_KEY="glm" \
  86. "$WORKER" "hi" >/dev/null 2>&1; ee "worker via keyring" 0 $?
  87. eh "keyring token reached claude" "TOKEN=KEYRING-TOKEN-glm" "$(cat "$PROBE")"
  88. # Key chain: GLM_API_KEY
  89. : > "$PROBE"
  90. PATH="$PC" FLEET_WORKER_CONFIG_DIR="$SB/cfg-c" \
  91. ANTHROPIC_AUTH_TOKEN="" ZHIPU_API_KEY="" GLM_API_KEY="GAK-1" \
  92. "$WORKER" "hi" >/dev/null 2>&1
  93. eh "GLM_API_KEY reached claude" "TOKEN=GAK-1" "$(cat "$PROBE")"
  94. # No key resolved -> exit 5
  95. PATH="$PC" FLEET_WORKER_CONFIG_DIR="$SB/cfg-d" \
  96. ANTHROPIC_AUTH_TOKEN="" ZHIPU_API_KEY="" GLM_API_KEY="" \
  97. "$WORKER" "hi" >/dev/null 2>&1; ee "no key -> 5" 5 $?
  98. # claude missing -> exit 5 (only when real claude isn't in the base PATH)
  99. if PATH="/usr/bin:/bin" command -v claude >/dev/null 2>&1; then
  100. echo " SKIP claude-missing (claude resolves via base PATH)"
  101. else
  102. MB2="$SB/bin2"; mkdir -p "$MB2"
  103. PATH="$MB2:/usr/bin:/bin" FLEET_WORKER_CONFIG_DIR="$SB/cfg-e" ZHIPU_API_KEY="X" \
  104. ANTHROPIC_AUTH_TOKEN="" GLM_API_KEY="" \
  105. "$WORKER" "hi" >/dev/null 2>&1; ee "claude missing -> 5" 5 $?
  106. fi
  107. echo "-- fleet-collect.sh --"
  108. "$COLLECT" --help >/dev/null 2>&1; ee "collect --help" 0 $?
  109. out="$(printf '{"is_error":false,"result":"DELIVERABLE"}' | "$COLLECT" -q)"; rc=$?
  110. ee "collect success -> 0" 0 "$rc"
  111. eh "collect prints .result" "DELIVERABLE" "$out"
  112. printf '{"is_error":true,"api_error_status":529,"result":""}' | "$COLLECT" -q >/dev/null 2>&1
  113. ee "collect worker-failed -> 10" 10 $?
  114. printf 'not json' | "$COLLECT" -q >/dev/null 2>&1; ee "collect bad json -> 4" 4 $?
  115. printf '{"x":1}' | "$COLLECT" -q >/dev/null 2>&1; ee "collect no is_error -> 4" 4 $?
  116. "$COLLECT" "$SB/nope.json" >/dev/null 2>&1; ee "collect missing file -> 3" 3 $?
  117. "$COLLECT" --bogus >/dev/null 2>&1; ee "collect bad flag -> 2" 2 $?
  118. echo "-- fleet-doctor.sh --"
  119. "$DOCTOR" --help >/dev/null 2>&1; ee "doctor --help" 0 $?
  120. "$DOCTOR" --offline -q >/dev/null 2>&1; ee "doctor --offline consistent -> 0" 0 $?
  121. "$DOCTOR" --bogus >/dev/null 2>&1; ee "doctor bad flag -> 2" 2 $?
  122. out="$("$DOCTOR" --offline --json -q 2>/dev/null)"
  123. eh "doctor --json schema" "claude-mods.fleet-worker.doctor/v1" "$out"
  124. e="$(TERM_ASCII=1 FORCE_COLOR=1 "$DOCTOR" --offline 2>&1 1>/dev/null)"
  125. if printf '%s' "$e" | LC_ALL=C grep -q '[^[:print:][:cntrl:]]'; then
  126. no "doctor framing pure ASCII under TERM_ASCII=1"
  127. else ok "doctor framing pure ASCII under TERM_ASCII=1"; fi
  128. grep -q '_lib/term.sh' "$DOCTOR" && ok "doctor sources term.sh" || no "doctor missing term.sh"
  129. # Drift tripwire: copy the skill into a temp dir with a SKILL.md that documents
  130. # no model name; the doctor run from there must report drift -> exit 10.
  131. DSB="$SB/skillcopy"; mkdir -p "$DSB/scripts" "$DSB/assets"
  132. cp "$WORKER" "$DSB/scripts/fleet-worker"
  133. cp "$DOCTOR" "$DSB/scripts/fleet-doctor.sh"
  134. printf '# fleet-worker\nThis doc deliberately mentions no model name or endpoint.\n' > "$DSB/SKILL.md"
  135. printf '{ "hooks": {}, "effortLevel": "high" }\n' > "$DSB/assets/worker-settings.json"
  136. bash "$DSB/scripts/fleet-doctor.sh" --offline -q >/dev/null 2>&1
  137. ee "drift (undocumented model) -> 10" 10 $?
  138. echo "=== $PASS passed, $FAIL failed ==="
  139. [ "$FAIL" -eq 0 ] || exit 1