diff --git a/tests/add-app-errors.bats b/tests/add-app-errors.bats index 39113de..4afaada 100644 --- a/tests/add-app-errors.bats +++ b/tests/add-app-errors.bats @@ -1,9 +1,9 @@ -# covers the failure paths task 8 asked for beyond the brief's five given -# tests: locating the project (outside git, inside a non-scaffold git repo), -# a pre-existing target surviving a refused add with its contents intact, -# resolving the target relative to the project root from a subdirectory, and -# the cleanup trap actually firing on a partial failure — all without a real -# adapter's network-hitting generator, so this file stays fast. +# Failure paths for `scaffold add`: locating the project (outside git, inside +# a non-scaffold git repo), a pre-existing target surviving a refused add with +# its contents intact, resolving the target relative to the project root from +# a subdirectory, and the cleanup trap actually firing on a partial failure — +# all without a real adapter's network-hitting generator, so this file stays +# fast. setup() { load 'helpers/setup' diff --git a/tests/compose.bats b/tests/compose.bats index ce11d3e..2675377 100644 --- a/tests/compose.bats +++ b/tests/compose.bats @@ -26,14 +26,10 @@ teardown() { @test "compose.yaml, install.sh and the build workflows name one registry path" { # build.yml pushes the image compose.yaml pulls, and install.sh downloads # the release that publishes compose.yaml. Three files, one path — written - # from the same owner and project name at generation time. - # - # They used to disagree by construction: the workflows were substituted and - # compose.yaml/install.sh shipped `CHANGEME/CHANGEME`, so a project's first - # release named an image nothing had pushed and needed a hand-edit plus a - # second release before install.sh worked at all. - # One entry per application now (ADR-0022), so this asks the question of - # every one of them rather than of a single `app` service. + # from the same owner and project name at generation time; letting them + # drift leaves install.sh downloading a release nothing pushed. One entry + # per application (ADR-0022), so this asks the question of every one of + # them rather than of a single `app` service. local images want_repo images="$(yq -r '[.jobs[] | select(has("with")) | .with.images] | .[0]' \ "${PROJECT}/.github/workflows/build.yml")" @@ -162,8 +158,8 @@ INNER_EOF @test "install.sh survives being piped into bash instead of dying on an unbound variable" { # documented as curl-piped (`curl ... | bash`), same as the immich script - # this is adapted from — piped in, BASH_SOURCE[0] is unbound, and - # `set -o nounset` used to kill the script before the source guard even ran. + # this is adapted from — piped in, BASH_SOURCE[0] is unbound, which + # `set -o nounset` would otherwise kill before the source guard even runs. mkdir -p "${WORKDIR}/piped" cd "${WORKDIR}/piped" run bash <"${PROJECT}/install.sh" diff --git a/tests/contract.bats b/tests/contract.bats index e513858..73d8b79 100644 --- a/tests/contract.bats +++ b/tests/contract.bats @@ -42,9 +42,8 @@ setup() { @test "test-unit's suites never run an adapter generator to completion" { # mise.toml's test-unit membership is an explicit file list backed by a - # comment claiming "no adapter generator anywhere in setup" — the same - # "synced by a comment, drifts silently" shape task 13's review round 1 - # existed to fix elsewhere. this greps the claim instead of trusting it. + # comment claiming "no adapter generator anywhere in setup" — a claim that + # drifts silently if trusted, so this greps it instead. local run_line files f run_line="$(awk '/^\[tasks\."test-unit"\]/{f=1} f && /^run = /{print; exit}' "${SCAFFOLD_ROOT}/mise.toml")" files="$(grep -oE 'tests/[A-Za-z0-9_-]+\.bats' <<<"$run_line")" @@ -104,8 +103,8 @@ setup() { # The missing-driver fixture above proves a family with no driver file # fails; nothing proved the mirror case — a driver file that exists and # sources cleanly but omits one of REQUIRED_DRIVER_FUNCTIONS. Deleting the - # whole `for fn` loop in lint_services left this suite green, which is the - # same "gate that cannot fail" shape task 1's own ruling already named. + # whole `for fn` loop in lint_services would leave this suite green too: a + # gate that cannot fail either way. run lint_services \ "${SCAFFOLD_ROOT}/tests/fixtures/lint-services/missing-driver-function" \ "${SCAFFOLD_ROOT}/adapters" @@ -185,12 +184,9 @@ setup() { } @test "every suite runs somewhere" { - # A suite in no lane is a suite that never runs: the service branch shipped - # thirty tests into that state, and nobody noticed until a review read - # mise.toml against ci.yml. Three tests used to guard this, one per suite, - # each added after the next suite fell through the same gap — a list that - # only ever grows by being wrong first. This asks the question of every - # suite in the directory instead, including the ones not written yet. + # A suite in no lane is a suite that never runs, so this checks every suite + # file in the directory against mise.toml directly rather than keeping a + # hardcoded list that could itself go stale. # # Scoped to each lane's own `run = ` line, not the whole file: a name greps # clean out of a comment, or out of [tasks.test]'s "tests/" glob, which CI @@ -270,16 +266,10 @@ setup() { @test "every caller of scaffold new supplies the environment it demands" { # scaffold new refuses to run without a GitHub owner, because the workflows - # it generates carry a `you/` placeholder that has to be substituted. Two - # things in this repository call it outside the test suite: - # - # scripts/deploy-check.sh exports its own - # .github/workflows/adapters.yml sets it on the step - # - # The workflow was the one that did not, and it failed every scheduled run - # from 2026-09-05 to 2026-09-08 — the second time a caller was missed after - # the same defect was fixed for deploy-check.sh alone. Grepping the call - # sites is what makes a third one impossible to miss. + # it generates carry a `you/` placeholder that has to be substituted, and + # every caller outside the test suite must export it. Grepping the call + # sites, rather than checking a hardcoded list of them, is what catches a + # caller that forgets to. # # tests/*.bats are excluded deliberately: they reach scaffold through # tests/helpers/setup.bash, which exports the variable once for all of them, diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash index f4b9e64..6e8d685 100644 --- a/tests/helpers/setup.bash +++ b/tests/helpers/setup.bash @@ -1,9 +1,5 @@ +# Shared bats setup: an owned environment, assert_ok, copy_toolbox. # shellcheck shell=bash -# ═══════════════════════════════════════════════════════════════════════════ -# Script : tests/helpers/setup.bash -# Description : Shared bats setup: an owned environment, assert_ok, copy_toolbox. -# Author : ttncode -# ═══════════════════════════════════════════════════════════════════════════ SCAFFOLD_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" export SCAFFOLD_ROOT PATH="${SCAFFOLD_ROOT}:${PATH}" @@ -13,29 +9,22 @@ export PATH # a project fails resolve_github_owner's guard. export SCAFFOLD_GITHUB_OWNER="${SCAFFOLD_GITHUB_OWNER:-test-owner}" -# Same shape: a runner has no git identity either, and `scaffold new` commits -# what it creates. Owned by this suite rather than written into the runner's -# HOME. -if [ -z "${GIT_CONFIG_GLOBAL:-}" ]; then +# A runner has no git identity either, and `scaffold new` commits what it +# creates. Owned by this suite rather than written into the runner's HOME. +if [[ -z "${GIT_CONFIG_GLOBAL:-}" ]]; then GIT_CONFIG_GLOBAL="${BATS_TEST_TMPDIR:-${BATS_SUITE_TMPDIR:-/tmp}}/gitconfig" export GIT_CONFIG_GLOBAL git config --global user.name "scaffold tests" git config --global user.email "tests@scaffold.invalid" - # `scaffold new` commits, and a commit can hand the repository to a detached - # `git gc`. That process outlives the command and keeps writing into .git, - # which is the likeliest thing remove_workdir below is racing. A throwaway - # repository has nothing worth maintaining. + # A commit can hand the repo to a detached `git gc` that outlives the + # command and keeps writing into .git — the likely race remove_workdir waits out. git config --global gc.auto 0 fi -# mise records every config it trusts under its state directory, so a suite -# that generates projects in throwaway tmpdirs writes an entry per run into -# the developer's real store and never removes it — it had grown past 7600 -# stale `tmp.*-demo` entries. Owned by the test, like GIT_CONFIG_GLOBAL above, -# which also makes the trust behaviour itself observable: the real store is -# pre-trusted on a runner, so a test asserting an untrusted parent could only -# ever skip there. -if [ -z "${MISE_STATE_DIR:-}" ]; then +# mise records every config it trusts under its state directory; owned here +# like GIT_CONFIG_GLOBAL above, so a suite generating projects in tmpdirs +# doesn't write into the developer's real (and already pre-trusted) store. +if [[ -z "${MISE_STATE_DIR:-}" ]]; then MISE_STATE_DIR="${BATS_TEST_TMPDIR:-${BATS_SUITE_TMPDIR:-/tmp}}/mise-state" export MISE_STATE_DIR fi @@ -44,19 +33,16 @@ fi # line that failed and nothing about why. assert_ok() { # shellcheck disable=SC2154 # status/output set by bats' run in the calling test - [ "$status" -eq 0 ] || { - echo "exit status ${status}; command output follows:" - echo "$output" + ((status == 0)) || { + printf 'exit status %s; command output follows:\n' "$status" + printf '%s\n' "$output" false } } -# copy_toolbox — a private copy of the toolbox for a test that has to modify -# it. Two tests need to see how the scripts behave against a broken adapter or -# a drifted file; editing the real tree made them race each other once the -# suites started running with --jobs, and one left ADAPTER_TIER="Z" behind in -# a tracked file. The scripts resolve their own root from their location, so -# running them out of the copy is enough. +# A private copy of the toolbox, for a test that modifies it and would +# otherwise race the real tree under --jobs. The scripts resolve their own +# root from their location, so running them out of the copy is enough. copy_toolbox() { local dest="${BATS_TEST_TMPDIR}/toolbox" mkdir -p "$dest" @@ -69,16 +55,9 @@ copy_toolbox() { printf '%s' "$dest" } -# remove_workdir — teardown's `rm -rf`, retried. -# -# `rm -rf` reports "Directory not empty" when an entry appears after it walked -# the directory, so a background process still writing into a generated project -# fails a teardown that has nothing to do with what the test asserted. Seen on -# CI against `tests/compose.bats`, which generates a project per test under -# --jobs, and on a different test each run; never reproduced locally, and the -# writer was never caught in the act, so this waits the race out rather than -# naming a cause. A tree that is genuinely stuck still fails, and says what is -# left in it. +# teardown's `rm -rf`, retried: it reports "Directory not empty" when a +# background writer still touches a generated project, which has nothing to +# do with what the test asserted. remove_workdir() { local -r dir="$1" @@ -88,7 +67,7 @@ remove_workdir() { done rm -rf "$dir" && return 0 - echo "could not remove ${dir}, left behind:" >&2 + printf 'could not remove %s, left behind:\n' "$dir" >&2 find "$dir" >&2 return 1 } diff --git a/tests/install.bats b/tests/install.bats index 673c779..8747450 100644 --- a/tests/install.bats +++ b/tests/install.bats @@ -68,8 +68,8 @@ INNER_EOF } @test "fetch_release_asset uses the api asset endpoint when a token is set" { - # A Bearer token on the browser URL returns 404 for a private repository — - # measured 2026-09-07 — so the endpoint has to change, not just the headers. + # A Bearer token on the browser URL returns 404 for a private repository, + # so the endpoint has to change, not just the headers. mkdir -p stub2 cat >stub2/curl <<'INNER_EOF' #!/usr/bin/env bash @@ -112,12 +112,13 @@ INNER_EOF @test "start_stack logs in to ghcr only when a token is set" { # A package's ghcr visibility is separate from its repository's — a - # private package refuses an anonymous pull with `unauthorized`, measured - # 2026-09-07. A public client pulling a public image must never be asked - # to authenticate, so the no-token half here must see no login at all. + # private package refuses an anonymous pull with `unauthorized`. A public + # client pulling a public image must never be asked to authenticate, so + # the no-token half here must see no login at all. mkdir -p stub3 cat >stub3/docker <<'INNER_EOF' #!/usr/bin/env bash +[[ "$1" == login ]] && cat >/dev/null printf '%s\n' "$*" >> "${DOCKER_LOG}" INNER_EOF chmod +x stub3/docker @@ -152,7 +153,7 @@ INNER_EOF @test "fetch_release_asset with no token names GITHUB_TOKEN on failure" { # Without this the operator sees only curl's `(22) ... 404`, which reads as # "no such release" rather than "you are not signed in" — see the comment - # on fetch_release_asset for the private-repository measurement behind it. + # on fetch_release_asset for why a private repository behaves that way. mkdir -p stub4 cat >stub4/curl <<'INNER_EOF' #!/usr/bin/env bash diff --git a/tests/service.bats b/tests/service.bats index 7aed187..f95dfb1 100644 --- a/tests/service.bats +++ b/tests/service.bats @@ -434,10 +434,9 @@ EOF @test "apply_service_drivers points a driver at the real project root, not the app's own ancestor" { # scaffold add creates its app one directory below the project root # ("worker", not "apps/worker"); a driver that assumed the fixed - # apps/ depth would land two directories above the app instead — - # reproduced directly against the pre-fix nest.sh, which mutated this - # decoy file with exit 0 and never noticed the real root had no - # pnpm-workspace.yaml of its own. + # apps/ depth would land two directories above the app instead, + # silently mutating a decoy file there rather than failing on the real + # root's missing pnpm-workspace.yaml. local root="${BATS_TEST_TMPDIR}/rootcheck/project" local decoy="${BATS_TEST_TMPDIR}/rootcheck" local app="${root}/worker" @@ -934,8 +933,7 @@ _password_literal_report() { # A project generated with --db none keeps this file as shipped: nothing # splices a probe in, so nothing in ready() awaits. Shipping it `async` # made every --db none project fail @typescript-eslint/require-await on - # its own lint task — green-on-generation is the whole promise (ADR-0021), - # and two of the sixteen nightly service cells were red on it. + # its own lint task — green-on-generation is the whole promise (ADR-0021). # # Both halves, because either alone is satisfiable by a broken file: the # shipped file must not say async, and the driver must be the thing that diff --git a/tests/wizard.bats b/tests/wizard.bats index 61d69f9..a7a007b 100644 --- a/tests/wizard.bats +++ b/tests/wizard.bats @@ -282,10 +282,11 @@ strip_ansi() { @test "Ctrl-D at the name prompt exits instead of spinning forever" { # read returns 1 on EOF, leaving name empty; tui_name_is_usable "" fails, - # and pre-fix the loop just re-asked forever. The TTY guard in main only - # keeps a non-terminal stdin out — it does nothing about Ctrl-D on a real - # one, so this drives tui_prompt_name itself under a pty. Bounded by an - # outer timeout so a regression here fails this test instead of hanging it. + # and a loop with no EOF check just re-asks forever. The TTY guard in main + # only keeps a non-terminal stdin out — it does nothing about Ctrl-D on a + # real one, so this drives tui_prompt_name itself under a pty. Bounded by + # an outer timeout so a regression here fails this test instead of hanging + # it. command -v script >/dev/null || skip "script(1) not available" local driver="${BATS_TEST_TMPDIR}/drive-name-prompt.sh" @@ -634,9 +635,8 @@ EOF # # Both run under a pty. tui_header measures with `tput cols`, which reads # the terminal, not $COLUMNS — so without a pty `stty cols` fails silently, - # tput falls back to 80, and both cases draw the same 80-column box. That - # is how the first version of this test passed here and failed on CI: the - # width it claimed to set was never the width being measured. + # tput falls back to 80, and both cases draw the same 80-column box: a + # width this test would claim to set without ever actually measuring it. command -v script >/dev/null || skip "script(1) not available" local body="source '${SCAFFOLD_ROOT}/lib/tui.sh'; tui_header" diff --git a/tests/workflows.bats b/tests/workflows.bats index 41edb2c..8fdf4a6 100644 --- a/tests/workflows.bats +++ b/tests/workflows.bats @@ -2,12 +2,10 @@ setup() { REAL_HOME="$HOME" # Redirecting HOME to hide a git identity moves mise's data directory with # it, and an empty one means every generated project re-downloads node, - # pnpm, lefthook and gitleaks. That is minutes per test and, worse, four - # network calls that can fail for reasons this suite is not testing — - # measured twice: `peer closed connection without sending TLS close_notify` - # while fetching pnpm, failing a test about git identity. The tests below - # point it back at the real store; HOME stays redirected, which is the - # thing they actually need. + # pnpm, lefthook and gitleaks — minutes per test, and network calls that + # can fail for reasons this suite is not testing. The tests below point it + # back at the real store; HOME stays redirected, which is the thing they + # actually need. REAL_MISE_DATA_DIR="${MISE_DATA_DIR:-${HOME}/.local/share/mise}" # a test that redirects HOME loses git's identity with it, and