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.
137 lines
5.5 KiB
Bash
Executable File
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" ]
|
|
}
|