9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
304 lines
9.9 KiB
Bash
Executable File
304 lines
9.9 KiB
Bash
Executable File
#!/bin/bash
|
|
#=============================================================================
|
|
# ods-test-functional.sh - Functional Testing for ODS
|
|
#
|
|
# Tests actual functionality, not just port availability:
|
|
# - LLM (llama-server) generates coherent text
|
|
# - Whisper transcribes actual audio
|
|
# - TTS generates valid audio files
|
|
# - Embeddings produce vectors
|
|
#
|
|
# This complements ods-test.sh which checks service health.
|
|
#=============================================================================
|
|
|
|
# Require Bash 4+ (associative arrays used for service-port lookup)
|
|
if (( BASH_VERSINFO[0] < 4 )); then
|
|
echo "ERROR: $(basename "$0") requires Bash 4.0+ (you have $BASH_VERSION)" >&2
|
|
echo " macOS ships Bash 3.2 due to licensing. Install a modern version:" >&2
|
|
echo " brew install bash" >&2
|
|
exit 1
|
|
fi
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors
|
|
RED='\e[0;31m'
|
|
GREEN='\e[0;32m'
|
|
YELLOW='\e[1;33m'
|
|
NC='\e[0m'
|
|
|
|
# Source service registry for port resolution
|
|
_FT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
if [[ -f "$_FT_DIR/lib/service-registry.sh" ]]; then
|
|
export SCRIPT_DIR="$_FT_DIR"
|
|
. "$_FT_DIR/lib/service-registry.sh"
|
|
sr_load
|
|
[[ -f "$_FT_DIR/lib/safe-env.sh" ]] && . "$_FT_DIR/lib/safe-env.sh"
|
|
load_env_file "$_FT_DIR/.env"
|
|
sr_resolve_ports
|
|
fi
|
|
|
|
# Ensure SERVICE_PORTS is declared even if service-registry.sh was not sourced
|
|
declare -A SERVICE_PORTS
|
|
|
|
# Service endpoints — resolved from registry
|
|
LLM_URL="${LLM_URL:-http://localhost:${SERVICE_PORTS[llama-server]:-11434}}"
|
|
WHISPER_URL="${WHISPER_URL:-http://localhost:${SERVICE_PORTS[whisper]:-9000}}"
|
|
TTS_URL="${TTS_URL:-http://localhost:${SERVICE_PORTS[tts]:-8880}}"
|
|
EMBEDDING_URL="${EMBEDDING_URL:-http://localhost:${SERVICE_PORTS[embeddings]:-9103}}"
|
|
|
|
# Test tracking
|
|
TESTS_PASSED=0
|
|
TESTS_FAILED=0
|
|
|
|
pass() {
|
|
echo -e "${GREEN}✓${NC} $1"
|
|
# Arithmetic expansion (not `((...))`) — the compound form returns
|
|
# exit 1 when the pre-increment value is 0, which would trip `set -e`
|
|
# on the first pass/fail and abort the script before any summary.
|
|
TESTS_PASSED=$((TESTS_PASSED + 1))
|
|
}
|
|
|
|
fail() {
|
|
echo -e "${RED}✗${NC} $1"
|
|
TESTS_FAILED=$((TESTS_FAILED + 1))
|
|
}
|
|
|
|
warn() {
|
|
echo -e "${YELLOW}⚠${NC} $1"
|
|
}
|
|
|
|
# Test 1: LLM generates coherent text
|
|
test_llm_functional() {
|
|
echo ""
|
|
echo "> Testing LLM Functional Generation"
|
|
|
|
# The grep-based extraction pipelines in this script may legitimately
|
|
# produce zero matches (LLM/TTS/embeddings/whisper offline or returning
|
|
# unexpected payload). Under `set -euo pipefail` such a no-match would
|
|
# abort the script before the `[[ -z ... ]]` guard below can treat it
|
|
# as a test failure. Using `if ! VAR=$(...)` keeps the set -e safety
|
|
# net engaged everywhere else; `set -e` is disabled only for the
|
|
# evaluation of the condition (per bash spec), so a failed pipeline
|
|
# leaves VAR empty and the explicit `fail` path below runs.
|
|
local model_id=""
|
|
if ! model_id=$(curl -s --max-time 10 "$LLM_URL/v1/models" 2>/dev/null | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4); then
|
|
model_id=""
|
|
fi
|
|
model_id="${model_id:-local}"
|
|
|
|
local prompt="What is 2+2? Answer with just the number."
|
|
local payload="{\"model\": \"$model_id\", \"messages\": [{\"role\": \"user\", \"content\": \"$prompt\"}], \"max_tokens\": 10, \"temperature\": 0.1}"
|
|
|
|
local response
|
|
response=$(curl -s --max-time 30 \
|
|
-X POST "$LLM_URL/v1/chat/completions" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" 2>/dev/null || echo "")
|
|
|
|
if [[ -z "$response" ]]; then
|
|
fail "LLM returned no response"
|
|
return 1
|
|
fi
|
|
|
|
local content=""
|
|
if ! content=$(echo "$response" | grep -oE '"content":[[:space:]]*"[^"]+"' | head -1 | cut -d'"' -f4); then
|
|
content=""
|
|
fi
|
|
|
|
if [[ -z "$content" ]]; then
|
|
fail "LLM returned empty content"
|
|
return 1
|
|
fi
|
|
|
|
# Check if response contains "4" (the answer to 2+2)
|
|
if echo "$content" | grep -q "4"; then
|
|
pass "LLM generates correct answer: '$content'"
|
|
else
|
|
warn "LLM generated: '$content' (expected '4')"
|
|
pass "LLM generates text (answer may vary)"
|
|
fi
|
|
}
|
|
|
|
# Test 2: TTS generates valid audio file
|
|
test_tts_functional() {
|
|
echo ""
|
|
echo "> Testing TTS Audio Generation"
|
|
|
|
local test_text="Hello, this is a test."
|
|
local output_file="/tmp/test_tts_output.wav"
|
|
|
|
local payload="{\"model\": \"kokoro\", \"input\": \"$test_text\", \"voice\": \"af_bella\", \"response_format\": \"wav\"}"
|
|
|
|
# Generate audio
|
|
local http_code
|
|
http_code=$(curl -s -w "%{http_code}" --max-time 30 \
|
|
-X POST "$TTS_URL/v1/audio/speech" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" \
|
|
-o "$output_file" 2>/dev/null)
|
|
|
|
if [[ "$http_code" != "200" ]]; then
|
|
fail "TTS returned HTTP $http_code"
|
|
rm -f "$output_file"
|
|
return 1
|
|
fi
|
|
|
|
# Check file exists and has content
|
|
if [[ ! -f "$output_file" ]]; then
|
|
fail "TTS did not create output file"
|
|
return 1
|
|
fi
|
|
|
|
local file_size
|
|
file_size=$(stat -c%s "$output_file" 2>/dev/null || stat -f%z "$output_file" 2>/dev/null || echo "0")
|
|
|
|
if [[ "$file_size" -lt 1000 ]]; then
|
|
fail "TTS output too small: $file_size bytes (expected >1KB)"
|
|
rm -f "$output_file"
|
|
return 1
|
|
fi
|
|
|
|
# Check it's a valid WAV file
|
|
if ! file "$output_file" | grep -qiE "audio|wav|riff"; then
|
|
warn "TTS output may not be valid WAV: $(file "$output_file")"
|
|
pass "TTS generates audio file ($file_size bytes)"
|
|
else
|
|
pass "TTS generates valid WAV audio ($file_size bytes)"
|
|
fi
|
|
|
|
rm -f "$output_file"
|
|
}
|
|
|
|
# Test 3: Embeddings produce vectors
|
|
test_embeddings_functional() {
|
|
echo ""
|
|
echo "> Testing Embeddings Vector Generation"
|
|
|
|
local test_text="This is a test sentence for embeddings."
|
|
local payload="{\"inputs\": \"$test_text\"}"
|
|
|
|
local response
|
|
response=$(curl -s --max-time 30 \
|
|
-X POST "$EMBEDDING_URL/embed" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" 2>/dev/null || echo "")
|
|
|
|
if [[ -z "$response" ]]; then
|
|
# Try alternate endpoint
|
|
response=$(curl -s --max-time 30 \
|
|
-X POST "$EMBEDDING_URL/" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" 2>/dev/null || echo "")
|
|
fi
|
|
|
|
if [[ -z "$response" ]]; then
|
|
fail "Embeddings returned no response"
|
|
return 1
|
|
fi
|
|
|
|
# Check if response contains array of numbers
|
|
if echo "$response" | grep -qE '\[\s*-?[0-9]+\.[0-9]+'; then
|
|
local vector_len=0
|
|
if ! vector_len=$(echo "$response" | grep -oE '-?[0-9]+\.[0-9]+' | wc -l); then
|
|
vector_len=0
|
|
fi
|
|
pass "Embeddings generates vectors ($vector_len dimensions)"
|
|
else
|
|
fail "Embeddings did not return valid vectors"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Test 4: Whisper transcribes audio (if test audio available)
|
|
test_whisper_functional() {
|
|
echo ""
|
|
echo "> Testing Whisper Transcription"
|
|
|
|
# Create a simple test audio file or use existing
|
|
local test_audio="/tmp/test_audio.wav"
|
|
|
|
# Try to generate test audio with TTS first
|
|
local tts_payload='{"model": "kokoro", "input": "Hello world", "voice": "af_bella", "response_format": "wav"}'
|
|
|
|
if ! curl -s --max-time 15 \
|
|
-X POST "$TTS_URL/v1/audio/speech" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$tts_payload" \
|
|
-o "$test_audio" 2>/dev/null; then
|
|
warn "Could not generate test audio for Whisper"
|
|
warn "Skipping Whisper functional test (TTS dependency)"
|
|
return 0
|
|
fi
|
|
|
|
if [[ ! -f "$test_audio" ]] || [[ "$(stat -c%s "$test_audio" 2>/dev/null || stat -f%z "$test_audio" 2>/dev/null || echo 0)" -lt 1000 ]]; then
|
|
warn "Test audio generation failed"
|
|
return 0
|
|
fi
|
|
|
|
# Transcribe with Whisper
|
|
local response
|
|
response=$(curl -s --max-time 30 \
|
|
-X POST "$WHISPER_URL/v1/audio/transcriptions" \
|
|
-H "Content-Type: multipart/form-data" \
|
|
-F "file=@$test_audio" \
|
|
-F "model=whisper-1" 2>/dev/null || echo "")
|
|
|
|
rm -f "$test_audio"
|
|
|
|
if [[ -z "$response" ]]; then
|
|
fail "Whisper returned no response"
|
|
return 1
|
|
fi
|
|
|
|
local transcription=""
|
|
if ! transcription=$(echo "$response" | grep -oE '"text":[[:space:]]*"[^"]+"' | head -1 | cut -d'"' -f4); then
|
|
transcription=""
|
|
fi
|
|
|
|
if [[ -z "$transcription" ]]; then
|
|
fail "Whisper returned empty transcription"
|
|
return 1
|
|
fi
|
|
|
|
if echo "$transcription" | grep -qiE "hello|world"; then
|
|
pass "Whisper transcribes correctly: '$transcription'"
|
|
else
|
|
warn "Whisper transcribed: '$transcription'"
|
|
pass "Whisper generates transcription"
|
|
fi
|
|
}
|
|
|
|
# Main
|
|
echo "========================================"
|
|
echo " ODS - FUNCTIONAL TESTS"
|
|
echo " Tests actual functionality, not ports"
|
|
echo "========================================"
|
|
|
|
# Each test returns 1 on failure via its internal `fail` call; we must not
|
|
# let `set -e` short-circuit the remaining tests or skip the summary. The
|
|
# TESTS_PASSED / TESTS_FAILED counters are updated inside pass/fail and
|
|
# drive the final exit code below, so suspending strict mode for exactly
|
|
# this block is the explicit expression of "errors here are accounted for."
|
|
# (CLAUDE.md forbids `|| true` / silent swallow; this bounded toggle makes
|
|
# the intent visible instead of hiding it behind a trailing `|| true`.)
|
|
set +e
|
|
test_llm_functional
|
|
test_tts_functional
|
|
test_embeddings_functional
|
|
test_whisper_functional
|
|
set -e
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo " Results: $TESTS_PASSED passed, $TESTS_FAILED failed"
|
|
echo "========================================"
|
|
|
|
if [[ $TESTS_FAILED -eq 0 ]]; then
|
|
echo -e "${GREEN}✓ All functional tests passed${NC}"
|
|
exit 0
|
|
else
|
|
echo -e "${RED}✗ Some functional tests failed${NC}"
|
|
exit 1
|
|
fi
|