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.
422 lines
21 KiB
Bash
Executable File
422 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bats
|
|
# Seam: frank-archon — the Dankserver LLM endpoint on :7770.
|
|
#
|
|
# This file asserts the RADIANCE shape (StillDeadcode engine, digest-pinned
|
|
# stilldeadcode/radiance image, BOTH R9700s TP=2 on the wht6 wire, serving
|
|
# the Swift1.5 Qwen3.8-Flash-Next GPTQ-w4 .rad built by
|
|
# scripts/convert-swift15-w4dn8.sh). It replaces the FreeToken seam of
|
|
# 2026-10-05 (git tag frank-archon-freetoken-pr587-20261005) and pins what
|
|
# flags alone do not say: the served window clears the harness worst case,
|
|
# the KV pool backs seqs x max-model-len at the declared cache dtype (an
|
|
# engine HARD limit, not a ratio), the per-card VRAM pair stays inside the
|
|
# measured card ceiling, the pinned-host ledger has a ttm pages cap floor and
|
|
# the serve script ENFORCES it, the served window is what models.yml must
|
|
# quote, the host-networking contract (no --publish), the image gate on a
|
|
# name@sha256 ref, the .rad payload gate with its size floor, the
|
|
# chat-template override mount, the self-healing law (the run returns before
|
|
# the engine is up, so the serve script must wait on the container for
|
|
# Restart=always to mean anything), the deployer's false-green MainPID check,
|
|
# the vendored hfget fetch path (no token for this ungated repo), and the
|
|
# client contract (reasoning auto -> reasoning_content; models.yml maps the
|
|
# field on the harness side, not here).
|
|
# The serve script is EXECUTED against fixtures (stub podman, stub device
|
|
# nodes, stub ttm sysfs knobs, a temp payload dir) so the gates are observed
|
|
# as the operator observes them: exit status and the words on stderr. The
|
|
# knobs that make that possible (FRANK_ARCHON_DEV_KFD, _DEV_DRI,
|
|
# _TTM_SYSFS_DIR, _LOG) default to the production paths.
|
|
load helpers
|
|
|
|
UNIT="$REPO_ROOT/scripts/frank-archon.service"
|
|
SERVE="$REPO_ROOT/scripts/serve-frank-archon.sh"
|
|
ENV_FILE="$REPO_ROOT/scripts/frank-archon.env"
|
|
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|
DOWNLOAD="$REPO_ROOT/scripts/download-frank-archon-swift15.sh"
|
|
BUILD="$REPO_ROOT/scripts/convert-swift15-w4dn8.sh"
|
|
HFGET="$REPO_ROOT/scripts/hfget.sh"
|
|
TEMPLATE="$REPO_ROOT/scripts/qwen38-froggeric-v22.5.jinja"
|
|
#
|
|
# Fixtures: a full env derived from the SHIPPED frank-archon.env with only
|
|
# the host paths redirected, so a payload rename moves the env, not this file.
|
|
setup() {
|
|
STAGE="$(mktemp -d)"
|
|
MODELS="$STAGE/models"
|
|
STATE="$STAGE/state"
|
|
TTM="$STAGE/ttm"
|
|
mkdir -p "$MODELS" "$STATE" "$TTM" "$STAGE/bin" "$STAGE/dri"
|
|
touch "$STAGE/kfd" "$STAGE/template.jinja"
|
|
# A 3 MiB .rad clears the 2 MiB floor the fixture env pins.
|
|
dd if=/dev/zero of="$MODELS/payload.rad" bs=1048576 count=3 status=none
|
|
# 200 GiB of pages, the shipped cap's value, in 4 KiB pages.
|
|
printf '52428800\n' > "$TTM/pages_limit"
|
|
cat > "$STAGE/bin/podman" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
case "$1" in
|
|
image)
|
|
case " ${STUB_IMAGE:-} " in
|
|
*" $3 "*) exit 0 ;;
|
|
*) exit 1 ;;
|
|
esac ;;
|
|
rm) exit 0 ;;
|
|
run) echo "PODMAN $*" >> "${STUB_ARGV:-/dev/null}"; exit 0 ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
STUB
|
|
chmod +x "$STAGE/bin/podman"
|
|
IMAGE="$(sed -nE 's/^FRANK_ARCHON_IMAGE=(.+)$/\1/p' "$ENV_FILE")"
|
|
# The fixture env IS the shipped env with only the host paths and the
|
|
# size floor redirected, so a payload rename moves frank-archon.env and
|
|
# this file follows with it.
|
|
local re="s#^FRANK_ARCHON_MODEL_DIR=.*#FRANK_ARCHON_MODEL_DIR=$MODELS#;"
|
|
re+=" s#^FRANK_ARCHON_MODEL_FILE=.*#FRANK_ARCHON_MODEL_FILE=payload.rad#;"
|
|
re+=" s#^FRANK_ARCHON_RAD_MIN_MIB=.*#FRANK_ARCHON_RAD_MIN_MIB=2#;"
|
|
re+=" s#^FRANK_ARCHON_TEMPLATE=.*#FRANK_ARCHON_TEMPLATE=$STAGE/template.jinja#;"
|
|
re+=" s#^FRANK_ARCHON_STATE_DIR=.*#FRANK_ARCHON_STATE_DIR=$STATE#;"
|
|
re+=" s#^FRANK_ARCHON_TTM_MIN_MIB=.*#FRANK_ARCHON_TTM_MIN_MIB=150000#"
|
|
sed -E "$re" "$ENV_FILE" > "$STAGE/serve.env"
|
|
{
|
|
echo "FRANK_ARCHON_TTM_SYSFS=$TTM"
|
|
echo "FRANK_ARCHON_DEV_KFD=$STAGE/kfd"
|
|
echo "FRANK_ARCHON_DEV_DRI=$STAGE/dri"
|
|
echo "FRANK_ARCHON_LOG=$STAGE/serve.log"
|
|
echo "STUB_IMAGE=$IMAGE"
|
|
echo "STUB_ARGV=$STAGE/argv.txt"
|
|
} >> "$STAGE/serve.env"
|
|
}
|
|
|
|
teardown() {
|
|
rm -rf "$STAGE"
|
|
}
|
|
|
|
# serve_run — execute the serve script against the fixtures.
|
|
serve_run() {
|
|
run env PATH="$STAGE/bin:$PATH" FRANK_ARCHON_ENV_FILE="$STAGE/serve.env" \
|
|
bash "$SERVE"
|
|
}
|
|
|
|
@test "the serve script refuses when the image is not in storage" {
|
|
# The env file wins over the ambient environment (the serve script
|
|
# sources it with `set -a`), so the stub's storage is emptied in the file.
|
|
sed -i 's#^STUB_IMAGE=.*#STUB_IMAGE=#' "$STAGE/serve.env"
|
|
serve_run
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"not in podman storage"* ]]
|
|
[[ "$output" == *"podman pull"* ]]
|
|
}
|
|
|
|
@test "the serve script refuses a missing .rad and names its builder" {
|
|
rm -f "$MODELS/payload.rad"
|
|
serve_run
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"container: missing"*"$MODELS/payload.rad"* ]]
|
|
[[ "$output" == *"scripts/convert-swift15-w4dn8.sh"* ]]
|
|
}
|
|
|
|
@test "the serve script refuses a .rad below the size floor, naming both sizes" {
|
|
# A truncated rad-convert is a guaranteed long-timeout boot; the floor is
|
|
# the fast failure (FRANK_ARCHON_RAD_MIN_MIB).
|
|
dd if=/dev/zero of="$MODELS/payload.rad" bs=1048576 count=1 status=none
|
|
serve_run
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"1 MiB"* ]]
|
|
[[ "$output" == *"2 MiB"* ]]
|
|
}
|
|
|
|
@test "the serve script refuses an absent chat template" {
|
|
# The engine is started with --override-chat-template; a missing file is
|
|
# a container that dies at argv parse, not a fast refusal.
|
|
rm -f "$STAGE/template.jinja"
|
|
serve_run
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"template: missing"*"$STAGE/template.jinja"* ]]
|
|
}
|
|
|
|
@test "the serve script refuses when the ttm pages cap is below the pinned ledger" {
|
|
printf '1048576\n' > "$TTM/pages_limit" # 4 GiB, under the ~147 GiB ledger
|
|
serve_run
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"ttm pages cap is 4096 MiB"* ]]
|
|
[[ "$output" == *"150000"* ]]
|
|
}
|
|
|
|
@test "the serve script refuses when the GPU device nodes are absent" {
|
|
rm -f "$STAGE/kfd"
|
|
serve_run
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"GPU device nodes absent"*"$STAGE/kfd"* ]]
|
|
}
|
|
|
|
@test "the serve script runs the engine with the .rad, the template and TP=2" {
|
|
serve_run
|
|
[ "$status" -eq 0 ]
|
|
argv="$(cat "$STAGE/argv.txt")"
|
|
grep -q -- "--model /models/payload.rad" <<<"$argv"
|
|
grep -q -- "-v $MODELS:/models:ro" <<<"$argv"
|
|
grep -q -- "--override-chat-template /templates/template.jinja" <<<"$argv"
|
|
grep -q -- "--tp $(sed -nE 's/^FRANK_ARCHON_TP=([0-9]+)$/\1/p' "$ENV_FILE")" <<<"$argv"
|
|
grep -q -- "--tp-wire $(sed -nE 's/^FRANK_ARCHON_TP_WIRE=(.+)$/\1/p' "$ENV_FILE")" <<<"$argv"
|
|
grep -q -- "--kv-cache-dtype $(sed -nE 's/^FRANK_ARCHON_KV_DTYPE=(.+)$/\1/p' "$ENV_FILE")" <<<"$argv"
|
|
# The container name is the unit's ExecStop target, and the host-network
|
|
# contract publishes no port.
|
|
grep -q -- "--name $(sed -nE 's/^FRANK_ARCHON_CONTAINER=(.+)$/\1/p' "$ENV_FILE")" <<<"$argv"
|
|
grep -q -- "--network host" <<<"$argv"
|
|
! grep -q -- "--publish" <<<"$argv"
|
|
}
|
|
|
|
@test "frank-archon unit, serve script, env, fetch and build scripts and template exist" {
|
|
[ -f "$UNIT" ]
|
|
[ -x "$SERVE" ]
|
|
[ -f "$ENV_FILE" ]
|
|
[ -f "$DOWNLOAD" ]
|
|
[ -x "$BUILD" ]
|
|
[ -x "$HFGET" ]
|
|
[ -f "$TEMPLATE" ]
|
|
# The FreeToken-era Dockerfile is retired with the runtime it built:
|
|
# the radiance image is the publisher's, pulled and digest-pinned, not
|
|
# built on the host.
|
|
[ ! -f "$REPO_ROOT/scripts/docker/frank-archon-freetoken.Dockerfile" ]
|
|
}
|
|
|
|
@test "frank-archon unit is boot-persistent, self-healing and unprivileged" {
|
|
# Root unit parity with frank-dragoon/frank-zealot: enable is the [Install]
|
|
# section, the restart policy survives a crash, and the pinned host RAM
|
|
# (expert pools + PLE table + prefix host tier, ~132 GiB) needs an
|
|
# unbounded memlock (a root unit grants it via PID1; the dank session's
|
|
# own hard limit is 8 MiB).
|
|
grep -q '^WantedBy=multi-user.target$' "$UNIT"
|
|
# Restart=always, not on-failure: a clean front-end exit after an
|
|
# engine fault reads as success to systemd, so on-failure never fires.
|
|
grep -q '^Restart=always$' "$UNIT"
|
|
! grep -q '^Restart=on-failure$' "$UNIT"
|
|
# ExecStop runs `podman stop -t 120`, so the stop timeout MUST exceed
|
|
# 120 s or systemd kills the stop and records 'Failed with result
|
|
# timeout' (measured 2026-10-01). Guard the relationship, not the
|
|
# literal.
|
|
stop_t=$(awk -F= '/^TimeoutStopSec=/{print $2}' "$UNIT")
|
|
[ -n "$stop_t" ] && [ "$stop_t" -gt 120 ]
|
|
grep -q '^ExecStop=.*podman stop -t 120' "$UNIT"
|
|
# StartLimitIntervalSec belongs to [Unit]; a copy in [Service] is an
|
|
# 'Unknown key' warning systemd logs on every load.
|
|
[ "$(grep -c '^StartLimitIntervalSec=' "$UNIT")" = "1" ]
|
|
awk '/^\[Service\]/{in_svc=1} /^StartLimitIntervalSec=/{if(in_svc) exit 1}' "$UNIT"
|
|
grep -q '^RestartSec=' "$UNIT"
|
|
grep -q '^User=dank$' "$UNIT"
|
|
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
|
|
}
|
|
|
|
@test "frank-archon runs the repo's own serve script with the repo's env" {
|
|
grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-archon.sh$' "$UNIT"
|
|
grep -q 'FRANK_ARCHON_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-archon.env' "$UNIT"
|
|
}
|
|
|
|
@test "the unit stops the container child explicitly — container identity is the unit name" {
|
|
# The serve script blocks on podman wait, which cannot stop the
|
|
# container child; systemd stop goes through ExecStop or the engine
|
|
# outlives the unit and holds the GPU. The container name IS the
|
|
# service name across runtimes (Frank's 2026-10-05 call).
|
|
grep -q '^ExecStop=/usr/bin/podman stop' "$UNIT"
|
|
grep -q 'podman stop -t 120 frank-archon$' "$UNIT"
|
|
grep -q '^FRANK_ARCHON_CONTAINER=frank-archon$' "$ENV_FILE"
|
|
grep -q 'frank-archon-qwen' "$ENV_FILE"
|
|
}
|
|
|
|
@test "the serve shape is direct podman run, not a launcher or a predecessor runtime" {
|
|
grep -q 'podman run' "$SERVE"
|
|
! grep -q 'launch.sh' "$SERVE"
|
|
! grep -q 'podman-shim' "$SERVE"
|
|
! grep -q 'R9V_STATE_FILE' "$SERVE"
|
|
# Every predecessor runtime is RETIRED — its image name and its
|
|
# engine-specific flags must not survive in the radiance run line.
|
|
! grep -q 'tcclaviger' "$SERVE"
|
|
! grep -q 'tcclaviger' "$ENV_FILE"
|
|
! grep -q 'freetoken\|ft serve' "$SERVE"
|
|
! grep -q 'freetoken' "$ENV_FILE"
|
|
! grep -q 'HIP_VISIBLE_DEVICES' "$SERVE"
|
|
! grep -q -- '--tensor-parallel-size' "$SERVE"
|
|
! grep -q -- '--moe-cache-rate' "$SERVE"
|
|
! grep -q -- '--num-tokens ' "$SERVE"
|
|
}
|
|
|
|
@test "the image is the publisher's, digest-pinned by manifest" {
|
|
# stilldeadcode/radiance@sha256:<64 hex> — a name@digest ref exists in
|
|
# storage only if THAT manifest was pulled. The tag form would float.
|
|
grep -qE '^FRANK_ARCHON_IMAGE=stilldeadcode/radiance@sha256:[0-9a-f]{64}$' "$ENV_FILE"
|
|
}
|
|
|
|
@test "the radiance engine shape is in the env file" {
|
|
# Served window must clear the harness worst case: OMP compacts at 70%
|
|
# of contextWindow; for 200,000 that is 140,000, and a pre-compaction
|
|
# prompt can legally reach that height — 140,492 + 36,864 = 177,356 at
|
|
# the FreeToken-era window was the record; the same relation must hold
|
|
# here (140,000 + 36,864 = 176,864 <= 200,000).
|
|
grep -q '^FRANK_ARCHON_MAX_MODEL_LEN=200000$' "$ENV_FILE"
|
|
ml=$(awk -F= '/^FRANK_ARCHON_MAX_MODEL_LEN=/{print $2}' "$ENV_FILE")
|
|
[ "$ml" -ge 177356 ]
|
|
# Concurrency: 2 slots x the served window is the capacity contract, and
|
|
# the fused TP all-reduce is declared over seqs x (1 + spec) = 6 rows,
|
|
# under the 25-row bound at which the wht6 wire stays bit-exact. The
|
|
# "seqs 2 cannot boot" law belongs to the retired tcclaviger vLLM fork
|
|
# (torch.compile query_start_loc constraint, 2026-10-02); radiance booted
|
|
# seqs 2 in the fp8-native era and the 2026-10-06 sweep had seqs 2 and 3
|
|
# tied inside the +-3% noise floor.
|
|
grep -q '^FRANK_ARCHON_MAX_SEQS=2$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_TP=2$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_TP_WIRE=wht6$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_SPEC_TOKENS=2$' "$ENV_FILE"
|
|
# expert_tiered placement: vram_weights + the pinned host pool serve the
|
|
# per-rank resident set TOGETHER, so cutting the VRAM slab to pay for the
|
|
# KV pool moves the pool with it. KV is UNQUANTIZED bf16 (the engine takes
|
|
# only bf16|fp8 — no per-tensor K/V split), the PLE n-gram table is pinned
|
|
# in host RAM per Frank's law.
|
|
grep -q '^FRANK_ARCHON_PLACEMENT=expert_tiered$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_HOST_POOL_MIB=44928$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_VRAM_WEIGHTS_MIB=[1-9]' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_VRAM_KV_MIB=[1-9]' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_KV_DTYPE=bf16$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_NGRAM_PLACEMENT=ram$' "$ENV_FILE"
|
|
# Prefix cache tiers (publisher pattern; disk tier sized for 2 slots).
|
|
grep -q '^FRANK_ARCHON_PREFIX_HOST_MIB=8192$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_PREFIX_DISK_MIB=65536$' "$ENV_FILE"
|
|
# The pinned ledger (2 x pool + 47.7 GiB PLE + prefix host) must be
|
|
# under the ttm pages cap, and the env states the floor the serve
|
|
# script enforces (>= 140,000 MiB — the current cap ~123 GiB fails it,
|
|
# which is the point).
|
|
grep -q '^FRANK_ARCHON_TTM_MIN_MIB=' "$ENV_FILE"
|
|
ttmm=$(awk -F= '/^FRANK_ARCHON_TTM_MIN_MIB=/{print $2}' "$ENV_FILE")
|
|
pool=$(awk -F= '/^FRANK_ARCHON_HOST_POOL_MIB=/{print $2}' "$ENV_FILE")
|
|
ph=$(awk -F= '/^FRANK_ARCHON_PREFIX_HOST_MIB=/{print $2}' "$ENV_FILE")
|
|
# 2 pools + PLE 47.7 GiB (48,824 MiB) + prefix host, rounded up.
|
|
ledger=$(( 2 * pool + 48824 + ph + 2048 ))
|
|
[ "$ttmm" -ge "$ledger" ]
|
|
# The .rad size floor is positive (a truncated convert must fail fast).
|
|
grep -q '^FRANK_ARCHON_RAD_MIN_MIB=' "$ENV_FILE"
|
|
[ "$(awk -F= '/^FRANK_ARCHON_RAD_MIN_MIB=/{print $2}' "$ENV_FILE")" -gt 0 ]
|
|
}
|
|
|
|
@test "the KV pool backs seqs x max-model-len at the declared cache dtype" {
|
|
# The engine's own hard limit: a request longer than the pool's token
|
|
# count is "accepted by the API and then refused as too large to ever fit
|
|
# -- this is a hard limit, not a backing ratio". Rates are measured, not
|
|
# derived: per rank, kv_attn (13 layers) pages 26.00 KiB per 4 tokens at
|
|
# fp8 and 52.00 KiB at bf16, while kv_qsa_bkey (13 layers) pages 3.25 KiB
|
|
# at BOTH — the QSA block key is an index, not attention state, and does
|
|
# not follow --kv-cache-dtype — so the rate is 7,488 B/token at fp8 and
|
|
# 14,144 at bf16 (1.89x, not 2x). The dtype-independent terms also live in
|
|
# the pool: ~110 MiB a sequence of recurrent state (kv_gdn_state 54 MiB x 2
|
|
# copies + conv/tail/ple) and 448 MiB of prefix checkpoints (8 x 55.99).
|
|
# Verified against BOTH live boots: fp8 3543-330-448 = 2765 MiB -> 387,240
|
|
# tokens (ledger 386,904); bf16 6912-220-448 = 6244 MiB -> 462,946
|
|
# (ledger 462,752). Both within 0.1%.
|
|
kv=$(awk -F= '/^FRANK_ARCHON_VRAM_KV_MIB=/{print $2}' "$ENV_FILE")
|
|
seqs=$(awk -F= '/^FRANK_ARCHON_MAX_SEQS=/{print $2}' "$ENV_FILE")
|
|
maxlen=$(awk -F= '/^FRANK_ARCHON_MAX_MODEL_LEN=/{print $2}' "$ENV_FILE")
|
|
case "$(awk -F= '/^FRANK_ARCHON_KV_DTYPE=/{print $2}' "$ENV_FILE")" in
|
|
fp8) bpt=7488 ;;
|
|
bf16) bpt=14144 ;;
|
|
*) fail "unknown FRANK_ARCHON_KV_DTYPE" ;;
|
|
esac
|
|
tokens=$(( (kv - 110 * seqs - 448) * 1048576 / bpt ))
|
|
[ "$tokens" -ge $(( seqs * maxlen )) ]
|
|
}
|
|
|
|
@test "the per-card VRAM pair stays inside the measured card ceiling" {
|
|
# Measured on Dankserver 2026-10-11: card 31,856 MiB, a boot consumes
|
|
# 1,710 MiB OUTSIDE both budgets (driver context, the 990 MiB activation
|
|
# arena, hardware queues, headroom, step staging) and the fp8 pair settled
|
|
# at 1,379 MiB free. Claiming more than 29,120 MiB in total is an
|
|
# allocation failure at graph capture, not a warning — so the KV pool and
|
|
# the weights slab are one budget, and this seam is the guard.
|
|
w=$(awk -F= '/^FRANK_ARCHON_VRAM_WEIGHTS_MIB=/{print $2}' "$ENV_FILE")
|
|
kv=$(awk -F= '/^FRANK_ARCHON_VRAM_KV_MIB=/{print $2}' "$ENV_FILE")
|
|
[ $(( w + kv )) -le 29120 ]
|
|
}
|
|
|
|
@test "the serve script wires the engine flags from the env" {
|
|
grep -q -- '--model "/models/$FRANK_ARCHON_MODEL_FILE"' "$SERVE"
|
|
grep -q -- '--served-model-name "$FRANK_ARCHON_SERVED_NAME"' "$SERVE"
|
|
grep -q -- '--host :: --port "$FRANK_ARCHON_PORT"' "$SERVE"
|
|
grep -q -- '--override-chat-template /templates/template.jinja' "$SERVE"
|
|
grep -q -- '--tp "$FRANK_ARCHON_TP" --tp-wire "$FRANK_ARCHON_TP_WIRE"' "$SERVE"
|
|
grep -q -- '--max-num-seqs "$FRANK_ARCHON_MAX_SEQS"' "$SERVE"
|
|
grep -q -- '--max-model-len "$FRANK_ARCHON_MAX_MODEL_LEN"' "$SERVE"
|
|
grep -q -- '--placement "$FRANK_ARCHON_PLACEMENT"' "$SERVE"
|
|
grep -q -- '--host-pool-mib "$FRANK_ARCHON_HOST_POOL_MIB"' "$SERVE"
|
|
grep -q -- '--vram-weights-mib "$FRANK_ARCHON_VRAM_WEIGHTS_MIB"' "$SERVE"
|
|
grep -q -- '--vram-kv-mib "$FRANK_ARCHON_VRAM_KV_MIB"' "$SERVE"
|
|
grep -q -- '--kv-cache-dtype "$FRANK_ARCHON_KV_DTYPE"' "$SERVE"
|
|
grep -q -- '--prefix-cache-host-mib "$FRANK_ARCHON_PREFIX_HOST_MIB"' "$SERVE"
|
|
grep -q -- '--prefix-cache-disk-mib "$FRANK_ARCHON_PREFIX_DISK_MIB"' "$SERVE"
|
|
grep -q -- '--ngram-placement "$FRANK_ARCHON_NGRAM_PLACEMENT"' "$SERVE"
|
|
grep -q -- '--num-speculative-tokens "$FRANK_ARCHON_SPEC_TOKENS"' "$SERVE"
|
|
# The boot-ladder escape hatch is opt-in: unset renders NO extra argv.
|
|
grep -q -- '${FRANK_ARCHON_EXTRA_ARGS:+$FRANK_ARCHON_EXTRA_ARGS}' "$SERVE"
|
|
! grep -q '^FRANK_ARCHON_EXTRA_ARGS=.' "$ENV_FILE"
|
|
}
|
|
|
|
@test "the container contract is the publisher's (host network, io_uring, pin, cards)" {
|
|
# docs/DOCKER.md: EVERY one of these is needed — host networking (the
|
|
# server binds the host's addresses; no --publish), --init (the engine
|
|
# releases the cards on SIGTERM), seccomp=unconfined (the default
|
|
# profile refuses io_uring, which the n-gram reads use, and the NUMA
|
|
# binding calls), memlock unbounded (the pinned ledger), kfd + dri.
|
|
grep -q -- '--network host' "$SERVE"
|
|
! grep -q -- '--publish' "$SERVE"
|
|
! grep -q -- '-p 7770' "$SERVE"
|
|
grep -q '^FRANK_ARCHON_PORT=7770$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_SERVED_NAME=frank-archon$' "$ENV_FILE"
|
|
grep -q -- '--init' "$SERVE"
|
|
grep -q -- '--security-opt seccomp=unconfined' "$SERVE"
|
|
grep -q -- '--ulimit memlock=-1' "$SERVE"
|
|
grep -q -- '--device /dev/kfd' "$SERVE"
|
|
grep -q -- '--device /dev/dri' "$SERVE"
|
|
grep -q -- '-v "$FRANK_ARCHON_MODEL_DIR":/models:ro' "$SERVE"
|
|
grep -q -- '-v "$FRANK_ARCHON_STATE_DIR":/data' "$SERVE"
|
|
grep -q -- '-v "$FRANK_ARCHON_TEMPLATE":/templates/template.jinja:ro' "$SERVE"
|
|
}
|
|
|
|
@test "parsers are AUTO — the engine infers them natively, the harness maps the field" {
|
|
# radiance --reasoning-format defaults to auto and emits thinking in
|
|
# `reasoning_content`; tool calls ride the chat format. NO parser
|
|
# override flags exist in this engine. models.yml's
|
|
# reasoningContentField is the harness-side contract and lives in the
|
|
# operator's home, not this repo.
|
|
! grep -q -- '--reasoning-parser' "$SERVE"
|
|
! grep -q -- '--tool-call-parser' "$SERVE"
|
|
! grep -q -- '--enable-auto-tool-choice' "$SERVE"
|
|
! grep -q 'FRANK_ARCHON_REASONING_PARSER' "$ENV_FILE"
|
|
! grep -q 'FRANK_ARCHON_TOOL_PARSER' "$ENV_FILE"
|
|
}
|
|
|
|
|
|
@test "the serve script clears stale containers and self-heals via podman wait" {
|
|
# A crashed unit can leave the container name registered forever after;
|
|
# the legacy identities hold the same GPUs and die here too. Then block
|
|
# on the container so a crash is a nonzero ExecStart.
|
|
grep -q 'FRANK_ARCHON_LEGACY_CONTAINERS' "$ENV_FILE"
|
|
grep -q 'frank-archon-qwen' "$ENV_FILE"
|
|
grep -q 'frank-archon-r9v' "$ENV_FILE"
|
|
grep -q 'qwen-fp8' "$ENV_FILE"
|
|
grep -q 'exec podman wait "$FRANK_ARCHON_CONTAINER"' "$SERVE"
|
|
}
|
|
|
|
@test "the model id matches the payload's build and fetch scripts" {
|
|
# The payload dir/file are the convert script's output layout; the fetch
|
|
# script must name the repo the convert stages from. (FP8-native era,
|
|
# the rollback payload, keeps its own trio: build-frank-archon-rad.sh +
|
|
# download-frank-archon.sh.)
|
|
grep -q '^FRANK_ARCHON_MODEL_DIR=/home/dank/models/rad$' "$ENV_FILE"
|
|
grep -q '^FRANK_ARCHON_MODEL_FILE=qwen4exp-swift15-w4dn8-i8-hc8m.rad$' "$ENV_FILE"
|
|
grep -q 'qwen4exp-swift15-w4dn8-i8-hc8m.rad' "$BUILD"
|
|
grep -q 'ukisai/Swift1.5-Qwen3.8-Flash-Next' "$DOWNLOAD"
|
|
grep -q 'hfget.sh' "$DOWNLOAD"
|
|
# The repo is UNGATED — no token may be required on the fetch path.
|
|
! grep -q 'HF_TOKEN' "$DOWNLOAD"
|
|
}
|
|
|
|
@test "no bare HIP-reserved env var is defined" {
|
|
# HIP reads bare TEMP for its JIT cache path (2026-08-13: TEMP=1.0
|
|
# segfaulted every load), and HOST/GPU_MAX_HEAP_SIZE are the same
|
|
# class. Every key here is FRANK_ARCHON_-prefixed.
|
|
! grep -qE '^(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$ENV_FILE"
|
|
! grep -qE '^(export )?(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$SERVE"
|
|
}
|