Dankclaw's endpoints are re-split. frank-large is the halogen-flash-server quadlet again -- the 2026-09-13 unit file was never touched by the two-day ik_llama.cpp interlude, so the runtime was still in the repo and its image is still in local storage -- retuned to the shape asked for: HALOGEN_CTX 200192 with a 400,384-position pool and two slots, exactly two 200k conversations. frank-small is new: mainline llama.cpp on the RTX 3090 serving ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF at IQ3_S, using the -mtp build (it carries blk.64.nextn.*, which is what lets --spec-type draft-mtp run on the main model's own weights instead of a second 12 GiB copy) plus the BF16 vision projector, as one 131,072-token slot with f16 KV on port 7771. The host is shared now, and that is what the halogen retune pays for: HALOGEN_MAX_TOK 24576 (arena 20.6 -> 16.4 GiB, ~4-5% prefill) and the vision tower dropped (1.97 GiB) hand ~6 GiB back to the host, and the reserve rises to 20 to keep the fit check honest. Vision moved wholesale to frank-small. Retired to scripts/.AGENTTRASH/: the ik trio (env, service, serve script), the Qwen-Sharp template only that runtime read, and four benchmark harnesses bound to retired runtimes. Because retiring a file makes any test that greps it pass by absence, the seams assert the archive exists AND the live path does not.
217 lines
8.7 KiB
Bash
217 lines
8.7 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 \
|
|
dankclaw/frank-large/quadlet/frank-large.container
|
|
[ "$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 service dir to frank-medium only" {
|
|
run python3 "$PLANNER" --target dankserver --paths \
|
|
dankserver/frank-medium/quadlet/frank-medium.container \
|
|
dankserver/frank-medium/radiance-entrypoint.sh \
|
|
dankserver/frank-medium/frank-medium-healthcheck.py
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "frank-medium" ]
|
|
}
|
|
|
|
@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" {
|
|
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" \
|
|
bash "$REMOTE_DEPLOY" dankclaw "$(git rev-parse HEAD)" --pending
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"unknown-service"* || "$output" == *"not deployed"* ]]
|
|
}
|
|
|
|
@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
|
|
}
|