#!/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" < — exact token match, so --port never satisfies a test # looking for a different flag. argv_has() { grep -qxF -- "$1" <<<"$output" } # argv_value — the token following , 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 }