Skip to content

feat(software-writer): complete BATS reference and correct bash rules - #49

Merged
SpiGAndromeda merged 6 commits into
mainfrom
fix/software-writer-bash-bats-rules
Nov 4, 2026
Merged

SpiGAndromeda merged 6 commits into
mainfrom
fix/software-writer-bash-bats-rules

Conversation

@SpiGAndromeda

@SpiGAndromeda SpiGAndromeda commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #17
Fixes #23

Summary

Completes the BATS framework reference in writing-tests, corrects and extends the Bash rules in writing-code, and fixes rules of the same kind in the other stack and framework references.

Changes

writing-tests: BATS reference

  • Written against bats-core 1.14.x and bats-assert 2.2.x.
  • New sections: error-path idiom, test doubles, helpers and temp directories, non-deterministic calls to flag.
  • Error path: run -<N>, run !, bats_require_minimum_version 1.5.0 (anywhere before the first flagged run), BW01 and BW02. refute, run ! and a bare assert_failure pass when the command does not exist (exit 127), so the reference asserts the exact status.
  • Independence: restore in teardown, exported-only variables from setup_file, --jobs prerequisites and opt-outs, no EXIT trap in the test shell, root ignoring file modes. The BATS_*_TMPDIR scopes sit under helpers and temp directories.
  • Corrections: run f does not apply set -e inside a function that does not enable it itself unless --errexit is passed (from 1.14.0, run honors a set -e the function turns on), so run -0 f can pass where a direct f fails; f || code=$? also runs f with errexit off (a failing command runs and only its failure is ignored), so that capture is limited to functions that propagate their own failures; a failing < <(…) registers no tests and the run can still exit 0; duplicate names abort the file.
  • The bats-referenced-by-name template lists bats_test_function … -- <function>.

writing-code: Bash reference

  • Command substitution: under set -o posix the $( … ) failure ends the shell on bash 3.2 through 5.3.
  • || true only when every non-zero status is an answer; otherwise branch on the exact status (grep exits 2 on an unreadable file). When a failure shares its status with an answer, no status check separates them.
  • The Bash 4 entry gains a leading rule to check bash NEWS for the minor version, with declare -g and local - as further examples.
  • New rules: restoring options by replaying saved state (re-asserting errexit, which a $(set +o) capture can drop), local a=x b="$a", read -a reading one line, redirection order with 2>, data into jq and awk through --arg and ENVIRON (a value interpolated into a jq filter can inject code; every awk processes -v escapes), octal leading zeros (guard digits before 10#$x), jq // empty bindings.

writing-code: workflow

  • Step 3: when an edit makes a function fail, search its callers for a fallback that now runs.
  • Step 6 ("Check and classify each comment") starts with the exemption-marker check, so Pre-Step-6 content runs first. For a comment written, edited, or kept on the lines being written or edited, it checks the comment against the code, then runs each claim about the language, interpreter or tool in an isolated probe, never by running the code under edit. A run counts only on the version or platform the claim names and corrects only the part it refutes; a claim that cannot be run here stays unchanged and is reported as unverified. The comment is then classified; when classification deletes a comment the step corrected, the report to the user names the deletion and its bucket. A load-bearing workaround comment stays.

Other stack and framework references

  • Python, Go, PHP and TypeScript references and comments.md say what a named exception, a discard or a catch-all still hides. Go allows _ = only with a comment that says why the discard is safe.
  • Vitest: vi.resetAllMocks() resets vi.fn() implementations, vi.restoreAllMocks() restores vi.spyOn spies only. Jest: pick the call by where implementations are set; jest.restoreAllMocks() leaves jest.fn() implementations in place, and jest.resetAllMocks() replaces jest.fn(impl) and jest.mock factory implementations with ones that return undefined.
  • Every framework reference names its helper idiom and per-test temp directory. Version lines added or moved: mocha 12.x, node:test on Node 26, unittest on Python 3.15, Go 1.27, Vitest 5.x, Pest 5.x on PHPUnit 13, pytest 9.2.x.

Tests and CI

  • inject_extension.bats (software-writer and work-orchestrator): the unreadable-file test no longer depends on chmod as root. A directory-at-path test covers the guard for every user, and the chmod 000 test skips as root.
  • setup-bats.sh pins bats-core 1.14.0 and bats-assert 2.2.4.

The BATS reference lacked the error-path idiom, isolation scopes, test doubles, and non-deterministic-call table that the other framework references carry, and it did not name the refusal idiom, restore mechanism, or helper idiom that the generic writing-tests references promise. It is now written against bats-core 1.14 and bats-assert 2.2 and covers `run -<N>`, the exit-127 blind spot of `refute`, `run !`, and a bare `assert_failure`, and the EXIT-trap and root hazards.

The bash reference said a command substitution never inherits errexit, which is false under `set -o posix`, and recommended `|| true` without saying it also hides `grep`'s exit 2 on an unreadable file. It now limits `|| true` to commands whose every non-zero status is an answer, and adds rules for restoring options, dependent `local` assignments, `read -a`, redirection order, passing data to jq and awk, octal leading zeros, and jq `// empty` bindings.

writing-code now searches callers for a fallback that becomes reachable when an edit makes a function fail, and runs a comment's claims about the language, interpreter, or tool before keeping it.

The other stack references now name what a construct still hides (Python exception subclasses, Go `_ =` and an unchecked `Close`, PHP `json_decode` returning null for both invalid input and a JSON `null`, TypeScript catch-alls), and every framework reference names its helper idiom and per-test temporary directory and states the version it was written against (Vitest 5, Pest 5 on PHPUnit 13, pytest 9.2).
By default `$(set +o)` runs in a subshell that drops errexit, so the saved text says `set +o errexit` and replaying it turned errexit off in the caller. The restore idiom now captures `$-` in the caller and re-asserts `set -e` after the replay.

The BATS reference also stated three rules too broadly: `run -0` adds nothing only for an external command, since `run` suppresses errexit inside a function; `--no-parallelize-across-files` needs only bats' own lock, not GNU parallel or rush; and a `for` loop around `@test` works only with one iteration. The bash `|| true` rule now says that no status check separates a failure that shares its status with an answer, as BusyBox `pgrep` does for an unknown option.
The BATS reference told tests to capture a function's status with `|| code=$?`, which runs the function with errexit off, so a failing command in its middle goes unnoticed. The rule now allows that capture only for a function that propagates its own failures (`cmd || return`) and otherwise calls the function bare.

The bash reference said interpolating a value into a jq filter is a compile error. A value with balanced quotes compiles and injects code instead. The awk `-v` rule now covers every awk, since POSIX requires the escape processing. Both rules, and the option-restore rule, now have their own bullets.

In writing-code Step 6 the digraph checked a comment after classifying it and skipped classification once the comment was corrected. The check now runs first, a corrected comment is still classified, and a claim that cannot be run here is cut or narrowed instead of kept.

The Go reference now allows `_ =` with a comment that says why the discard is safe, matching the comment rules, and states that a bare `defer f.Close()` is fine on a file opened only for reading.
The Vitest and Jest references told tests to clear mock implementations between tests with `vi.restoreAllMocks()` and `jest.restoreAllMocks()`, which restore only `vi.spyOn` and `jest.spyOn` spies and leave a `vi.fn()` or `jest.fn()` implementation in place. Both now name `vi.resetAllMocks()` and `jest.resetAllMocks()` for those mocks and keep restore for spies.

bats-core 1.14.0 changed `run` to honor a `set -e` the called function turns on itself, so the BATS reference now says `run` suppresses errexit only for a function that does not enable it. A failing `< <(…)` registry read registers no tests and the run can still exit 0, since "Found no tests" only fires when the whole run has none.

writing-code Step 6 now applies the run requirement to comments written, edited, or kept on the lines being written or edited, and a load-bearing workaround comment stays only once its claim has been run or narrowed.
writing-code Step 6 removed or narrowed any comment claim that could not be run on the current machine, which deleted load-bearing workaround comments and rewrote true claims about other versions into false ones. A claim now runs in an isolated probe and counts only on the version or platform it names. A run corrects only the part it refutes, and a claim that cannot be run stays and is reported to the user as unverified. The exemption check now opens Step 6, so Pre-Step-6 content runs before it.

The Jest reference recommended `jest.resetAllMocks()` in `afterEach`, which replaces every `jest.fn(impl)` and `jest.mock` factory implementation with one that returns undefined. It now picks the call by where implementations are set and says the `resetMocks` and `restoreMocks` flags run the same calls before every test.

The BATS error-path example now calls `bats_require_minimum_version 1.5.0`, so a copied `run -2` no longer prints BW02, and the rule says the call works anywhere before the first flagged `run`. Under `|| code=$?` a failing command runs and only its failure is ignored. The octal rule no longer claims a per-version result for an empty `10#$x`.
writing-code Step 6 corrects a comment and then classifies it, and the classification can delete the comment it just corrected, which the user never learned about. The report to the user now names such a deletion and its bucket. The step heading now reads "Check and classify each comment", and the digraph's classify node names loading `references/comments.md`, so every prose action in the step has a node.
@SpiGAndromeda
SpiGAndromeda merged commit df59a8d into main Nov 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant