Files
serverAppConfigs/tests/llm_deploy_gates.bats
T
Frank c5a816e6a8 refact(deployer): the LLM handlers gate through the shared library
deploy_llm_dragoon/zealot/arkon now call require_image / require_bytes /
require_min_mib / require_file / require_dir_files, so the deployer and the
serve scripts refuse the same missing payload with the same words — the
2026-10-01 crash-loop law, enforced by construction instead of by duplicated
shell. require_exec joins the library for the native-runtime gate (a
compiled tree that lost its +x refuses like a missing one). relink_unit
takes SYSTEMD_SYSTEM_DIR so a handler can be driven end to end in a sandbox.

The library's own refusals now carry their status: a bash function that
ends inside an if returns the CONDITION's status, so a gate whose last
statement is its refusal printed the message and returned SUCCESS — the
new seam caught it, and the gate tests pin it.

New seam tests/llm_deploy_gates.bash + tests/stubs/: deploy-remote.sh's LLM
handlers EXECUTED against stub systemctl/podman/sudo/ss/curl, asserting
exit status, refusal text, no restart, and the pending state marker.
2026-10-13 21:58:13 -07:00

134 lines
5.3 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, naming both sizes" {
truncate -s 9 "$STAGE/models/ck.hgn"
run_handler dankclaw frank-dragoon
[ "$status" -ne 0 ]
[[ "$output" == *"checkpoint: is 9 bytes, expected 10"* ]]
[ "$(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" ]
}