Ctrl K
ComunidadGuías…

hack/run-kubernetes-serial-console_test.bats

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# Regression coverage for tenant-worker guest serial console capture in
# hack/e2e-chainsaw/_lib/run-kubernetes.sh. cozytest.sh ends an @test block at
# the first bare closing brace, so command mocks stay at top level.
#
# The capture exists for the one failure class no other collector reaches: a
# worker that stalls in the guest before Talos apid answers. Its defining
# property is therefore what it does NOT need — no talosctl, no reader
# certificate, no helper Pod, nothing inside the guest. These tests pin that
# property, because a capture that grew an in-guest dependency would still look
# healthy while being blind to exactly the runs it was written for.

load test_helper

kubectl_calls=/dev/null
kubectl_pod_names=
kubectl_list_rc=0
kubectl_list_stderr=
kubectl_init_names=
kubectl_init_rc=0
kubectl_wedge_rows=
kubectl_wedge_rc=0
kubectl_wedge_stderr=
mktemp_fail=0
kubectl_logs_output='mock console output'
kubectl_logs_rc=0
kubectl_logs_stderr=
kubectl_timeline_rows='name=virt-launcher-worker-a-11111 phase=Running podStartTime=2026-08-15T14:35:26Z computeStartedAt=2026-08-15T14:35:47Z consoleStartedAt=2026-08-15T14:35:47Z'
kubectl_timeline_rc=0
kubectl_timeline_stderr=
talosctl_calls=/dev/null
timeout_calls=/dev/null
timeout_fail_pod=
date_now=

date() {
  # Same shape the node-join suite uses: a fixed clock so the passing path's
  # remaining-time arithmetic is decided by the test rather than by when it ran.
  if [ -n "${date_now}" ] && [ "${1:-}" = +%s ]; then
    printf '%s\n' "${date_now}"
    return 0
  fi
  command date "$@"
}

kubectl() {
  printf '%s\n' "$*" >>"${kubectl_calls}"

  if [ "${3:-}" = get ] && [ "${4:-}" = pods ]; then
    case "$*" in
      *computeStartedAt*)
        # Rows first, status after, so a cut-off read can be modelled as what it
        # is: a prefix that arrived plus a non-zero status. Returning early on
        # the status instead would make "failed" and "returned nothing" the same
        # scenario, and the branch that tells them apart untestable.
        #
        # stderr is staged separately for the same reason every other arm here
        # stages it: kubectl writes warnings beside a healthy read as well as
        # errors beside a failed one, and which file those land in is decided by
        # the status rather than by anything in the text.
        [ -z "${kubectl_timeline_stderr}" ] || printf '%s\n' "${kubectl_timeline_stderr}" >&2
        [ -z "${kubectl_timeline_rows}" ] || printf '%s\n' "${kubectl_timeline_rows}"
        return "${kubectl_timeline_rc}"
        ;;
      *initContainerStatuses*)
        [ -z "${kubectl_wedge_stderr}" ] || printf '%s\n' "${kubectl_wedge_stderr}" >&2
        [ "${kubectl_wedge_rc}" -eq 0 ] || return "${kubectl_wedge_rc}"
        [ -z "${kubectl_wedge_rows}" ] || printf '%s\n' "${kubectl_wedge_rows}"
        return 0
        ;;
      *initContainers*)
        [ "${kubectl_init_rc}" -eq 0 ] || return "${kubectl_init_rc}"
        [ -z "${kubectl_init_names}" ] || printf '%s\n' "${kubectl_init_names}"
        return 0
        ;;
    esac
    [ -z "${kubectl_list_stderr}" ] || printf '%s\n' "${kubectl_list_stderr}" >&2
    [ "${kubectl_list_rc}" -eq 0 ] || return "${kubectl_list_rc}"
    [ -z "${kubectl_pod_names}" ] || printf '%s\n' ${kubectl_pod_names}
    return 0
  fi

  if [ "${3:-}" = logs ]; then
    [ -z "${kubectl_logs_stderr}" ] || printf '%s\n' "${kubectl_logs_stderr}" >&2
    [ "${kubectl_logs_rc}" -eq 0 ] || return "${kubectl_logs_rc}"
    [ -z "${kubectl_logs_output}" ] || printf '%s for %s\n' "${kubectl_logs_output}" "${4:-}"
    return 0
  fi

  return 0
}

talosctl() {
  printf '%s\n' "$*" >>"${talosctl_calls}"
}

timeout() {
  local command_rc=0
  printf '%s\n' "$*" >>"${timeout_calls}"
  [ "${1:-}" = -k ] || return 97
  shift 3
  "$@" || command_rc=$?
  if [ -n "${timeout_fail_pod}" ]; then
    case " $* " in
      *" ${timeout_fail_pod} "*) return 124 ;;
    esac
  fi
  return "${command_rc}"
}

mktemp() {
  # The scratch file the wedge check allocates is the one failure the function
  # cannot report through kubectl, so it needs its own knob. `-d` is left
  # working because the tests themselves use it for their own temp dirs.
  if [ "${mktemp_fail}" = 1 ] && [ "${1:-}" != -d ]; then
    return 1
  fi
  command mktemp "$@"
}

assert_file_contains() {
  local needle="$1"
  local file="$2"

  case "$(cat "${file}")" in
    *"${needle}"*) return 0 ;;
  esac
  printf 'expected %s to contain: %s\n' "${file}" "${needle}" >&2
  return 1
}

assert_file_lacks_pattern() {
  local pattern="$1"
  local file="$2"

  # A missing file must fail rather than vacuously pass: awk exits 2 on an
  # unreadable path, which is indistinguishable from "no line matched" once the
  # status is folded into an if.
  if [ ! -f "${file}" ]; then
    printf 'expected %s to exist so it could be checked for: %s\n' "${file}" "${pattern}" >&2
    return 1
  fi
  if awk -v pattern="${pattern}" '$0 ~ pattern { found = 1 } END { exit found ? 0 : 1 }' "${file}"; then
    printf 'expected %s not to match: %s\n' "${file}" "${pattern}" >&2
    return 1
  fi
}

# The function under test redirects a command's stderr into a report file and
# decides which artifact to write from it. cozytest.sh runs under `set -x`, so
# the tracer's own line for that command is emitted on the very stderr being
# redirected, and every assertion about those files would be reading the
# tracer. Call through this wrapper so the sinks hold what production would
# put in them.
run_capture() {
  local _rc=0
  ( set +x; cozy_capture_tenant_serial_console "${1:-capture staged by a unit test}" "${2:-6}" ) || _rc=$?
  return "${_rc}"
}

@test "a wedged console container is named in the headline, not buried" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rows="$(printf 'false PodInitializing virt-launcher-worker-a-11111\nfalse PodInitializing virt-launcher-worker-b-22222')"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # If kubevirt/kubevirt#15989 still bites, the suite's own headline reads
  # "fewer than 2 tenant nodes Ready within 29m" -- byte-identical to the
  # failure this instrumentation exists to study. A triager would file the
  # experiment's answer as the known flake. The diagnostic has to name its
  # own worst case before the noise starts.
  assert_file_contains 'guest-console-log' "$tmp/out"
  assert_file_contains '15989' "$tmp/out"
  assert_file_contains 'virt-launcher-worker-a-11111' "$tmp/out"
  rm -rf "$tmp"
}

@test "a read that never answered is not reported as a healthy console" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rc=1
  kubectl_wedge_rows="$(printf 'false PodInitializing virt-launcher-worker-a-11111')"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # The guard against a false headline must not become a false all-clear.
  # An apiserver blip here would otherwise leave the reader with no line at
  # all, which reads as "the console container is fine" -- nothing was
  # checked. Same rule the sibling attach check follows.
  assert_file_lacks_pattern '15989' "$tmp/out"
  assert_file_contains 'unknown, not fine' "$tmp/out"
  rm -rf "$tmp"
}

@test "the wedge row carries the waiting reason, not just the ready bit" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rows="$(printf 'false PodInitializing virt-launcher-worker-a-11111')"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # ready=false on a sidecar means "not running", which a terminated Pod
  # satisfies as well as a wedged one. The reason keeps the two apart.
  assert_file_contains 'PodInitializing' "$tmp/out"
  assert_file_contains 'state.waiting.reason' "$kubectl_calls"
  rm -rf "$tmp"
}

@test "the taxonomy does not claim to be exhaustive" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rows="$(printf 'false OOMKilled virt-launcher-worker-a-11111')"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # Two named reasons do not cover the space -- OOMKilled and ContainerCreating
  # reach this line too, and a superseded Pod killed past its grace period
  # reads Error. A headline that enumerates two and stops implies the rest
  # cannot happen, which is the over-claim this whole line was rewritten to
  # stop making.
  assert_file_contains 'any other reason, or none, is not covered' "$tmp/out"
  rm -rf "$tmp"
}

@test "the wedge taxonomy names CrashLoopBackOff as the bug, not as a superseded Pod" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rows="$(printf 'false CrashLoopBackOff virt-launcher-worker-a-11111')"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # The sidecar restarts on failure, so the hang settles into CrashLoopBackOff
  # and passes through Error. Filing those under "superseded Pod" would hand a
  # triager the experiment's own failure labelled as noise -- the precise
  # inversion this function exists to prevent.
  assert_file_contains 'CrashLoopBackOff or Error below is the wedge shape' "$tmp/out"
  assert_file_lacks_pattern 'Completed or Error below means a superseded' "$tmp/out"
  rm -rf "$tmp"
}

@test "a superseded Pod is labelled, not called a wedge" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rows="$(printf 'false Completed virt-launcher-worker-a-11111')"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # ContainerState is a union, so a waiting reason alone is blank for the
  # terminated case and cannot separate it from anything else at ready=false.
  # The terminated reason is what labels the superseded Pod, and Completed is
  # the shape that means superseded rather than wedged.
  assert_file_contains 'Completed' "$tmp/out"
  assert_file_contains 'state.terminated.reason' "$kubectl_calls"
  rm -rf "$tmp"
}

@test "the headline points at the describe rather than concluding" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rows="$(printf 'false  virt-launcher-worker-a-11111')"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # The bit cannot separate every shape, so the line must not assert one. A
  # headline that concludes more than it established is the same mislabel this
  # function exists to prevent, pointing the other way.
  assert_file_contains 'if these are current Pods' "$tmp/out"
  assert_file_contains 'describe that follows settles it' "$tmp/out"
  rm -rf "$tmp"
}

@test "a scratch file that could not be allocated is not silence either" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rows="$(printf 'false CrashLoopBackOff virt-launcher-worker-a-11111')"
  mktemp_fail=1

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
  mktemp_fail=0

  # The function argues twelve lines further down that silence would read as
  # "the console container is fine" when nothing was checked. That argument
  # does not stop applying one branch earlier.
  assert_file_contains 'unknown, not fine' "$tmp/out"
  assert_file_lacks_pattern '15989' "$tmp/out"
  rm -rf "$tmp"
}

@test "a failed read keeps the reason it failed" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rc=1
  kubectl_wedge_stderr="Error from server: etcdserver: leader changed"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # Discarding stderr here would report a bare exit code for a failure whose
  # cause kubectl already named, on the one path that exists to stop a reader
  # guessing. Same rule the sibling attach check follows.
  assert_file_contains 'leader changed' "$tmp/out"
  rm -rf "$tmp"
}

@test "a healthy console container says nothing in the headline" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_wedge_rows="$(printf 'true  virt-launcher-worker-a-11111')"

  ( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1

  # A marker that fires on healthy runs is one a reader learns to skip.
  assert_file_lacks_pattern '15989' "$tmp/out"
  rm -rf "$tmp"
}

@test "the wedge check runs before the rest of the node-join diagnostics" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  head=$(grep -n 'node-join failed: fewer than 2 tenant nodes Ready' "$lib" | head -n 1 | cut -d: -f1)
  wedge=$(grep -n '^ *cozy_report_guest_console_wedge || true$' "$lib" | head -n 1 | cut -d: -f1)
  first=$(grep -n 'describe nodes' "$lib" | head -n 1 | cut -d: -f1)
  if [ -z "$wedge" ]; then
    echo "expected the node-join failure path to check for a wedged console container" >&2
    return 1
  fi
  if [ "$wedge" -le "$head" ] || [ "$wedge" -ge "$first" ]; then
    echo "the wedge check (line $wedge) must sit between the headline ($head) and the first diagnostic ($first)" >&2
    return 1
  fi
}

@test "console capture needs nothing from inside the guest" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  talosctl_calls="$tmp/talosctl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-smoke

  run_capture

  assert_file_contains 'logs virt-launcher-worker-a-11111 -c guest-console-log' "$kubectl_calls"
  assert_file_lacks_pattern 'certificate|exec|apply|talosconfig' "$kubectl_calls"
  [ ! -s "$talosctl_calls" ]
  assert_file_contains 'mock console output' "$COZY_REPORT_DIR/snapshots/console-smoke/tenant-serial-console/virt-launcher-worker-a-11111.log"
  rm -rf "$tmp"
}

@test "each read is bounded and keeps its exit code" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-bounded

  run_capture

  assert_file_contains '-k 5 30 kubectl -n tenant-test logs virt-launcher-worker-a-11111 -c guest-console-log' "$timeout_calls"
  assert_file_contains '[capture exit code: 0]' "$COZY_REPORT_DIR/snapshots/console-bounded/tenant-serial-console/virt-launcher-worker-a-11111.log"
  rm -rf "$tmp"
}

@test "the read keeps the boot output instead of the tail" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-head

  run_capture

  # A guest that stalls before apid is diagnosed from what it printed while
  # booting, and a guest that instead repaints the console indefinitely would
  # push that boot output out of any --tail window. --limit-bytes truncates
  # from the far end instead, which keeps whatever the kubelet still holds.
  # It cannot recover what container-log rotation already dropped.
  assert_file_contains '--limit-bytes=' "$kubectl_calls"
  assert_file_lacks_pattern '--tail' "$kubectl_calls"
  rm -rf "$tmp"
}

@test "a pod whose read times out does not stop the next pod" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111 virt-launcher-worker-b-22222"
  timeout_fail_pod=virt-launcher-worker-a-11111
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-partial

  run_capture

  assert_file_contains '[capture exit code: 124]' "$COZY_REPORT_DIR/snapshots/console-partial/tenant-serial-console/virt-launcher-worker-a-11111.log"
  assert_file_contains '[capture exit code: 0]' "$COZY_REPORT_DIR/snapshots/console-partial/tenant-serial-console/virt-launcher-worker-b-22222.log"
  rm -rf "$tmp"
}

@test "an empty virt-launcher list is recorded rather than passed over" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-empty

  capture_rc=0
  run_capture || capture_rc=$?

  [ "$capture_rc" -eq 1 ]
  assert_file_contains 'no virt-launcher Pod found' "$COZY_REPORT_DIR/snapshots/console-empty/tenant-serial-console/setup-error.log"
  assert_file_lacks_pattern 'guest-console-log' "$timeout_calls"
  rm -rf "$tmp"
}

@test "a failed Pod listing is recorded rather than passed over" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_list_rc=1
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-list-error

  capture_rc=0
  run_capture || capture_rc=$?

  [ "$capture_rc" -eq 1 ]
  assert_file_contains 'failed to list tenant virt-launcher Pods' "$COZY_REPORT_DIR/snapshots/console-list-error/tenant-serial-console/setup-error.log"
  assert_file_lacks_pattern 'guest-console-log' "$timeout_calls"
  rm -rf "$tmp"
}

@test "a warning on a successful listing is kept apart from a setup failure" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_list_stderr="W0807 partial results warning"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-warn

  run_capture

  # kubectl writes to stderr and exits 0 for warnings and for "No resources
  # found" alike, so the exit status decides the file. A warning filed as a
  # setup error reads as a broken collector on an otherwise healthy run.
  assert_file_contains 'partial results warning' "$COZY_REPORT_DIR/snapshots/console-warn/tenant-serial-console/READ-WARNINGS.txt"
  [ ! -f "$COZY_REPORT_DIR/snapshots/console-warn/tenant-serial-console/setup-error.log" ]
  rm -rf "$tmp"
}

@test "a healthy listing leaves no warnings file behind" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-quiet

  run_capture

  [ ! -f "$COZY_REPORT_DIR/snapshots/console-quiet/tenant-serial-console/READ-WARNINGS.txt" ]
  [ ! -f "$COZY_REPORT_DIR/snapshots/console-quiet/tenant-serial-console/setup-error.log" ]
  rm -rf "$tmp"
}

@test "a failed listing keeps the stderr that explains it" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_list_rc=1
  kubectl_list_stderr="Error from server (Forbidden): pods is forbidden"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-forbidden

  capture_rc=0
  run_capture || capture_rc=$?

  [ "$capture_rc" -eq 1 ]
  assert_file_contains 'Forbidden' "$COZY_REPORT_DIR/snapshots/console-forbidden/tenant-serial-console/setup-error.log"
  rm -rf "$tmp"
}

@test "the attach check passes when the console container is present" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names="virt-launcher-worker-a-11111=guest-console-log"

  attach_rc=0
  ( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?

  [ "$attach_rc" -eq 0 ]
  rm -rf "$tmp"
}

@test "a matched Pod with no init containers at all is the finding, not a miss" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names="virt-launcher-worker-a-11111="

  attach_rc=0
  ( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?

  # This is the shape an inert setting actually produces here: guest-console-log
  # is the only init container these Pods ever get, so "the field did not take
  # effect" means initContainers is absent, not that some other name appears.
  # Reading a bare list of container names would render it byte-identical to
  # "no Pod matched" and let the suite pass over the one thing this checks.
  [ "$attach_rc" -eq 2 ]
  rm -rf "$tmp"
}

@test "a read that never answered is not reported as an inert setting" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_rc=1

  attach_rc=0
  ( set +x; cozy_assert_guest_console_attached ) 2>"$tmp/err" || attach_rc=$?

  # Piping the read into grep would collapse this onto the absent-container
  # branch: no output, grep fails, and an API blip gets published as "the
  # override did not take effect" -- a cause nothing established. The message
  # is asserted, not just the code: a failed read and a read that matched
  # nothing share an exit code but are different facts, and only the status
  # tells them apart.
  [ "$attach_rc" -eq 1 ]
  assert_file_contains 'could not read tenant virt-launcher Pods' "$tmp/err"
  rm -rf "$tmp"
}

@test "a read that matched no Pod is not reported as an inert setting either" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names=

  attach_rc=0
  ( set +x; cozy_assert_guest_console_attached ) 2>"$tmp/err" || attach_rc=$?

  # A selector that matched nothing says nothing about any Pod's containers,
  # so it belongs with the failed read rather than with the absent container.
  [ "$attach_rc" -eq 1 ]
  assert_file_contains 'no tenant virt-launcher Pod matched' "$tmp/err"
  rm -rf "$tmp"
}

@test "the absent-container diagnostics are bounded like the check itself" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names="virt-launcher-worker-a-11111="

  attach_rc=0
  ( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?

  [ "$attach_rc" -eq 2 ]
  # These run on a path that ends in exit 1; an unbounded hang here would take
  # the step's deadline and the tenant snapshot the exit is meant to reach.
  [ "$(grep -c 'get virtualmachineinstances' "$timeout_calls")" -eq 1 ]
  [ "$(grep -c 'get pods' "$timeout_calls")" -eq 2 ]
  rm -rf "$tmp"
}

@test "a silent read records whether the container ever started" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_logs_output=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-silent

  run_capture

  # An empty console log has two causes that look identical in the artifact:
  # the guest printed nothing, or the container never started so there was no
  # stream at all. The second is what kubevirt/kubevirt#15989 produces, and it
  # is the reading that would otherwise be filed as "the guest was silent".
  # The Pod's own state is the only thing that separates them.
  d="$COZY_REPORT_DIR/snapshots/console-silent/tenant-serial-console"
  assert_file_contains 'describe pod' "$d/POD-STATE.txt"
  assert_file_contains 'events' "$d/POD-STATE.txt"
  assert_file_contains 'returned nothing' "$d/virt-launcher-worker-a-11111.log"
  rm -rf "$tmp"
}

@test "a container that never started is not called truncated or silent" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_logs_output=
  kubectl_logs_rc=1
  kubectl_logs_stderr="Error from server (BadRequest): container guest-console-log is not valid for pod virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-absent

  run_capture

  # This is the shape kubevirt/kubevirt#15989 produces and the headline case
  # for the whole collector. Folding stderr into the capture would make the
  # file non-empty and the console would read as having said something;
  # nothing was truncated, because nothing was ever read.
  d="$COZY_REPORT_DIR/snapshots/console-absent/tenant-serial-console"
  assert_file_contains 'no console at all' "$d/virt-launcher-worker-a-11111.log"
  assert_file_lacks_pattern 'truncated' "$d/virt-launcher-worker-a-11111.log"
  assert_file_contains 'is not valid for pod' "$d/virt-launcher-worker-a-11111.read-error.log"
  [ -f "$d/POD-STATE.txt" ]
  rm -rf "$tmp"
}

@test "a read killed without a word does not point at a missing file" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_logs_output=
  timeout_fail_pod=virt-launcher-worker-a-11111
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-killed

  run_capture

  # timeout kills the child without a word, so both streams can be empty.
  # Naming read-error.log then sends the reader after evidence that was
  # never written.
  d="$COZY_REPORT_DIR/snapshots/console-killed/tenant-serial-console"
  [ ! -f "$d/virt-launcher-worker-a-11111.read-error.log" ]
  assert_file_contains 'failed silently' "$d/virt-launcher-worker-a-11111.log"
  assert_file_lacks_pattern 'see read-error.log' "$d/virt-launcher-worker-a-11111.log"
  rm -rf "$tmp"
}

@test "a warning beside a healthy capture is not filed as a read error" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_logs_stderr="Warning: deprecated flag"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-podwarn

  run_capture

  # Same rule the Pod-list read follows: kubectl writes warnings on stderr
  # with a zero status, so presence of stderr does not make it an error.
  d="$COZY_REPORT_DIR/snapshots/console-podwarn/tenant-serial-console"
  assert_file_contains 'deprecated flag' "$d/virt-launcher-worker-a-11111.READ-WARNINGS.txt"
  [ ! -f "$d/virt-launcher-worker-a-11111.read-error.log" ]
  [ ! -f "$d/POD-STATE.txt" ]
  rm -rf "$tmp"
}

@test "a healthy read leaves no read-error file" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-noerr

  run_capture

  [ ! -f "$COZY_REPORT_DIR/snapshots/console-noerr/tenant-serial-console/virt-launcher-worker-a-11111.read-error.log" ]
  rm -rf "$tmp"
}

@test "a truncated read is not reported as silence" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  timeout_fail_pod=virt-launcher-worker-a-11111
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-truncated

  out=$( ( set +x; cozy_capture_tenant_serial_console 'a unit test' 6 ) 2>/dev/null )
  printf '%s\n' "$out" >"$tmp/out"

  # The read broke off mid-stream, so the file holds a prefix. Calling that
  # "no console output" is the same unestablished claim about silence the
  # Pod-state capture exists to prevent.
  f="$COZY_REPORT_DIR/snapshots/console-truncated/tenant-serial-console/virt-launcher-worker-a-11111.log"
  assert_file_contains 'mock console output' "$f"
  assert_file_contains 'truncated' "$f"
  assert_file_lacks_pattern 'no console output' "$f"
  # And the summary line the passing path reads must not undo that. It is fed
  # by the same counter that gates the Pod-state read, which a cut-short console
  # needs as much as an empty one -- so counting them together is right and
  # naming them together is not. On the green path this line is the only thing
  # anyone sees, and a partial baseline reported as no baseline sends the reader
  # past evidence that is sitting in the artifact.
  assert_file_contains 'of the 1 guest consoles read, 0 came back empty and 1 were cut short' "$tmp/out"
  rm -rf "$tmp"
}

@test "a failed read records the Pod state too" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  timeout_fail_pod=virt-launcher-worker-a-11111
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-failed-read

  run_capture

  assert_file_contains 'describe pod' "$COZY_REPORT_DIR/snapshots/console-failed-read/tenant-serial-console/POD-STATE.txt"
  rm -rf "$tmp"
}

@test "a healthy capture leaves no Pod-state file" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-healthy

  run_capture

  [ ! -f "$COZY_REPORT_DIR/snapshots/console-healthy/tenant-serial-console/POD-STATE.txt" ]
  rm -rf "$tmp"
}

@test "the Pod-state reads are bounded and taken once for the whole selector" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="p1 p2 p3"
  kubectl_logs_output=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-once

  run_capture

  # One describe and one events read for the selector, not per Pod: three
  # silent workers is one finding, and paying for it per Pod would put an
  # unbounded term back into the failure path the cap exists to bound.
  [ "$(grep -c 'describe pods' "$timeout_calls")" -eq 1 ]
  [ "$(grep -c 'get events' "$timeout_calls")" -eq 1 ]
  rm -rf "$tmp"
}

@test "the attach check accepts the container wherever KubeVirt puts it" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  # It is an init container today. The question the check asks is whether the
  # container exists at all, so reading both lists costs nothing and removes
  # the one way a KubeVirt-side move would red a green run over a diagnostic.
  kubectl_init_names="virt-launcher-worker-a-11111= guest-console-log"
  accept_rc=0
  ( set +x; cozy_assert_guest_console_attached ) || accept_rc=$?
  [ "$accept_rc" -eq 0 ]

  kubectl_init_names="virt-launcher-worker-a-11111="
  attach_rc=0
  ( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?
  [ "$attach_rc" -eq 2 ]
  assert_file_contains '{.spec.containers[*].name}' "$kubectl_calls"
  rm -rf "$tmp"
}

@test "a passing attach check records the positive outcome, not just the negative" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names="$(printf 'p1=\np2=guest-console-log')"

  ( set +x; cozy_assert_guest_console_attached ) >"$tmp/out" 2>&1

  # This run doubles as the revalidation of a platform-wide workaround, so the
  # positive outcome has to leave an artifact too. Returning 0 in silence
  # leaves "the override worked" to be inferred from the absence of a
  # complaint -- the exact inference this collector refuses to make elsewhere.
  assert_file_contains 'attached on 1 of 2 virt-launcher Pods' "$tmp/out"
  rm -rf "$tmp"
}

@test "a truncated read names the error file when there is one" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_logs_stderr="Warning: stream reset"
  timeout_fail_pod=virt-launcher-worker-a-11111
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-trunc-err

  run_capture

  # Same discipline as the silent branch: name a file only when it is there.
  d="$COZY_REPORT_DIR/snapshots/console-trunc-err/tenant-serial-console"
  assert_file_contains 'truncated' "$d/virt-launcher-worker-a-11111.log"
  assert_file_contains 'read-error.log' "$d/virt-launcher-worker-a-11111.log"
  assert_file_contains 'stream reset' "$d/virt-launcher-worker-a-11111.read-error.log"
  rm -rf "$tmp"
}

@test "a truncated read with no error file does not name one" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  timeout_fail_pod=virt-launcher-worker-a-11111
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-trunc-noerr

  run_capture

  d="$COZY_REPORT_DIR/snapshots/console-trunc-noerr/tenant-serial-console"
  assert_file_contains 'truncated' "$d/virt-launcher-worker-a-11111.log"
  assert_file_lacks_pattern 'read-error.log' "$d/virt-launcher-worker-a-11111.log"
  rm -rf "$tmp"
}

@test "the typical-cost figure in both chainsaw comments counts the Pod-state reads" {
  # A comment carrying a number that the code contradicts is worse than no
  # number: it outlives the session and is trusted by the next reader.
  for f in hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml \
           hack/e2e-chainsaw/kubernetes-previous/chainsaw-test.yaml; do
    grep -q '4m05s' "$f" || {
      echo "expected $f to state the minReplicas-2 cost including the wedge and Pod-state reads" >&2
      return 1
    }
    grep -q '6m25s' "$f" || {
      echo "expected $f to state the worst-case cost including the wedge read" >&2
      return 1
    }
  done
}

@test "the attach read gives every matched Pod a non-empty line" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names="virt-launcher-worker-a-11111=guest-console-log"

  ( set +x; cozy_assert_guest_console_attached )

  # Asserted on the query rather than the answer, because no mock can stand in
  # for kubectl's jsonpath engine and this is the property the answer depends
  # on. Without the Pod name, a Pod whose initContainers list is absent emits a
  # bare newline that command substitution strips, so "Pods matched, none
  # carries the container" arrives byte-identical to "no Pod matched" -- and
  # the check silently stops being able to report the one thing it is for.
  assert_file_contains '{.metadata.name}{"="}{.spec.initContainers[*].name}' "$kubectl_calls"
  rm -rf "$tmp"
}

@test "the failure-path diagnostics carry a client deadline too" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names="virt-launcher-worker-a-11111="

  attach_rc=0
  ( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?

  [ "$attach_rc" -eq 2 ]
  # timeout bounds the process; --request-timeout bounds the call the process
  # makes. The wrapper alone leaves kubectl free to sit in its own retry loop
  # for the whole 30s and return nothing.
  [ "$(grep -c -- '--request-timeout=30s' "$kubectl_calls")" -eq 3 ]
  rm -rf "$tmp"
}

@test "one Pod carrying the container is enough to pass" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names="$(printf 'virt-launcher-worker-a-11111=\nvirt-launcher-worker-b-22222=guest-console-log')"

  attach_rc=0
  ( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?

  # Deliberately "any", not "every": the namespace is not scoped to this
  # release, so an every-Pod rule would fail a run over a Pod that is not
  # this test's. It can miss; it cannot false-fail.
  [ "$attach_rc" -eq 0 ]
  rm -rf "$tmp"
}

@test "the attach read is bounded like every other read here" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_init_names="virt-launcher-worker-a-11111=guest-console-log"

  ( set +x; cozy_assert_guest_console_attached )

  assert_file_contains '-k 5 30 kubectl -n tenant-test get pods' "$timeout_calls"
  rm -rf "$tmp"
}

@test "only an answered read that lacks the container fails the suite" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  # The two non-zero outcomes must not be treated alike: exit 1 says the read
  # did not answer and is not evidence, exit 2 says it did and the container
  # is absent.
  grep -q 'if \[ "${attach_rc}" -eq 2 \]; then' "$lib"
}

@test "the console capture stops at a cap and records what it dropped" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="p1 p2 p3 p4 p5 p6 p7 p8"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-capped

  run_capture

  d="$COZY_REPORT_DIR/snapshots/console-capped/tenant-serial-console"
  [ -f "$d/p6.log" ]
  [ ! -f "$d/p7.log" ]
  assert_file_contains 'stopped after 6 Pods; 8 matched in total' "$d/COLLECTION-TRUNCATED.txt"
  rm -rf "$tmp"
}

@test "an uncapped pool leaves no truncation marker" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="p1 p2"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-uncapped

  run_capture

  [ ! -f "$COZY_REPORT_DIR/snapshots/console-uncapped/tenant-serial-console/COLLECTION-TRUNCATED.txt" ]
  rm -rf "$tmp"
}

@test "the attach check sits on the passing path, after the functional block" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  # Two placements are wrong and this pins against both. In the failure block
  # it would answer only in the run where the console can no longer be
  # recovered. Ahead of the functional assertions it would let a diagnostic
  # preempt the checks the suite exists for.
  gate=$(grep -n '^ *cozy_assert_guest_console_attached || attach_rc=$?$' "$lib" | head -n 1 | cut -d: -f1)
  capture=$(grep -n "^ *cozy_capture_tenant_serial_console 'node-join failed" "$lib" | head -n 1 | cut -d: -f1)
  if [ -z "$gate" ]; then
    echo "expected the suite to verify guest-console-log attached on the passing path" >&2
    return 1
  fi
  if [ -z "$capture" ]; then
    echo "expected to find the failure-path console capture in $lib" >&2
    return 1
  fi
  if [ "$gate" -le "$capture" ]; then
    echo "the attach check (line $gate) must sit after node-join succeeds, not in the failure block (line $capture)" >&2
    return 1
  fi
  # Anchored on the LAST functional assertion in the function, not a
  # mid-file one, or everything between it and the gate is unprotected.
  #
  # On the call rather than on the text of its arguments. The arguments name
  # local variables of the library, which a refactor there is free to rename
  # without touching what this test asserts; an anchor that spells them out
  # stops matching on that rename, and the failure lands on whoever rebases
  # second rather than on either author. The call is what the ordering is
  # about, so it is what the anchor reads.
  last=$(grep -n '^ *cozy_guard_all_helmreleases ' "$lib" | tail -n 1 | cut -d: -f1)
  if [ -z "$last" ]; then
    echo "expected to find the HelmRelease remediation guard in $lib" >&2
    return 1
  fi
  if [ "$gate" -le "$last" ]; then
    echo "the attach check (line $gate) must sit after every functional assertion (last at line $last)" >&2
    return 1
  fi
}

@test "the node-join failure path captures the console before the talosctl capture" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  # Ordering is the whole point: the talosctl capture needs an apid that the
  # failure class this collects never reached, so running it first spends the
  # failure path's budget on the collector that cannot answer.
  console=$(grep -n "^ *cozy_capture_tenant_serial_console 'node-join failed" "$lib" | head -n 1 | cut -d: -f1)
  talos=$(grep -n '^ *cozy_capture_tenant_talos "${test_name}" || true$' "$lib" | head -n 1 | cut -d: -f1)
  if [ -z "$console" ]; then
    echo "expected the node-join failure path to capture the guest serial console" >&2
    return 1
  fi
  if [ -z "$talos" ]; then
    echo "expected to find the talosctl capture call in $lib" >&2
    return 1
  fi
  if [ "$console" -ge "$talos" ]; then
    echo "serial console capture (line $console) must run before the talosctl capture (line $talos)" >&2
    return 1
  fi
}

@test "the passing path captures the console and leaves teardown to Chainsaw finally" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  # An instrument that only runs on the failure path answers in one direction.
  # Every console this tree has ever captured came from a run that failed, so
  # the boot stages a healthy worker goes through, and how long each takes, have
  # no measured value to compare a failing run against -- and "slower than
  # usual" is the claim the failure keeps resting on. The passing run is where
  # that value comes from, and it exists only until the tenant is deleted.
  gate=$(grep -n '^ *cozy_assert_guest_console_attached || attach_rc=$?$' "$lib" | head -n 1 | cut -d: -f1)
  green=$(grep -n "^ *if ! cozy_capture_tenant_serial_console 'the suite passed" "$lib" | head -n 1 | cut -d: -f1)
  function_end=$(awk -v start="$green" 'NR > start && /^}$/ { print NR; exit }' "$lib")
  for v in gate green function_end; do
    eval "n=\$$v"
    if [ -z "$n" ]; then
      echo "expected to locate $v in $lib" >&2
      return 1
    fi
  done
  # After the attach check, because that check is what says there is a container
  # to read. The function must not start an asynchronous tenant teardown after
  # the capture: each suite's finally block owns the single authoritative delete.
  if [ "$green" -le "$gate" ]; then
    echo "the passing-path capture (line $green) must sit after the attach check ($gate)" >&2
    return 1
  fi
  if sed -n "${green},${function_end}p" "$lib" | grep -Eq 'delete kubernetes(nodeses|es)[.]apps[.]cozystack[.]io'; then
    echo "run_kubernetes_test still starts tenant teardown on its success path" >&2
    return 1
  fi
  for suite in latest previous; do
    grep -Fq "cozy_cleanup test-${suite}-version" "hack/e2e-chainsaw/kubernetes-${suite}/chainsaw-test.yaml" || {
      echo "kubernetes-${suite} finally does not own authoritative tenant teardown" >&2
      return 1
    }
  done
}

@test "the passing path asks for the pool minimum, not the pool" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  # The failure path runs inside a phase whose budget is derived against the
  # whole operation, so it can afford the pool at its maximum. The passing path
  # has neither a phase nor a budget: it runs inside the same 67m operation the
  # suite has already spent most of, and every read it issues is bounded
  # individually but not as a group. A walk over the maximum pool could
  # therefore turn a suite that proved everything it exists to prove into a red
  # run for collecting a debugging aid slowly. Two workers booting is the same
  # baseline as ten, so that is what it asks for.
  green=$(grep -n "cozy_capture_tenant_serial_console 'the suite passed[^']*' 2;" "$lib" | head -n 1 | cut -d: -f1)
  red=$(grep -n "cozy_capture_tenant_serial_console 'node-join failed[^']*' 6 || true" "$lib" | head -n 1 | cut -d: -f1)
  if [ -z "$green" ]; then
    echo "expected the passing path to cap its console walk at the pool minimum" >&2
    return 1
  fi
  if [ -z "$red" ]; then
    echo "expected the failure path to keep covering the pool at its maximum" >&2
    return 1
  fi
}

@test "the walk covers exactly the number of Pods the caller asked for" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-a virt-launcher-b virt-launcher-c"
  kubectl_logs_output=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-cap-arg

  out=$( ( set +x; cozy_capture_tenant_serial_console 'a unit test' 2 ) 2>/dev/null )
  printf '%s\n' "$out" >"$tmp/out"

  # The cap is the caller's, not a literal in the walk, or the passing path
  # cannot be cheaper than the failure path and the argument above is decorative.
  dir="$COZY_REPORT_DIR/snapshots/console-cap-arg/tenant-serial-console"
  [ -f "$dir/virt-launcher-a.log" ]
  [ -f "$dir/virt-launcher-b.log" ]
  [ ! -f "$dir/virt-launcher-c.log" ]
  assert_file_contains 'capture stopped after 2 Pods; 3 matched in total' \
    "$dir/COLLECTION-TRUNCATED.txt"
  # And the count on stdout is over the Pods actually read. The cap probe runs
  # on the Pod past the cap, so the counter driving it is one higher than the
  # walk; reported as the denominator it would name a number that is neither
  # what was read nor what matched, and disagree with the truncation marker
  # written beside it -- on the path where nobody opens that marker.
  assert_file_contains 'of the 2 guest consoles read, 2 came back empty and 0 were cut short' "$tmp/out"
  rm -rf "$tmp"
}

@test "a passing-path capture that failed is said out loud instead of failing the suite" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  # Two failure modes, and this pins against both. Left bare, a non-zero return
  # would fail a suite that proved everything it exists to prove, on a
  # debugging aid. Silenced with `|| true`, a capture that collected nothing
  # would leave a green run whose only trace of the gap is an artifact nobody
  # opens on a green run.
  green=$(grep -n "^ *if ! cozy_capture_tenant_serial_console 'the suite passed" "$lib" | head -n 1 | cut -d: -f1)
  if [ -z "$green" ]; then
    echo "expected the passing path to capture the console through a tested call" >&2
    return 1
  fi
  # Bounded by the branch rather than by a line count: the warning is what the
  # branch is for, and a fixed offset would break on a comment being added to it
  # while a whole-file search would accept a warning belonging to something
  # else.
  warn=$(awk -v g="$green" '
    NR <= g { next }
    /^  fi$/ { exit }
    /WARNING: the guest console capture/ { print NR; exit }
  ' "$lib")
  if [ -z "$warn" ]; then
    echo "expected the failed passing-path capture to warn in the job log" >&2
    return 1
  fi
}

@test "the capture says which run produced it" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-context

  run_capture 'the suite passed: captured after the last assertion'

  # Both paths write the same directory, and the two readings mean opposite
  # things: one is the failure being studied, the other is the baseline it is
  # compared against. Which one a tarball holds is otherwise recoverable only
  # from the run's verdict somewhere else entirely.
  assert_file_contains 'the suite passed: captured after the last assertion' \
    "$COZY_REPORT_DIR/snapshots/console-context/tenant-serial-console/CAPTURE-CONTEXT.txt"
  rm -rf "$tmp"
}

@test "a walk that came back with empty consoles says so on stdout" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_logs_output=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-silent-count

  out=$( ( set +x; cozy_capture_tenant_serial_console 'a unit test' 6 ) 2>/dev/null )
  printf '%s\n' "$out" >"$tmp/out"

  # The per-pod notes and POD-STATE.txt already record this in the report, and
  # on the failure path that is enough because the report gets opened. The
  # passing path is the one that does not: a baseline that came back empty
  # there would otherwise be visible only to whoever downloads a green run's
  # artifact. The count is not a failure and the capture still returns zero, so
  # stdout is the only place the two callers share.
  assert_file_contains 'of the 1 guest consoles read, 1 came back empty and 0 were cut short' "$tmp/out"
  rm -rf "$tmp"
}

@test "a healthy walk says nothing about silent consoles" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-silent-none

  out=$( ( set +x; cozy_capture_tenant_serial_console 'a unit test' 6 ) 2>/dev/null )
  printf '%s\n' "$out" >"$tmp/out"

  # The other half of the pair. A line printed on every run is a line nobody
  # reads, and this one is meant to be read on the green path.
  assert_file_lacks_pattern 'guest consoles read, ' "$tmp/out"
  rm -rf "$tmp"
}

@test "the context is written even when there is nothing to capture" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-context-empty
  rc=0

  run_capture 'the suite passed: captured after the last assertion' || rc=$?

  # The empty case is the one where the context matters most: a directory
  # holding only a failure note is where a reader most needs to know which run
  # left it, and the early return is the path most likely to skip writing it.
  [ "$rc" -ne 0 ]
  assert_file_contains 'the suite passed' \
    "$COZY_REPORT_DIR/snapshots/console-context-empty/tenant-serial-console/CAPTURE-CONTEXT.txt"
  rm -rf "$tmp"
}

@test "each console says when it was read, so its last kernel stamp has a wall clock to sit against" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-read-instant

  run_capture

  # A console log is stamped in guest seconds since the guest's own kernel
  # started, and it ends wherever the guest had got to when the read ran. Those
  # two facts alone cannot say whether a short log means a guest that started
  # late or a guest that went quiet, because the instant of the read is not in
  # the file. Without this line the only trace of it is the artifact's mtime,
  # which is metadata rather than content: nothing in the report says it, and a
  # reader has to know to go looking outside the file for it.
  f="$COZY_REPORT_DIR/snapshots/console-read-instant/tenant-serial-console/virt-launcher-worker-a-11111.log"
  assert_file_contains 'console read started at ' "$f"
  assert_file_contains ' epoch seconds' "$f"
  rm -rf "$tmp"
}

@test "the capture records when each guest started, beside the console it captured" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline

  run_capture

  # The other end of the subtraction. With the read instant above and the
  # compute container's start here, how long the guest had been alive when the
  # console was read is one subtraction away, and comparing that against the
  # last stamp in the log says which of the two happened: a guest that had not
  # lived long enough to reach a milestone, or one that lived and stopped
  # printing. Without it both look like the same short file.
  f="$COZY_REPORT_DIR/snapshots/console-timeline/tenant-serial-console/CAPTURE-TIMELINE.txt"
  assert_file_contains 'computeStartedAt=2026-08-15T14:35:47Z' "$f"
  # The subtraction is spelled out rather than left to be reconstructed. This
  # file is read by whoever opens a failed run's tarball months later, and the
  # relation between a guest-relative kernel stamp and two absolute instants is
  # exactly the step a reader gets wrong.
  assert_file_contains 'epoch seconds' "$f"
  rm -rf "$tmp"
}

@test "a start-time read that failed is named rather than left out" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  # Non-zero AND nothing written: the read that never got a row across, which is
  # a different fact from the one cut off part way through and takes a different
  # note. Both have to be staged explicitly or the mock decides which arm runs.
  kubectl_timeline_rc=9
  kubectl_timeline_rows=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline-failed

  run_capture

  # An absent file reads as a capture that had no reason to write one, which is
  # the silence every collector here is built to refuse. The read failing is a
  # statement about this machine's view of the cluster, not about when the
  # guests started, and the file has to say which of those it is holding.
  f="$COZY_REPORT_DIR/snapshots/console-timeline-failed/tenant-serial-console/CAPTURE-TIMELINE.txt"
  assert_file_contains 'exit 9' "$f"
  assert_file_lacks_pattern 'computeStartedAt=2026' "$f"
  rm -rf "$tmp"
}

@test "a start-time read cut off part way is not reported as having observed nothing" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_timeline_rc=124
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline-cut

  run_capture

  # A read killed at its bound can leave rows behind and still report failure.
  # Saying "when these guests started was not observed" under start times that
  # are plainly sitting there makes the file contradict itself, and a reader who
  # believes the note discards the rows -- which for a capture that exists to be
  # subtracted is the whole value of the file.
  f="$COZY_REPORT_DIR/snapshots/console-timeline-cut/tenant-serial-console/CAPTURE-TIMELINE.txt"
  assert_file_contains 'computeStartedAt=2026-08-15T14:35:47Z' "$f"
  assert_file_contains 'cut off (exit 124)' "$f"
  assert_file_lacks_pattern 'was not observed, which is not the same' "$f"
  rm -rf "$tmp"
}

@test "a start-time read that answered with nothing is not reported as a start time" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_timeline_rows=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline-empty

  run_capture

  # The third outcome, and the one that looks most like success: the read
  # answered, and answered with nothing. A zero-length file here would be read
  # as a capture that ran and found the guests had no start times, which is not
  # a state a Pod can be in -- the walk above just read consoles from these
  # same Pods.
  f="$COZY_REPORT_DIR/snapshots/console-timeline-empty/tenant-serial-console/CAPTURE-TIMELINE.txt"
  assert_file_contains 'returned no rows' "$f"
  rm -rf "$tmp"
}

@test "the start-time read is bounded like every other read in this capture" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline-bound

  run_capture

  # This read runs on the same failure path as the rest, where the apiserver is
  # the component least likely to answer. An unbounded one does not lose only
  # itself: it holds the op until the op is killed, and the tenant snapshot
  # queued behind it is lost rather than truncated.
  assert_file_contains 'computeStartedAt' "$timeout_calls"
  assert_file_contains 'request-timeout' "$kubectl_calls"
  rm -rf "$tmp"
}

@test "a warning beside a healthy start-time read is not filed as an error" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_timeline_stderr='Warning: v1 Pod is deprecated in this build'
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline-warn

  run_capture

  # kubectl writes deprecation and partial-result warnings on stderr with a zero
  # exit, so what a message means is decided by the status and not by the fact
  # that something was written. Filed as an error, a healthy read sends the next
  # reader after a failure that did not happen; folded into the rows, it sits
  # inside the data the success probe reads.
  dir="$COZY_REPORT_DIR/snapshots/console-timeline-warn/tenant-serial-console"
  assert_file_contains 'v1 Pod is deprecated' "$dir/CAPTURE-TIMELINE-warnings.txt"
  [ ! -f "$dir/CAPTURE-TIMELINE-error.log" ]
  assert_file_lacks_pattern 'deprecated' "$dir/CAPTURE-TIMELINE.txt"
  rm -rf "$tmp"
}

@test "the stderr of a start-time read that failed is kept as the diagnosis" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_timeline_rc=1
  kubectl_timeline_rows=
  kubectl_timeline_stderr='error: the server could not find the requested resource'
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline-err

  run_capture

  # The other direction: on a failed read that message IS the diagnosis, and
  # dropping it leaves a note saying the read failed with nothing saying why.
  # The note has to name the file, or the evidence sits in a directory the
  # reader has no reason to open.
  dir="$COZY_REPORT_DIR/snapshots/console-timeline-err/tenant-serial-console"
  assert_file_contains 'could not find the requested resource' "$dir/CAPTURE-TIMELINE-error.log"
  [ ! -f "$dir/CAPTURE-TIMELINE-warnings.txt" ]
  assert_file_contains 'CAPTURE-TIMELINE-error.log' "$dir/CAPTURE-TIMELINE.txt"
  rm -rf "$tmp"
}

@test "a start-time read that failed without a word does not point at a missing file" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  kubectl_timeline_rc=124
  kubectl_timeline_rows=
  kubectl_timeline_stderr=
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline-silent

  run_capture

  # timeout kills the child without a word, so this read can fail having written
  # to neither stream. Naming the error log unconditionally would then send the
  # reader after a file that was never created, which is the same trap the
  # per-Pod notes above are written to avoid.
  dir="$COZY_REPORT_DIR/snapshots/console-timeline-silent/tenant-serial-console"
  [ ! -f "$dir/CAPTURE-TIMELINE-error.log" ]
  assert_file_contains 'gave no reason on either stream' "$dir/CAPTURE-TIMELINE.txt"
  assert_file_lacks_pattern 'reason it gave is in' "$dir/CAPTURE-TIMELINE.txt"
  rm -rf "$tmp"
}

@test "the console container's start is asked for in both status lists" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  kubectl_calls="$tmp/kubectl.calls"
  timeout_calls="$tmp/timeout.calls"
  kubectl_pod_names="virt-launcher-worker-a-11111"
  COZY_REPORT_DIR="$tmp/report"
  COZY_SNAPSHOT_NAME=console-timeline-lists

  run_capture

  # KubeVirt runs guest-console-log as an init container, so its runtime state
  # lives under initContainerStatuses. Asking only containerStatuses returns
  # empty for a container that started perfectly well, and an empty start time
  # here is supposed to mean the container never ran -- the one reading this
  # field exists to support. The attach check reads both spec lists for the
  # same reason; a missing key costs an empty string, so naming both is free.
  assert_file_contains 'initContainerStatuses[?(@.name=="guest-console-log")]' "$kubectl_calls"
  assert_file_contains 'containerStatuses[?(@.name=="compute")]' "$kubectl_calls"
  rm -rf "$tmp"
}

@test "a run near its operation ceiling declines the baseline instead of being killed collecting it" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  # The capture costs real wall clock and runs after everything the suite
  # proves. Inside an operation with a fixed ceiling that makes it a way for a
  # green run to end red, which is the one thing a passing-path collector may
  # not do. Driven rather than grepped: the direction of the comparison IS the
  # safety property, and an inverted one runs the capture on exactly the runs
  # that cannot afford it while every structural check still passes.
  date_now=1000000000
  _COZY_RUN_STARTED_AT=$(( date_now - (COZY_OP_CEILING - COZY_GREEN_CAPTURE_RESERVE) - 1 ))
  rc=0
  ( set +x; cozy_green_capture_has_room 'the baseline' ) >"$tmp/out" 2>&1 || rc=$?

  [ "$rc" -eq 1 ]
  # And the decline is spoken. A collector that stops running silently leaves a
  # green run that looks exactly like one where the baseline was collected and
  # came back empty, which is the reading every note in this file exists to
  # prevent.
  assert_file_contains 'not collected' "$tmp/out"
  assert_file_contains 'the baseline' "$tmp/out"
  rm -rf "$tmp"
}

@test "a run with the operation's room to spare collects the baseline" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  # The other direction, and the one an inverted comparison breaks silently: a
  # gate that declines a run with forty minutes left costs the baseline on every
  # healthy run, and the only symptom is a diagnostic that stopped appearing.
  date_now=1000000000
  _COZY_RUN_STARTED_AT=$(( date_now - 60 ))
  rc=0
  ( set +x; cozy_green_capture_has_room 'the baseline' ) >"$tmp/out" 2>&1 || rc=$?

  [ "$rc" -eq 0 ]
  assert_file_lacks_pattern 'not collected' "$tmp/out"
  rm -rf "$tmp"
}

@test "a run whose start was never stamped declines rather than dying on the arithmetic" {
  . hack/e2e-chainsaw/_lib/run-kubernetes.sh
  tmp=$(mktemp -d)
  # The stamp is set by run_kubernetes_test, which no unit test invokes, so
  # nothing else here would notice it being deleted. Under the `set -eu` the
  # chainsaw script runs with, an unbound name inside the arithmetic aborts the
  # whole function -- ending a passing run red, which is the outcome this gate
  # exists to prevent. Declared zero at file scope, it fails closed instead.
  date_now=1000000000
  rc=0
  ( set -u; set +x; cozy_green_capture_has_room 'the baseline' ) >"$tmp/out" 2>&1 || rc=$?

  [ "$rc" -eq 1 ]
  assert_file_contains 'not collected' "$tmp/out"
  rm -rf "$tmp"
}

@test "the passing path decides through the gate rather than around it" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  # The behavioural tests above cover the predicate. Its wiring is what they
  # cannot reach: run_kubernetes_test is not callable from a unit test, so the
  # stamp it takes and the gate it calls are only assertable as source. Both
  # halves fail the same silent way -- a correct predicate nothing calls
  # protects nothing, and a predicate called without the stamp reads the whole
  # epoch as elapsed and declines the baseline on every run forever.
  stamp=$(grep -n '^  _COZY_RUN_STARTED_AT=\$(date +%s)$' "$lib" | head -n 1 | cut -d: -f1)
  gate=$(grep -n 'cozy_green_capture_has_room ' "$lib" | tail -n 1 | cut -d: -f1)
  green=$(grep -n "cozy_capture_tenant_serial_console 'the suite passed" "$lib" | head -n 1 | cut -d: -f1)
  for v in stamp gate green; do
    eval "n=\$$v"
    if [ -z "$n" ]; then
      echo "expected to locate $v in $lib" >&2
      return 1
    fi
  done
  # Inside the function whose elapsed time it measures, not at file scope: taken
  # once when the library is sourced, it would time the sourcing rather than the
  # run, and every suite after the first would read as already over its ceiling.
  fn=$(grep -n '^run_kubernetes_test() {$' "$lib" | head -n 1 | cut -d: -f1)
  if [ -z "$fn" ] || [ "$stamp" -le "$fn" ]; then
    echo "the run's start stamp (line $stamp) must be taken inside run_kubernetes_test (line $fn)" >&2
    return 1
  fi
  if [ "$stamp" -ge "$gate" ] || [ "$gate" -ge "$green" ]; then
    echo "expected the stamp ($stamp) before the gate ($gate) before the capture ($green)" >&2
    return 1
  fi
}

@test "the operation ceiling the reserve is measured against is the one the suites give" {
  lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
  # A reserve is only meaningful against the right ceiling. This number is a
  # restatement of one that lives in the suite files, so it is read from both
  # sides rather than trusted: a suite that raises or lowers its `timeout:`
  # otherwise leaves the reserve protecting a ceiling that moved, and the
  # decline either stops firing when it should or starts firing when it should
  # not.
  ceiling=$(grep -oE '^COZY_OP_CEILING=[0-9]+' "$lib" | head -n 1 | sed -E 's/.*=//')
  reserve=$(grep -oE '^COZY_GREEN_CAPTURE_RESERVE=[0-9]+' "$lib" | head -n 1 | sed -E 's/.*=//')
  for v in ceiling reserve; do
    eval "n=\$$v"
    if [ -z "$n" ]; then
      echo "expected to read $v from $lib; without it this guard reports success for having lost its input" >&2
      return 1
    fi
  done
  for f in hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml \
           hack/e2e-chainsaw/kubernetes-previous/chainsaw-test.yaml; do
    # The first `timeout:` in each file is the bringup op's, which is the one
    # this whole helper runs inside; the teardown step below it has its own.
    minutes=$(grep -oE '^ *timeout: [0-9]+m$' "$f" | head -n 1 | grep -oE '[0-9]+')
    if [ -z "$minutes" ]; then
      echo "expected to read the operation timeout from $f" >&2
      return 1
    fi
    if [ "$((minutes * 60))" -ne "$ceiling" ]; then
      echo "$f gives the operation ${minutes}m but the reserve is measured against ${ceiling}s" >&2
      return 1
    fi
  done
  # And the reserve has to leave room for the capture it is reserving for, or it
  # protects nothing: the walk it admits would still run past the ceiling.
  if [ "$reserve" -lt 210 ]; then
    echo "the reserve ${reserve}s is under the 210s the capture can cost at this caller's cap" >&2
    return 1
  fi
}

@test "the e2e tenant node group asks KubeVirt for the guest console log" {
  # Anchored to a bare two-space-indented key so a comment or a prose mention
  # of the field cannot stand in for the setting itself. Since the Phase 2 split
  # the worker pool is a separate KubernetesNodes CR whose fields sit two spaces
  # under spec, not six spaces under a nodeGroups.<name> block. It does not prove
  # the key sits in that CR specifically -- the render is what would catch that,
  # and the chart's own suites cover it.
  grep -q '^  logSerialConsole: true$' hack/e2e-chainsaw/_lib/run-kubernetes.sh
}