#!/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 and serving # unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL with the DFlash2 drafter. It replaced # the stock llama.cpp prebuilt (unsloth b11030) on the same port, the same # alias and the same weights. # # 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.sh" BUILD="$REPO_ROOT/scripts/build-gufo.sh" REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh" setup() { STAGE="$(mktemp -d)" RUNTIME_DIR="$STAGE/gufo" MODEL_DIR="$STAGE/models" mkdir -p "$RUNTIME_DIR/bin" "$MODEL_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" : > "$MODEL_DIR/Qwen3.8-27B-UD-Q8_K_XL.gguf" : > "$MODEL_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 two # on-host paths are redirected at the stub, so every fleet value in the # shipped file is the one under test. sed -e "s#^RUNTIME_DIR=.*#RUNTIME_DIR=$RUNTIME_DIR#" \ -e "s#^MODEL_DIR=.*#MODEL_DIR=$MODEL_DIR#" \ "$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" ] } @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 the fetch pointer" { rm -f "$MODEL_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf" serve_argv [ "$status" -ne 0 ] [[ "$output" == *"missing asset"* ]] [[ "$output" == *"download-frank-large.sh"* ]] ! argv_has serve } @test "the serve script refuses a missing target and names the file" { rm -f "$MODEL_DIR/Qwen3.8-27B-UD-Q8_K_XL.gguf" serve_argv [ "$status" -ne 0 ] [[ "$output" == *"missing asset"* ]] [[ "$output" == *"Qwen3.8-27B-UD-Q8_K_XL.gguf"* ]] } @test "the payload is fetched resumably and verified against declared sizes + sha256" { grep -q 'unsloth/Qwen3.8-27B-GGUF' "$FETCH" grep -q 'z-lab/Qwen3.8-27B-DFlash2-GGUF' "$FETCH" grep -q -- '-fL -C -' "$FETCH" # Each file's local sha256 is compared to its declared oid and a # mismatch fails the run. grep -q 'SHA256 MISMATCH' "$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 cannot take frank-small down" { # frank-small on this host serves a different GGUF. The sweep must match # the model path, not a binary name, or a frank-large deploy restarts the # 3090 endpoint. grep -q "pkill -f 'Qwen3.8-27B-UD-Q8_K_XL'" "$REMOTE_DEPLOY" 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 }