#!/usr/bin/env bats # Seam: the deployer's LLM handlers are the twin of the serve scripts' # artifact gates (the 2026-10-01 crash-loop: a green deploy against a # missing payload left a restart-looping unit and a dark endpoint). # # These tests EXECUTE scripts/deploy-remote.sh's LLM handlers in a sandbox # (tests/deploy_sandbox.bash): every host touchpoint is a stub, the artifacts # are fixtures, and the assertions are exit status, the refusal on stderr, # and what the handler asked systemd to do. A handler that started the unit # before checking its artifacts would pass a text-grep seam and fail here. load helpers load deploy_sandbox setup() { sandbox_setup; } teardown() { sandbox_teardown; } @test "the dragoon handler refuses when the engine image is not in storage" { export STUB_IMAGE= run_handler dankclaw frank-dragoon [ "$status" -ne 0 ] [[ "$output" == *"frank-dragoon: image:"*"not in podman storage"* ]] [ "$(restarts_requested)" = "0" ] } @test "the dragoon handler refuses a truncated checkpoint, pointing at the fetch" { truncate -s 9 "$STAGE/models/ck.hgn" run_handler dankclaw frank-dragoon [ "$status" -ne 0 ] [[ "$output" == *"checkpoint: is 9 bytes, expected 10"* ]] # The refusal is only useful if it names the remedy: the fetch script # re-verifies byte counts and hashes. [[ "$output" == *"download-frank-dragoon-flashnext.sh"* ]] [ "$(restarts_requested)" = "0" ] } @test "the dragoon handler refuses a symlinked tokenizer file" { # The flat-tokenizer law: a HuggingFace cache snapshot links its # members, and the link dangles inside the container mount. rm "$STAGE/models/tokenizer/vocab.json" ln -s /nowhere/vocab.json "$STAGE/models/tokenizer/vocab.json" run_handler dankclaw frank-dragoon [ "$status" -ne 0 ] [[ "$output" == *"frank-dragoon: tokenizer: $STAGE/models/tokenizer/vocab.json is missing or a symlink"* ]] [ "$(restarts_requested)" = "0" ] } @test "the dragoon handler starts the unit once every artifact gate passes" { run_handler dankclaw frank-dragoon [ "$status" -eq 0 ] grep -q 'restart frank-dragoon.service' "$STAGE/systemctl.log" grep -qx 'rm -f frank-dragoon frank-dragoon-halogen' "$STAGE/podman.log" } @test "the zealot handler refuses when the llama.cpp runtime is not executable" { # The runtime is an operator-built binary; a tree that compiled but lost # its exec bit must refuse exactly like a missing one. rm "$STAGE/models/llama-server" run_handler dankclaw frank-zealot [ "$status" -ne 0 ] [[ "$output" == *"frank-zealot: runtime: missing at $STAGE/models/llama-server"* ]] [[ "$output" == *"cmake"* ]] [ "$(restarts_requested)" = "0" ] } @test "the zealot handler refuses a missing projector — a text-only endpoint is not deployable" { # The projector IS the vision capability: deploying without it silently # ships a text-only endpoint. rm "$STAGE/models/mmproj.gguf" run_handler dankclaw frank-zealot [ "$status" -ne 0 ] [[ "$output" == *"frank-zealot: mmproj: missing at $STAGE/models/mmproj.gguf"* ]] [ "$(restarts_requested)" = "0" ] } @test "the zealot handler starts the unit once every artifact gate passes" { run_handler dankclaw frank-zealot [ "$status" -eq 0 ] grep -q 'restart frank-zealot.service' "$STAGE/systemctl.log" } @test "the archon handler refuses when the radiance image is not in storage" { export STUB_IMAGE= run_handler dankserver frank-archon [ "$status" -ne 0 ] [[ "$output" == *"frank-archon: image:"*"not in podman storage"* ]] [[ "$output" == *"podman pull"* ]] [ "$(restarts_requested)" = "0" ] } @test "the archon handler refuses a .rad below the env's size floor" { truncate -s 1048576 "$STAGE/models/payload.rad" # 1 MiB, far under the floor archon_payload_env export STUB_IMAGE="$ARCHON_IMAGE" run_handler dankserver frank-archon [ "$status" -ne 0 ] [[ "$output" == *"frank-archon: .rad: is 1 MiB, expected >= "* ]] [[ "$output" == *"convert-swift15-w4dn8.sh"* ]] [ "$(restarts_requested)" = "0" ] } @test "the archon handler clears the legacy container identities and relinks the unit" { # The retired identities (the R9V container, the ad hoc probe, the glm # experiment) hold the same GPUs, so a deploy that leaves one running # leaves the new engine unable to bind. The unit link is the root-unit # shape's whole install. archon_payload_env 2 export STUB_IMAGE="$ARCHON_IMAGE" run_handler dankserver frank-archon [ "$status" -eq 0 ] grep -q 'restart frank-archon.service' "$STAGE/systemctl.log" [ -L "$STAGE/systemd/frank-archon.service" ] local ctr for ctr in frank-archon frank-archon-qwen frank-archon-r9v qwen-fp8 glm-probe; do grep -qx "rm -f $ctr" "$STAGE/podman.log" done } @test "no LLM handler reports green when its unit has no MainPID behind the port" { # The 2026-09-26 false green: a leftover probe held :7770 and satisfied # the health curl while the real unit was stopped. archon_payload_env 2 export STUB_IMAGE="$ARCHON_IMAGE" export STUB_MAIN_PID=0 run_handler dankserver frank-archon [ "$status" -ne 0 ] [[ "$output" == *"frank-archon.service has no MainPID"* ]] } @test "a refused LLM handler leaves its service pending in the deploy state" { export STUB_IMAGE= run_handler dankserver frank-archon [ "$status" -ne 0 ] grep -q 'status=pending' "$STAGE/state/frank-archon" [ ! -e "$STAGE/state/frank-zealot" ] }