Files
serverAppConfigs/tests/frank_large_unit.bats
T
Frank 32abc8d543 chore(llm): add dankstatus.sh — one verified parse path for Dankserver state
The ad-hoc status commands kept producing wrong readings, most seriously
reporting "MemAvailable 0.0 GiB" on a host that actually had 240 GiB free.
Cause: /proc/meminfo separates key from value with a TAB, so

  grep MemAvailable /proc/meminfo | tr -s " " | cut -d" " -f2

returns an empty field and awk prints 0.0. A false "host is out of memory"
reading is worse than no reading, because it invites an unnecessary abort.

dankstatus.sh parses on a colon split (tab-agnostic), cross-checks
MemAvailable against MemTotal before printing either, and refuses to print
a zero value. It also pins the rocm-smi CSV column order (device, total,
used) which was previously read swapped, and takes only the first line of
systemctl is-active/is-enabled, which emit more than one line here.

Incidental finding while validating: rocm-smi reports card2 (the 7700X iGPU)
as 0.50 GiB total, not the 123.0 GiB torch.cuda reports. torch was exposing
the GTT aperture. The device is smaller than documented, not larger.

Tests: 346/346 pass (no test surface touched).
2026-09-28 23:27:50 -07:00

335 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bats
# Seam: the frank-large root unit on dankclaw.
#
# Since 2026-09-28 this endpoint is served by gufo (gufo-org/gufo), the
# Strix Halo inference engine built from the pinned v0.1.1 tag, serving
# Swift-1.5-Qwen3.8-27B-Uncensored (abliterated) UD-Q8_K_XL with the z-lab
# DFlash2 Q4_K_M drafter. Two changes landed on this endpoint that day: the
# engine (from the stock llama.cpp prebuilt) and the payload (from
# unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL). Port, alias and client contract did
# not move.
#
# Most of this seam runs the serve script against a stub engine and asserts
# on the argv it actually execs, so what is pinned is the command the
# operator gets rather than the text of the script: a tunable renamed,
# dropped, or silently defaulted away fails here the way it would fail a
# deployment.
load helpers
ENV_FILE="$REPO_ROOT/scripts/frank-large.env"
UNIT="$REPO_ROOT/scripts/frank-large.service"
SERVE="$REPO_ROOT/scripts/serve-frank-large.sh"
FETCH="$REPO_ROOT/scripts/download-frank-large-swift.sh"
DRAFTER_FETCH="$REPO_ROOT/scripts/download-frank-large.sh"
BUILD="$REPO_ROOT/scripts/build-gufo.sh"
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
# The deployed target's filename, kept here so a rename in the env file and a
# rename in this stub cannot drift apart unnoticed.
TARGET_STUB="Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-UD-Q8_K_XL.gguf"
setup() {
STAGE="$(mktemp -d)"
RUNTIME_DIR="$STAGE/gufo"
MODEL_DIR="$STAGE/models"
DRAFTER_DIR="$STAGE/draft"
mkdir -p "$RUNTIME_DIR/bin" "$MODEL_DIR" "$DRAFTER_DIR"
# A stub engine: the serve script execs it, so $@ is exactly what gufo
# would be invoked with.
cat > "$RUNTIME_DIR/bin/gufo" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' "$@"
STUB
chmod +x "$RUNTIME_DIR/bin/gufo"
# The target and the drafter live in SEPARATE directories since the
# 2026-09-28 payload swap: the target came from a different repository,
# the DFlash2 drafter did not move. A stub that colocated them would
# never notice DRAFTER_DIR going stale.
: > "$MODEL_DIR/$TARGET_STUB"
: > "$DRAFTER_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf"
STUB_ENV="$STAGE/stub.env"
cat > "$STUB_ENV" <<EOF
RUNTIME_DIR=$RUNTIME_DIR
MODEL_DIR=$MODEL_DIR
DRAFTER_DIR=$DRAFTER_DIR
EOF
}
teardown() {
if [ -n "${STAGE:-}" ]; then
rm -rf "$STAGE"
fi
}
# serve_argv [env-file] — run the serve script and capture the engine's argv,
# one token per line, in $output. No argument exercises the built-in defaults
# with the stub's paths.
serve_argv() {
local envfile="${1:-$STUB_ENV}"
if [ -n "$envfile" ]; then
FRANK_LARGE_ENV_FILE="$envfile" run bash "$SERVE"
else
run env FRANK_LARGE_ENV_FILE="$STAGE/absent.env" bash "$SERVE"
fi
}
# argv_has <token> — exact token match, so --port never satisfies a test
# looking for a different flag.
argv_has() {
grep -qxF -- "$1" <<<"$output"
}
# argv_value <flag> — the token following <flag>, empty if the flag is absent.
argv_value() {
local i
i="$(grep -nx -- "$1" <<<"$output" | cut -d: -f1)"
[ -n "$i" ] || return 0
sed -n "$((i + 1))p" <<<"$output"
}
@test "the frank-large unit, env, serve and build files exist where the deployer installs them from" {
[ -f "$ENV_FILE" ]
[ -f "$UNIT" ]
[ -x "$SERVE" ]
[ -x "$BUILD" ]
}
@test "frank-large 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"
# The engine pins buffers; the memlock budget must be unbounded or the
# load fails on the RLIMIT.
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
# The unit runs the repo's own serve script with the repo's env.
grep -q 'ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-large.sh' "$UNIT"
grep -q 'FRANK_LARGE_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-large.env' "$UNIT"
}
@test "the fleet endpoint and alias survive the cutover" {
serve_argv
[ "$status" -eq 0 ]
# frank-large at :7770, whatever engine answers it.
[ "$(argv_value --served-model-name)" = "frank-large" ]
[ "$(argv_value --port)" = "7770" ]
[ "$(argv_value --host)" = "0.0.0.0" ]
}
@test "the built-in defaults are one session of the full 262144 window with DFlash2" {
serve_argv
[ "$status" -eq 0 ]
# 262144 is the model's native window (the operator's call, 2026-09-21).
[ "$(argv_value --context)" -eq 262144 ]
# One session is the single stream every prior frank-large served.
[ "$(argv_value --sessions)" -eq 1 ]
[ "$(argv_value --speculative)" = "dflash2" ]
# A draft ceiling of 0 would silently serve the autoregressive rate.
[ "$(argv_value --draft-tokens)" -gt 0 ]
}
@test "the shipped env file, not just the defaults, serves the deployed shape" {
# The defaults are a floor; systemd sources the env file. Only the
# on-host paths are redirected at the stub, so every fleet value in the
# shipped file is the one under test. MODEL_FILE and DRAFTER_FILE are
# absolute in the shipped file, so they need redirecting too — a sed that
# only rewrote MODEL_DIR would leave them pointing at /home/dank and the
# test would pass for the wrong reason (or fail on a box that has them).
sed -e "s#^RUNTIME_DIR=.*#RUNTIME_DIR=$RUNTIME_DIR#" \
-e "s#^MODEL_DIR=.*#MODEL_DIR=$MODEL_DIR#" \
-e "s#^MODEL_FILE=.*#MODEL_FILE=$MODEL_DIR/$TARGET_STUB#" \
-e "s#^DRAFTER_DIR=.*#DRAFTER_DIR=$DRAFTER_DIR#" \
-e "s#^DRAFTER_FILE=.*#DRAFTER_FILE=$DRAFTER_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf#" \
"$ENV_FILE" > "$STAGE/shipped.env"
serve_argv "$STAGE/shipped.env"
[ "$status" -eq 0 ]
[ "$(argv_value --served-model-name)" = "frank-large" ]
[ "$(argv_value --host)" = "0.0.0.0" ]
[ "$(argv_value --port)" = "7770" ]
[ "$(argv_value --context)" -eq 262144 ]
[ "$(argv_value --sessions)" -eq 1 ]
[ "$(argv_value --speculative)" = "dflash2" ]
[ "$(argv_value --draft-tokens)" -gt 0 ]
[ "$(argv_value --draft-policy)" = "adaptive" ]
# The shipped file must name the target the fleet actually serves, not a
# path that merely happens to exist.
[ "$(argv_value --model)" = "$MODEL_DIR/$TARGET_STUB" ]
[ "$(argv_value --dflash-model)" = "$DRAFTER_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf" ]
}
@test "the engine is pointed at the payload that is actually on disk" {
serve_argv
[ "$status" -eq 0 ]
[ -f "$(argv_value --model)" ]
# A drafter path that does not exist would load the target and decode at
# the autoregressive rate, which reads as "the new runtime is slow".
[ -f "$(argv_value --dflash-model)" ]
[ "$(argv_value --model)" != "$(argv_value --dflash-model)" ]
}
@test "frank-large stays text-only" {
# The payload has no mmproj, so an --mmproj would point at nothing. The
# fleet's image path is frank-small; models.yml declares frank-large
# text-only.
serve_argv
! argv_has --mmproj
grep -q -- '--mmproj' "$REPO_ROOT/scripts/serve-frank-small.sh"
}
@test "the engine is asked for greedy decoding and for preserved prior reasoning" {
# Greedy is what this endpoint has always served; a silent switch to
# sampled output would be a model-behaviour change disguised as a
# runtime swap. Prior thinking blocks must survive across turns or a
# multi-turn agent loses its own reasoning between tool calls.
serve_argv
[ "$(argv_value --temperature)" = "0" ]
[ "$(argv_value --preserve-thinking)" = "on" ]
}
@test "the serve script refuses a missing runtime with the build pointer" {
rm -f "$RUNTIME_DIR/bin/gufo"
serve_argv
[ "$status" -ne 0 ]
[[ "$output" == *"runtime not found"* ]]
[[ "$output" == *"build-gufo.sh"* ]]
# A restart loop would be the alternative; nothing may be exec'd.
! argv_has serve
}
@test "the serve script refuses a missing drafter with a fetch pointer" {
# The drafter lives in its own directory now, so the target can be present
# and this still has to be caught.
rm -f "$DRAFTER_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf"
serve_argv
[ "$status" -ne 0 ]
[[ "$output" == *"missing asset"* ]]
[[ "$output" == *"Qwen3.8-27B-DFlash2-Q4_K_M.gguf"* ]]
[[ "$output" == *"download-frank-large"* ]]
! argv_has serve
}
@test "the serve script refuses a missing target and names the file" {
rm -f "$MODEL_DIR/$TARGET_STUB"
serve_argv
[ "$status" -ne 0 ]
[[ "$output" == *"missing asset"* ]]
[[ "$output" == *"$TARGET_STUB"* ]]
}
@test "both payload files are fetched resumably and verified against declared sizes + sha256" {
# The target and the drafter come from DIFFERENT repositories, fetched by
# two scripts. Anchor the repo checks to the URL ASSIGNMENT, not to the
# file: the same repo string also appears in each script's header comment,
# so a plain grep stayed green after the URL was repointed at another
# repository -- the script would then download something the serve script
# never asked for.
grep -qE '^URL="https://huggingface\.co/ajgazin/Swift-1\.5-Qwen3\.8-27B-Uncensored-Dynamic-MTP-GGUF/resolve/main/' "$FETCH"
grep -qE 'https://huggingface\.co/z-lab/Qwen3\.8-27B-DFlash2-GGUF/resolve/main/' "$DRAFTER_FETCH"
for f in "$FETCH" "$DRAFTER_FETCH"; do
# -f so an HTTP error is a failure rather than a small "artifact";
# -C - so an interrupted 29 GiB run resumes.
grep -q -- '-fL -C -' "$f"
# The size and the sha256 are compared after the transfer, not trusted.
grep -q 'SHA256 MISMATCH' "$f"
grep -q 'SIZE MISMATCH' "$f"
done
}
@test "the serve script and the fetch scripts agree on the payload filenames" {
# The serve script's MODEL_FILE and the fetch script's REL are two
# independent spellings of one filename, and a repo is not required to
# keep them in step: a re-upload under a new name, or an env edited
# without the fetch, leaves the endpoint unable to start with a
# "missing asset" that no test would have predicted. Bind them here.
local served fetched drafter_served drafter_fetched
served="$(basename "$(grep '^MODEL_FILE=' "$ENV_FILE" | cut -d= -f2-)")"
fetched="$(sed -n 's/^REL="\(.*\)"$/\1/p' "$FETCH")"
[ -n "$served" ]
[ "$served" = "$fetched" ]
drafter_served="$(basename "$(grep '^DRAFTER_FILE=' "$ENV_FILE" | cut -d= -f2-)")"
drafter_fetched="$(sed -n 's/^ *"\(Qwen3\.8-27B-DFlash2[^ ]*\.gguf\) .*/\1/p' "$DRAFTER_FETCH")"
[ -n "$drafter_served" ]
[ "$drafter_served" = "$drafter_fetched" ]
}
@test "the drafter is still fetched by the script that carries its Hub repo" {
# Guards a future "simplification" that folds the drafter into the Swift
# script and silently orphans the artifact the serve script still points
# at via DRAFTER_DIR.
grep -q 'Qwen3.8-27B-UD-Q8_K_XL' "$DRAFTER_FETCH"
! grep -q 'DFlash2' "$FETCH"
}
@test "the build script refuses a source tree that is not the pinned commit" {
# gufo ships no prebuilt Linux binaries, so the runtime is built here.
# A tag that moves under us would silently install different kernels
# under the same RUNTIME_DIR, so the pin is enforced, not documented.
local src="$STAGE/not-the-pinned-commit"
mkdir -p "$src"
git -C "$src" init -q
git -C "$src" -c user.email=t@t -c user.name=t commit -q --allow-empty -m drift
run env GUFO_SRC_DIR="$src" GUFO_PREFIX="$STAGE/absent-prefix" bash "$BUILD" --verify
[ "$status" -ne 0 ]
[[ "$output" == *"GUFO_COMMIT mismatch"* ]]
}
@test "the build script reports a runtime that was never built" {
# --verify on a host with no install must say so rather than exit 0 on
# an empty check.
run env GUFO_SRC_DIR="$STAGE/no-source-tree" GUFO_PREFIX="$STAGE/absent-prefix" bash "$BUILD" --verify
[ "$status" -ne 0 ]
[[ "$output" == *"no gufo at"* ]]
}
@test "the deploy handler gates on the runtime, the payload and the port" {
grep -q 'relink_unit frank-large' "$REMOTE_DEPLOY"
grep -q 'systemctl enable frank-large.service' "$REMOTE_DEPLOY"
grep -q 'wait_port_free 7770' "$REMOTE_DEPLOY"
grep -q '127.0.0.1:7770/health' "$REMOTE_DEPLOY"
grep -q 'LLM_LARGE_RUNTIME' "$REMOTE_DEPLOY"
grep -q 'LLM_LARGE_MODEL' "$REMOTE_DEPLOY"
grep -q 'LLM_LARGE_DRAFT' "$REMOTE_DEPLOY"
}
@test "the deploy handler will not stop a serving endpoint it cannot replace" {
# The runtime gate must be live (a real conditional on an executable
# path, not a branch that never fires) and it must return BEFORE the
# stop, or a host whose gufo was never built takes frank-large down on
# its way to reporting the failure.
local large gate bail stop
large="$(sed -n '/^deploy_llm_large() {/,/^}/p' "$REMOTE_DEPLOY")"
[[ "$large" == *'build-gufo.sh'* ]]
gate="$(grep -n 'if \[\[ ! -x "\$LLM_LARGE_RUNTIME" \]\]' <<<"$large" | head -1 | cut -d: -f1)"
stop="$(grep -n 'systemctl stop frank-large' <<<"$large" | head -1 | cut -d: -f1)"
[ -n "$gate" ]
[ -n "$stop" ]
bail="$(awk -v g="$gate" 'NR > g && /return 1/ { print NR; exit }' <<<"$large")"
[ -n "$bail" ]
[ "$bail" -lt "$stop" ]
}
@test "the stray-run sweep keys on the drafter and cannot take frank-small down" {
# The sweep used to key on the TARGET filename, which the 2026-09-28
# payload swap moved to a different repository. It would have stopped
# matching the live process, and a stray run would then hold 7770 past the
# port gate and fail the deploy. The drafter is the part that did NOT
# move, and every frank-large command line carries it via --dflash-model.
grep -q "pkill -f 'Qwen3.8-27B-DFlash2-Q4_K_M'" "$REMOTE_DEPLOY"
# frank-small serves a different GGUF on the 3090 with no DFlash2 drafter,
# so a binary-name sweep is the only thing that would endanger it.
local large
large="$(sed -n '/^deploy_llm_large() {/,/^}/p' "$REMOTE_DEPLOY")"
! grep -q 'pkill -x llama-server' <<<"$large"
! grep -q 'pkill -x gufo' <<<"$large"
}
@test "the deploy handler retires the halogen quadlet without deleting it" {
grep -q 'retire_halogen_large_quadlet' "$REMOTE_DEPLOY"
grep -q 'HOME/.AGENTTRASH' "$REMOTE_DEPLOY"
! grep -qE '(^|[^[:alnum:]_])rm (-[a-zA-Z]+ )?.*frank-large' "$REMOTE_DEPLOY"
# Nothing live ships for this endpoint as a container any more. The
# archived copy is host state in an ignored .AGENTTRASH — deliberately
# untracked, and present only in a primary checkout — so search the
# tracked tree, not the filesystem.
[ ! -e "$REPO_ROOT/dankclaw/frank-large/quadlet/frank-large.container" ]
! git -C "$REPO_ROOT" grep -q 'Image=.*halogen' -- dankclaw
}