From 37a703ce19022d971884aa2b905f97380ee93d12 Mon Sep 17 00:00:00 2001 From: kellymichels Date: Sun, 26 Jul 2026 13:02:23 -0500 Subject: [PATCH] test: bats suite for the bash port, + fix underscore-key guard (phase 5 of #26) (#32) * test: add bats suite for the bash port + fix underscore-key guard (phase 5) Part of #26. Closes #30. The bash port reimplements the exclude lists, config accessors and argument parsing, so it can drift from PowerShell independently. 50 bats tests mirror the Pester suites assertion-for-assertion where the two are meant to agree: z_archive_excludes (per-kind lists and the deploy-vs-backup gating), config accessors, z_path Windows->WSL translation, json_build_label, and argument handling (bare invocation, unknown key, 'all' expansion, --port override). Fixes #30 along the way, because the alternative was a test enshrining the bug: zproj_require accepted underscore comment keys. It only checked the key was non-null, and a comment is a non-null JSON string, so 'zkill _note' sailed through and exited 0 having done nothing - the silent-success failure mode. Now rejects any _-prefixed key and requires the value to be a JSON object. Both checks earn their place: the type check catches string comments, the prefix rule catches an object-valued _template key that PowerShell refuses and the type check alone would allow. Documents #31 rather than fixing it: a leading dash on a project key works in every PowerShell script but only in bash/zdeploy - the others reject -myapp as an unknown option. Stripping it everywhere would make a mistyped flag resolve as a project key, so the tests pin current behaviour and bash/README.md now states the difference instead of the README's blanket claim. Verified by mutation testing: all 9 mutations turn the suite red - removing the python and vite backup gates, reintroducing #23 in bash, unfiltering underscore keys in zproj_keys and zproj_require, breaking zremote_compose_dir fallback and z_path translation, and removing zkill's all-expansion and no-args guard. Both mutated files confirmed restored byte-for-byte. An early run also caught a bug in the tests themselves: the membership helper used 'grep -qx' (regex), so the needle '.env' matched 'venv' and several 'excludes .env' assertions were false passes. Now uses -qxF. * fix(gitattributes): keep .bats files LF so 'bats tests/bash' works on a Windows checkout The LF rule was scoped to 'bash/**', which does not match tests/bash/. With core.autocrlf a Windows working copy got CRLF .bats files, and bats fails on them - so the command the README documents would not run on the machine the suite was written on without stripping \r first. Adds tests/bash/** and *.bats to the same eol=lf rule and renormalises. Verified by running 'bats tests/bash' with no sed preprocessing: 50/50. --- .gitattributes | 6 + bash/README.md | 20 ++++ bash/zhelpers.sh | 9 +- tests/bash/args.bats | 232 +++++++++++++++++++++++++++++++++++++++ tests/bash/helpers.bats | 237 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 tests/bash/args.bats create mode 100644 tests/bash/helpers.bats diff --git a/.gitattributes b/.gitattributes index 4dc0456..7ae3d52 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,12 @@ # Bash scripts must stay LF — CRLF breaks the shebang on Linux/macOS/WSL. bash/** text eol=lf +# The bats suites are bash too, and they live outside bash/ — without this a +# Windows checkout gets CRLF and `bats tests/bash` fails there (including when +# run through WSL against the Windows working copy). +tests/bash/** text eol=lf +*.bats text eol=lf + # PowerShell/cmd are happiest with CRLF on Windows. *.ps1 text eol=crlf *.cmd text eol=crlf diff --git a/bash/README.md b/bash/README.md index 8b598a3..6cf73c3 100644 --- a/bash/README.md +++ b/bash/README.md @@ -79,9 +79,29 @@ Where it's written, first match wins: Set `ZTOKENS_MODEL` to tag records with a specific model; it defaults to `est. chars/3.5`. +## Tests + +The bash port has its own [bats](https://github.com/bats-core/bats-core) suite, mirroring the PowerShell Pester tests assertion-for-assertion where the two are meant to agree — the exclude lists, config accessors, `z_path` translation, and argument handling: + +```bash +bats tests/bash +``` + +Install bats without root: + +```bash +git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core +/tmp/bats-core/install.sh ~/.local # then ensure ~/.local/bin is on PATH +``` + +A fixture config is injected through `ZCONFIG`, so the suite never reads your real `zconfig.json` and passes on a machine that has never been configured. Tests that run the scripts stay on paths that exit before doing any work; only `zkill` runs with a real target, against deliberately unused ports. + +Why a separate suite: the bash port reimplements the exclude lists and argument parsing, so it can drift from the PowerShell side independently. Both suites assert the same deploy-vs-backup rule — **deploys drop `.env*` and `uploads/`, backups keep them** — because that one has broken in production. + ## Differences from the PowerShell versions - `zsync ` mirrors `dist/` with `rsync -a --delete` (robocopy /MIR equivalent). - Scheduling uses cron (`setup_backup_schedule`) instead of Windows Task Scheduler. - The MOTD banner picks a random `motd/*.txt` instead of tracking a shuffle rotation. - Windows-only helpers (`.cmd` launchers) don't exist — scripts are directly executable. +- **A leading dash on a project key is only tolerated by `zdeploy`.** PowerShell accepts `-myapp` anywhere; in bash every other script treats `-myapp` as an unknown option and exits 1. Tracked as a parity gap — use bare keys in the bash port. diff --git a/bash/zhelpers.sh b/bash/zhelpers.sh index 9586d57..abfabb0 100644 --- a/bash/zhelpers.sh +++ b/bash/zhelpers.sh @@ -81,7 +81,14 @@ zproj() { jq -r --arg k "$1" ".projects[\$k]$2 // empty" "$ZCONFIG"; } # A project's local root, translated to the host's native path form. zproj_root() { z_path "$(zproj "$1" .localRoot)"; } zproj_require() { - if [ "$(jq -r --arg k "$1" '(.projects[$k] != null)' "$ZCONFIG")" != "true" ]; then + # Keys starting with "_" are comments, not projects (zproj_keys already skips + # them). Reject them here too, or `zkill _comment` would sail past this guard + # on the mere existence of the key and then act on a string as if it were a + # project - PowerShell's Get-ZProject has always refused them. + case "$1" in + _*) err "Unknown project key '$1'. Available: $(zproj_csv)"; exit 1 ;; + esac + if [ "$(jq -r --arg k "$1" '(.projects[$k] | type) == "object"' "$ZCONFIG")" != "true" ]; then err "Unknown project key '$1'. Available: $(zproj_csv)" exit 1 fi diff --git a/tests/bash/args.bats b/tests/bash/args.bats new file mode 100644 index 0000000..0f0c8a9 --- /dev/null +++ b/tests/bash/args.bats @@ -0,0 +1,232 @@ +#!/usr/bin/env bats +# Evomedia.net Token Savers - https://github.com/kellymichels/zscripts-token-savers +# Created by Kelly Michels - dev@evomedia.net +# Licensed under the MIT License. See LICENSE. +# +# args.bats - how the bash scripts read their target arguments. +# +# bats tests/bash +# +# Mirrors tests/ArgumentParsing.Tests.ps1. Same safety rule as the PowerShell +# side: these run the real scripts, so they stay on code paths that exit BEFORE +# doing any work (no args, unknown key). Only zkill runs with a valid target, +# and only because the fixture's dev ports are deliberately unused and its +# localRoots do not exist - it finds no listeners and kills nothing. zdeploy, +# zbackup_ec2, zec2, zec2online, zrepair, zstop, zstart and zbackup are never +# invoked with a real target; that needs a disposable server. + +setup() { + REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" + BASHDIR="$REPO/bash" + export ZCONFIG="$BATS_TEST_TMPDIR/zconfig.json" + cat > "$ZCONFIG" <<'JSON' +{ + "ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/fixtures/test.pem", "stackRoot": "/home/testuser/stack" }, + "paths": { "temp": "/fixtures/temp", "backupsLocal": "/fixtures/backups", "backupsEc2": "/fixtures/backups-ec2", "scriptsRoot": "/fixtures/zscripts", "oneDriveBackups": "" }, + "projects": { + "_note": "comment key - must never be treated as a project", + "pyapp": { + "label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp", + "startModule": "pyapp.main", "ports": { "dev": 59990 }, + "remote": { "path": "/home/testuser/stack/pyapp" } + }, + "viteapp": { + "label": "Fixture Vite Site", "kind": "vite", "localRoot": "/fixtures/nonexistent-viteapp", + "ports": { "dev": 59991 }, "remote": { "path": "/home/testuser/stack/viteapp" } + }, + "edgeproxy": { + "label": "Fixture Edge", "kind": "edge", "localRoot": "/fixtures/nonexistent-edge", + "remote": { "path": "/home/testuser/stack/edge" } + } + } +} +JSON +} + +# ---- no arguments ----------------------------------------------------------- +# Running bare must never mean "do it to everything". + +@test "zkill with no args shows usage and exits 1" { + run "$BASHDIR/zkill" + [ "$status" -eq 1 ] + [[ "$output" == *"Usage"* ]] +} + +@test "zstart with no args shows usage and exits non-zero" { + run "$BASHDIR/zstart" + [ "$status" -ne 0 ] + [[ "$output" == *"Usage"* ]] +} + +@test "zdeploy with no args shows usage and exits non-zero" { + run "$BASHDIR/zdeploy" + [ "$status" -ne 0 ] + [[ "$output" == *"Usage"* ]] +} + +@test "zbackup with no args shows usage and exits non-zero" { + run "$BASHDIR/zbackup" + [ "$status" -ne 0 ] + [[ "$output" == *"Usage"* ]] +} + +@test "usage lists the configured project keys" { + run "$BASHDIR/zkill" + [[ "$output" == *"pyapp"* ]] + [[ "$output" == *"viteapp"* ]] +} + +@test "usage never advertises the underscore comment key" { + run "$BASHDIR/zkill" + [[ "$output" != *"_note"* ]] +} + +# ---- unknown key ------------------------------------------------------------ +# Exiting 0 having selected nothing is the dangerous outcome. + +@test "zkill rejects an unknown project key" { + run "$BASHDIR/zkill" definitelynotaproject + [ "$status" -ne 0 ] + [[ "$output" == *"Unknown project key"* ]] +} + +@test "zstart rejects an unknown project key" { + run "$BASHDIR/zstart" definitelynotaproject + [ "$status" -ne 0 ] +} + +@test "zdeploy rejects an unknown project key" { + run "$BASHDIR/zdeploy" definitelynotaproject + [ "$status" -ne 0 ] +} + +@test "the unknown-key error names both the bad key and the valid ones" { + run "$BASHDIR/zkill" definitelynotaproject + [[ "$output" == *"definitelynotaproject"* ]] + [[ "$output" == *"pyapp"* ]] +} + +@test "the underscore comment key is not addressable as a project" { + run "$BASHDIR/zkill" _note + [ "$status" -ne 0 ] +} + +# The convention is that ANY "_"-prefixed key is a comment, not a project - +# PowerShell's Get-ZProject refuses them outright. A string-valued comment is +# also caught by the "is it an object?" check, so this uses an OBJECT-valued +# underscore key (e.g. someone keeping a "_template" block to copy from) to +# prove the prefix rule itself is enforced. +@test "an object-valued underscore key is still not addressable" { + cat > "$BATS_TEST_TMPDIR/objkey.json" <<'JSON' +{ + "ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/f/t.pem", "stackRoot": "/s" }, + "paths": { "temp": "/f/t", "backupsLocal": "/f/b", "backupsEc2": "/f/be", "scriptsRoot": "/f/z", "oneDriveBackups": "" }, + "projects": { + "_template": { + "label": "Template To Copy", "kind": "python", "localRoot": "/fixtures/none", + "ports": { "dev": 59993 }, "remote": { "path": "/s/t" } + }, + "pyapp": { + "label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp", + "ports": { "dev": 59990 }, "remote": { "path": "/s/py" } + } + } +} +JSON + ZCONFIG="$BATS_TEST_TMPDIR/objkey.json" run "$BASHDIR/zkill" _template + [ "$status" -ne 0 ] + [[ "$output" == *"Unknown project key"* ]] +} + +@test "an object-valued underscore key is excluded from 'all'" { + cat > "$BATS_TEST_TMPDIR/objkey2.json" <<'JSON' +{ + "ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/f/t.pem", "stackRoot": "/s" }, + "paths": { "temp": "/f/t", "backupsLocal": "/f/b", "backupsEc2": "/f/be", "scriptsRoot": "/f/z", "oneDriveBackups": "" }, + "projects": { + "_template": { + "label": "Template To Copy", "kind": "python", "localRoot": "/fixtures/none", + "ports": { "dev": 59993 }, "remote": { "path": "/s/t" } + }, + "pyapp": { + "label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp", + "ports": { "dev": 59990 }, "remote": { "path": "/s/py" } + } + } +} +JSON + ZCONFIG="$BATS_TEST_TMPDIR/objkey2.json" run "$BASHDIR/zkill" all + [ "$status" -eq 0 ] + [[ "$output" == *"Fixture Python App"* ]] + [[ "$output" != *"Template To Copy"* ]] +} + +# ---- leading dash: CURRENT bash behaviour ------------------------------------ +# NOTE: this is a known divergence from the PowerShell port, pinned here as it +# actually behaves rather than as it ideally would. PowerShell tolerates a +# leading dash on any project key (zdeploy -myapp == zdeploy myapp). In bash, +# only zdeploy strips it ("${1#-}"); every other script treats -myapp as an +# unknown option and exits 1. These tests document today's behaviour so a fix +# is a deliberate change with a failing test to update, not a silent surprise. + +@test "bash zdeploy strips a leading dash from a project key" { + # Unknown key keeps this on the no-work path; the error must name the key + # without its dash, proving the strip happened. + run "$BASHDIR/zdeploy" -definitelynotaproject + [ "$status" -ne 0 ] + [[ "$output" == *"definitelynotaproject"* ]] + [[ "$output" != *"-definitelynotaproject"* ]] +} + +@test "bash zkill rejects a dashed key as an unknown option (diverges from PowerShell)" { + run "$BASHDIR/zkill" -pyapp + [ "$status" -eq 1 ] + [[ "$output" == *"Unknown option"* ]] +} + +@test "bash zstart rejects a dashed key as an unknown option (diverges from PowerShell)" { + run "$BASHDIR/zstart" -pyapp + [ "$status" -ne 0 ] + [[ "$output" == *"Unknown option"* ]] +} + +# ---- zkill target resolution (safe: fixture ports are unused) ---------------- + +@test "zkill accepts a bare project key" { + run "$BASHDIR/zkill" pyapp + [ "$status" -eq 0 ] + [[ "$output" == *"Fixture Python App"* ]] + [[ "$output" == *"59990"* ]] +} + +@test "zkill accepts several keys at once" { + run "$BASHDIR/zkill" pyapp viteapp + [ "$status" -eq 0 ] + [[ "$output" == *"Fixture Python App"* ]] + [[ "$output" == *"Fixture Vite Site"* ]] +} + +@test "zkill 'all' expands to every project with a dev port" { + run "$BASHDIR/zkill" all + [ "$status" -eq 0 ] + [[ "$output" == *"Fixture Python App"* ]] + [[ "$output" == *"Fixture Vite Site"* ]] +} + +@test "zkill 'all' skips projects with no dev port (edge/docker stacks)" { + run "$BASHDIR/zkill" all + [[ "$output" != *"Fixture Edge"* ]] +} + +@test "zkill --port overrides the configured dev port" { + run "$BASHDIR/zkill" pyapp --port 59999 + [ "$status" -eq 0 ] + [[ "$output" == *"59999"* ]] + [[ "$output" != *"59990"* ]] +} + +@test "zkill announces that --kill-all is not implemented rather than silently ignoring it" { + run "$BASHDIR/zkill" pyapp --kill-all + [ "$status" -eq 0 ] + [[ "$output" == *"kill-all"* ]] +} diff --git a/tests/bash/helpers.bats b/tests/bash/helpers.bats new file mode 100644 index 0000000..85d999b --- /dev/null +++ b/tests/bash/helpers.bats @@ -0,0 +1,237 @@ +#!/usr/bin/env bats +# Evomedia.net Token Savers - https://github.com/kellymichels/zscripts-token-savers +# Created by Kelly Michels - dev@evomedia.net +# Licensed under the MIT License. See LICENSE. +# +# helpers.bats - coverage for the pure/config functions in bash/zhelpers.sh. +# +# bats tests/bash +# +# The bash port has its own implementation of the exclude lists and config +# accessors, so it can drift from the PowerShell side independently. These +# mirror tests/ZHelpers.Tests.ps1 assertion-for-assertion where the two are +# meant to agree. +# +# A fixture config is injected through ZCONFIG (zhelpers.sh has always honored +# it), so no real zconfig.json is read. + +setup() { + REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" + export ZCONFIG="$BATS_TEST_TMPDIR/zconfig.json" + cat > "$ZCONFIG" <<'JSON' +{ + "ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/fixtures/test.pem", "stackRoot": "/home/testuser/stack" }, + "paths": { "temp": "/fixtures/temp", "backupsLocal": "/fixtures/backups", "backupsEc2": "/fixtures/backups-ec2", "scriptsRoot": "/fixtures/zscripts", "oneDriveBackups": "" }, + "projects": { + "_note": "comment key - must never be treated as a project", + "pyapp": { + "label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/pyapp", + "startModule": "pyapp.main", "ports": { "dev": 59990 }, "domain": "pyapp.example.com", + "remote": { "path": "/home/testuser/stack/pyapp", "composeDir": "/home/testuser/stack/pyapp/docker", "appService": "app" }, + "deploy": { "zipName": "PyAppDeploy.zip", "exclude": ["docs", "fixtures-extra"] } + }, + "viteapp": { + "label": "Fixture Vite Site", "kind": "vite", "localRoot": "/fixtures/viteapp", + "ports": { "dev": 59991 }, "domain": "www.example.com", + "remote": { "path": "/home/testuser/stack/viteapp" } + }, + "nextapp": { + "label": "Fixture Next App", "kind": "nextjs", "localRoot": "/fixtures/nextapp", + "ports": { "dev": 59992 }, "remote": { "path": "/home/testuser/stack/nextapp" } + }, + "edgeproxy": { + "label": "Fixture Edge", "kind": "edge", "localRoot": "/fixtures/edge", + "proxyContainer": "edge_proxy", "remote": { "path": "/home/testuser/stack/edge" } + }, + "noports": { + "label": "Fixture No Dev Port", "kind": "python", "localRoot": "/fixtures/noports", + "remote": { "path": "/home/testuser/stack/noports" } + } + } +} +JSON + # shellcheck disable=SC1090 + source "$REPO/bash/zhelpers.sh" +} + +# Emit the exclude list for a project; $2=1 means "for backup". +excludes() { z_archive_excludes "$1" "${2:-0}"; } + +# Fixed-string, whole-line match. -F matters: without it grep treats the needle +# as a regex, and '.env' would match 'venv' (leading '.' = any char) - which +# silently turns every ".env is excluded" assertion into a false pass. +has() { printf '%s\n' "$1" | grep -qxF "$2"; } + +# ---- config accessors ------------------------------------------------------- + +@test "zproj_keys returns every project in config order" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zproj_keys | paste -sd, -" + [ "$status" -eq 0 ] + [ "$output" = "pyapp,viteapp,nextapp,edgeproxy,noports" ] +} + +@test "zproj_keys omits underscore comment keys" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zproj_keys" + [ "$status" -eq 0 ] + ! has "$output" "_note" +} + +@test "zproj reads a project field" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zproj pyapp .label" + [ "$output" = "Fixture Python App" ] +} + +@test "zproj returns empty for a missing field rather than the string null" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zproj viteapp .startModule" + [ "$output" = "" ] +} + +@test "zec2_target composes user@ip" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zec2_target" + [ "$output" = "testuser@203.0.113.10" ] +} + +@test "zremote_compose_dir prefers composeDir" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zremote_compose_dir pyapp" + [ "$output" = "/home/testuser/stack/pyapp/docker" ] +} + +@test "zremote_compose_dir falls back to remote.path" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zremote_compose_dir viteapp" + [ "$output" = "/home/testuser/stack/viteapp" ] +} + +@test "zedge_key finds the first edge-kind project" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zedge_key" + [ "$output" = "edgeproxy" ] +} + +@test "zproj_keys_with_domain lists only projects that have a domain" { + run bash -c "source '$REPO/bash/zhelpers.sh'; zproj_keys_with_domain | paste -sd, -" + [ "$output" = "pyapp,viteapp" ] +} + +# ---- z_path (Windows -> host path translation) ------------------------------ +# A shared zconfig.json may carry Windows paths when the same file is used from +# a Windows checkout and from WSL. + +@test "z_path translates a Windows drive path to /mnt form" { + run bash -c "source '$REPO/bash/zhelpers.sh'; z_path 'F:\\evomedia.net\\app'" + [ "$output" = "/mnt/f/evomedia.net/app" ] +} + +@test "z_path lowercases the drive letter" { + run bash -c "source '$REPO/bash/zhelpers.sh'; z_path 'C:\\Users\\Someone'" + [ "$output" = "/mnt/c/Users/Someone" ] +} + +@test "z_path leaves a unix path untouched (no-op on native Linux/macOS)" { + run bash -c "source '$REPO/bash/zhelpers.sh'; z_path '/home/kelly/project'" + [ "$output" = "/home/kelly/project" ] +} + +@test "z_path leaves an empty string empty" { + run bash -c "source '$REPO/bash/zhelpers.sh'; z_path ''" + [ "$output" = "" ] +} + +# ---- z_archive_excludes ----------------------------------------------------- +# Parity target: tests/ZHelpers.Tests.ps1 "Get-ArchiveExcludes". + +@test "excludes: common junk applies to every kind" { + run excludes pyapp + for n in .git .idea .vscode .claude tmp nul .DS_Store backups; do + has "$output" "$n" || { echo "missing common exclude: $n"; return 1; } + done +} + +@test "excludes: python build/venv cruft" { + run excludes pyapp + for n in .venv venv __pycache__ .pytest_cache .nicegui archive dist build htmlcov; do + has "$output" "$n" || { echo "missing python exclude: $n"; return 1; } + done +} + +@test "excludes: vite drops node_modules and dist" { + run excludes viteapp + has "$output" "node_modules" + has "$output" "dist" +} + +@test "excludes: nextjs drops .next and .vercel" { + run excludes nextapp + has "$output" ".next" + has "$output" ".vercel" + has "$output" "next-env.d.ts" +} + +# The regression this suite exists for: a dev .env shipped to production in a +# deploy zip. Deploys drop secrets and uploads; backups must keep them or the +# backup is not a restore point. +@test "excludes: python DEPLOY drops .env, .env.local, .env.production and uploads" { + run excludes pyapp 0 + for n in .env .env.local .env.production uploads; do + has "$output" "$n" || { echo "deploy should exclude: $n"; return 1; } + done +} + +@test "excludes: python BACKUP keeps .env, .env.local, .env.production and uploads" { + run excludes pyapp 1 + for n in .env .env.local .env.production uploads; do + if has "$output" "$n"; then echo "backup must NOT exclude: $n"; return 1; fi + done +} + +@test "excludes: python BACKUP still drops the virtualenv (bulk, not a secret)" { + run excludes pyapp 1 + has "$output" ".venv" +} + +@test "excludes: vite DEPLOY drops .env files" { + run excludes viteapp 0 + for n in .env .env.local .env.production; do + has "$output" "$n" || { echo "deploy should exclude: $n"; return 1; } + done +} + +@test "excludes: vite BACKUP keeps .env files" { + run excludes viteapp 1 + for n in .env .env.local .env.production; do + if has "$output" "$n"; then echo "backup must NOT exclude: $n"; return 1; fi + done +} + +@test "excludes: vite BACKUP still drops node_modules" { + run excludes viteapp 1 + has "$output" "node_modules" +} + +# nextjs has no backup gate on the PowerShell side either - both agree. +@test "excludes: nextjs drops .env in a backup too (no gate, matches PowerShell)" { + run excludes nextapp 1 + has "$output" ".env" +} + +@test "excludes: the project's own deploy.exclude entries are merged in" { + run excludes pyapp + has "$output" "docs" + has "$output" "fixtures-extra" +} + +@test "excludes: a kind with no specific list still gets the common entries" { + run excludes edgeproxy + has "$output" ".git" +} + +# ---- json_build_label ------------------------------------------------------- + +@test "json_build_label formats v." { + printf '{ "productVersion": "1.0", "buildNumber": 42 }' > "$BATS_TEST_TMPDIR/bv.json" + run bash -c "source '$REPO/bash/zhelpers.sh'; json_build_label '$BATS_TEST_TMPDIR/bv.json'" + [ "$output" = "v1.0.42" ] +} + +@test "json_build_label reports unknown for a missing file" { + run bash -c "source '$REPO/bash/zhelpers.sh'; json_build_label '$BATS_TEST_TMPDIR/nope.json'" + [ "$output" = "unknown" ] +}