#!/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/.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 (the suite lives in worktrees, where uncommitted state is # the norm), 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. 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" }