* 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.
Part of #26. Phase 2 asserted that Get-ArchiveExcludes returns the right list;
this asserts the archive that actually ships. A name can be on the exclude list
and still land in the zip - only opening the zip proves otherwise. Each test
builds a real temp source tree, archives it, and reads back the entries.
66 tests across: TopLevelExclude (files, directories, multiple names, absent
names), recursive junk-directory pruning at top level and any depth, junk
extensions, nested archives, OS junk filenames, script-file inclusion via
-IncludeScriptFiles, ExtraFiles (how zbackup bundles a pg_dump), zip mechanics
(overwrite, destination creation, forward-slash entry paths), and two
end-to-end shapes - a python deploy zip built from the real exclude list, and a
backup of the same tree that must retain .env and uploads.
Two behaviours are pinned deliberately:
- TopLevelExclude matches TOP-LEVEL names only, so a nested backend/.env is
NOT excluded by listing '.env'. That is current behaviour and the reason
deploys rely on server-side preservation; the test exists so changing it is
a decision rather than an accident.
- An all-junk tree throws instead of producing an empty zip - a silent empty
deploy would unpack to nothing on the server.
Verified by mutation testing, which paid for itself immediately: the first run
showed the junk-directory tests were vacuous. Inside a Where-Object, $_ rebinds
to the pipeline item and shadowed Pester's -ForEach value, so the pattern
matched nothing and the tests passed while asserting nothing. Fixed by
capturing the value first; re-run now catches all 8 mutations (disabling
TopLevelExclude, top-level and nested junk pruning, each file filter, and the
empty-archive throw). ZHelpers.ps1 restored byte-for-byte afterwards.
Part of #26. This layer has regressed more than any other - bare vs dashed
keys, 'all' expansion, and whether a bad key fails loudly or quietly selects
nothing.
42 tests over three guarantees:
- Running bare shows usage and exits non-zero, for all ten target-taking
scripts. Some of these used to mean 'do it to everything' when run with no
args, which is how an unintended full backup or deploy happens.
- An unknown key fails loudly. Exiting 0 having selected nothing is the
dangerous outcome: a typo'd key in a scheduled task looks like a
successful run that backed up nothing.
- A leading dash is stripped before the lookup, so -myapp == myapp. Asserted
via a dashed *unknown* key, so the error must name 'x' rather than '-x'.
Plus zkill's own resolution: bare key, dashed key, several keys, 'all' and
'-all' expanding to projects that have a ports.dev, edge/docker stacks skipped,
and -Port overriding the configured port.
Safety: the tests run the real scripts as child processes, so they are confined
to paths that exit before doing any work. Only zkill runs with a valid target,
because the fixture's dev ports (59990/59991) 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 is integration territory needing a disposable
server.
Each test runs against an isolated temp installation - the .ps1 files copied
beside a fixture zconfig.json - so nothing touches this repo, no real config is
read, and the suite passes on a machine that has never been configured. That
also makes it independent of the ZCONFIG seam in #27, so the PRs can merge in
any order.
Verified by mutation testing: removing zkill's all-expansion, dash tolerance
and no-args guard, making an unknown key exit 0, and letting underscore comment
keys leak in as projects each turn the suite red (2/1/2/16/4 tests). Both
mutated files confirmed restored byte-for-byte.
Part of #26. The toolkit had no automated tests at all - including for the
functions that decide what goes into a deploy zip, which is where a dev .env
reached production (#23).
Phase 1 - the seam. Get-ZConfig read a hardcoded $PSScriptRoot\zconfig.json,
so nothing config-dependent could be tested without touching the real config.
Adds Get-ZConfigPath honoring $env:ZCONFIG (the bash port has always had this,
so it also closes a parity gap) and Reset-ZConfigCache to drop the memoised
config between fixtures. Deliberately did NOT convert the exit 1 paths to
throw: that changes observed CLI output, and the pure functions don't need it.
Phase 2 - 61 tests over the functions with no side effects: Get-ArchiveExcludes
(common/python/vite/nextjs lists, deploy.exclude merging, dedupe, array shape),
Get-ZConfig / Get-ZConfigPath / Get-ZProjectKeys / Get-ZProject (dash tolerance,
underscore-key filtering, memoisation), Get-ZEdgeProject, Get-RemoteComposeDir,
Get-Ec2Target / Get-Ec2Home, Get-LabelFromBuildJsonObj, Read-JsonBuildVersion.
The suite is verified by mutation testing rather than assumed useful - six
deliberate regressions were each introduced and confirmed to turn it red,
including reintroducing the exact #23 bug and its inverse (backups silently
dropping .env/uploads, which would produce restore points that cannot restore).
Runs off a fixture config injected via ZCONFIG, so it never reads a real
zconfig.json and passes on a machine that has never been configured.