Files
praxis/scripts/proxmox/test/lxc-config.bats
T
Praxis CI 1b3617da3b docs(P02): review + audit — APPROVE_WITH_NOTES, HEALTHY, 2 P0 fixed
REVIEW.md: 2 P0 fixed (stale test defaults, sandbox isolation),
  8 P1+ flagged for post-hoc review. Verdict: APPROVE_WITH_NOTES.
AUDIT.md: 0 critical, 5 warnings. Reconstruction PASS, file
  discipline PASS, branch hygiene PASS, commit discipline PASS.
  Verdict: HEALTHY. Doc-drift fixed (REQ statuses → complete).

P0 fixes in working tree:
  1. lxc-config.bats: aligned stale defaults with production code
  2. lxc-deploy.bats: fixed sandbox isolation (HOME redirect)

---ci---
project: praxis
phase: 2
milestone: v0.2
status: review
---/ci---
2026-08-03 18:54:38 +00:00

220 lines
9.0 KiB
Bash

#!/usr/bin/env bats
# Bats tests for scripts/proxmox/lxc-config.sh (praxis CT config).
#
# Run: bats scripts/proxmox/test/lxc-config.bats
#
# lxc-config.sh sets memory + onboot via REST PUT /config (API-token-
# accepted), then sets hookscript + lxc.environment via SSH to the PVE
# host (root-only fields rejected by REST). The SSH heredoc sed -i's
# prior lines then cat >> appends the new ones — idempotent on re-run.
# These tests exercise the real lxc-config.sh with a mocked api.sh
# (pve_curl records the PUT) + a mocked ssh that runs the heredoc body
# locally so sed/cat operate on a sandbox conf file.
#
# Praxis v0.2 (vs coreci) key differences asserted here:
# - hookscript snippet name is "praxis-firstboot.sh" (NOT "coreci-firstboot.sh")
# - lxc.environment includes PRAXIS_PORT=8789 (NOT CORECI_HTTP_PORT=18080)
# - lxc.environment includes voice-service vars (DEEPGRAM, CARTESIA, OLLAMA)
# - memory default 4096 (NOT 2048)
# - PRAXIS_VERSION, PRAXIS_DB_PATH, PRAXIS_TTS, PRAXIS_SCENARIO present
setup() {
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
CONFIG="${SCRIPT_DIR}/lxc-config.sh"
STUB_DIR="$(mktemp -d)"
export STUB_DIR
LOG="${STUB_DIR}/calls.log"
export CALL_LOG="$LOG"
: > "$LOG" 2>/dev/null || true
CONF_FILE="${STUB_DIR}/pve-lxc-200.conf"
export CONF_FILE
# Sandbox: <ROOT>/lxc-config.sh (SCRIPT_DIR) + <ROOT>/api.sh (sourced)
# + <ROOT>/ssh (mocked) on PATH ahead of /usr/bin.
ROOT="${STUB_DIR}/root"
mkdir -p "$ROOT"
cp "$CONFIG" "${ROOT}/lxc-config.sh"
# Mocked api.sh — pve_env validates required env vars (mirrors the
# real helper so the env-validation path is exercised); pve_curl
# records method + path + body.
cat > "${ROOT}/api.sh" <<'ASTUB'
pve_env() {
missing=0
for var in "$@"; do
eval "val=\"\${${var}:-}\""
if [ -z "$val" ]; then
echo "pve_env: $var is required but not set" >&2
missing=1
fi
done
return "$missing"
}
pve_curl() {
method="$1"; path="$2"; shift 2
printf '%s\n' "${method} ${path} $*" >> "$CALL_LOG"
printf '%s\n' "${STUB_PVE_CURL_OUT:-null}"
}
pve_tls_insecure() { :; }
pve_auth_header() { :; }
ASTUB
# Mocked ssh — writes everything after the remote host arg into a
# script and runs it with sh, so the sed -i + cat >> execute locally
# against $CONF_FILE (the heredoc references $conf set from
# $conf_file which the script sets to /etc/pve/lxc/<vmid>.conf — we
# override that path by rewriting the conf= line to point at our
# sandbox file). Records the raw heredoc body to $CALL_LOG.
cat > "${ROOT}/ssh" <<'SSTUB'
#!/bin/sh
# ssh [opts] host <remote-script>
# Drop the opts (-o ...) and the host (root@...); the rest is the script.
shift # drop -o StrictHostKeyChecking=no
host="$1"; shift
remote="$*"
printf '%s\n' "$remote" >> "$CALL_LOG"
# Run the remote script locally so sed/cat operate on the sandbox conf.
# The heredoc sets conf='<path>' then sed -i + cat >> operate on $conf.
# We rewrite the conf path to point at our sandbox file.
remote_fixed=$(printf '%s\n' "$remote" | sed "s|/etc/pve/lxc/[0-9]*\.conf|${CONF_FILE}|g")
sh -c "$remote_fixed"
SSTUB
chmod +x "${ROOT}"/*.sh "${ROOT}/ssh"
export PATH="${ROOT}:${PATH}"
export PROXMOX_API_URL="https://proxmox.test:8006/api2/json"
export PROXMOX_API_TOKEN="root@pam!test=secret"
export PROXMOX_NODE="testnode"
export PROXMOX_STORAGE="local"
export GITEA_TOKEN="gitea-test-token"
export PRAXIS_VERSION="v0.2"
export PRAXIS_PORT="8789"
}
teardown() {
[ -n "${STUB_DIR:-}" ] && rm -rf "$STUB_DIR"
}
@test "config: REST PUT /nodes/{node}/lxc/{vmid}/config with onboot + memory=4096" {
run "${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
grep -q '^PUT /nodes/testnode/lxc/200/config onboot=1 memory=4096$' "$LOG"
# Default memory is 4096 (NOT 2048 — coreci was 2048).
! grep -q 'memory=2048' "$LOG"
grep -q 'lxc-config: VMID 200 configured' <<< "$output"
}
@test "config: PROXMOX_MEMORY_MB override → memory field reflects it" {
PROXMOX_MEMORY_MB=8192 run "${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
grep -q 'PUT /nodes/testnode/lxc/200/config onboot=1 memory=8192' "$LOG"
}
@test "config: SSH appends hookscript=local:snippets/praxis-firstboot.sh (NOT coreci-firstboot.sh)" {
run "${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
[ -f "$CONF_FILE" ]
grep -q '^onboot: 1$' "$CONF_FILE"
grep -q '^hookscript: local:snippets/praxis-firstboot.sh$' "$CONF_FILE"
# NOT coreci (praxis rebrand).
! grep -q 'coreci-firstboot.sh' "$CONF_FILE"
}
@test "config: lxc.environment includes PRAXIS_PORT=8789 (NOT CORECI_HTTP_PORT=18080)" {
run "${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
[ -f "$CONF_FILE" ]
grep -q '^lxc.environment: PRAXIS_PORT=8789$' "$CONF_FILE"
# NOT the coreci var name + port.
! grep -q 'CORECI_HTTP_PORT' "$CONF_FILE"
! grep -q '18080' "$CONF_FILE"
}
@test "config: lxc.environment includes PRAXIS_VERSION + PRAXIS_DB_PATH + PRAXIS_TTS + PRAXIS_SCENARIO" {
PRAXIS_DB_PATH=/app/data/praxis.db
PRAXIS_TTS=deepgram
PRAXIS_SCENARIO=default
export PRAXIS_DB_PATH PRAXIS_TTS PRAXIS_SCENARIO
run "${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
grep -q '^lxc.environment: PRAXIS_VERSION=v0.2$' "$CONF_FILE"
grep -q '^lxc.environment: PRAXIS_DB_PATH=/app/data/praxis.db$' "$CONF_FILE"
grep -q '^lxc.environment: PRAXIS_TTS=deepgram$' "$CONF_FILE"
grep -q '^lxc.environment: PRAXIS_SCENARIO=default$' "$CONF_FILE"
}
@test "config: lxc.environment includes GITEA_TOKEN when set" {
run "${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
grep -q '^lxc.environment: GITEA_TOKEN=gitea-test-token$' "$CONF_FILE"
}
@test "config: GITEA_TOKEN unset → no GITEA_TOKEN lxc.environment line" {
run env -u GITEA_TOKEN "${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
[ -f "$CONF_FILE" ]
grep -q '^hookscript: local:snippets/praxis-firstboot.sh$' "$CONF_FILE"
! grep -q '^lxc.environment: GITEA_TOKEN=' "$CONF_FILE"
# The other env lines are still present.
grep -q '^lxc.environment: PRAXIS_PORT=8789$' "$CONF_FILE"
}
@test "config: lxc.environment includes voice-service vars (DEEPGRAM, CARTESIA, OLLAMA)" {
DEEPGRAM_API_KEY="dg-key"
CARTESIA_API_KEY="cart-key"
OLLAMA_API_KEY="oll-key"
run env DEEPGRAM_API_KEY="$DEEPGRAM_API_KEY" CARTESIA_API_KEY="$CARTESIA_API_KEY" \
OLLAMA_API_KEY="$OLLAMA_API_KEY" "${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
grep -q '^lxc.environment: DEEPGRAM_API_KEY=dg-key$' "$CONF_FILE"
grep -q '^lxc.environment: CARTESIA_API_KEY=cart-key$' "$CONF_FILE"
grep -q '^lxc.environment: OLLAMA_API_KEY=oll-key$' "$CONF_FILE"
# Ollama config defaults present (match lxc-config.sh + .env.example).
grep -q '^lxc.environment: OLLAMA_BASE_URL=https://ollama.com/v1$' "$CONF_FILE"
grep -q '^lxc.environment: OLLAMA_ROLEPLAY_MODEL=gemma4:cloud$' "$CONF_FILE"
grep -q '^lxc.environment: OLLAMA_DEBRIEF_MODEL=deepseek-v4-flash:cloud$' "$CONF_FILE"
# Deepgram defaults present (match lxc-config.sh + .env.example).
grep -q '^lxc.environment: DEEPGRAM_MODEL=nova-3$' "$CONF_FILE"
grep -q '^lxc.environment: DEEPGRAM_LANGUAGE=en$' "$CONF_FILE"
grep -q '^lxc.environment: DEEPGRAM_REGION=na$' "$CONF_FILE"
}
@test "config: voice-service keys default to empty (v0.2 infrastructure-only)" {
run env -u DEEPGRAM_API_KEY -u CARTESIA_API_KEY -u OLLAMA_API_KEY \
"${ROOT}/lxc-config.sh" 200
[ "$status" -eq 0 ]
# The lines are present but with empty values (v0.2 may ship without
# the secrets; the CT boots and install-service writes the env file).
grep -q '^lxc.environment: DEEPGRAM_API_KEY=$' "$CONF_FILE"
grep -q '^lxc.environment: CARTESIA_API_KEY=$' "$CONF_FILE"
grep -q '^lxc.environment: OLLAMA_API_KEY=$' "$CONF_FILE"
}
@test "config: idempotent — re-run does not duplicate hookscript/lxc.environment lines" {
# First run appends the lines.
"${ROOT}/lxc-config.sh" 200 >/dev/null 2>&1
# Seed a stale line that the sed should remove (simulates prior state).
printf 'hookscript: local:snippets/OLD.sh\n' >> "$CONF_FILE"
# Second run — sed -i removes prior lines, then cat >> appends fresh.
"${ROOT}/lxc-config.sh" 200 >/dev/null 2>&1
[ -f "$CONF_FILE" ]
! grep -q 'OLD.sh' "$CONF_FILE"
[ "$(grep -c '^hookscript:' "$CONF_FILE")" -eq 1 ]
[ "$(grep -c '^onboot:' "$CONF_FILE")" -eq 1 ]
[ "$(grep -c '^lxc.environment: PRAXIS_PORT=' "$CONF_FILE")" -eq 1 ]
[ "$(grep -c '^lxc.environment: GITEA_TOKEN=' "$CONF_FILE")" -eq 1 ]
[ "$(grep -c '^lxc.environment: OLLAMA_BASE_URL=' "$CONF_FILE")" -eq 1 ]
}
@test "config: missing VMID arg → exit non-zero (usage)" {
run "${ROOT}/lxc-config.sh"
[ "$status" -ne 0 ]
grep -q 'usage: lxc-config.sh' <<< "$output"
}
@test "config: pve_env fails on missing PROXMOX_API_TOKEN → exit non-zero" {
run env -u PROXMOX_API_TOKEN "${ROOT}/lxc-config.sh" 200
[ "$status" -ne 0 ]
}