Files
serverAppConfigs/tests/frank_small_unit.bats
T
Frank 7403bd3d50 deploy: never start the replacement while the predecessor holds the port
The 2026-09-16 halogen restore failed exactly here. The ik root unit stopped,
systemd logged "Deactivated successfully", pasta tried to bind 7770 about a
second later, reported "Address already in use", and the quadlet exited 126.
Only Restart=on-failure recovered it -- minutes later, which is not a deploy.

The handler now waits for every socket on the port to clear (listener, accepted
and TIME_WAIT) before installing and starting the quadlet, and fails loudly with
the socket table attached if it never does. The retirement also reports when the
unit file was already gone, because this host's `disable` deletes it.
2026-09-16 14:06:03 -07:00

109 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bats
# Seam: the frank-small mainline-llama.cpp root unit on dankclaw.
# frank-small is the RTX 3090 endpoint created on 2026-09-16, when frank-large
# went back to the halogen engine on the iGPU. This seam pins what the flags
# alone do not say: boot persistence for a NATIVE runtime (no generated quadlet
# is involved), the repo-relative paths the unit executes, the separate client
# port, the MTP contract (the head lives in the weights file, and no second copy
# of the model is loaded for it), the vision handoff from frank-large, and the
# KV budget that has to fit a 24 GiB card beside 12 GiB of weights.
load helpers
UNIT="$REPO_ROOT/scripts/frank-small.service"
SERVE="$REPO_ROOT/scripts/serve-frank-small.sh"
ENV_FILE="$REPO_ROOT/scripts/frank-small.env"
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
QUADLET_LARGE="$REPO_ROOT/dankclaw/frank-large/quadlet/frank-large.container"
@test "frank-small unit, serve script and env exist in scripts/" {
[ -f "$UNIT" ]
[ -f "$SERVE" ]
[ -f "$ENV_FILE" ]
}
@test "frank-small is boot-persistent, self-healing and unprivileged" {
grep -q '^WantedBy=multi-user.target$' "$UNIT"
grep -q '^Restart=on-failure$' "$UNIT"
grep -q '^RestartSec=' "$UNIT"
grep -q '^User=dank$' "$UNIT"
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
}
@test "frank-small runs the repo's own serve script with the repo's env" {
grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-small.sh$' "$UNIT"
grep -q 'FRANK_SMALL_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-small.env' "$UNIT"
}
@test "frank-small offloads every layer to the 3090 with flash attention" {
grep -q 'llama.cpp-mainline/build-cuda/bin/llama-server' "$ENV_FILE"
grep -q '^NGL=999$' "$ENV_FILE"
grep -q -- '--gpu-layers "$NGL"' "$SERVE"
grep -q '^FLASH_ATTN=on$' "$ENV_FILE"
grep -q -- '--flash-attn "$FLASH_ATTN"' "$SERVE"
# Prometheus scrapes this endpoint; without --metrics there is no /metrics.
grep -q -- '--metrics' "$SERVE"
}
@test "frank-small's MTP head comes from the weights file, not a second copy" {
grep -q '^SPEC_TYPE=draft-mtp$' "$ENV_FILE"
grep -q -- '--spec-type "$SPEC_TYPE"' "$SERVE"
# The -mtp build is the one carrying blk.64.nextn.*; the plain IQ3_S has no
# MTP tensors at all, so MTP is a property of the file, not of the flags.
grep -q '^MODEL_FILE=.*-mtp\.gguf$' "$ENV_FILE"
# A separate draft model would load a second 12 GiB copy of a 12 GiB model.
# Only the effective configuration counts: the script's own comment explains
# why there is no draft path, and a textual grep would match that prose.
! grep -vE '^[[:space:]]*#' "$SERVE" | grep -q -- '--model-draft'
}
@test "frank-small carries the fleet's image path, and only it" {
grep -q '^MMPROJ_PATH=.*mmproj-Qwen3\.8-27B-BF16\.gguf$' "$ENV_FILE"
grep -q -- '--mmproj "$MMPROJ_PATH"' "$SERVE"
! grep -q 'HALOGEN_VISION_TOWER' "$QUADLET_LARGE"
}
@test "frank-small's KV fits the card beside the weights it loads" {
local ctx parallel parallel_slots cache_k cache_v
ctx="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
parallel="$(sed -nE 's/^PARALLEL=([0-9]+)$/\1/p' "$ENV_FILE")"
cache_k="$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")"
cache_v="$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")"
[ -n "$ctx" ] && [ -n "$parallel" ] && [ -n "$cache_k" ] && [ -n "$cache_v" ]
[ "$parallel" -ge 1 ]
# qwen35 spends KV on its 16 full-attention layers only: 2 (K,V) x 4 heads x
# 256 dim x 2 B = 64 KiB per token at f16. 11.29 GiB of weights + 0.87 of
# mmproj + ~2 GiB of context/compute/MTP leave under 10 GiB of the card's
# 24 GiB for KV, and --parallel divides --ctx-size between the slots.
[ $((ctx * 64)) -le $((10 * 1024 * 1024)) ]
# f16 is the deployed type on purpose: a quantized KV needs the
# GGML_CUDA_FA_ALL_QUANTS build option, without which prefill on CUDA
# collapses about 12x (see the CUDA lesson in AGENTS.md).
[ "$cache_k" = "f16" ] && [ "$cache_v" = "f16" ]
}
@test "frank-small runs on its own port beside frank-large's" {
grep -q '^LISTEN_PORT=7771$' "$ENV_FILE"
grep -q '^ALIAS=frank-small$' "$ENV_FILE"
grep -q -- '--port "$LISTEN_PORT"' "$SERVE"
grep -q -- '--host "$BIND_ADDR"' "$SERVE"
# The two endpoints cannot share a port, and only the halogen unit publishes
# 7770.
grep -q '^PublishPort=7770:8731$' "$QUADLET_LARGE"
! grep -q 'PublishPort=.*7771' "$QUADLET_LARGE"
}
@test "the deploy handler installs the root unit and refuses a missing model" {
grep -q 'deploy_llm_small' "$REMOTE_DEPLOY"
grep -q 'relink_unit frank-small' "$REMOTE_DEPLOY"
grep -q 'systemctl enable frank-small.service' "$REMOTE_DEPLOY"
grep -q 'systemctl restart frank-small.service' "$REMOTE_DEPLOY"
grep -q 'Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp' "$REMOTE_DEPLOY"
grep -q 'refusing to start a unit that cannot load' "$REMOTE_DEPLOY"
grep -q '127.0.0.1:7771/health' "$REMOTE_DEPLOY"
}
@test "frank-small's serve script refuses to start on a half-finished download" {
grep -q 'frank-small: missing' "$SERVE"
}