Files
serverAppConfigs/tests/deployment_scoping.bats
T
Frank b6ce3afb1f frank-large: CIRU v4.4.1 Orca cutover — root unit, one stream, full context
Operator call 2026-09-19: switch the iGPU endpoint from
halogen-flash-server to jcbtc/Qwen3.8-Flash-CIRU-STRIX-Orca on the CIRU
v4.4.1 runtime with the QSA window hotfix (host build against the private
ROCm 10 SDK), one stream of the full 262144 trained window.  The service
leaves the quadlet shape for a root unit (the same shape as frank-small and
frank-medium: a private-SDK host binary has no image to install and no
generated unit to dodge); the halogen quadlet is archived, the deploy
handler retires it on the host, gates on 7770, and its stray-run sweep
matches the model path so it cannot take frank-small down.
2026-09-19 02:59:01 -07:00

230 lines
9.5 KiB
Bash

#!/usr/bin/env bats
# Seam: service-scoped deployment planning.
# A repository commit can contain changes for several workloads while only a
# subset is safe to restart. The planner must keep inference isolated from
# unrelated service changes and treat docs-only changes as a no-op.
load helpers
PLANNER="$REPO_ROOT/scripts/deploy-plan.py"
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
@test "deployment planner maps SearXNG changes to SearXNG only" {
run python3 "$PLANNER" --target dankclaw --paths \
shared/searxng/settings.yml serverAppConfigs-docs/Dankclaw/SearXNG.md
[ "$status" -eq 0 ]
[ "$output" = "searxng" ]
}
@test "deployment planner maps large-model changes to the large model only" {
run python3 "$PLANNER" --target dankclaw --paths \
scripts/frank-large.env
[ "$status" -eq 0 ]
[ "$output" = "frank-large" ]
}
@test "the retired NPU endpoint is absent from the deployment inventory" {
! grep -q '"frank-npu"' "$PLANNER"
! grep -q 'frank_npu\|frank-npu' "$REMOTE_DEPLOY"
}
@test "the retired ik chat template plans nothing, and its archive is still here" {
# scripts/chat-template.jinja existed only for the ik_llama.cpp runtime
# (retired 2026-09-16 in favour of halogen-flash-server again). The
# llama.cpp frank-small runtime serves the template the GGUF itself
# carries, which is the one with reasoning_effort, preserve_thinking and
# the tool-call grammar. Editing the archive must deploy nothing -- and the
# archive must still be present, so this cannot pass by the file having
# vanished.
[ -f "$REPO_ROOT/scripts/.AGENTTRASH/chat-template.jinja.ik-llama-qwen-sharp" ]
run python3 "$PLANNER" --target dankclaw --paths scripts/chat-template.jinja
[ "$status" -eq 0 ]
[ -z "$output" ]
run python3 "$PLANNER" --target dankserver --paths scripts/chat-template.jinja
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "deployment planner maps the frank-medium scripts trio to frank-medium only" {
run python3 "$PLANNER" --target dankserver --paths \
scripts/frank-medium.env scripts/frank-medium.service scripts/serve-frank-medium.sh
[ "$status" -eq 0 ]
[ "$output" = "frank-medium" ]
# The path is not the assertion: with the vllm-radiance service dir
# retired into the trash, a planner rule still naming dankserver/frank-medium
# would plan nothing and look identical to a correct one. Assert the
# invariant (the dir is gone) as well as the mapping above.
[ ! -d "$REPO_ROOT/dankserver/frank-medium" ]
run python3 "$PLANNER" --target dankserver --paths dankserver/frank-medium/quadlet/frank-medium-a.container
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "the small endpoint is Dankclaw's again, and Dankserver's copy stays retired" {
run python3 "$PLANNER" --target dankclaw --paths \
scripts/frank-small.env scripts/frank-small.service scripts/serve-frank-small.sh
[ "$status" -eq 0 ]
[ "$output" = "frank-small" ]
# frank-small runs on Dankclaw on the RTX 3090; the Dankserver endpoint of
# that name was retired with the FreeToken runtime on 2026-09-15 and must
# not come back through a planner mapping.
run python3 "$PLANNER" --target dankserver --paths \
scripts/frank-small.env scripts/frank-small.service scripts/serve-frank-small.sh
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "documentation-only changes plan no runtime deployment" {
run python3 "$PLANNER" --target dankclaw --paths \
serverAppConfigs-docs/Architecture/Deployment\ Flow.md serverAppConfigs-docs/Dankclaw/SearXNG.md
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "deployment planner maps shared config to one service" {
run python3 "$PLANNER" --target dankclaw --paths \
shared/prometheus/prometheus.yml
[ "$status" -eq 0 ]
[ "$output" = "prometheus" ]
}
@test "deployment planner maps shared observability configs independently" {
run python3 "$PLANNER" --target dankclaw --paths shared/loki/loki-config.yaml
[ "$status" -eq 0 ]
[ "$output" = "loki" ]
run python3 "$PLANNER" --target dankclaw --paths shared/alloy/config.alloy
[ "$status" -eq 0 ]
[ "$output" = "alloy" ]
}
@test "deployment planner maps service-owned non-compose files" {
run python3 "$PLANNER" --target dankclaw --paths dankclaw/hermes/config.yaml.example
[ "$status" -eq 0 ]
[ "$output" = "hermes" ]
}
@test "homepage is owned by Dankclaw's consolidated proxy" {
run python3 "$PLANNER" --target dankserver --paths shared/homepage/index.html
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "compose planner narrows a compose edit to the changed service" {
local base head
base=$(git -C "$REPO_ROOT" rev-parse HEAD)
head=$(git -C "$REPO_ROOT" rev-parse HEAD)
run python3 "$PLANNER" --target dankclaw --repo "$REPO_ROOT" \
--base "$base" --head "$head" --paths dankclaw/compose.yaml
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "network-coupled SearXNG config changes include the required peers" {
run python3 "$PLANNER" --target dankclaw --paths \
dankclaw/searxng/quadlet/gluetun.container
[ "$status" -eq 0 ]
[ "$output" = $'gluetun\nsearxng\nmarreta' ]
}
@test "retiring the standalone Gitea proxy deploys the canonical Caddy" {
run python3 "$PLANNER" --target dankclaw --paths \
dankclaw/gitea/quadlet/gitea-caddy.container
[ "$status" -eq 0 ]
[ "$output" = "caddy" ]
}
@test "Gitea network changes exclude the retired standalone proxy" {
run python3 "$PLANNER" --target dankclaw --paths \
dankclaw/gitea/quadlet/gitea.network
[ "$status" -eq 0 ]
[ "$output" = $'gitea-postgres\ngitea-server' ]
}
@test "frankbook-pro gluetun changes carry the whole netns family; arr changes stay scoped" {
run python3 "$PLANNER" --target frankbook-pro --paths \
frankbook-pro/gluetun/quadlet/gluetun.container
[ "$status" -eq 0 ]
[ "$output" = $'gluetun\nprowlarr\nqbittorrent\nradarr\nsonarr\nlidarr\nreadarr' ]
run python3 "$PLANNER" --target frankbook-pro --paths \
frankbook-pro/sonarr/quadlet/sonarr.container
[ "$status" -eq 0 ]
[ "$output" = "sonarr" ]
}
@test "remote Caddy deployment no longer blocks on the retired Gitea proxy" {
! grep -q 'refusing to replace it with caddy' "$REMOTE_DEPLOY"
! grep -q 'podman container exists gitea-caddy &&' "$REMOTE_DEPLOY"
}
@test "remote Caddy deployment retires the installed Gitea proxy" {
grep -q 'retire_legacy_gitea_caddy' "$REMOTE_DEPLOY"
grep -q 'systemctl --user stop gitea-caddy.service' "$REMOTE_DEPLOY"
grep -q 'systemctl --user disable gitea-caddy.service' "$REMOTE_DEPLOY"
grep -q 'gitea-caddy.container' "$REMOTE_DEPLOY"
grep -q 'HOME/.AGENTTRASH' "$REMOTE_DEPLOY"
grep -q 'stage_legacy_gitea_caddy_state' "$REMOTE_DEPLOY"
}
@test "remote deployment inventory no longer accepts gitea-caddy as a service" {
! grep -qE 'dankclaw:gitea-caddy|gitea-caddy\) deploy' "$REPO_ROOT/scripts/deploy.sh" "$REPO_ROOT/scripts/deploy-remote.sh"
! grep -q '"gitea-caddy"' "$REPO_ROOT/scripts/deploy-plan.py"
}
@test "remote deployer carries per-service deployment state" {
[ -x "$REMOTE_DEPLOY" ]
grep -q 'deploy-state' "$REMOTE_DEPLOY"
grep -q 'STATE\[' "$REMOTE_DEPLOY"
grep -q 'mark_status.*pending\|status=pending' "$REMOTE_DEPLOY"
grep -q 'mark_status.*deployed\|status=deployed' "$REMOTE_DEPLOY"
}
@test "remote deployer routes inference restarts through selected handlers" {
# The runner may restart inference only from the service-dispatch path. It
# must not contain a host-wide restart loop.
grep -q 'deploy_llm_large' "$REMOTE_DEPLOY"
grep -q 'deploy_llm_medium' "$REMOTE_DEPLOY"
grep -q 'deploy_service' "$REMOTE_DEPLOY"
! grep -qE 'for .*frank-(small|large|npu)-?' "$REMOTE_DEPLOY"
}
@test "remote runner rejects services belonging to the other host" {
run env DEPLOY_STATE_DIR="$(mktemp -d)" REMOTE_DIR="$REPO_ROOT" \
bash "$REMOTE_DEPLOY" dankserver "$(git rev-parse HEAD)" frank-default-large
[ "$status" -eq 2 ]
}
@test "remote runner recognizes a pending service at the same revision" {
# Contract superseded 2026-09-17: recognition used to mean "retry and
# hard-fail at the dispatcher" (exit 2), which let a retired service's
# stale pending entry (frank-default-sightsound) block every deploy
# before the e2e gate. Recognition now means: name the entry, prune it
# to the trash, and keep the run going.
local state_dir="$(mktemp -d)"
printf 'revision=%s\nstatus=pending\n' "$(git rev-parse HEAD)" > "$state_dir/unknown-service"
run env DEPLOY_STATE_DIR="$state_dir" REMOTE_DIR="$REPO_ROOT" HOME="$(mktemp -d)" \
bash "$REMOTE_DEPLOY" dankclaw "$(git rev-parse HEAD)" --pending
[ "$status" -eq 0 ]
[[ "$output" == *"unknown-service"* ]]
[ ! -e "$state_dir/unknown-service" ]
}
@test "top-level deployer delegates to the service-scoped remote runner" {
grep -q 'deploy-remote.sh' "$REPO_ROOT/scripts/deploy.sh"
grep -qE 'SERVICE|service' "$REPO_ROOT/scripts/deploy.sh"
grep -q 'OLD_REVISION' "$REPO_ROOT/scripts/deploy.sh"
grep -q 'OLD_REVISION\|old checkout revision' "$REPO_ROOT/scripts/deploy.sh"
}
@test "legacy service deploy wrappers delegate to the scoped deployer" {
for script in deploy-searxng-quadlet.sh deploy-pihole-quadlet.sh deploy-open-webui-quadlet.sh; do
grep -q 'deploy.sh' "$REPO_ROOT/scripts/$script" || {
echo "$script does not delegate to deploy.sh"
return 1
}
done
}