Repository navigation
[Sprint] sprint-loop-1 - #3
Merged
Merged
Conversation
Tiny bash test runner and assert helpers to back the SUR-1810/1811/1812/ 1813/1819 sprint fixes. tests/run.sh iterates tests/sur-*.sh in subshells and reports pass/fail counts; tests/lib/assert.sh exposes assert_eq, assert_zero, assert_nonzero, assert_contains, and assert_no_var.
@include called exit 1 from a sourced library, killing the parent shell on any missing-include typo. Switch to return 1 and redirect the diagnostic to stderr so callers can decide whether to escalate via error::exit. Adds tests/sur-1813.sh covering: non-zero return without killing the runner, idempotent re-include guard, and stderr diagnostic.
commands::use checked the literal $_cmd (never assigned) instead of the
dynamic $_${cmd}, so every call re-ran command -v. Use a local
var=_${cmd} and dereference via ${!var}, dropping the SC2154 disable
that hid the typo. Adds tests/sur-1810.sh covering: cache populates
$_<cmd>, second call hits the cache even when command -v is broken,
and missing binaries still trigger commands::err_not_found.
FORMAT_LOG used echo with a trailing literal '\n', which the file
handler hid by stripping the sequence but the colorterm path emitted as
literal characters. LOG() did indirect variable expansion expecting
INFO/DEBUG/etc. to be variables (they are functions), silently dropping
every message routed through log()/log::log. Switch FORMAT_LOG to
printf '%s\n', rewrite LOG() as a case on ${1^^} dispatching to the
log::* functions with an Unknown log level error fallback, and remove
the now-redundant '\n'-stripping workaround in LOG_HANDLER_LOGFILE.
Adds tests/sur-1811.sh covering: real newline (no literal sequence),
unknown-level fallback, and case-insensitive dispatch.
dirs::noreplace was a no-op despite documenting an existence-and-empty check; combined with dirs::replace's safe_rmrf this silently allowed clobbering when the wrong helper was called. Implement the documented contract: error::exit when the directory exists and is non-empty, otherwise dirs::ensure. Drop the misleading 'will replace the contents' log line lifted from the replace function. Adds tests/sur-1812.sh covering all three cases (created, no-op, error+preserved marker).
options::add used $opt without local in the parse loop and reset OPTARG= globally, clobbering the caller's getopts state. options::syntax and options::doc similarly leaked items, count, mandatory, and args. Because options.sh calls options::add at source time, every include of the library leaked these names. Add the missing local declarations and drop the redundant OPTARG= reset (getopts manages it). Adds tests/sur-1819.sh covering: no leak after source, no leak after explicit options::syntax/options::doc invocation, and a clean-branches -h end-to-end smoke for the parsed-options path.
Code review caught that the FORMAT_LOG printf '%s\n' fix exposed a latent bug in LOG_HANDLER_DEFAULT, which captures the formatted log via command substitution (which strips trailing newlines). The colorterm handler was emitting log entries with echo -en (-n suppresses the final newline), so consecutive log calls concatenated onto a single line. Drop the -n so each entry is properly terminated; the file handler is unaffected because plain echo already terminates. Adds tests/sur-1811.sh case D, which captures three consecutive log::log calls and asserts the cleaned output spans >=3 lines. The test was confirmed to fail against the un-fixed handler before the fix landed.
Strengthens the dedupe coverage required by the SUR-1813 DoD. The prior B subtest only confirmed >=1 cksum guard variable existed, which does not rule out the body being sourced twice. New B2 overrides the source builtin to count invocations of bash/log.sh, double-includes the library, and asserts the count is exactly 1. Confirmed to fail against a deliberately broken dedupe (force-true guard check) before merging.
scealiontach
marked this pull request as ready for review
April 28, 2026 16:15
scealiontach
marked this pull request as draft
April 28, 2026 16:16
scealiontach
marked this pull request as ready for review
April 28, 2026 16:17
Code-review nit: tests trusted bash and mktemp to be on PATH. Add early-skip guards so unusual CI images do not false-fail.
scealiontach
force-pushed
the
sprint/2026-04-28-sprint-loop-1
branch
from
April 28, 2026 16:19
9a475eb to
de52744
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sprint Plan — 2026-04-28 Sprint Loop 1
Sprint Goal
Restore correctness in the foundational
bash/*.shlibraries that every other script and command in this repository sources. Five high-confidence, low-risk bug fixes target the include mechanism (includer.sh), the command-path cache (commands.sh), the logging dispatch (log.sh), the directory-safety helper (dirs.sh), and the options parser (options.sh). Each issue ships with a precise reproduction, file/line location, and a suggested fix; the work is independent except for a soft ordering preference (fixincluder.shfirst because the rest are sourced through it). Outcome: the core libraries behave as their docstrings andAGENTS.mdalready promise, with shellcheck/shfmt clean and no new regressions in the existing pre-commit suite.Selected Issues
1. SUR-1813 — Bug: includer.sh
@includecallsexit 1from a sourced library, killing parent shells@includeinbash/includer.sh(lines 22–40, line 38 in particular) callsexit 1when an include cannot be found. Because library files are sourced (not executed), a missing-include in an interactive shell or a script-with-cleanup kills the parent shell. The fix is toreturn 1from@includeand let the caller decide whether to escalate viaerror::exit. Stderr should also be used for the diagnostic.@includereturning to the test runner cleanly on failure. Doing this first unblocks faster iteration on the other four fixes.bash/includer.sh@includeusesreturn 1(notexit 1) on missing-file, with the diagnostic written to stderr (>&2).pre-commit run --all-filesis clean (shellcheck, shfmt, commitizen subjects).make packagestill produces all three tarballs (doc-*,bin-*,lib-*) underdist/.2. SUR-1810 — Bug:
commands::usecache check uses wrong variable, defeating PATH lookup cachingbash/commands.shlines 33–46.commands::usechecks$_cmd(a literal name that is never assigned) instead of the dynamic$_${cmd}(which the function does set on the next line).# shellcheck disable=SC2154on the line above had been hiding the typo. Result: every$(commands::use git|jq|aws|kubectl|…)call re-runscommand -vin a subshell. Fix: dereference the indirect variable vialocal var=_${cmd}; [ -z "${!var}" ], and remove theSC2154disable.git.sh,aws.sh,docker.sh,k8s.sh, and many command scripts, on hot paths inside loops. Fix is one-function, surgical, and independently verifiable.commands::usecaches vialocal var=_${cmd}; [ -z "${!var}" ]and the no-op# shellcheck disable=SC2154is removed.commands::err_not_foundpath still triggers when the binary is absent (covered by an added test that stubscommand -vto fail).commands::use bashtwice and asserts the cached value is used on the second call.pre-commit run --all-filesis clean.commands.shvia@include.3. SUR-1811 — Bug:
log.shFORMAT_LOGemits literal\nandLOG()dispatcher is brokenbash/log.sh. (1) Line 197echo "$formatted_log\n"emits a literal\nbecauseechodoes not interpret backslash escapes by default; the file handler masks this by stripping\n(line 256), but the colorterm/stderr path emits the literal sequence. Fix: useprintf '%s\n' "$formatted_log". (2) Lines 203–209LOG()does indirect variable expansion${!level}expectingINFO,DEBUG, etc. to be variables; they are functions, so the dispatcher always invokes the empty string and silently drops messages routed throughlog()/log::log. Fix: convert to acasestatement on the upper-cased level, dispatching tolog::trace|debug|info|warn|error|critical|notice|alert|emergency.FORMAT_LOGusesprintf '%s\n' "$formatted_log"and theLOG_HANDLER_LOGFILEworkaround that stripped\\nis either removed or kept and verified harmless.LOG()dispatches viacaseon${1^^}to the appropriatelog::*function, with an*) log::error "Unknown log level: $level"default branch.log::log INFO "hello"produceshellofollowed by a real newline (not\n); (b)log::log NOTAREALLEVEL "x"returns successfully via the error branch.\n(manual eyeball check + automated assertion viaprintfcapture).pre-commit run --all-filesis clean.4. SUR-1812 — Bug:
dirs::noreplaceis a no-op despite docstring promising existence checkbash/dirs.shlines 84–89.dirs::noreplaceis documented as "Return an error if the directory exists and is not empty, otherwise ensure that it exists" but the body only emits a misleading log message ("This command will replace the contents …" — wording lifted from the replace function) and unconditionally returns success. Combined withdirs::replace(which doessafe_rmrf), calling the wrong function silently allows clobbering. Fix: implement the documented behavior —[ -d "$d" ] && [ -n "$(ls -A "$d" 2>/dev/null)" ] && error::exit …; else dirs::ensure "$d"— and correct the log message.dirs::noreplaceerrors when the target exists and is non-empty (viaerror::exitwith a clear message naming the directory).dirs::noreplacecreates the directory viadirs::ensurewhen it does not exist, OR exits 0 when it exists but is empty.pre-commit run --all-filesis clean.5. SUR-1819 — Bug:
options::addleaksoptand resetsOPTARGgloballybash/options.sh.options::adddeclares most of its variableslocalbut usesopt(in the parsing loop ~line 74) and writesOPTARG=(clobbering the caller'sgetoptsstate) withoutlocal. Same pattern inoptions::syntax(items=()at ~164) andoptions::doc(count=0at 181,mandatory,argsat 186/191). Becauseoptions.shcallsoptions::add -o h …at source time (line 154), the leak fires on every include. Fix: addlocal opt; drop theOPTARG=reset entirely (getoptsmanages it); addlocaltoitems,count,mandatory,args.OPTARGmid-parse, or a top-level$optvariable, can corrupt subsequent argument parsing in subtle and intermittent ways. Fix is purely additive (local) — zero behavioral change to the documented contract, but eliminates a class of heisenbugs.local optadded to theoptions::addparsing loop; redundantOPTARG=removed.local items,local count,local mandatory,local argsadded tooptions::syntax/options::docas appropriate.options.shand asserts that none ofopt,items,count,mandatory,argsare set in the caller's environment afterward (usingdeclare -p VARand checking exit status).bash/clean-branches -horbash/changelog -h) still prints help correctly.pre-commit run --all-filesis clean.Risks + Mitigations
Mitigation: Use plain bash test scripts under
tests/at the repo root, invoked by a singletests/run.shentry point. Do NOT block sprint completion on adoptingbats-core(separate Backlog issue, SUR-1850); useset -e; trapplus[[ … ]]assertions for now. Ifbats-coreis already installed locally, prefer it.commands::usecache fix changes hot-path behavior in many libraries; subtle regressions could surface only in long-running scripts (e.g.aws.sh,k8s.sh).Mitigation: The fix is a pure correctness win (cache now actually caches), so the only behavioral delta is fewer
command -vinvocations. Add a smoke test that callscommands::use git,commands::use jqtwice each and inspects$_git,$_jqare populated and stable.log.shLOG()dispatcher fix may unmask previously-hidden noisy log statements that were silently dropped.Mitigation: Run
make packageand a manual exercise of any command-script that useslog::log/log(); if a flood of unexpected log output appears, treat it as a separate finding and file a follow-up issue rather than re-suppressing the dispatcher. DefaultLOG_LEVEL=0(per SUR-1847, out of scope here) will continue to gate verbose levels.includer.shexit → returnchange shifts failure-handling responsibility to callers; some command scripts may rely on the abort behavior.Mitigation: Grep for
@includecallers (grep -rn '@include' bash/) and confirm none rely onexit. Where command scripts want hard-fail behavior, have the caller use@include foo || error::exit "Failed to load foo". Update at most a handful of call sites; if more than three need adjustment, file a follow-up rather than expanding sprint scope.Mitigation: Issues are ordered (1813 first); 1810/1811/1812/1819 are mutually independent and can be parallelized across separate
feature/sur-XXXXbranches using each issue'sgitBranchName. Each PR is small and reviewable in isolation.Mitigation: Run
pre-commit run --all-fileslocally before pushing each PR. Repo CI (.github/workflows/pre-commit.yaml) only runs pre-commit hooks, so local-clean = CI-clean.# shellcheck disable=SC2154incommands.shmight surface unrelated SC2154 findings in nearby code.Mitigation: Scope the removal to the single line documented in SUR-1810. If shellcheck flags additional warnings, fix them in-scope only if trivial; otherwise file a follow-up.
^(fix|feature|refactor)/[a-zA-Z0-9\-]+$.Mitigation: Use the Linear-provided
gitBranchName(feature/sur-1810etc.); they already conform.Out of Scope
bats-coreas the standard test framework (SUR-1850 — separate Backlog item).semver(SUR-1851) orpack-scriptproduced binaries (SUR-1852).@doc/bashadoc/pack-scriptmechanisms beyond what is strictly required by this sprint's tests.Makefile,standard_defs.mk, ordist/build outputs except as incidental side effects ofmake packagevalidation.package::nameform (SUR-1840).Linear Evidence
Surinis(idce9ebfde-ff2b-4f54-90f1-c388591ca110, keySUR) — confirmed viamcp__linear__get_teamand re-confirmed from each issue'steamfield.shell-scripts(ida43901a0-b02b-4009-aae1-a6e8903d127d) — confirmed viamcp__linear__get_project;teams[0].name == "Surinis".mcp__linear__list_issues team="Surinis" project="shell-scripts" state="Backlog" limit=250 includeArchived=true(default in this MCP client).hasNextPage=false).manuallabel).Sub-issue Status
None of the five selected parent issues has any sub-issues (verified via
mcp__linear__list_issues parentId=<ID>returning empty list for each):No parent was skipped due to incomplete sub-issues.
Linear State Transitions