Files
serverAppConfigs/tests/llm_deploy_gates.bats
T
Frank ab80c1366d refact(deployer): one MainPID gate for all three LLM endpoints
frank-dragoon's health gate never checked WHO answered :7770 — the
false-green the fleet learned about on 2026-09-26 (a stray probe holding
the port satisfies a bare curl while the unit is stopped). The other two
handlers carried that check as inlined copies; all three now call
assert_unit_owns_port, and dragoon gets the protection it was missing.

The seam follows: the endpoints' text-grep deployer tests are deleted,
because tests/llm_deploy_gates.bats EXECUTES the same handlers end to end
(stub systemctl/podman/sudo/ss/curl) and asserts refusals, restarts, unit
links, the pending marker, and the MainPID hardening. GATE_PREFIX is
exported at each handler for the same reason the serve scripts export it.
2026-10-13 22:02:48 -07:00

137 lines
5.5 KiB
Bash
Executable File

#!/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" ]
}