Files
computer-playbook/roles/svc-ai-lmstudio/files/test/test.sh
kevinveenbirkenbachandClaude Opus 5 5a651f05d5 test(lmstudio): probe the server on its network address, not loopback
The role test asked for /v1/models on 127.0.0.1 from inside the container, the one address that still answered while the server was bound to loopback alone. It stayed green and the failure surfaced one role later as a 500 in litellm's warm-up.

Step 3 now resolves the container's first IPv4 through hostname -I and queries that address, which a loopback-only bind refuses. It reads the interfaces directly, so it needs neither the service name nor swarm's route back through the service VIP, and compose and swarm take the same path. When the models never appear, one more request tells an unreachable server apart from an unindexed store.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:25:13 +02:00

115 lines
3.9 KiB
Bash

#!/usr/bin/env bash
# shellcheck shell=bash
#
# Env (rendered into test.env from templates/test.env.j2):
# LMSTUDIO_CONTAINER resolved container id on this node
# LMSTUDIO_PORT the daemon's HTTP port
# LMSTUDIO_EXPECTED_MODELS JSON array of model names the daemon must serve
# LMSTUDIO_EXPECTED_BLOBS JSON array of <repo>/<file> paths under models/
# LMSTUDIO_EXPECTED_DIGESTS JSON array of sha256, aligned with the blobs
# LMSTUDIO_MODELS_DIR the models mount inside the container
# RETRIES attempts (default 30)
# SLEEP_SECONDS wait between (default 10)
set -uo pipefail
RETRIES="${RETRIES:-30}"
SLEEP_SECONDS="${SLEEP_SECONDS:-10}"
MODELS_DIR="${LMSTUDIO_MODELS_DIR}"
[ -n "${LMSTUDIO_CONTAINER}" ] || {
echo "[FATAL] LMSTUDIO_CONTAINER unset; the harness did not resolve a container" >&2
exit 2
}
in_container() {
container exec "${LMSTUDIO_CONTAINER}" "$@"
}
json_items() {
python3 -c 'import json,sys; [print(x) for x in json.loads(sys.argv[1])]' "$1"
}
server_url=""
serves_model() {
in_container curl -fsS --connect-timeout 5 --max-time 30 "${server_url}/v1/models" 2>/dev/null |
python3 -c 'import json,sys; d=json.load(sys.stdin); print("\n".join(m.get("id","") for m in d.get("data",[])))' 2>/dev/null |
grep -qxF "$1"
}
failed=0
echo "=== 1. the CLI authenticates against its own daemon ==="
attempt=1
auth_out=""
while [ "${attempt}" -le "${RETRIES}" ]; do
if auth_out="$(in_container /app/.bundle/lms ls 2>&1)"; then
echo "[OK] lms ls authenticated"
break
fi
if printf '%s' "${auth_out}" | grep -q 'authPacket'; then
echo "[FAIL] the daemon rejected its own CLI: $(printf '%s' "${auth_out}" | grep -m1 authPacket)" >&2
echo " a second daemon shares /root/.lmstudio and rewrote .internal/lms-key-2" >&2
exit 1
fi
attempt=$((attempt + 1))
[ "${attempt}" -le "${RETRIES}" ] && sleep "${SLEEP_SECONDS}"
done
if [ "${attempt}" -gt "${RETRIES}" ]; then
echo "[FAIL] lms ls never succeeded after ${RETRIES} attempts: ${auth_out}" >&2
exit 1
fi
echo "=== 2. every declared blob is in place with its declared digest ==="
mapfile -t blobs < <(json_items "${LMSTUDIO_EXPECTED_BLOBS}")
mapfile -t digests < <(json_items "${LMSTUDIO_EXPECTED_DIGESTS}")
for i in "${!blobs[@]}"; do
path="${MODELS_DIR}/${blobs[$i]}"
if ! in_container test -f "${path}"; then
echo "[FAIL] ${blobs[$i]} is not in the model store" >&2
failed=1
continue
fi
actual="$(in_container sha256sum "${path}" 2>/dev/null | awk '{print $1}')"
if [ "${actual}" != "${digests[$i]}" ]; then
echo "[FAIL] ${blobs[$i]} hashes to ${actual}, declared ${digests[$i]}" >&2
failed=1
continue
fi
echo "[OK] ${blobs[$i]} present and matching"
done
echo "=== 3. the daemon serves every declared model on its network address ==="
address="$(in_container hostname -I | tr ' ' '\n' | grep -m1 -E '^[0-9]+(\.[0-9]+){3}$')"
if [ -z "${address}" ]; then
echo "[FAIL] hostname -I reported no IPv4 address inside the container" >&2
exit 1
fi
server_url="http://${address}:${LMSTUDIO_PORT}"
mapfile -t models < <(json_items "${LMSTUDIO_EXPECTED_MODELS}")
for model in "${models[@]}"; do
attempt=1
while [ "${attempt}" -le "${RETRIES}" ]; do
if serves_model "${model}"; then
echo "[OK] ${model} is served"
break
fi
attempt=$((attempt + 1))
[ "${attempt}" -le "${RETRIES}" ] && sleep "${SLEEP_SECONDS}"
done
if [ "${attempt}" -gt "${RETRIES}" ]; then
if ! in_container curl -fsS --connect-timeout 5 --max-time 30 -o /dev/null "${server_url}/v1/models"; then
echo "[FAIL] ${server_url} does not answer" >&2
echo " other containers cannot reach the server; check that LMS_SERVER_HOST binds it beyond loopback" >&2
else
echo "[FAIL] ${model} is on disk but /v1/models never listed it" >&2
echo " the daemon did not index the store it was given at startup" >&2
fi
failed=1
fi
done
[ "${failed}" -eq 0 ] || exit 1
echo "[OK] svc-ai-lmstudio serves every declared model from the seeded store"