Ctrl K
ComunidadGuías…

hack/select-install_test.bats

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for hack/select-install.sh
#
# 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`, setup(), and teardown() for that compatibility path. Cleanup
# stays at the last reachable line rather than in an EXIT trap, which would
# replace Bats' bookkeeping trap and hide a failing TAP result.
#
# Tests that only READ the production sources call the script directly (it
# defaults sources-dir to packages/core/platform/sources); only the tests that
# build a synthetic graph or suite tree use a scratch dir.
#
# Run with: bats hack/select-install_test.bats
# -----------------------------------------------------------------------------

load test_helper

assert_contains_package() {
    printf '%s\n' "$1" | tr ' ' '\n' | grep -Fxq "$2"
}

assert_output_graph_error() {
    mode=$1
    sources=$2
    expected=$3
    requested_suite=${4:-postgres}
    out=$(mktemp)
    err=$(mktemp)
    if [ "$mode" = disabled ]; then
        if hack/select-install.sh --disabled "$requested_suite" "$sources" >"$out" 2>"$err"; then
            echo "expected disabled mode to reject invalid graph $sources" >&2
            exit 1
        fi
    elif hack/select-install.sh "$requested_suite" "$sources" >"$out" 2>"$err"; then
        echo "expected closure mode to reject invalid graph $sources" >&2
        exit 1
    fi
    [ ! -s "$out" ]
    grep -Fq "$expected" "$err"
    rm -f "$out" "$err"
}

@test "single app selects its forward dependency closure" {
    output=$(hack/select-install.sh "postgres")
    assert_contains_package "$output" cozystack.postgres-application
    assert_contains_package "$output" cozystack.postgres-operator
    assert_contains_package "$output" cozystack.networking
    # engine edge is KEPT on the install walk (unlike select-e2e.sh)
    assert_contains_package "$output" cozystack.cozystack-engine
}

@test "closure is transitive (deps of deps are pulled in)" {
    output=$(hack/select-install.sh "postgres")
    # cert-manager is a 2-hop dep (via postgres-operator and via the engine)
    assert_contains_package "$output" cozystack.cert-manager
    # gateway-api-crds is a 3-hop dep (postgres-application -> networking -> gateway-api-crds)
    assert_contains_package "$output" cozystack.gateway-api-crds
}

@test "app pulls its direct operator dependencies" {
    output=$(hack/select-install.sh "harbor")
    assert_contains_package "$output" cozystack.harbor-application
    assert_contains_package "$output" cozystack.postgres-operator
    assert_contains_package "$output" cozystack.redis-operator
    assert_contains_package "$output" cozystack.objectstorage-controller
}

@test "kubernetes suites keep the cluster and worker pool APIs" {
    for suite in kubernetes-latest kubernetes-previous; do
        output=$(hack/select-install.sh "$suite")
        assert_contains_package "$output" cozystack.kubernetes-application
        assert_contains_package "$output" cozystack.kubernetes-nodes-application
    done
}

@test "mongodb keeps the operator for its PerconaServerMongoDB resources" {
    output=$(hack/select-install.sh mongodb)
    assert_contains_package "$output" cozystack.mongodb-application
    assert_contains_package "$output" cozystack.mongodb-operator
}

@test "root SeaweedFS keeps COSI for suites without backup steps" {
    output=$(hack/select-install.sh kuberture)
    assert_contains_package "$output" cozystack.seaweedfs-application
    assert_contains_package "$output" cozystack.objectstorage-controller

    # A backup suite would hide a lost SeaweedFS edge by retaining COSI itself.
    tmp=$(mktemp -d)
    cp -r packages/core/platform/sources "$tmp/sources"
    yq -i '(.spec.variants[].dependsOn) -= ["cozystack.objectstorage-controller"]' \
      "$tmp/sources/seaweedfs-application.yaml"
    for mode in closure disabled; do
        assert_output_graph_error "$mode" "$tmp/sources" \
          "runtime baseline dependency closure is missing: cozystack.objectstorage-controller" kuberture
    done
    rm -rf "$tmp"
}

@test "vminstance keeps both application owners" {
    output=$(hack/select-install.sh "vminstance")
    printf '%s\n' "$output" | tr ' ' '\n' | grep -Fxq cozystack.vm-instance-application
    printf '%s\n' "$output" | tr ' ' '\n' | grep -Fxq cozystack.vm-disk-application
    printf '%s\n' "$output" | tr ' ' '\n' | grep -Fxq cozystack.kubevirt-cdi
}

@test "multiple suites union their closures" {
    output=$(hack/select-install.sh "postgres kafka")
    assert_contains_package "$output" cozystack.postgres-application
    assert_contains_package "$output" cozystack.kafka-application
}

@test "suite list can be read from stdin with -" {
    output=$(printf '%s\n' "postgres kafka" | hack/select-install.sh -)
    assert_contains_package "$output" cozystack.postgres-application
    assert_contains_package "$output" cozystack.kafka-application
    assert_contains_package "$output" cozystack.cozystack-engine
}

@test "empty suites select nothing" {
    output=$(hack/select-install.sh "")
    [ -z "$output" ]
}

@test "unmapped suites are a hard error, all reported, no partial output" {
    # Fail closed: a suite that maps to nothing must abort, not silently emit an
    # empty set (which would install nothing and let the run pass vacuously).
    # Also lock the contract that EVERY bad suite is reported in one run (not
    # just the first) and that no partial closure leaks to stdout.
    out=$(mktemp); err=$(mktemp)
    if hack/select-install.sh "bad-one postgres bad-two" >"$out" 2>"$err"; then
        echo "expected a non-zero exit for unmapped suites" >&2
        exit 1
    fi
    grep -q "bad-one" "$err"
    grep -q "bad-two" "$err"
    [ ! -s "$out" ]
    rm -f "$out" "$err"
}

@test "suite whose source omits the -application suffix resolves via fallback" {
    # kuberture's PackageSource is cozystack.kuberture (no -application suffix)
    output=$(hack/select-install.sh "kuberture")
    assert_contains_package "$output" cozystack.kuberture
}

@test "securitygroup closure includes the engine that serves sdn.cozystack.io" {
    # Regression for the under-selection bug: the securitygroup suite applies
    # sdn.cozystack.io/v1alpha1, served by the aggregated apiserver
    # (a cozystack-engine component). The forward walk from the controller can't
    # reach the engine, so it must be seeded. Assert the controller AND the
    # engine AND the engine's stable prerequisites — asserting only the
    # controller (a single member) is what let the bug through.
    output=$(hack/select-install.sh "securitygroup")
    assert_contains_package "$output" cozystack.securitygroup-controller
    assert_contains_package "$output" cozystack.cozystack-engine
    assert_contains_package "$output" cozystack.cert-manager
    assert_contains_package "$output" cozystack.networking
    assert_contains_package "$output" cozystack.gateway-api-crds
}

@test "runtime baseline rejects a lost etcd operator dependency edge" {
    # etcd-application is also a baseline seed, so merely selecting etcd cannot
    # prove that its operator edge is necessary.
    tmp=$(mktemp -d)
    cp -r packages/core/platform/sources "$tmp/sources"
    hack/select-install.sh postgres "$tmp/sources" >/dev/null
    yq -i '(.spec.variants[].dependsOn) -= ["cozystack.etcd-operator"]' \
      "$tmp/sources/etcd-application.yaml"
    for mode in closure disabled; do
        assert_output_graph_error "$mode" "$tmp/sources" \
          "runtime baseline dependency closure is missing: cozystack.etcd-operator"
    done
    rm -rf "$tmp"
}

@test "backup suites keep the APIs used by their roundtrip examples" {
    for suite in clickhouse etcd kafka kafka-metadata mariadb mongodb postgres rabbitmq redis valkey vminstance; do
        keep=$(hack/select-install.sh "$suite")
        drop=$(hack/select-install.sh --disabled "$suite")
        for required in cozystack.backupstrategy-controller cozystack.backup-controller \
            cozystack.bucket-application cozystack.objectstorage-controller cozystack.velero; do
            assert_contains_package "$keep" "$required"
            case " $drop " in
                *" $required "*) echo "suite '$suite' disables backup prerequisite '$required'" >&2; exit 1 ;;
            esac
        done
    done
    # Suites without backup steps must not acquire the backup stack as baseline.
    drop=$(hack/select-install.sh --disabled kuberture)
    assert_contains_package "$drop" cozystack.backupstrategy-controller
}

@test "gateway keeps the pod admission webhook used by its churn test" {
    output=$(hack/select-install.sh gateway)
    assert_contains_package "$output" cozystack.kubeovn-webhook
}

@test "suite runtime requirements must resolve in every selector mode" {
    for pair in "postgres backupstrategy-controller" "gateway kubeovn-webhook"; do
        set -- $pair
        suite=$1
        requirement=$2
        tmp=$(mktemp -d)
        cp -r packages/core/platform/sources "$tmp/sources"
        mkdir -p "$tmp/suites/$suite"
        : > "$tmp/suites/$suite/chainsaw-test.yaml"
        hack/select-install.sh --validate "$tmp/sources" "$tmp/suites"
        mv "$tmp/sources/$requirement.yaml" "$tmp/$requirement.yaml"
        hack/select-install.sh kuberture "$tmp/sources" >/dev/null
        for mode in closure disabled; do
            assert_output_graph_error "$mode" "$tmp/sources" \
              "'cozystack.$requirement' is not a PackageSource" "$suite"
        done
        if hack/select-install.sh --validate "$tmp/sources" "$tmp/suites" >"$tmp/out" 2>"$tmp/err"; then
            echo "expected validation to reject a missing suite runtime requirement" >&2
            exit 1
        fi
        [ ! -s "$tmp/out" ]
        grep -Fq "maps to 'cozystack.$requirement', not a PackageSource" "$tmp/err"
        rm -rf "$tmp"
    done
}

@test "validate passes on the real source graph and suite mapping" {
    hack/select-install.sh --validate
}

@test "validate detects a dangling dependency" {
    tmp=$(mktemp -d)
    mkdir -p "$tmp/sources" "$tmp/suites/foo"
    : > "$tmp/suites/foo/chainsaw-test.yaml"
    cat > "$tmp/sources/foo-application.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
  name: cozystack.foo-application
spec:
  variants:
    - name: default
      dependsOn:
        - cozystack.does-not-exist
YAML
    out="$tmp/out"; err="$tmp/err"
    if hack/select-install.sh --validate "$tmp/sources" "$tmp/suites" >"$out" 2>"$err"; then
        echo "expected validation to fail on a dangling dependency" >&2
        exit 1
    fi
    [ ! -s "$out" ]
    grep -Fq "dangling dependency 'cozystack.does-not-exist'" "$err"
    if grep -Fq "found no chainsaw-test.yaml" "$err"; then
        echo "positive-control suite discovery failed instead of graph validation" >&2
        exit 1
    fi
    rm -rf "$tmp"
}

@test "validate detects a dependency cycle" {
    tmp=$(mktemp -d)
    mkdir -p "$tmp/sources" "$tmp/suites/a"
    : > "$tmp/suites/a/chainsaw-test.yaml"
    cat > "$tmp/sources/a.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
  name: cozystack.a
spec:
  variants:
    - name: default
      dependsOn:
        - cozystack.b
YAML
    cat > "$tmp/sources/b.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
  name: cozystack.b
spec:
  variants:
    - name: default
      dependsOn:
        - cozystack.a
YAML
    out="$tmp/out"; err="$tmp/err"
    if hack/select-install.sh --validate "$tmp/sources" "$tmp/suites" >"$out" 2>"$err"; then
        echo "expected validation to fail on a dependency cycle" >&2
        exit 1
    fi
    [ ! -s "$out" ]
    grep -Fq "dependency cycle detected involving 'cozystack.a'" "$err"
    if grep -Fq "found no chainsaw-test.yaml" "$err"; then
        echo "positive-control suite discovery failed instead of graph validation" >&2
        exit 1
    fi
    rm -rf "$tmp"
}

@test "output modes reject a removed runtime source" {
    tmp=$(mktemp -d)
    cp -r packages/core/platform/sources "$tmp/sources"
    mv "$tmp/sources/reloader.yaml" "$tmp/reloader.yaml"
    for mode in closure disabled; do
        assert_output_graph_error "$mode" "$tmp/sources" \
          "dangling dependency 'cozystack.reloader'"
    done

    yq -i '(.spec.variants[].dependsOn) -= ["cozystack.reloader"]' \
      "$tmp/sources/linstor.yaml"
    for mode in closure disabled; do
        assert_output_graph_error "$mode" "$tmp/sources" \
          "required runtime PackageSource(s) not found in $tmp/sources: cozystack.reloader"
    done
    rm -rf "$tmp"
}

@test "output modes reject a dangling dependency in an unselected package" {
    tmp=$(mktemp -d)
    cp -r packages/core/platform/sources "$tmp/sources"
    yq -i '.spec.variants[0].dependsOn += ["cozystack.does-not-exist"]' \
      "$tmp/sources/kafka-application.yaml"
    for mode in closure disabled; do
        assert_output_graph_error "$mode" "$tmp/sources" \
          "dangling dependency 'cozystack.does-not-exist' (referenced by: cozystack.kafka-application)"
    done
    rm -rf "$tmp"
}

@test "output modes reject selected and unselected dependency cycles" {
    tmp=$(mktemp -d)
    cp -r packages/core/platform/sources "$tmp/selected"
    cp -r packages/core/platform/sources "$tmp/unselected"
    yq -i '.spec.variants[0].dependsOn += ["cozystack.postgres-application"]' \
      "$tmp/selected/postgres-application.yaml"
    yq -i '.spec.variants[0].dependsOn += ["cozystack.kafka-application"]' \
      "$tmp/unselected/kafka-application.yaml"
    for mode in closure disabled; do
        assert_output_graph_error "$mode" "$tmp/selected" \
          "dependency cycle detected involving 'cozystack.postgres-application'"
        assert_output_graph_error "$mode" "$tmp/unselected" \
          "dependency cycle detected involving 'cozystack.kafka-application'"
    done
    rm -rf "$tmp"
}

@test "validate detects a suite directory with no source mapping" {
    tmp=$(mktemp -d)
    mkdir -p "$tmp/suites/totally-unmapped-suite"
    : > "$tmp/suites/totally-unmapped-suite/chainsaw-test.yaml"
    # real graph (so graph checks pass); the only failure is the unmapped suite
    if hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>/dev/null; then
        echo "expected validation to fail on an unmapped suite dir" >&2
        exit 1
    fi
    rm -rf "$tmp"
}

@test "validate sees a suite written as chainsaw-test.yml" {
    # Chainsaw runs a chainsaw-test.yml suite, so the mapping check has to see
    # one too, or an unmapped suite spelled that way passes validation.
    tmp=$(mktemp -d)
    mkdir -p "$tmp/suites/totally-unmapped-suite"
    : > "$tmp/suites/totally-unmapped-suite/chainsaw-test.yml"
    err=$(hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>&1 1>/dev/null) && {
        echo "expected validation to fail on an unmapped .yml suite" >&2
        exit 1
    }
    echo "$err" | grep -q "suite 'totally-unmapped-suite' has no PackageSource mapping"
    rm -rf "$tmp"
}

@test "validate sees a suite made only of numbered step files" {
    # Chainsaw runs a directory of 01-*.yaml step files as a suite when it holds
    # no chainsaw-test.*, so the mapping check has to see that layout too. The
    # step file is a symlink because Chainsaw counts one.
    tmp=$(mktemp -d)
    mkdir -p "$tmp/suites/totally-unmapped-suite"
    : > "$tmp/install.yaml"
    ln -s "$tmp/install.yaml" "$tmp/suites/totally-unmapped-suite/00-install.yaml"
    err=$(hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>&1 1>/dev/null) && {
        echo "expected validation to fail on an unmapped step-file suite" >&2
        exit 1
    }
    echo "$err" | grep -q "suite 'totally-unmapped-suite' has no PackageSource mapping"
    rm -rf "$tmp"
}

@test "validate ignores what Chainsaw does not load as a step file" {
    # Chainsaw skips a step-file name when the entry is a directory, and a name
    # with no leading number is not a step file, so neither makes a suite that
    # needs a mapping.
    tmp=$(mktemp -d)
    mkdir -p "$tmp/suites/postgres" "$tmp/suites/totally-unmapped-suite/01-x.yaml"
    : > "$tmp/suites/totally-unmapped-suite/-install.yaml"
    : > "$tmp/suites/postgres/chainsaw-test.yaml"
    hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites"
    rm -rf "$tmp"
}

@test "validate sees a directory named chainsaw-test.yaml or .yml" {
    # Chainsaw stats chainsaw-test.yaml or .yml without checking its type, so a
    # directory of that name fails discovery loudly. Listing its suite keeps
    # that failure where a run reaches it instead of leaving the suite out.
    for name in chainsaw-test.yaml chainsaw-test.yml; do
        tmp=$(mktemp -d)
        mkdir -p "$tmp/suites/totally-unmapped-suite/$name"
        : > "$tmp/suites/totally-unmapped-suite/$name/x"
        err=$(hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>&1 1>/dev/null) && {
            echo "expected validation to fail on an unmapped suite with a $name directory" >&2
            exit 1
        }
        echo "$err" | grep -q "suite 'totally-unmapped-suite' has no PackageSource mapping"
        rm -rf "$tmp"
    done
}

@test "validate sees a suite nested below a top-level directory" {
    # Chainsaw walks every directory under the one it is handed, so a test in a
    # subdirectory runs as part of its top-level directory, which then needs a
    # mapping like any other suite.
    tmp=$(mktemp -d)
    mkdir -p "$tmp/suites/totally-unmapped-suite/cases"
    : > "$tmp/suites/totally-unmapped-suite/cases/chainsaw-test.yaml"
    err=$(hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>&1 1>/dev/null) && {
        echo "expected validation to fail on an unmapped nested suite" >&2
        exit 1
    }
    echo "$err" | grep -q "suite 'totally-unmapped-suite' has no PackageSource mapping"
    rm -rf "$tmp"
}

@test "validate fails when the suite listing itself fails" {
    # A failed find and an empty suites dir used to read the same: the error
    # went to /dev/null and the pipeline reported sort's status, so the mapping
    # check validated nothing and passed.
    tmp=$(mktemp -d)
    mkdir "$tmp/bin"
    printf '#!/bin/sh\necho "find: broken fixture" >&2\nexit 1\n' > "$tmp/bin/find"
    chmod +x "$tmp/bin/find"
    err=$(PATH="$tmp/bin:$PATH" hack/select-install.sh --validate 2>&1 1>/dev/null) && {
        echo "expected validate to fail when find fails" >&2
        exit 1
    }
    echo "$err" | grep -q "select-install: find failed listing the suites"
    rm -rf "$tmp"
}

@test "validate detects a suite mapped to a source absent from the graph" {
    # suite_to_source() hardcodes securitygroup -> cozystack.securitygroup-controller;
    # a sources dir without that PackageSource must fail validation (fail closed),
    # pinning that the elif validates every mapping, including hardcoded ones,
    # against the graph.
    tmp=$(mktemp -d)
    mkdir -p "$tmp/sources" "$tmp/suites/securitygroup"
    : > "$tmp/suites/securitygroup/chainsaw-test.yaml"
    cat > "$tmp/sources/standalone.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
  name: cozystack.standalone
spec:
  variants:
    - name: default
YAML
    err=$(hack/select-install.sh --validate "$tmp/sources" "$tmp/suites" 2>&1 1>/dev/null) && {
        echo "expected validation to fail on a mapping to a missing source" >&2
        exit 1
    }
    echo "$err" | grep -q "maps to 'cozystack.securitygroup-controller', not a PackageSource"
    rm -rf "$tmp"
}

@test "validate fails when the suites dir does not exist" {
    tmp=$(mktemp -d)
    # real graph passes the graph checks; a missing suites dir must still FAIL
    # (find would yield nothing and silently pass the mapping check otherwise).
    if hack/select-install.sh --validate packages/core/platform/sources "$tmp/does-not-exist" 2>/dev/null; then
        echo "expected validate to fail on a missing suites dir" >&2
        exit 1
    fi
    rm -rf "$tmp"
}

@test "validate fails when suite discovery fails" {
    tmp=$(mktemp -d)
    mkdir -p "$tmp/bin" "$tmp/suites/one"
    : > "$tmp/suites/one/chainsaw-test.yaml"
    cat > "$tmp/bin/find" <<'SH'
#!/bin/sh
exit 42
SH
    chmod +x "$tmp/bin/find"
    out=$(mktemp); err=$(mktemp)
    if PATH="$tmp/bin:$PATH" hack/select-install.sh --validate \
        packages/core/platform/sources "$tmp/suites" >"$out" 2>"$err"; then
        echo "expected validate to fail when find fails" >&2
        exit 1
    fi
    [ ! -s "$out" ]
    grep -Fq "find failed listing the suites" "$err"
    rm -rf "$tmp"; rm -f "$out" "$err"
}

@test "yq producer failures cannot become a successful partial graph" {
    tmp=$(mktemp -d)
    mkdir -p "$tmp/bin"
    real_yq=$(command -v yq)
    cat > "$tmp/bin/yq" <<'SH'
#!/bin/sh
case "${FAIL_YQ:-}" in
  names)
    case "$*" in
      *metadata.name*) echo cozystack.partial; exit 42 ;;
    esac
    ;;
  dependencies)
    case "$*" in
      *spec.variants*) printf 'cozystack.partial\tcozystack.missing\n'; exit 42 ;;
    esac
    ;;
esac
exec "$REAL_YQ" "$@"
SH
    chmod +x "$tmp/bin/yq"
    for stage in names dependencies; do
        out="$tmp/$stage.out"; err="$tmp/$stage.err"
        if REAL_YQ="$real_yq" FAIL_YQ="$stage" PATH="$tmp/bin:$PATH" \
            hack/select-install.sh postgres >"$out" 2>"$err"; then
            echo "expected the $stage yq failure to propagate" >&2
            exit 1
        fi
        [ ! -s "$out" ]
        grep -Fq "yq failed while reading PackageSource" "$err"
    done
    rm -rf "$tmp"
}

@test "closure fails closed when the engine PackageSource is absent" {
    tmp=$(mktemp -d)
    mkdir -p "$tmp/sources"
    # a lone app source, with no cozystack.cozystack-engine in the graph
    cat > "$tmp/sources/foo-application.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
  name: cozystack.foo-application
spec:
  variants:
    - name: default
YAML
    if hack/select-install.sh "foo" "$tmp/sources" 2>/dev/null; then
        echo "expected a non-zero exit when the engine source is missing" >&2
        exit 1
    fi
    rm -rf "$tmp"
}

# --disabled emits the complement of the closure: what bundles.disabledPackages
# must hold so an ordinary platform variant installs only what the suites need.
@test "disabled mode: closure and complement partition the source list" {
    # Count names only after yq itself has succeeded; dash has no pipefail.
    names=$(yq -rN '.metadata.name | select(. != null and . != "")' \
              packages/core/platform/sources/*.yaml)
    total=$(printf '%s\n' "$names" | sort -u | wc -l | tr -d ' ')
    keep_output=$(hack/select-install.sh "postgres")
    drop_output=$(hack/select-install.sh --disabled "postgres")
    keep=$(printf '%s\n' "$keep_output" | wc -w | tr -d ' ')
    drop=$(printf '%s\n' "$drop_output" | wc -w | tr -d ' ')
    sum=$((keep + drop))
    if [ "$sum" -ne "$total" ]; then
        echo "closure ($keep) + complement ($drop) = $sum, but there are $total PackageSources" >&2
        exit 1
    fi
    inventory=$(printf '%s\n' "$names" | sort -u)
    partition=$(printf '%s\n%s\n' "$keep_output" "$drop_output" | tr ' ' '\n' | sort -u)
    [ "$partition" = "$inventory" ]
}

@test "disabled mode accepts a valid inventory with an empty complement" {
    tmp=$(mktemp -d)
    mkdir -p "$tmp/sources"
    keep=$(hack/select-install.sh postgres)
    for source in $keep; do
        name=${source#cozystack.}
        cp "packages/core/platform/sources/$name.yaml" "$tmp/sources/"
    done
    output=$(hack/select-install.sh --disabled postgres "$tmp/sources")
    [ -z "$output" ]
    rm -rf "$tmp"
}

@test "disabled mode: never lists a package the closure keeps" {
    keep=$(hack/select-install.sh "postgres kafka")
    drop=$(hack/select-install.sh --disabled "postgres kafka")
    for k in $keep; do
        case " $drop " in
            *" $k "*) echo "package '$k' is both kept and disabled" >&2; exit 1 ;;
        esac
    done
}

# A declared dependency of something kept can never land in the complement.
# Assert the property over every suite instead of citing an unreachable pair.
@test "disabled mode: no kept package has a dependency in the complement" {
    deps=$(mktemp)
    yq -rN '.metadata.name as $n | .spec.variants[]?.dependsOn[]? | select(. != null and . != "") | $n + " " + .' \
      packages/core/platform/sources/*.yaml > "$deps"

    for suite in $(find hack/e2e-chainsaw -mindepth 2 -maxdepth 2 -name chainsaw-test.yaml \
                    | sed -e 's,^hack/e2e-chainsaw/,,' -e 's,/chainsaw-test\.yaml$,,'); do
        keep=$(hack/select-install.sh "$suite")
        drop=$(hack/select-install.sh --disabled "$suite")
        for k in $keep; do
            for d in $(awk -v owner="$k" '$1 == owner {print $2}' "$deps"); do
                case " $drop " in
                    *" $d "*)
                        echo "suite '$suite': kept '$k' depends on '$d', which is in the disable list" >&2
                        rm -f "$deps"; exit 1 ;;
                esac
            done
        done
    done
    rm -f "$deps"
}

# Direct seeds correspond to unconditional install steps, even when another
# baseline source also reaches them through dependsOn.
@test "disabled mode: never disables the direct runtime baseline" {
    drop=$(hack/select-install.sh --disabled "postgres")
    for b in cozystack.cozystack-engine cozystack.flux-shard-operator cozystack.cozystack-basics \
             cozystack.tenant-application cozystack.etcd-application \
             cozystack.ingress-application cozystack.monitoring-application \
             cozystack.seaweedfs-application cozystack.linstor \
             cozystack.kubevirt-cdi cozystack.metallb cozystack.capi-operator \
             cozystack.capi-provider-bootstrap-kubeadm \
             cozystack.capi-provider-core cozystack.capi-provider-cp-kamaji \
             cozystack.capi-provider-infra-kubevirt cozystack.keycloak \
             cozystack.keycloak-operator cozystack.multus \
             cozystack.cozystack-scheduler cozystack.linstor-scheduler \
             cozystack.flux-plunger cozystack.cozy-proxy cozystack.ouroboros; do
        case " $drop " in
            *" $b "*) echo "baseline package '$b' is in the disable list" >&2; exit 1 ;;
        esac
    done
}

@test "runtime baseline reaches every derived controller and CRD prerequisite" {
    keep=$(hack/select-install.sh "postgres")
    for required in cozystack.cert-manager cozystack.networking \
        cozystack.gateway-api-crds cozystack.etcd-operator \
        cozystack.grafana-operator cozystack.postgres-operator \
        cozystack.victoria-metrics-operator cozystack.prometheus-operator-crds \
        cozystack.vertical-pod-autoscaler cozystack.reloader \
        cozystack.snapshot-controller cozystack.gateway-application \
        cozystack.info-application cozystack.kamaji cozystack.kubevirt; do
        if ! printf '%s\n' "$keep" | tr ' ' '\n' | grep -Fxq "$required"; then
            echo "runtime baseline is missing derived prerequisite '$required'" >&2
            exit 1
        fi
    done
}

@test "runtime baseline fails closed when a provider prerequisite edge is lost" {
    tmp=$(mktemp -d)
    cp -r packages/core/platform/sources "$tmp/sources"
    yq -i '(.spec.variants[].dependsOn) -= ["cozystack.kamaji"]' \
      "$tmp/sources/capi-provider-cp-kamaji.yaml"
    out="$tmp/out"; err="$tmp/err"
    if hack/select-install.sh postgres "$tmp/sources" >"$out" 2>"$err"; then
        echo "expected a missing Kamaji prerequisite to fail closed" >&2
        exit 1
    fi
    [ ! -s "$out" ]
    grep -Fq "runtime baseline dependency closure is missing: cozystack.kamaji" "$err"

    cp packages/core/platform/sources/capi-provider-cp-kamaji.yaml \
      "$tmp/sources/capi-provider-cp-kamaji.yaml"
    yq -i '(.spec.variants[].dependsOn) -= ["cozystack.kubevirt"]' \
      "$tmp/sources/capi-provider-infra-kubevirt.yaml"
    if hack/select-install.sh postgres "$tmp/sources" >"$out" 2>"$err"; then
        echo "expected a missing KubeVirt prerequisite to fail closed" >&2
        exit 1
    fi
    [ ! -s "$out" ]
    grep -Fq "runtime baseline dependency closure is missing: cozystack.kubevirt" "$err"
    rm -rf "$tmp"
}

@test "runtime baseline fails closed when storage prerequisite edges are lost" {
    tmp=$(mktemp -d)
    cp -r packages/core/platform/sources "$tmp/sources"
    yq -i '(.spec.variants[].dependsOn) -= ["cozystack.reloader", "cozystack.snapshot-controller"]' \
      "$tmp/sources/linstor.yaml"
    out="$tmp/out"; err="$tmp/err"
    if hack/select-install.sh postgres "$tmp/sources" >"$out" 2>"$err"; then
        echo "expected missing LINSTOR prerequisites to fail closed" >&2
        exit 1
    fi
    [ ! -s "$out" ]
    grep -Fq "cozystack.reloader" "$err"
    grep -Fq "cozystack.snapshot-controller" "$err"
    rm -rf "$tmp"
}

@test "generated Helm values fixture matches the current disable list" {
    generated_raw=$(hack/select-install.sh --disabled postgres)
    generated=$(printf '%s\n' "$generated_raw" | tr ' ' '\n' | sort)
    fixture_raw=$(yq -r '.bundles.disabledPackages[]' \
      packages/core/platform/tests/fixtures/selective-install-postgres-values.yaml)
    fixture=$(printf '%s\n' "$fixture_raw" | sort)
    if [ "$generated" != "$fixture" ]; then
        echo "selective-install-postgres-values.yaml is stale; regenerate it from select-install.sh" >&2
        exit 1
    fi

    tmp=$(mktemp -d)
    cp -r packages/core/platform/sources "$tmp/sources"
    cat > "$tmp/sources/fixture-drift.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
  name: cozystack.fixture-drift
spec:
  variants:
    - name: default
YAML
    drifted_raw=$(hack/select-install.sh --disabled postgres "$tmp/sources")
    drifted=$(printf '%s\n' "$drifted_raw" | tr ' ' '\n' | sort)
    if [ "$drifted" = "$fixture" ]; then
        echo "a new PackageSource did not invalidate the generated fixture" >&2
        exit 1
    fi
    printf '%s\n' "$drifted" | grep -Fxq cozystack.fixture-drift
    rm -rf "$tmp"
}

# An empty closure would make the complement the entire platform. As an install
# instruction that is a platform with no packages, so it must refuse rather than
# emit it.
@test "disabled mode: refuses an empty selection" {
    out=$(mktemp); err=$(mktemp)
    if hack/select-install.sh --disabled "" >"$out" 2>"$err"; then
        echo "expected a non-zero exit for an empty selection, got: $(cat "$out")" >&2
        rm -f "$out" "$err"; exit 1
    fi
    grep -q "refusing to emit a disable list" "$err" \
      || { echo "expected the refusal message, got: $(cat "$err")" >&2; rm -f "$out" "$err"; exit 1; }
    rm -f "$out" "$err"
}