Ctrl K
ComunidadGuías…

hack/seaweedfs-naming-audit.bats

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for hack/seaweedfs-naming-audit.sh.
#
# The chart's naming guard refuses whenever both naming generations exist, so the
# audit IS the classifier — it is what an operator acts on, and acting on it
# deletes PVCs. Two earlier revisions of this classification shipped as an
# untested shell snippet inside the runbook, and both were wrong in ways that
# routed a live tenant into the step that strands its data:
#
#   - a selector `^data1-(.*seaweedfs.*)-volume` also matched the CHART-named
#     claims, so the release-named age range spanned both generations, every
#     tenant read as "ranges overlap / mid-rebind", and a genuine S-damaged tenant
#     was routed AWAY from Step 2a (which would have been correct) into Step 2,
#     which deletes its data claim;
#   - matching claims by name with `grep seaweedfs` cannot see a long instance
#     name, whose claims the chart truncates past `seaweedfs`, so such a tenant
#     read as "L — nothing to do" while the chart refused it.
#
# Moving the classifier into a file with tests is the point. These drive it
# against a fake kubectl, mocking only the cluster boundary.
#
# CI runs this file under Bats through `make bats-unit-tests`. It also remains
# compatible with the legacy `hack/cozytest.sh` translator, whose awk parser
# recognizes only @test blocks and a bare `}` on its own line. The tests avoid
# `run`, `$status`, and setup() for that compatibility path.
#
# Run with: bats hack/seaweedfs-naming-audit.bats
# -----------------------------------------------------------------------------

load test_helper

SEAWEEDFS_AUDIT_LIB=1
export SEAWEEDFS_AUDIT_LIB
# shellcheck source=seaweedfs-naming-audit.sh
. "$PWD/hack/seaweedfs-naming-audit.sh"

@test "reconstructs the renamed volume prefix for a default instance" {
  [ "$(renamed_volume_prefix seaweedfs-system)" = "seaweedfs-system-volume" ]
}

@test "reconstructs the renamed volume prefix for a non-default instance" {
  # The release name does not contain the chart name, so 4.31 appends it.
  [ "$(renamed_volume_prefix foo-system)" = "foo-system-seaweedfs-volume" ]
}

@test "reconstructs the truncated prefix for a long instance name" {
  # componentName cuts the fullname to 62-len("volume")=56 before appending, so
  # `seaweedfs` falls off the tail — the case a `grep seaweedfs` name match cannot
  # see, and the reason this is reconstructed rather than pattern-matched.
  got=$(renamed_volume_prefix archive-of-quarterly-financial-statements-x1-system)
  [ "$got" = "archive-of-quarterly-financial-statements-x1-system-seaw-volume" ]
  # 56 chars of fullname + "-volume"
  [ "${#got}" -eq 63 ]
}

@test "reconstructs a distinct prefix for an instance named seaweedfs-volume" {
  # The pathological case: this instance's RELEASE-named objects
  # (seaweedfs-volume-system-volume, data1-seaweedfs-volume-system-volume-0) also
  # satisfy the CHART-named prefixes. The reconstruction must return the
  # release-named prefix so the release-named branch can be tested first.
  got=$(renamed_volume_prefix seaweedfs-volume-system)
  [ "$got" = "seaweedfs-volume-system-volume" ]
  # It must NOT collide with the chart-named prefix.
  [ "$got" != "seaweedfs-volume" ]
}

@test "the reconstructed prefix never equals the chart-named prefix" {
  # If it did, both generations would match one branch and the guard/audit could
  # not separate them at all.
  for r in seaweedfs-system foo-system seaweedfs-volume-system a-system; do
    [ "$(renamed_volume_prefix "$r")" != "seaweedfs-volume" ]
  done
}

@test "clean duplicate: release-named PVs all strictly newer => legacy is original" {
  # A tenant that passed through the 4.31 rename: legacy PVs at install time,
  # duplicate PVs provisioned by the bad upgrade much later.
  [ "$(classify_mixed_direction 1000 1010 5000 5020)" = "legacy-original" ]
}

@test "S-damaged: chart-named PVs all strictly newer => release-named is original" {
  # Installed fresh on 1.5.x (release-named PVs first); an unguarded 1.6 upgrade
  # then created empty chart-named claims beside them.
  [ "$(classify_mixed_direction 5000 5020 1000 1010)" = "renamed-original" ]
}

@test "interrupted Step 2 re-bind: overlapping vintages => no candidate" {
  # Step 2 re-binds release-named claims onto their ORIGINAL PVs under chart
  # names, one claim at a time. Interrupted part-way, BOTH generations sit on
  # original-vintage PVs, so the ranges interleave. The old absolute-window
  # classifier fell through to a coin flip here and its advice deleted the
  # un-re-bound claims; the relative rule must refuse instead.
  [ "$(classify_mixed_direction 1000 1010 1005 1015)" = "overlap" ]
}

@test "a tie is overlap, not a candidate" {
  # Second-resolution timestamps: a duplicate provisioned within the same second
  # as the newest original PV is not STRICTLY newer. Refusing is recoverable;
  # naming the wrong candidate is not.
  [ "$(classify_mixed_direction 1000 1010 1010 1020)" = "overlap" ]
}

@test "direction needs no clock: vintages far from any anchor still classify" {
  # The shipped StorageClasses are WaitForFirstConsumer, so PVs appear at
  # pod-SCHEDULE time — on a cold cluster minutes after first_deployed. The rule
  # must not care: only the two generations' ranges relative to EACH OTHER count.
  # (The previous classifier anchored a 120s window on first_deployed and
  # misclassified exactly this case.)
  [ "$(classify_mixed_direction 100000 100600 200000 200600)" = "legacy-original" ]
}

@test "the audit's reconstruction agrees with the chart helper it mirrors" {
  # hack/seaweedfs-naming-audit.sh and
  # packages/system/seaweedfs/templates/_naming.tpl reimplement the same two
  # upstream helpers in two languages. If they drift, the audit classifies a
  # tenant differently from the render that refuses it, and the operator is
  # working from a different picture than the chart. Render the chart helper
  # through helm and compare it to this script's output, release by release.
  chart=$(mktemp -d)
  printf 'apiVersion: v2\nname: probe\nversion: 0.0.0\n' > "$chart/Chart.yaml"
  mkdir -p "$chart/templates"
  cp packages/system/seaweedfs/templates/_naming.tpl "$chart/templates/"
  cat > "$chart/templates/out.yaml" <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: probe
data:
{{- range $r := list "seaweedfs-system" "foo-system" "seaweedfs-volume-system" "archive-of-quarterly-financial-statements-x1-system" "a-system" }}
  {{ $r }}: {{ include "seaweedfs.renamedVolumePrefix" $r }}
{{- end }}
EOF
  helm template probe "$chart" 2>/dev/null | sed -n 's/^  \([a-z0-9-]*-system\): \(.*\)$/\1=\2/p' > "$chart/rendered"
  [ -s "$chart/rendered" ]
  [ "$(wc -l < "$chart/rendered")" -eq 5 ]
  while IFS='=' read -r rel expected; do
    [ -n "$rel" ] || continue
    got=$(renamed_volume_prefix "$rel")
    echo "chart: $rel -> $expected ; audit: $got"
    [ "$got" = "$expected" ]
  done < "$chart/rendered"
  rm -rf "$chart"
}

# -----------------------------------------------------------------------------
# Fail-closed tests (issue #3431).
#
# The classification tests above mock nothing below the classifier. These drive
# the KUBECTL layer, where the fail-open bug lived: a kubectl call that failed
# used to return empty stdout, byte-identical to a genuinely clean fleet, so the
# audit printed an empty table and exited 0 -- the exact false "nothing to do" the
# operator is told to trust before deleting PVCs. They shim `kubectl` on PATH with
# a fake and assert the audit fails LOUDLY (non-zero exit + a FATAL naming the
# query) on any real error, while still treating a genuinely-absent object as
# clean. Two families:
#
#   * enumerating LISTs (get ns / secret / pvc / sts) -- a failure is always fatal;
#   * by-NAME GETs (a release secret in system_releases, a PVC/PV in pv_epoch) --
#     `--ignore-not-found` splits a real error (fatal) from a legitimate absence:
#     an absent release revision is a clean skip; an absent PV degrades to the
#     safe "cannot establish direction" note and must NOT become a deletion
#     candidate (the range-narrowing flip Codex reproduced).
#
# The two golden tests guard the reverse: on a healthy cluster the output stays
# byte-for-byte what it was on origin/main, including the MIXED path that exercises
# every pv_epoch GET.
#
# The fake is a real executable on PATH, so it exercises the actual exit-status
# handling in run_kubectl and the propagation up through every caller -- a for
# loop over `$(...)` swallows the status, so this is where a regression would hide.

# _release_blob [chart-name] -- the value kubectl returns for a Helm release
# secret's `.data.release`: base64(base64(gzip(json))). release_json base64-decodes
# twice and gunzips it, and system_releases reads the chart name from
# chart.metadata.name (as real Helm payloads carry it). Defaults to a cozy-seaweedfs
# release so the audit confirms the tenant; pass another chart name for a non-
# SeaweedFS release, or the literal EMPTY for a chartless {} payload.
#
# Three payload SHAPES beyond the default compact one, because the chart-name
# extraction is now fatal on a miss and every shape below is something a
# re-serializer or a user's values could produce:
#   SPACED  whitespace around the JSON punctuation (json.dumps' default)
#   PRETTY  indented and MULTI-LINE (jq . / yq -o=json), which a line-based
#           matcher cannot read at all unless newlines are folded first
#   DECOY   a real cozy-seaweedfs chart PLUS a values subtree that spells
#           chart.metadata.name with a different value. Helm marshals "config"
#           (the values) AFTER "chart", so a last-match extraction returns the
#           decoy and silently declares the tenant non-SeaweedFS.
#   FIRSTDEP a payload that carries info.first_deployed, so the first_deployed
#           date call site (the other one #4273 names) is actually exercised --
#           the default blob omits the field, so first_deployed returns early
#           without ever touching date.
_release_blob() {
  _rb_name=${1:-cozy-seaweedfs}
  case "$_rb_name" in
    EMPTY)  _rb_json='{}' ;;
    SPACED) _rb_json='{"name": "seaweedfs-system", "chart": {"metadata": {"name": "cozy-seaweedfs", "version": "1.0.0"}}}' ;;
    PRETTY) _rb_json='{
  "name": "seaweedfs-system",
  "chart": {
    "metadata": {
      "name": "cozy-seaweedfs",
      "version": "1.0.0"
    }
  }
}' ;;
    DECOY)  _rb_json='{"name":"seaweedfs-system","chart":{"metadata":{"name":"cozy-seaweedfs"}},"config":{"chart":{"metadata":{"name":"decoy-not-seaweedfs"}}}}' ;;
    FIRSTDEP) _rb_json='{"name":"seaweedfs-system","info":{"first_deployed":"2019-01-01T00:00:00Z"},"chart":{"metadata":{"name":"cozy-seaweedfs"}}}' ;;
    *)      _rb_json='{"name":"seaweedfs-system","chart":{"metadata":{"name":"'"$_rb_name"'"}}}' ;;
  esac
  printf '%s' "$_rb_json" | gzip | base64 | base64 | tr -d '\n'
}

# _write_fake_kubectl <dir> <fail> <absent-pv> <blob> <pvcs> -- drop a fake kubectl
# into <dir>.
#   <fail>       one of ns | secretlist | secretget | secretget_absent |
#                secret_missing_field | secret_bad_payload | pvclist | pvcget |
#                sts | pvget | "" -- the single call to make behave badly. Real-
#                error targets exit non-zero with a stderr message; secretget_absent
#                mirrors an absent Secret (--ignore-not-found: exit 0, empty -o
#                name); secret_missing_field / secret_bad_payload keep the Secret
#                present but return an empty / undecodable .data.release payload.
#   <absent-pv>  a PV name (pv-legacy|pv-legacy2|pv-renamed) to report as absent
#                (exit 0, empty), exercising the "bound claim, PV gone" path.
#   <pvcs>       newline-separated `get pvc -o name` LIST output.
#   <bad-ts-pv>  optional PV name whose creationTimestamp GET returns a present
#                but UNPARSEABLE value (exit 0, garbage), exercising the parse-
#                failure path #4273 splits from absent evidence.
# It otherwise walks one confirmed seaweedfs-system tenant. The by-name PVC->PV and
# PV->timestamp maps are fixed here; timestamps are chosen so the two legacy PVs
# straddle the single renamed PV (true answer OVERLAP), which is what makes the
# range-narrowing flip observable. Kept POSIX and free of a column-0 `}` so
# cozytest.sh's awk converter passes the heredoc through untouched.
_write_fake_kubectl() {
  # Grouped redirect (one open of the file). The closing brace is indented, so
  # cozytest.sh's awk -- which only rewrites a `}` in column 0 -- leaves it and the
  # heredoc alone.
  {
    printf '#!/bin/sh\n'
    printf "FAIL='%s'\n" "$2"
    printf "ABSENT_PV='%s'\n" "$3"
    printf "BLOB='%s'\n" "$4"
    printf "PVCS='%s'\n" "$5"
    printf "BAD_TS_PV='%s'\n" "${6:-}"
    cat <<'FAKE'
verb=${1:-}; res=${2:-}; args="$*"
fail() { echo "fake kubectl: $1 (real error)" >&2; exit 1; }
# Anything this fake does not model must NOT look like a successful empty
# answer: that is the fail-open shape the audited script exists to reject, and
# it would let a new query added to the script pass these goldens unnoticed.
unmodelled() { echo "fake kubectl: unmodelled invocation: $args" >&2; exit 97; }
if [ "$verb $res" = "get ns" ]; then
  [ "$FAIL" = ns ] && fail "get ns"
  printf 'namespace/tenant-test\n'; exit 0
fi
if [ "$verb $res" = "get secret" ]; then
  case "$args" in
    *sh.helm.release.v1*)
      # release_json now asks two questions: existence (-o name) then payload
      # (jsonpath .data.release). Mirror that split so absence, real error, and
      # corrupt-payload are all reachable independently.
      case "$args" in
        *"-o name"*)
          [ "$FAIL" = secretget ] && fail "release secret existence GET"
          [ "$FAIL" = secretget_absent ] && exit 0
          printf 'secret/sh.helm.release.v1.seaweedfs-system.v1\n'; exit 0 ;;
        *)
          [ "$FAIL" = secret_missing_field ] && exit 0
          if [ "$FAIL" = secret_bad_payload ]; then printf '@@@not-base64@@@\n'; exit 0; fi
          printf '%s\n' "$BLOB"; exit 0 ;;
      esac ;;
    *owner=helm*) printf '1\n'; exit 0 ;;
    *) [ "$FAIL" = secretlist ] && fail "namespace secret LIST"
       printf 'seaweedfs-system\n'; exit 0 ;;
  esac
fi
if [ "$verb $res" = "get pvc" ]; then
  case "$args" in
    *"-o name"*)
      [ "$FAIL" = pvclist ] && fail "pvc LIST"
      [ -n "$PVCS" ] && printf '%s\n' "$PVCS"
      exit 0 ;;
    *data1-seaweedfs-system-volume-0*) [ "$FAIL" = pvcget ] && fail "pvc GET"; printf 'pv-renamed\n'; exit 0 ;;
    *data1-seaweedfs-volume-1*)        [ "$FAIL" = pvcget ] && fail "pvc GET"; printf 'pv-legacy2\n'; exit 0 ;;
    *data1-seaweedfs-volume-0*)        [ "$FAIL" = pvcget ] && fail "pvc GET"; printf 'pv-legacy\n'; exit 0 ;;
  esac
  unmodelled
fi
if [ "$verb $res" = "get sts" ]; then
  [ "$FAIL" = sts ] && fail "sts LIST"
  exit 0
fi
if [ "$verb $res" = "get pv" ]; then
  [ "$FAIL" = pvget ] && fail "get pv"
  if [ -n "$BAD_TS_PV" ]; then
    case "$args" in *"$BAD_TS_PV"*) printf 'not-a-timestamp\n'; exit 0 ;; esac
  fi
  case "$args" in
    *pv-legacy2*) [ "$ABSENT_PV" = pv-legacy2 ] && exit 0; printf '2099-01-01T00:00:00Z\n'; exit 0 ;;
    *pv-legacy*)  [ "$ABSENT_PV" = pv-legacy ]  && exit 0; printf '2020-01-01T00:00:00Z\n'; exit 0 ;;
    *pv-renamed*) [ "$ABSENT_PV" = pv-renamed ] && exit 0; printf '2020-06-01T00:00:00Z\n'; exit 0 ;;
  esac
  unmodelled
fi
unmodelled
FAKE
  } > "$1/kubectl"
  chmod +x "$1/kubectl"
}

# _write_fake_date <dir> -- drop a BSD-flavoured `date` shim into <dir>. It rejects
# the GNU `-d` spelling exactly as macOS/BSD date does, forcing the audited script
# down its portable BSD branch, and services that branch with whatever real date
# lives beneath us (GNU on a CI runner, BSD on a developer's mac). That is what
# reproduces issue #4273 on EITHER kind of host: without the shim a GNU runner
# never hits the failing spelling and the bug is invisible in CI. Same heredoc
# discipline as _write_fake_kubectl -- indented grouped-redirect brace, no
# column-0 `}` inside, so cozytest.sh's awk converter leaves it untouched.
_write_fake_date() {
  {
    cat <<'FAKEDATE'
#!/bin/sh
for _a in "$@"; do
  case "$_a" in
    -d) echo "date: illegal option -- d" >&2; exit 1 ;;
  esac
done
# The audit's only other date form: -u -j -f FMT VALUE +%s. Capture BOTH the
# format and the value the audit actually passed, so this shim exercises the
# script's real BSD spelling instead of substituting its own. If it dropped the
# format, a regression in that exact string would ship green -- and the BSD path
# is the whole of issue #4273, so an inert guard here would defeat the test.
_fmt=""; _val=""
while [ "$#" -gt 0 ]; do
  if [ "$1" = -f ]; then _fmt=${2:-}; _val=${3:-}; break; fi
  shift
done
if [ -n "$_val" ]; then
  # A real BSD host has `-j -f`, so honor the format the audit passed: a wrong
  # format fails here exactly as production would.
  /bin/date -u -j -f "$_fmt" "$_val" +%s 2>/dev/null && exit 0
  /usr/bin/date -u -j -f "$_fmt" "$_val" +%s 2>/dev/null && exit 0
  # A GNU-only runner (the CI gate) has no `-j` to enforce the format, so
  # recompute via `-d` -- but ONLY for the exact format the audit is supposed to
  # pass. A regressed format must NOT be rescued by GNU here, or these BSD tests
  # would go green on a broken spelling on the very runner they are meant to guard.
  if [ "$_fmt" = '%Y-%m-%dT%H:%M:%S' ]; then
    /usr/bin/date -u -d "$_val" +%s 2>/dev/null && exit 0
    /bin/date -u -d "$_val" +%s 2>/dev/null && exit 0
  fi
fi
echo "fake date: unsupported invocation: $*" >&2
exit 1
FAKEDATE
  } > "$1/date"
  chmod +x "$1/date"
}

# _pvcs_mixed -- the `get pvc -o name` LIST for a MIXED tenant: two legacy claims
# and one release-named claim, whose PV vintages truly overlap.
_pvcs_mixed() {
  printf '%s\n' \
    persistentvolumeclaim/data1-seaweedfs-volume-0 \
    persistentvolumeclaim/data1-seaweedfs-volume-1 \
    persistentvolumeclaim/data1-seaweedfs-system-volume-0
}

# _expected_L / _expected_mixed_overlap -- golden output built with the SAME printf
# contract the script uses, so a drift in row count, text, or padding fails the diff.
_expected_L() {
  printf '%-24s %-14s %-8s %s\n' NAMESPACE RELEASE CLASS NOTE
  printf '%-24s %-14s %-8s %s\n' --------- ------- ----- ----
  printf '%-24s %-14s %-8s %s\n' tenant-test seaweedfs-system L 'chart-named only; the upgrade adopts it, nothing to do'
}
_expected_mixed_overlap() {
  printf '%-24s %-14s %-8s %s\n' NAMESPACE RELEASE CLASS NOTE
  printf '%-24s %-14s %-8s %s\n' --------- ------- ----- ----
  printf '%-24s %-14s %-8s %s\n' tenant-test seaweedfs-system MIXED 'both generations; the chart REFUSES until one is removed'
  printf '%-24s %-14s %-8s   %s\n' '' '' '' 'PV vintages OVERLAP => no candidate. An interrupted Step 2 re-bind looks exactly like this (both generations on original PVs). Finish Step 2 if one is in progress; otherwise escalate. Do NOT run Step 2a.'
  printf '%-24s %-14s %-8s   %s\n' '' '' '' 'CANDIDATE ONLY: "original" does not mean the other set is EMPTY. A duplicate that'
  printf '%-24s %-14s %-8s   %s\n' '' '' '' 'served writes and later crashed looks identical here. Verify emptiness before deleting.'
}

@test "fails closed when 'kubectl get ns' fails (whole-cluster mode)" {
  # No namespace args -> main enumerates namespaces itself. If that LIST fails,
  # the OLD code audited zero namespaces and still printed an empty clean table.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" ns "" "$(_release_blob)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'list namespaces'
}

@test "fails closed when the namespace-wide secret LIST fails" {
  # system_releases enumerates Helm releases with a namespace-wide secret LIST --
  # the exact call that timed out in the field and reported a false clean.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" secretlist "" "$(_release_blob)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'list Helm release secrets'
}

@test "fails closed when the by-name release-secret existence GET hits a real error" {
  # Codex Finding 1: system_releases uses release_json to decide whether a release
  # IS SeaweedFS. Both LISTs succeed, then a transient/forbidden by-name Secret GET
  # used to leave `chart` empty -> the tenant was silently skipped -> exit 0, empty
  # table. A real error here must now be fatal, not a false clean.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" secretget "" "$(_release_blob)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'Helm release secret'
}

@test "an absent release-secret revision is a clean skip, not a failure" {
  # The one legitimately-empty case for release_json: the revision secret does not
  # exist (pruned by Helm history limit). The existence check (--ignore-not-found -o
  # name) returns empty + exit 0, so the release is simply not confirmed as
  # SeaweedFS. No error, no crash -- the audit completes and reports nothing here.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" secretget_absent "" "$(_release_blob)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -eq 0 ]
  [ "$(printf '%s\n' "$out" | grep -c 'FATAL')" -eq 0 ]
}

@test "fails closed when a present release secret has no .data.release payload" {
  # Codex re-review round 2: the existence check passes (Secret EXISTS), but its
  # .data.release field is empty/missing. That is CORRUPT state, not an absence --
  # and because the earlier fix used --ignore-not-found -o jsonpath, which returns
  # empty+0 for BOTH, it used to read as a clean skip. It must be a loud stop.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" secret_missing_field "" "$(_release_blob)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'no .data.release payload'
}

@test "fails closed when a present release secret has an undecodable payload" {
  # Existence passes, .data.release is non-empty but is not base64(base64(gzip(...))),
  # so every decode fails. The old `|| return 0` converted that decode failure into
  # a clean 0 return -> silent skip. A corrupt payload must be fatal.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" secret_bad_payload "" "$(_release_blob)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'not decodable'
}

@test "fails closed when a present release secret decodes to a chartless payload" {
  # Codex round 3: the payload fetches and DECODES cleanly (valid JSON {}), so the
  # decode guards all pass -- but it carries no chart name. system_releases then
  # extracted chart='' and silently skipped the tenant -> exit 0, clean table. A
  # decoded helm.sh/release.v1 release without a chart name is corrupt: fatal.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob EMPTY)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'could not read the chart name'
  # The diagnostic must name the path it looked at, so a future Helm payload
  # format change is diagnosable as such instead of reading as real corruption.
  printf '%s\n' "$out" | grep -q '\.chart\.metadata\.name'
}

@test "a present release secret for a non-SeaweedFS chart is a silent legitimate skip" {
  # The counterpart the guard must NOT break: a real, well-formed release whose
  # chart is simply not cozy-seaweedfs. It has a chart name (so it is not corrupt),
  # but the wrong one, so it is filtered out exactly as before -- no row, no error.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob cozy-postgres)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -eq 0 ]
  [ "$(printf '%s\n' "$out" | grep -c 'FATAL')" -eq 0 ]
  # Not confirmed as SeaweedFS -> no classification row for the release.
  [ "$(printf '%s\n' "$out" | grep -c 'seaweedfs-system')" -eq 0 ]
}

@test "fails closed when 'kubectl get pvc' LIST fails" {
  # The secret enumeration succeeds and confirms a seaweedfs-system tenant, so the
  # audit reaches the per-namespace PVC LIST; a failure there must abort, not read
  # as "this tenant has no claims".
  d=$(mktemp -d)
  _write_fake_kubectl "$d" pvclist "" "$(_release_blob)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'list PVCs'
}

@test "fails closed when 'kubectl get sts' LIST fails" {
  # PVC LIST succeeds (empty), so the audit reaches the StatefulSet LIST; a
  # failure there must abort rather than read as "no StatefulSets".
  d=$(mktemp -d)
  _write_fake_kubectl "$d" sts "" "$(_release_blob)" ""
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'StatefulSets'
}

@test "fails closed when the by-name PV GET hits a real error" {
  # Codex Finding 2, error half: pv_epoch reads each bound PV's age by name. A real
  # error (RBAC/timeout) must abort, not silently drop that PV from the range.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" pvget "" "$(_release_blob)" "$(_pvcs_mixed)"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q "get PV '"
}

@test "an absent PV degrades to the safe fallback, never a wrong candidate" {
  # Codex Finding 2, absence half. True vintages: legacy PVs 2020 + 2099 straddle
  # the single renamed PV 2020-06 => OVERLAP. Report the newest legacy PV (2099) as
  # ABSENT: the observed legacy range collapses to {2020} and, unguarded, "every
  # release-named PV is strictly newer" would fire -- naming the release-named set a
  # deletion candidate on incomplete evidence. The generation must instead read as
  # incomplete and fall to "cannot establish direction".
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" pv-legacy2 "$(_release_blob)" "$(_pvcs_mixed)"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -eq 0 ]
  [ "$(printf '%s\n' "$out" | grep -c 'FATAL')" -eq 0 ]
  printf '%s\n' "$out" | grep -q 'direction cannot be established from PV ages'
  # The whole point: incomplete evidence must NOT be reported as a deletion candidate.
  [ "$(printf '%s\n' "$out" | grep -c 'candidate duplicate')" -eq 0 ]
}

@test "success path (L) is byte-identical to the expected fixture" {
  # A single chart-named claim, no release-named one, no StatefulSets => L, adopt in
  # place. Full-output compare, so an extra row / warning / duplicate line fails.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob)" "persistentvolumeclaim/data1-seaweedfs-volume-0"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  printf '%s\n' "$out" > "$d/got"
  _expected_L > "$d/want"
  echo "rc=$rc"; echo "--- got ---"; cat "$d/got"; echo "--- want ---"; cat "$d/want"
  [ "$rc" -eq 0 ]
  diff "$d/want" "$d/got"
  rm -rf "$d"
}

@test "success path (MIXED/overlap) is byte-identical and exercises pv_epoch" {
  # Both generations present with all PVs readable => the MIXED path runs every
  # pv_epoch GET and lands on OVERLAP. Full-output compare against the golden.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob)" "$(_pvcs_mixed)"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  printf '%s\n' "$out" > "$d/got"
  _expected_mixed_overlap > "$d/want"
  echo "rc=$rc"; echo "--- got ---"; cat "$d/got"; echo "--- want ---"; cat "$d/want"
  [ "$rc" -eq 0 ]
  diff "$d/want" "$d/got"
  rm -rf "$d"
}

@test "a whitespace-spaced Helm payload still classifies the tenant" {
  # The chart-name read is FATAL on a miss, so any payload shape a re-serializer
  # can produce must parse. Byte-compare against the same golden as the compact
  # payload: the shape must make no difference to the report at all.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob SPACED)" "persistentvolumeclaim/data1-seaweedfs-volume-0"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  printf '%s\n' "$out" > "$d/got"
  _expected_L > "$d/want"
  echo "rc=$rc"; echo "--- got ---"; cat "$d/got"
  [ "$rc" -eq 0 ]
  diff "$d/want" "$d/got"
  rm -rf "$d"
}

@test "a pretty-printed multi-line Helm payload still classifies the tenant" {
  # Line-based sed/grep cannot see across newlines at all, so this shape is the
  # one that fails hardest without the newline fold -- and jq/yq re-serialization
  # is exactly how a payload would arrive indented.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob PRETTY)" "persistentvolumeclaim/data1-seaweedfs-volume-0"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  printf '%s\n' "$out" > "$d/got"
  _expected_L > "$d/want"
  echo "rc=$rc"; echo "--- got ---"; cat "$d/got"
  [ "$rc" -eq 0 ]
  diff "$d/want" "$d/got"
  rm -rf "$d"
}

@test "a values subtree spelling chart.metadata.name cannot shadow the real chart" {
  # Helm marshals "config" (the user's values) AFTER "chart", so a LAST-match
  # extraction returns the decoy, the release reads as non-SeaweedFS, and the
  # tenant vanishes from the report with exit 0 -- a false clean, the failure this
  # whole script exists to prevent. First-match extraction is what stops it.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob DECOY)" "persistentvolumeclaim/data1-seaweedfs-volume-0"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  printf '%s\n' "$out" > "$d/got"
  _expected_L > "$d/want"
  echo "rc=$rc"; echo "--- got ---"; cat "$d/got"
  [ "$rc" -eq 0 ]
  # The decoy name must never reach the report.
  [ "$(printf '%s\n' "$out" | grep -c 'decoy-not-seaweedfs')" -eq 0 ]
  diff "$d/want" "$d/got"
  rm -rf "$d"
}

@test "fails closed when the by-name PVC GET hits a real error" {
  # pv_epoch resolves each claim's bound PV by name before reading the PV's age.
  # A real error there (RBAC/timeout) must abort: silently dropping the claim
  # shrinks the observed vintage range, which is what produces a wrong "candidate
  # duplicate" verdict. The PV half of this pair was already covered; this is the
  # PVC half, whose fake FAIL mode existed with no test behind it.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" pvcget "" "$(_release_blob)" "$(_pvcs_mixed)"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q "get PVC '"
}

# -----------------------------------------------------------------------------
# Portable timestamp parsing (issue #4273).
#
# first_deployed and pv_epoch turn an RFC3339 timestamp into epoch seconds. The
# call sites used `date -u -d`, which only GNU date accepts; on a host with BSD
# date (macOS) the call failed, its stderr was swallowed, and `|| return 0`
# converted that host-tool failure into the script's "no epoch" signal -- so every
# tenant read as MIXED / "direction cannot be established". These drive both call
# sites, the parse_epoch helper directly, and the split the fix introduces between
# an ABSENT timestamp (benign, still silent) and a PRESENT-but-unreadable one
# (fatal, loud). The BSD-date reproduction shims `date` on PATH so it is
# deterministic on a GNU CI runner too, where the raw bug is otherwise invisible.

@test "parse_epoch reads an RFC3339 timestamp on either GNU or BSD date" {
  # The fix's core: whichever spelling the host's date speaks, the same epoch
  # comes out. On a BSD host this exercises the branch #4273 was about; on a GNU
  # runner the other. Either way the answer is the known epoch, Z-suffix and all.
  #
  # These are ABSOLUTE epochs and hold on any host timezone: both forms carry
  # `-u`, which forces the zoneless string (the fix strips the Z at 19 chars) to
  # be parsed as UTC on GNU and BSD alike -- not just formatted as UTC. So a
  # non-UTC TZ shifts neither value, which is also why the audit's clock-free
  # relative comparison is timezone-immune. Run under a deliberately non-UTC TZ
  # so this stays a regression guard on that `-u`: drop it and the values move.
  [ "$(TZ='XXX5'; export TZ; parse_epoch 2026-09-15T11:00:00Z)" = 1789470000 ]
  [ "$(TZ='XXX5'; export TZ; parse_epoch 2020-01-01T00:00:00Z)" = 1577836800 ]
}

@test "parse_epoch fails loudly on a value neither date form can read" {
  # A present-but-unreadable timestamp is NOT the absent-evidence case: it is a
  # clock the host could not read. It must be fatal and name the value, never
  # folded into the silent "no epoch" path (#4273). audit_fatal writes to stderr.
  rc=0
  out=$(parse_epoch 'not-a-timestamp' 2>&1) || rc=$?
  echo "rc=$rc out=$out"
  [ "$rc" -ne 0 ]
  # Nothing on stdout: a caller capturing the epoch gets empty, not garbage.
  [ -z "$(parse_epoch 'not-a-timestamp' 2>/dev/null)" ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'not-a-timestamp'
}

@test "MIXED classifies under a BSD-only date, not 'direction cannot be established'" {
  # #4273 reproduced end-to-end at the pv_epoch call site. With a date that has no
  # GNU -d, the OLD code swallowed every parse, marked both generations INCOMPLETE,
  # and reported "direction cannot be established" for a tenant whose PV ages ARE
  # readable. The portable parse must classify the SAME fixtures as OVERLAP,
  # byte-identical to the golden the GNU path produces.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob)" "$(_pvcs_mixed)"
  _write_fake_date "$d"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  printf '%s\n' "$out" > "$d/got"
  _expected_mixed_overlap > "$d/want"
  echo "rc=$rc"; echo "--- got ---"; cat "$d/got"; echo "--- want ---"; cat "$d/want"
  [ "$rc" -eq 0 ]
  [ "$(printf '%s\n' "$out" | grep -c 'direction cannot be established')" -eq 0 ]
  diff "$d/want" "$d/got"
  rm -rf "$d"
}

@test "first_deployed parses under a BSD-only date (the second call site)" {
  # #4273 names BOTH date call sites. With a payload that carries first_deployed
  # and a BSD-only date, the OLD line returned non-zero and aborted the whole
  # audit before the MIXED report; the portable parse reads it, so the
  # 'oldest PV vs first_deployed' context line appears and the audit completes.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob FIRSTDEP)" "$(_pvcs_mixed)"
  _write_fake_date "$d"
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  echo "rc=$rc"; echo "$out"
  [ "$rc" -eq 0 ]
  printf '%s\n' "$out" | grep -q 'oldest PV vs first_deployed'
}

@test "an unparseable PV timestamp is fatal, not absent evidence" {
  # The distinction #4273 asks for, at the pv_epoch site: a PV whose
  # creationTimestamp is PRESENT but unreadable must abort loudly, NOT degrade to
  # the absent-evidence 'direction cannot be established' branch that a Pending
  # claim legitimately takes. The fake returns garbage for one bound PV; real
  # host date is used, so this fails on GNU and BSD alike.
  d=$(mktemp -d)
  _write_fake_kubectl "$d" "" "" "$(_release_blob)" "$(_pvcs_mixed)" pv-renamed
  rc=0
  out=$(PATH="$d:$PATH" SEAWEEDFS_AUDIT_LIB=0 sh "$PWD/hack/seaweedfs-naming-audit.sh" tenant-test 2>&1) || rc=$?
  rm -rf "$d"
  echo "rc=$rc"; echo "$out"
  [ "$rc" -ne 0 ]
  printf '%s\n' "$out" | grep -q 'FATAL'
  printf '%s\n' "$out" | grep -q 'not-a-timestamp'
  [ "$(printf '%s\n' "$out" | grep -c 'direction cannot be established')" -eq 0 ]
}