Files
light-heart-labs--dreamserver/ods/tests/bats-tests/test-compose-summary-wrapper.bats
T
wehub-resource-sync 9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:33 +08:00

362 lines
14 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bats
# ============================================================================
# BATS tests for _compose_run_with_summary in ods-cli.
# ============================================================================
# Guards against re-breakage of PR #406 (compose wrapper surfaces a compact
# summary on success, an error banner + grep'd keywords + log path on
# failure, and propagates the compose exit code so `ods restart/stop/start`
# aborts under `set -e`).
#
# Test strategy:
# - Extract _compose_run_with_summary + its helper (log/success/warn/
# log_error) text from ods-cli.
# - PATH-inject a `docker` stub that can be told to exit 0 / exit 1 with
# error-keyword output / exit 1 with no-keyword output.
# - Run the wrapper and assert on printed output + return code.
load '../bats/bats-support/load'
load '../bats/bats-assert/load'
setup() {
export TMPDIR_TEST="$BATS_TEST_TMPDIR"
export TMPDIR="$TMPDIR_TEST"
mkdir -p "$TMPDIR_TEST/bin"
# Color vars used by log/success/warn/log_error — stub to empty so
# assert_output matches plain strings without escape sequences.
export RED="" GREEN="" YELLOW="" BLUE="" CYAN="" NC=""
# Minimal logger helpers that match ods-cli's line 4550 definitions.
# We redefine rather than extract because (a) they're trivial and (b)
# ods-cli's real definitions pull in color vars we've already stubbed.
log() { echo "[ods] $1"; }
success() { echo "✓ $1"; }
warn() { echo "⚠ $1"; }
log_error() { echo "✗ $1" >&2; }
_check_docker_access() { :; }
export -f log success warn log_error _check_docker_access
# Extract _compose_run_with_summary from ods-cli.
local _cli="$BATS_TEST_DIRNAME/../../ods-cli"
eval "$(awk '/^_compose_run_with_summary\(\) \{/,/^\}$/' "$_cli")"
# docker stub — behavior driven by DOCKER_STUB_MODE env var:
# success → exit 0 with benign stdout
# fail-keyword→ exit 1, stdout contains 'error', 'failed', 'unhealthy', 'dependency'
# fail-nomatch→ exit 1, stdout has no error keywords
cat > "$TMPDIR_TEST/bin/docker" <<'STUB'
#!/usr/bin/env bash
echo "DOCKER_ARGS: $*" >> "$DOCKER_CALL_LOG"
if [[ "$1" == "rm" ]]; then
echo "removed $*"
exit 0
fi
case "${DOCKER_STUB_MODE:-success}" in
success)
echo "Network ods-net created"
echo "Container ods-foo started"
exit 0
;;
fail-keyword)
echo "Container ods-foo starting"
echo "Error response from daemon: service ods-foo failed to start"
echo "unhealthy container ods-bar"
echo "dependency ods-baz not ready"
exit 1
;;
fail-nomatch)
echo "Container ods-foo starting"
echo "network busy (try again later)"
exit 1
;;
fail-port-once)
state_file="$TMPDIR_TEST/port-retry-count"
count=0
[[ -f "$state_file" ]] && count=$(cat "$state_file")
count=$((count + 1))
echo "$count" > "$state_file"
if [[ "$count" -eq 1 ]]; then
echo "Error response from daemon: failed to set up container networking: Bind for 127.0.0.1:11434 failed: port is already allocated"
exit 1
fi
echo "Container ods-foo started"
exit 0
;;
fail-startup-once)
state_file="$TMPDIR_TEST/startup-retry-count"
count=0
[[ -f "$state_file" ]] && count=$(cat "$state_file")
count=$((count + 1))
echo "$count" > "$state_file"
if [[ "$count" -eq 1 ]]; then
echo "dependency failed to start: container ods-llama-server exited (137)"
exit 1
fi
echo "Container ods-llama-server started"
exit 0
;;
fail-startup-twice)
state_file="$TMPDIR_TEST/startup-retry-count"
count=0
[[ -f "$state_file" ]] && count=$(cat "$state_file")
count=$((count + 1))
echo "$count" > "$state_file"
if [[ "$count" -le 2 ]]; then
echo "dependency failed to start: Error response from daemon: No such container: stale-compose-id"
exit 1
fi
echo "Container ods-llama-server started"
exit 0
;;
fail-startup-twice-then-name-conflict)
state_file="$TMPDIR_TEST/startup-retry-count"
count=0
[[ -f "$state_file" ]] && count=$(cat "$state_file")
count=$((count + 1))
echo "$count" > "$state_file"
if [[ "$count" -le 2 ]]; then
echo "dependency failed to start: Error response from daemon: No such container: stale-compose-id"
exit 1
fi
if [[ "$count" -eq 3 ]]; then
echo "Error response from daemon: Conflict. The container name \"/ods-llama-server\" is already in use by container \"d287436ffd1902846f8d81593e4a29c92761457cb57167290e9d7f039d318733\". You have to remove (or rename) that container to be able to reuse that name."
exit 1
fi
echo "Container ods-llama-server started"
exit 0
;;
fail-docker-sock)
echo "unable to get image 'ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4': permission denied while trying to connect to the docker API at unix:///var/run/docker.sock"
exit 1
;;
*)
echo "unknown DOCKER_STUB_MODE: $DOCKER_STUB_MODE" >&2
exit 127
;;
esac
STUB
chmod +x "$TMPDIR_TEST/bin/docker"
export PATH="$TMPDIR_TEST/bin:$PATH"
export DOCKER_CALL_LOG="$TMPDIR_TEST/docker.log"
export ODS_COMPOSE_STARTUP_RETRY_DELAY=0
: > "$DOCKER_CALL_LOG"
}
teardown() {
rm -rf "$TMPDIR_TEST/bin" "$TMPDIR_TEST/docker.log"
}
# ── success path ────────────────────────────────────────────────────────────
@test "wrapper: success prints compact '<verb> — done' banner and returns 0" {
export DOCKER_STUB_MODE=success
run _compose_run_with_summary "Restarting all services" up -d
assert_success
assert_output --partial "Restarting all services..."
assert_output --partial "Restarting all services — done"
}
@test "wrapper: success removes the compose log tmpfile" {
export DOCKER_STUB_MODE=success
# Track how many mktemp-produced files exist before & after. We rely on
# $TMPDIR being our per-test tempdir; the wrapper uses `mktemp` with no args.
local before=$(find "$TMPDIR_TEST" -maxdepth 1 -name 'tmp.*' -type f 2>/dev/null | wc -l)
run _compose_run_with_summary "Starting all services" up -d
assert_success
local after=$(find "$TMPDIR_TEST" -maxdepth 1 -name 'tmp.*' -type f 2>/dev/null | wc -l)
# Zero net new temp files (create + rm).
[ "$after" -le "$before" ]
}
@test "wrapper: success does NOT print error banner or log path" {
export DOCKER_STUB_MODE=success
run _compose_run_with_summary "Starting all services" up -d
assert_success
refute_output --partial "Full compose output:"
refute_output --partial "failed:"
}
# ── failure path, matching keywords (PR #406) ───────────────────────────────
@test "wrapper: failure prints error banner, matched lines, and log path" {
export DOCKER_STUB_MODE=fail-keyword
run _compose_run_with_summary "Restarting service x" up -d x
assert_failure
assert_output --partial "Restarting service x failed:"
# At least one error-keyword line should be surfaced (indented two spaces).
assert_output --partial "Error response from daemon"
assert_output --partial "Full compose output:"
}
@test "wrapper: failure propagates the compose exit code (1)" {
export DOCKER_STUB_MODE=fail-keyword
run _compose_run_with_summary "Restarting" up -d
# docker stub exits 1; wrapper must return 1.
[ "$status" -eq 1 ]
}
@test "wrapper: failure preserves the compose log file (not auto-removed)" {
export DOCKER_STUB_MODE=fail-keyword
run _compose_run_with_summary "Restarting" up -d
assert_failure
# Extract the "Full compose output: /tmp/tmp.XXXXXX" path from output.
local log_line
log_line=$(printf '%s\n' "$output" | grep -E 'Full compose output:' | head -1)
[ -n "$log_line" ]
local log_path="${log_line##*: }"
[ -f "$log_path" ]
rm -f "$log_path"
}
# ── failure path, zero keyword matches (nounset/pipefail-hardening) ─────────
# These two tests must execute the wrapper under `set -euo pipefail` because
# the `|| warn "(no error keywords matched...)"` branch only fires when
# grep's exit-1 propagates through the pipeline — and only pipefail makes
# grep the pipeline's final exit code. ods-cli line 6 IS `set -euo pipefail`
# in production; bats' setup() doesn't inherit that, so we set it explicitly
# via the subshell.
@test "wrapper: failure with no keyword match fires the warn fallback (under pipefail)" {
export DOCKER_STUB_MODE=fail-nomatch
# Re-extract the wrapper inside a pipefail-enabled subshell.
run bash -c '
set -euo pipefail
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" NC=""
log() { echo "[ods] $1"; }
success() { echo "$1"; }
warn() { echo "$1"; }
log_error() { echo "$1" >&2; }
_check_docker_access() { :; }
eval "$(awk "/^_compose_run_with_summary\(\) \{/,/^\}$/" "'"$BATS_TEST_DIRNAME/../../ods-cli"'")"
_compose_run_with_summary "Stopping service y" down y
'
assert_failure
assert_output --partial "Stopping service y failed:"
assert_output --partial "(no error keywords matched in compose log)"
assert_output --partial "Full compose output:"
}
@test "wrapper: failure with no keyword match still propagates exit code" {
export DOCKER_STUB_MODE=fail-nomatch
run bash -c '
set -euo pipefail
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" NC=""
log() { echo "[ods] $1"; }
success() { echo "$1"; }
warn() { echo "$1"; }
log_error() { echo "$1" >&2; }
_check_docker_access() { :; }
eval "$(awk "/^_compose_run_with_summary\(\) \{/,/^\}$/" "'"$BATS_TEST_DIRNAME/../../ods-cli"'")"
_compose_run_with_summary "Stopping" down
'
[ "$status" -eq 1 ]
}
@test "wrapper: docker socket permission denied is surfaced with a fix hint" {
export DOCKER_STUB_MODE=fail-docker-sock
run _compose_run_with_summary "Restarting all services" up -d
assert_failure
assert_output --partial "permission denied while trying to connect to the docker API"
assert_output --partial "Docker socket permission denied"
assert_output --partial "sudo usermod -aG docker"
assert_output --partial "newgrp docker"
assert_output --partial "Full compose output:"
}
# ── docker compose args passthrough ─────────────────────────────────────────
@test "wrapper: compose up retries once on transient port allocation race" {
export DOCKER_STUB_MODE=fail-port-once
export ODS_COMPOSE_PORT_RETRY_DELAY=0
run _compose_run_with_summary "Restarting all services" up -d
assert_success
assert_output --partial "Docker reported a port allocation race"
assert_output --partial "Restarting all services"
assert_output --partial "done"
run grep -c '^DOCKER_ARGS:' "$DOCKER_CALL_LOG"
assert_output "2"
}
@test "wrapper: non-up command does not retry on port allocation failure" {
export DOCKER_STUB_MODE=fail-port-once
export ODS_COMPOSE_PORT_RETRY_DELAY=0
run _compose_run_with_summary "Stopping all services" down
assert_failure
run grep -c '^DOCKER_ARGS:' "$DOCKER_CALL_LOG"
assert_output "1"
}
@test "wrapper: compose up retries once on transient dependency startup failure" {
export DOCKER_STUB_MODE=fail-startup-once
run _compose_run_with_summary "Restarting all services" up -d
assert_success
assert_output --partial "Docker reported a transient service startup failure"
assert_output --partial "Restarting all services"
assert_output --partial "done"
run grep -c '^DOCKER_ARGS:' "$DOCKER_CALL_LOG"
assert_output "2"
}
@test "wrapper: compose up retries repeated transient dependency startup failures" {
export DOCKER_STUB_MODE=fail-startup-twice
export ODS_COMPOSE_STARTUP_RETRY_ATTEMPTS=3
run _compose_run_with_summary "Restarting all services" up -d
assert_success
assert_output --partial "retrying (1/2)"
assert_output --partial "retrying (2/2)"
assert_output --partial "Restarting all services"
assert_output --partial "done"
run grep -c '^DOCKER_ARGS:' "$DOCKER_CALL_LOG"
assert_output "3"
}
@test "wrapper: compose up cleans stale ODS name conflict after startup retries" {
export DOCKER_STUB_MODE=fail-startup-twice-then-name-conflict
export ODS_COMPOSE_STARTUP_RETRY_ATTEMPTS=3
export ODS_COMPOSE_NAME_CONFLICT_RETRY_DELAY=0
run _compose_run_with_summary "Restarting all services" up -d
assert_success
assert_output --partial "retrying (1/2)"
assert_output --partial "retrying (2/2)"
assert_output --partial "stale ODS container-name conflict"
assert_output --partial "Restarting all services"
assert_output --partial "done"
run grep -c '^DOCKER_ARGS: compose' "$DOCKER_CALL_LOG"
assert_output "4"
run grep -F 'DOCKER_ARGS: rm -f d287436ffd1902846f8d81593e4a29c92761457cb57167290e9d7f039d318733' "$DOCKER_CALL_LOG"
assert_success
}
@test "wrapper: all args after verb are passed to docker compose" {
export DOCKER_STUB_MODE=success
run _compose_run_with_summary "Up" -f extra.yml up -d my-service
assert_success
# Stub records its argv to DOCKER_CALL_LOG; first arg must be `compose`
# because the wrapper calls `docker compose <args>`.
run cat "$DOCKER_CALL_LOG"
assert_output --partial "DOCKER_ARGS: compose --progress quiet -f extra.yml up -d my-service"
}
# ── propagation into callers (cmd_restart/cmd_stop/cmd_start) ───────────────
@test "wrapper: caller returns wrapper's non-zero exit (smoke)" {
export DOCKER_STUB_MODE=fail-keyword
# Simulate `cmd_restart`: wrapper is the last statement, so its exit
# code becomes the function's exit code.
_simulated_caller() {
_compose_run_with_summary "Restarting all services" up -d
}
run _simulated_caller
[ "$status" -eq 1 ]
}
@test "wrapper: caller returns 0 on success (smoke)" {
export DOCKER_STUB_MODE=success
_simulated_caller() {
_compose_run_with_summary "Restarting all services" up -d
}
run _simulated_caller
[ "$status" -eq 0 ]
}