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.
130 lines
5.9 KiB
Bash
Executable File
130 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# deploy_sandbox.bash — the deploy-handler seam's fixture builder.
|
|
#
|
|
# The seam (tests/llm_deploy_gates.bats) EXECUTES scripts/deploy-remote.sh's
|
|
# LLM handlers with every host touchpoint stubbed (tests/stubs/) and every
|
|
# artifact pointed at a fixture, so the assertions are the handler's exit
|
|
# status, its refusal text, and what it ASKED systemd/podman to do.
|
|
#
|
|
# Usage:
|
|
# source tests/deploy_sandbox.bash
|
|
# sandbox_setup
|
|
# run_handler dankserver frank-archon
|
|
# sandbox_teardown
|
|
#
|
|
# After sandbox_setup the caller owns:
|
|
# $STAGE the temp root (payload/, bin/, systemd/, state/)
|
|
# $REMOTE_DIR the sandbox checkout (a git clone; the handler
|
|
# cds into it, so relative paths resolve there)
|
|
# $STAGE/systemctl.log every systemctl invocation
|
|
# $STAGE/podman.log every podman invocation
|
|
# $STAGE/systemd/<unit>.service the unit link a green deploy leaves behind
|
|
# Env the caller sets per case: STUB_IMAGE ("" = empty storage),
|
|
# STUB_MAIN_PID (0 = a stray process owns the port), the LLM_* overrides.
|
|
|
|
sandbox_setup() {
|
|
STAGE="$(mktemp -d)"
|
|
export HOME="$STAGE/home"
|
|
mkdir -p "$HOME" "$STAGE/state" "$STAGE/systemd" "$STAGE/bin"
|
|
|
|
# Host touchpoints: the seam's stubs, on PATH under their real names.
|
|
local stub repo_root
|
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
for stub in "$repo_root"/tests/stubs/*; do
|
|
ln -s "$stub" "$STAGE/bin/$(basename "$stub")"
|
|
done
|
|
export PATH="$STAGE/bin:$PATH"
|
|
export STUB_SYSTEMCTL_LOG="$STAGE/systemctl.log"
|
|
export STUB_PODMAN_LOG="$STAGE/podman.log"
|
|
# A deploy marks state; a sandbox with no git identity would make the
|
|
# identity, not the handler, the thing under test.
|
|
export GIT_AUTHOR_NAME=seam GIT_AUTHOR_EMAIL=seam@localhost
|
|
export GIT_COMMITTER_NAME=seam GIT_COMMITTER_EMAIL=seam@localhost
|
|
export SYSTEMD_SYSTEM_DIR="$STAGE/systemd"
|
|
export DEPLOY_STATE_DIR="$STAGE/state"
|
|
# run_privileged pipes the password from FRANK_SUDO_PW into `sudo -S --`;
|
|
# the stub drops it and execs, so no privilege gate hides a handler bug.
|
|
export FRANK_SUDO_PW=test
|
|
|
|
# Fixture payload: 3 MiB where a size floor applies, 10 bytes elsewhere.
|
|
mkdir -p "$STAGE/models/tokenizer"
|
|
truncate -s 3145728 "$STAGE/models/payload.rad"
|
|
truncate -s 10 "$STAGE/models/ck.hgn"
|
|
truncate -s 10 "$STAGE/models/ngram.hgn"
|
|
truncate -s 10 "$STAGE/models/vision.hgn"
|
|
printf 'x\n' > "$STAGE/models/llama-server"
|
|
chmod +x "$STAGE/models/llama-server"
|
|
truncate -s 10 "$STAGE/models/model.gguf"
|
|
truncate -s 10 "$STAGE/models/mmproj.gguf"
|
|
local tf
|
|
for tf in tokenizer.json chat_template.jinja tokenizer_config.json \
|
|
merges.txt vocab.json generation_config.json; do
|
|
printf 'x\n' > "$STAGE/models/tokenizer/$tf"
|
|
done
|
|
|
|
# $HOME/serverAppConfigs is the checkout the handler works in: a git
|
|
# WORKTREE of the caller's checkout, so the handler runs the EXACT files
|
|
# under test, and it is a real git repo: the deployer refuses a revision
|
|
# it cannot resolve. The handler cds into REMOTE_DIR, so every relative
|
|
# path it uses resolves there.
|
|
#
|
|
# HEAD is committed state, so the seam asserts on what is in history —
|
|
# the same rule as the whole suite, and the reason an uncommitted
|
|
# deployer change is invisible here until it is committed.
|
|
git -C "$repo_root" worktree add -q --detach "$HOME/serverAppConfigs" HEAD
|
|
export REMOTE_DIR="$HOME/serverAppConfigs"
|
|
|
|
# Every endpoint variable is env-overridable, so the handlers see the
|
|
# fixtures and never the real /home/dank paths.
|
|
export STUB_IMAGE=ghcr.io/peonist-ai/halogen-flash-server@sha256:deadbeef
|
|
export LLM_DRAGOON_IMAGE="$STUB_IMAGE"
|
|
export LLM_DRAGOON_CKPT="$STAGE/models/ck.hgn" LLM_DRAGOON_CKPT_BYTES=10
|
|
export LLM_DRAGOON_NGRAM="$STAGE/models/ngram.hgn" LLM_DRAGOON_NGRAM_BYTES=10
|
|
export LLM_DRAGOON_VISION="$STAGE/models/vision.hgn" LLM_DRAGOON_VISION_BYTES=10
|
|
export LLM_DRAGOON_TOK_DIR="$STAGE/models/tokenizer"
|
|
export LLM_ZEALOT_RUNTIME="$STAGE/models/llama-server"
|
|
export LLM_ZEALOT_MODEL_FILE="$STAGE/models/model.gguf"
|
|
export LLM_ZEALOT_MMPROJ="$STAGE/models/mmproj.gguf"
|
|
export LLM_ARCHON_ENV_FILE="$REMOTE_DIR/scripts/frank-archon.env"
|
|
# The archon handler reads the served image out of the unit's own env
|
|
# file, so the stub's storage must hold exactly that ref.
|
|
ARCHON_IMAGE="$(sed -nE 's/^FRANK_ARCHON_IMAGE=(.+)$/\1/p' "$LLM_ARCHON_ENV_FILE")"
|
|
}
|
|
|
|
sandbox_teardown() {
|
|
[[ -z "${STAGE:-}" ]] && return 0
|
|
# A worktree leaves a registration in the caller's .git/worktrees, so
|
|
# remove it through git, not just the directory.
|
|
[[ -d "$STAGE/home/serverAppConfigs" ]] &&
|
|
git -C "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" \
|
|
worktree remove --force "$STAGE/home/serverAppConfigs"
|
|
rm -rf "$STAGE"
|
|
}
|
|
|
|
run_handler() {
|
|
local target="$1" service="$2"
|
|
run bash "$REMOTE_DIR/scripts/deploy-remote.sh" \
|
|
"$target" "$(git -C "$REMOTE_DIR" rev-parse HEAD)" "$service"
|
|
}
|
|
|
|
# archon_payload_env — the unit's env file with the payload coordinates
|
|
# redirected into the sandbox (and the size floor optionally lowered, which
|
|
# is how a test builds the truncated-.rad case).
|
|
archon_payload_env() {
|
|
local re="s#^FRANK_ARCHON_MODEL_DIR=.*#FRANK_ARCHON_MODEL_DIR=$STAGE/models#;"
|
|
re+=" s#^FRANK_ARCHON_MODEL_FILE=.*#FRANK_ARCHON_MODEL_FILE=payload.rad#"
|
|
if [[ -n "${1:-}" ]]; then
|
|
re+="; s#^FRANK_ARCHON_RAD_MIN_MIB=.*#FRANK_ARCHON_RAD_MIN_MIB=$1#"
|
|
fi
|
|
sed -E "$re" "$LLM_ARCHON_ENV_FILE" > "$STAGE/archon.env"
|
|
export LLM_ARCHON_ENV_FILE="$STAGE/archon.env"
|
|
}
|
|
|
|
restarts_requested() {
|
|
# grep -c prints 0 and exits 1 on no match; the count, not the status,
|
|
# is what the assertions read.
|
|
local n=0
|
|
[[ -f "$STAGE/systemctl.log" ]] && n="$(grep -c 'restart ' "$STAGE/systemctl.log")"
|
|
printf '%s\n' "$n"
|
|
}
|