#!/usr/bin/env bats # Seam: frank-primary — the Dankserver LLM endpoint on :7770. # # This file asserts the llama.cpp ROOT-UNIT shape (the 2026-09-26 swap that # replaced the Lucebox DS4.1 Quadlet): the unsloth b11030-mix prebuilt on # the two R9700s (gfx1201) serving unsloth/Qwen3.8-Flash-Next-GGUF at Q8_0 # with the shared Q8_0 MTP head, the BF16 projector, and the # draft-mtp + ngram-mod chain. The seam pins what the flags alone do not # say: the CPU-override placement law (experts and the 51B n-gram table # are COMPUTED in host RAM, never streamed), mmap staying on, the # shared-head borrow requirement (per_layer_token_embd on CPU), the # explicit projector (never autoloaded by this build), HIP env hygiene, # and the deployer's byte-count gates over a six-part payload. load helpers UNIT="$REPO_ROOT/scripts/frank-primary.service" SERVE="$REPO_ROOT/scripts/serve-frank-primary.sh" ENV_FILE="$REPO_ROOT/scripts/frank-primary.env" REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh" @test "frank-primary unit, serve script and env exist in scripts/" { [ -f "$UNIT" ] [ -x "$SERVE" ] [ -f "$ENV_FILE" ] } @test "frank-primary is boot-persistent, self-healing and unprivileged" { # Root unit parity with frank-large/frank-small: enable is the [Install] # section, the restart policy survives a crash, and the mmap'd payload # needs an unbounded memlock. 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-primary runs the repo's own serve script with the repo's env" { grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-primary.sh$' "$UNIT" grep -q 'FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env' "$UNIT" } @test "frank-primary drafts with the shared MTP head chained behind ngram-mod" { # The chain is draft-mtp (context-backed) + ngram-mod (draftless): the # only legal way to layer two spec implementations in one process. The # shared Q8_0 head borrows the target's embedding tensors — which is # why per_layer_token_embd.weight must be CPU-resident (a GPU-resident # borrow crashes the draft load with "invalid vector subscript"). grep -q -- '--spec-type draft-mtp,ngram-mod' "$SERVE" grep -q '^SPEC_DRAFT_N_MAX=2$' "$ENV_FILE" grep -q -- '--spec-draft-n-max "$SPEC_DRAFT_N_MAX"' "$SERVE" grep -q 'DRAFTER_FILE="$MODEL_ROOT/MTP/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf"' "$SERVE" grep -q -- '--spec-draft-device "$SPEC_DRAFT_DEVICE"' "$SERVE" } @test "the bandwidth-huge tensors are computed in host RAM, not streamed" { # The x4-pair law: streaming experts over PCIe loses (3.7 tok/s) while # computing them in RAM does not care (20+ tok/s on the same model # shape). exps=CPU plus the n-gram embedding table on CPU; the PLE # table must NEVER carry a GPU target (the 23 -> 13.4 t/s regression). # ONE joined flag (b11030 deprecates repeated --override-tensor), and # first-match-wins means the block-specific patterns must precede the # broad exps=CPU sweep — the exact value pins the whole law. grep -q -- '--override-tensor "$OT_OVERRIDES"' "$SERVE" ! grep -q -- '--no-mmap' "$SERVE" ! grep -q '^MMPROJ_ON_GPU' "$ENV_FILE" ot="$(sed -nE "s/^OT_OVERRIDES='(.*)'$/\1/p" "$ENV_FILE")" [ "$ot" = 'blk\.[0-4]\.ffn_(up|gate|down)_exps=ROCm0,blk\.4[5-7]\.ffn_(up|gate|down)_exps=ROCm1,exps=CPU,per_layer_token_embd.weight=CPU' ] } @test "the KV pool is V-quantized, K held at f16" { # V q8_0 halves nothing (it is 1/4 of the per-token law: 18,816 of the # 24,576 B/token f16/f16 cost) but it is the deploy's stated shape; the # flags must be env-wired tunables so the quant can be reverted # without touching the serve script. grep -q '^CACHE_TYPE_K=f16$' "$ENV_FILE" grep -q '^CACHE_TYPE_V=q8_0$' "$ENV_FILE" grep -q -- '--cache-type-k "$CACHE_TYPE_K"' "$SERVE" grep -q -- '--cache-type-v "$CACHE_TYPE_V"' "$SERVE" } @test "the projector is explicit, never autoloaded" { # This build never autoloads mmproj from the model directory: without # the flag the server comes up healthy and text-only, answering image # requests with 500 (the frank-medium 2026-09-17 trap). grep -q -- '--mmproj "$MMPROJ_FILE"' "$SERVE" grep -q 'MMPROJ_FILE="$MODEL_ROOT/mmproj-BF16.gguf"' "$SERVE" } @test "frank-primary is vision-serving but holds the single fleet port" { grep -q '^PORT=7770$' "$ENV_FILE" grep -q '^BIND_HOST=0.0.0.0$' "$ENV_FILE" grep -q -- '--alias frank-primary' "$SERVE" grep -q -- '--host "$BIND_HOST"' "$SERVE" grep -q -- '--port "$PORT"' "$SERVE" } @test "four streams, each the model's full native window" { # --parallel divides --ctx-size into slots; the contract is four # conversations each holding the FULL native 262144 window, so the # per-slot window (ctx / parallel) equals the native size. local ctx parallel ctx="$(sed -nE 's/^CONTEXT_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")" parallel="$(sed -nE 's/^PARALLEL_SLOTS=([0-9]+)$/\1/p' "$ENV_FILE")" [ -n "$ctx" ] && [ -n "$parallel" ] [ "$parallel" -eq 4 ] [ "$((ctx / parallel))" -eq 262144 ] grep -q -- '--ctx-size "$CONTEXT_SIZE"' "$SERVE" grep -q -- '--parallel "$PARALLEL_SLOTS"' "$SERVE" } @test "reasoning is extracted to reasoning_content and tools are legal" { # The harness contract: llama.cpp endpoints emit reasoning_content # (the deepseek format is the one that fills it), and tools require # --jinja (the ik_llama 500 lesson). The Qwen3.8 template gates # reasoning_effort to {xhigh, medium, low} — folded in models.yml. grep -q -- '--jinja' "$SERVE" grep -q -- '--reasoning auto' "$SERVE" grep -q -- '--reasoning-format deepseek' "$SERVE" grep -q -- '--reasoning-budget -1' "$SERVE" } @test "the greedy server default is explicit" { # Deterministic e2e probes; the harness overrides sampling per request. grep -q -- '--temp 0' "$SERVE" grep -q -- '--top-p 1.0' "$SERVE" grep -q -- '--top-k 0' "$SERVE" grep -q -- '--min-p 0.0' "$SERVE" } @test "both R9700s are visible and the Raphael iGPU is not" { # The host has a third (Raphael) GPU the server must never place # tensors on; the bundled ROCm runtime is the only one linked. grep -q 'export HIP_VISIBLE_DEVICES=0,1$' "$SERVE" grep -q 'export LD_LIBRARY_PATH="$RUNTIME_DIR' "$SERVE" } @test "no bare HIP-reserved env var is defined" { # HIP reads bare TEMP for its JIT cache path (2026-08-13: TEMP=1.0 # segfaulted every load), and HOST/GPU_MAX_HEAP_SIZE are the same # class. The bind address is BIND_HOST, not HOST. ! grep -qE '^(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$ENV_FILE" grep -q '^BIND_HOST=' "$ENV_FILE" ! grep -qE '^(export )?(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$SERVE" } @test "the serve script refuses to start on a half-finished download" { # Six split parts plus the head and the projector: a missing tail part # fails at LOAD time with a confusing partial-mmap error, so every # artifact is checked before the server execs. grep -q 'missing asset' "$SERVE" grep -q 'download-frank-primary.sh' "$SERVE" } @test "the prompt-cache cap is set below the RAM the host can spare" { grep -q '^CACHE_RAM=16384$' "$ENV_FILE" grep -q -- '--cache-ram "$CACHE_RAM"' "$SERVE" } @test "the deployer retires the lucebox quadlet, gates every part and waits out the port" { # The cutover order: the user-scope quadlet is stopped and its # generated .container moved to dated home trash BEFORE the root unit # binds 7770 (nothing starts until the port is released — the 2026-09-26 # and 2026-09-16 cutover failures were both here). grep -q 'retire_lucebox_primary_quadlet' "$REMOTE_DEPLOY" grep -q 'systemctl --user stop frank-primary.service' "$REMOTE_DEPLOY" grep -q 'relink_unit frank-primary' "$REMOTE_DEPLOY" grep -q 'systemctl enable frank-primary.service' "$REMOTE_DEPLOY" grep -q 'systemctl restart frank-primary.service' "$REMOTE_DEPLOY" grep -q 'wait_port_free 7770' "$REMOTE_DEPLOY" grep -q '127.0.0.1:7770/health' "$REMOTE_DEPLOY" } @test "the deployer gates the six-part payload at declared byte counts" { # stat -c %s, never wc -c — wc read the whole 377 GiB lucebox file for # minutes on 2026-09-26 before it could reach the cutover. A partial # download must never be served. grep -q 'LLM_PRIMARY_PART_SIZES' "$REMOTE_DEPLOY" grep -q 'stat -c %s' "$REMOTE_DEPLOY" grep -q '2786568256' "$REMOTE_DEPLOY" grep -q '907542944' "$REMOTE_DEPLOY" grep -q 'wait_for_health frank-primary http://127.0.0.1:7770/health 1800' "$REMOTE_DEPLOY" }