@objctp/opencode-shell-routines 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/agents/shell-architect.md +88 -0
  4. package/agents/shell-expert.md +60 -0
  5. package/commands/shell-audit.md +47 -0
  6. package/commands/shell-batch-exec.md +48 -0
  7. package/commands/shell-new.md +57 -0
  8. package/commands/shell-routines-setup.md +66 -0
  9. package/commands/shell-test-run.md +46 -0
  10. package/opencode.json +19 -0
  11. package/package.json +34 -0
  12. package/plugins/shell-hooks.ts +150 -0
  13. package/scripts/lib-batch.sh +297 -0
  14. package/scripts/lib-common.sh +332 -0
  15. package/skills/shell-batch-operations/SKILL.md +97 -0
  16. package/skills/shell-batch-operations/assets/batch-template.sh +124 -0
  17. package/skills/shell-batch-operations/examples/data-pipeline.sh +157 -0
  18. package/skills/shell-batch-operations/examples/file-batch.sh +140 -0
  19. package/skills/shell-batch-operations/references/decision-tree.md +53 -0
  20. package/skills/shell-best-practices/SKILL.md +313 -0
  21. package/skills/shell-best-practices/assets/library.sh +142 -0
  22. package/skills/shell-best-practices/assets/minimal.sh +54 -0
  23. package/skills/shell-best-practices/assets/posix.sh +180 -0
  24. package/skills/shell-best-practices/assets/standard.sh +203 -0
  25. package/skills/shell-best-practices/references/patterns.md +386 -0
  26. package/skills/shell-best-practices/references/security.md +195 -0
  27. package/skills/shell-debugging/SKILL.md +115 -0
  28. package/skills/shell-debugging/examples/debug-session.md +165 -0
  29. package/skills/shell-debugging/references/debugging-guide.md +336 -0
  30. package/skills/shell-profiling/SKILL.md +154 -0
  31. package/skills/shell-profiling/examples/profile-session.md +225 -0
  32. package/skills/shell-profiling/references/optimisation-patterns.md +373 -0
  33. package/skills/shell-profiling/references/profiling-tools.md +318 -0
  34. package/skills/shell-profiling/scripts/bench.sh +82 -0
  35. package/skills/shell-profiling/scripts/trace-aggregate.sh +34 -0
  36. package/skills/shell-review/SKILL.md +61 -0
  37. package/skills/shell-review/examples/sample-review.md +42 -0
  38. package/skills/shell-review/references/guidelines.md +48 -0
  39. package/skills/shell-review/references/review-template.md +56 -0
  40. package/skills/shell-security/SKILL.md +128 -0
  41. package/skills/shell-security/examples/dangerous-command-review.md +231 -0
  42. package/skills/shell-security/examples/secure-script-example.sh +317 -0
  43. package/skills/shell-security/references/dangerous-commands.md +561 -0
  44. package/skills/shell-security/references/security-patterns.md +30 -0
  45. package/skills/shell-security/references/sensitive-files.md +525 -0
  46. package/skills/shell-security/scripts/security-audit.sh +208 -0
  47. package/skills/shell-test/SKILL.md +237 -0
  48. package/skills/shell-test/examples/test-example.md +74 -0
  49. package/skills/shell-test/references/advanced-patterns.md +52 -0
  50. package/skills/shell-test/references/assertions.md +184 -0
  51. package/skills/shell-test/references/test-template.md +60 -0
  52. package/skills/shell-test/scripts/public-coverage.sh +93 -0
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env bash
2
+ # security-audit.sh -- Scan shell scripts for security vulnerabilities
3
+ #
4
+ # Usage:
5
+ # ./security-audit.sh <file|directory>
6
+ #
7
+ # Scans .sh files for destructive commands, hardcoded credentials,
8
+ # insecure permissions, fork bombs, and other security risks.
9
+ # For detailed explanations and fix commands, see references/dangerous-commands.md
10
+ #
11
+ # shellcheck disable=SC2329
12
+
13
+ set -euo pipefail
14
+
15
+ ###
16
+ ### :::: Helpers :::: #################
17
+ ###
18
+
19
+ readonly RED='\033[0;31m'
20
+ readonly PUR='\033[0;35m'
21
+ readonly YEL='\033[0;33m'
22
+ readonly LGRN='\033[92m'
23
+ readonly RST='\033[0m'
24
+ readonly BOLD='\033[1m'
25
+
26
+ function fatal() { echo -e "${PUR}${BOLD}◆ [FATAL]${RST} $*"; }
27
+ function severe() { echo -e "${RED}● [SEVERE]${RST} $*"; }
28
+ function moderate() { echo -e "${YEL}▲ [MODERATE]${RST} $*"; }
29
+ function clean() { echo -e "${LGRN}✔ [OK]${RST} $*"; }
30
+
31
+ ###
32
+ ### :::: Check functions :::: #########
33
+ ###
34
+
35
+ # Internal: grep a file for a pattern, report matches via severity function
36
+ # Usage: _check_grep FILE PATTERN SEVERITY_FUNC OK_MSG [MSG_PREFIX]
37
+ # SEVERITY_FUNC: fatal, severe, or moderate
38
+ # OK_MSG: message shown when no matches found
39
+ # MSG_PREFIX: optional prefix before the match (e.g. "Possible fork bomb: ")
40
+ # Returns: 0 if clean, 1 if issues found
41
+ function _check_grep() {
42
+ local file="$1"
43
+ local pattern="$2"
44
+ local severity="$3"
45
+ local ok_msg="$4"
46
+ local msg_prefix="${5:-}"
47
+
48
+ local found=0
49
+ while IFS= read -r raw_line; do
50
+ local line="${raw_line%%:*}"
51
+ local match="${raw_line#*:}"
52
+ [[ -z "$match" ]] && continue
53
+ "$severity" "Line ${line}: ${msg_prefix}${match}"
54
+ found=1
55
+ done < <(grep -nE "$pattern" "$file" 2>/dev/null || true)
56
+
57
+ if ((found == 0)); then
58
+ clean "$ok_msg"
59
+ fi
60
+ return "$found"
61
+ }
62
+
63
+ function check_destructive_commands() {
64
+ _check_grep "$1" \
65
+ 'rm\s+-rf\s+(/|\$|--no-preserve-root)|dd\s+(if=|of=).*/dev/sd|dd\s+(if=|of=).*/dev/nvme|mkfs\b' \
66
+ fatal "No destructive commands found"
67
+ }
68
+
69
+ function check_fork_bombs() {
70
+ _check_grep "$1" \
71
+ ':\s*\(\)\s*\{.*\|.*:&\s*\}\s*;|\w+\(\)\s*\{\s*\w+\|\w+&\s*\}' \
72
+ fatal "No fork bomb patterns found" "Possible fork bomb: "
73
+ }
74
+
75
+ function check_system_file_writes() {
76
+ _check_grep "$1" \
77
+ '>\s*/etc/(passwd|shadow|sudoers|group|hosts|crontab)|cat\s+>.*etc/' \
78
+ severe "No system file writes found" "Writes to system file: "
79
+ }
80
+
81
+ function check_hardcoded_credentials() {
82
+ _check_grep "$1" \
83
+ '(password|passwd|pwd|api_key|apikey|secret|token|auth)\s*=\s*["\047][^"\047]{8,}' \
84
+ severe "No hardcoded credentials found" "Hardcoded credential: "
85
+ }
86
+
87
+ function check_credential_formats() {
88
+ _check_grep "$1" \
89
+ '(AKIA[0-9A-Z]{16}|gh[pou]_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{20,}|AIza[0-9A-Za-z_-]{35}|xox[baprs]-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24})' \
90
+ severe "No recognised credential formats found" "Recognised credential format: "
91
+ }
92
+
93
+ function check_insecure_permissions() {
94
+ _check_grep "$1" \
95
+ 'chmod\s+777|chmod\s+-R.*777|chmod\s+a\+rwx' \
96
+ severe "No insecure permissions (chmod 777) found" "Insecure permissions: "
97
+ }
98
+
99
+ function check_trap_injection() {
100
+ _check_grep "$1" \
101
+ 'trap\s+.*\$' \
102
+ moderate "No trap injection patterns found" "Trap with variable: "
103
+ }
104
+
105
+ function check_dangerous_sudo() {
106
+ _check_grep "$1" \
107
+ 'sudo\s+(rm|dd|mkfs|chmod|kill|killall)' \
108
+ severe "No dangerous sudo commands found" "Dangerous sudo command: "
109
+ }
110
+
111
+ function check_system_config_writes() {
112
+ _check_grep "$1" \
113
+ '(echo|printf|sed|awk).*>>.*\/etc\/(ssh|systemd|network)' \
114
+ moderate "No system config writes found" "System config write: "
115
+ }
116
+
117
+ function check_dynamic_execution() {
118
+ _check_grep "$1" \
119
+ 'eval\s+.*\$|source\s+.*\$[^({]' \
120
+ moderate "No dynamic execution patterns found" "Dynamic execution: "
121
+ }
122
+
123
+ ###
124
+ ### :::: Audit a single file :::: #####
125
+ ###
126
+
127
+ function audit_file() {
128
+ local file="$1"
129
+ local issues=0
130
+
131
+ echo ""
132
+ echo -e "${BOLD}=== Security Audit: ${file} ===${RST}"
133
+
134
+ local checks=(
135
+ check_destructive_commands
136
+ check_fork_bombs
137
+ check_system_file_writes
138
+ check_hardcoded_credentials
139
+ check_credential_formats
140
+ check_insecure_permissions
141
+ check_trap_injection
142
+ check_dangerous_sudo
143
+ check_system_config_writes
144
+ check_dynamic_execution
145
+ )
146
+
147
+ for check in "${checks[@]}"; do
148
+ echo ""
149
+ echo -e "${BOLD}[$check]${RST}"
150
+ if ! "$check" "$file"; then
151
+ ((issues++)) || true
152
+ fi
153
+ done
154
+
155
+ echo ""
156
+ if ((issues > 0)); then
157
+ echo -e "${RED}${BOLD}Found ${issues} categories with issues in ${file}${RST}"
158
+ return 1
159
+ else
160
+ echo -e "${LGRN}${BOLD}No security issues found in ${file}${RST}"
161
+ return 0
162
+ fi
163
+ }
164
+
165
+ ###
166
+ ### :::: Main :::: ####################
167
+ ###
168
+
169
+ function main() {
170
+ local target="${1:-}"
171
+
172
+ if [[ -z "$target" ]]; then
173
+ echo "Usage: $0 <file|directory>" >&2
174
+ exit 1
175
+ fi
176
+
177
+ if [[ ! -e "$target" ]]; then
178
+ echo "Error: ${target} does not exist" >&2
179
+ exit 1
180
+ fi
181
+
182
+ local total_issues=0
183
+
184
+ if [[ -d "$target" ]]; then
185
+ # Scan all .sh files in directory
186
+ local file_count=0
187
+ while IFS= read -r -d '' file; do
188
+ ((file_count++)) || true
189
+ if ! audit_file "$file"; then
190
+ ((total_issues++)) || true
191
+ fi
192
+ done < <(find "$target" -maxdepth 1 -name '*.sh' -print0)
193
+
194
+ if ((file_count == 0)); then
195
+ echo "No .sh files found in ${target}" >&2
196
+ exit 1
197
+ fi
198
+ else
199
+ audit_file "$target" || total_issues=1
200
+ fi
201
+
202
+ if ((total_issues > 0)); then
203
+ exit 1
204
+ fi
205
+ exit 0
206
+ }
207
+
208
+ main "$@"
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: shell-test
3
+ description: Generate bashunit test files for bash scripts — analyse the target, scaffold a test file with setup/teardown, and write happy-path, edge, and error cases targeting ~80% public-function coverage. Use when creating tests for a shell script ("write tests", "test this script", "add test coverage"). For running tests use /shell-test-run; for debugging failing tests use shell-debugging.
4
+ allowed-tools: Read, Write, Edit, Bash
5
+ argument-hint: [script-path]
6
+ ---
7
+
8
+ # Shell Test Generation
9
+
10
+ Generates bashunit test files for bash scripts.
11
+
12
+ Scope: generate test files and verify they meet the ~80% public-function coverage target. For running the full suite in CI, use `/shell-test-run`. For debugging failing tests, use `shell-debugging` instead.
13
+
14
+ ## Process
15
+
16
+ **Target script:** `$ARGUMENTS`
17
+
18
+ If `$ARGUMENTS` is not provided, ask the user which script to generate tests for. If `$ARGUMENTS` is a directory, generate tests for each `.sh` file in that directory.
19
+
20
+ 1. **Read and analyse the target script** at `$ARGUMENTS`
21
+ 2. **Create test file** — `tests/$(basename "$ARGUMENTS" .sh)-test.sh` (create `tests/` directory if it does not exist)
22
+ 3. **Write test cases** — happy-path, edge, and error coverage for every public function
23
+ 4. **Verify coverage** — run `scripts/public-coverage.sh`; if below target, return to step 3
24
+ 5. **Show usage** — inform the user to run the full suite with `/shell-test-run`
25
+
26
+ ### Step 1: Analyse the Target Script
27
+
28
+ Before writing any tests, identify:
29
+
30
+ - **Public functions** — functions intended to be called externally; test all of these
31
+ - **Private functions** — helper functions prefixed with `_` or `__`; test only if they contain non-trivial logic
32
+ - **Side effects** — file I/O, network calls, process creation, environment variable mutation
33
+ - **External dependencies** — calls to other scripts, CLI tools, or APIs
34
+ - **Main block** — code executed at source time (outside any function); determines sourcing strategy
35
+
36
+ ### Step 2: Create the Test File
37
+
38
+ Place the test file at `tests/[script-name]-test.sh`. Create the `tests/` directory if it does not exist.
39
+
40
+ Use the `set_up_before_script()` function to source the target script. This keeps the sourcing path explicit and easy to adjust.
41
+
42
+ ### Step 3: Write Test Cases
43
+
44
+ Write tests so **every public function** has happy-path, edge, and error coverage — this is the real target. Prioritise:
45
+
46
+ 1. **Happy path** — each public function called with valid input, asserting expected output or exit code
47
+ 2. **Edge cases** — empty input, whitespace, special characters, zero/null values (see `references/assertions.md` edge-case table)
48
+ 3. **Error paths** — invalid input, missing arguments, failed preconditions
49
+
50
+ Aim for ~80% coverage of public-function behaviour.
51
+
52
+ For the complete assertion reference, consult `references/assertions.md`.
53
+
54
+ ### Step 4: Verify Coverage
55
+
56
+ Run the public-function coverage check on the generated tests:
57
+
58
+ ```bash
59
+ scripts/public-coverage.sh "tests/$(basename "$ARGUMENTS" .sh)-test.sh" --coverage-paths "$(dirname "$ARGUMENTS")/"
60
+ ```
61
+
62
+ If the result is below the threshold (default 80%), the report names the uncovered public functions — return to **Step 3** and add happy/edge/error cases for them, then re-run. Stop when the target is met.
63
+
64
+ If it reports "no public functions", the target has no `<namespace>::` functions: either namespace them per the convention, or skip this check and rely on whole-file coverage (`/shell-test-run --coverage-min`).
65
+
66
+ ### Step 5: Show Usage
67
+
68
+ After generating the test file, display a summary of generated tests and instruct the user to run them with `/shell-test-run`.
69
+
70
+ **Done when** every public function has happy-path, edge, and error tests; every assertion used is confirmed available in the installed bashunit (`bashunit doc assert`); the test file sources cleanly under `set_up_before_script()`; and main-block and side-effect patterns are handled. Verify coverage with `scripts/public-coverage.sh`.
71
+
72
+ ### Coverage Target
73
+
74
+ Target: ~80% line coverage of **public functions** (`<namespace>::name`). bashunit's `--coverage`/`--coverage-min` report **whole-file** coverage — which includes main blocks, private helpers, and untestable code — so the skill measures the public-function figure directly in Step 4 (`scripts/public-coverage.sh`; `--min N` to change the threshold).
75
+
76
+ For all bashunit CLI flags, consult `references/assertions.md` -- Coverage section.
77
+
78
+ ## Test Structure
79
+
80
+ bashunit provides four lifecycle hooks — `set_up_before_script`, `set_up`, `tear_down`, `tear_down_after_script`. See `references/test-template.md` for the full hook table and the complete test-file skeleton.
81
+
82
+ ## Handling Common Patterns
83
+
84
+ ### Scripts with a Main Block
85
+
86
+ Many bash scripts contain top-level code that runs on source (e.g. argument parsing, `main "$@"`). This code executes when the test file sources the script, causing unintended side effects.
87
+
88
+ Detect this pattern by looking for code outside function definitions. When present, wrap the main block in a guard:
89
+
90
+ ```bash
91
+ # In the source script, wrap the main block:
92
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
93
+ main "$@"
94
+ fi
95
+ ```
96
+
97
+ If modifying the source script is not possible, use environment variables to control behaviour:
98
+
99
+ ```bash
100
+ function set_up_before_script() {
101
+ export SKIP_MAIN=1
102
+ source path/to/script.sh
103
+ unset SKIP_MAIN
104
+ }
105
+ ```
106
+
107
+ ### Sourcing and Strict Mode
108
+
109
+ bashunit runs tests with strict mode **off** (`set +euo pipefail`) so a failing command doesn't abort before an assertion can inspect `$?`. But `shell-best-practices` mandates `set -euo pipefail` at the top of every script — so sourcing the script under test re-enables strict mode, and `failing_cmd; assert_general_error` aborts before the assertion runs. Save and restore shell options around the source (the default in the test template):
110
+
111
+ ```bash
112
+ function set_up_before_script() {
113
+ local _opts
114
+ _opts=$(shopt -po errexit nounset pipefail 2>/dev/null || true)
115
+ source path/to/script.sh
116
+ eval "$_opts" # restore bashunit's non-strict execution model
117
+ }
118
+ ```
119
+
120
+ Also: when bashunit sources the test, `$0` is bashunit's binary, not the script under test — so a script that resolves its own directory via `$(dirname "$0")` computes the wrong path. Set the relevant path variable before sourcing.
121
+
122
+ ### Scripts with Side Effects
123
+
124
+ Functions that write files, create directories, or modify global state require cleanup. Use `set_up()` and `tear_down()` for per-test isolation. Use `bashunit::temp_file` for a temporary file or `bashunit::temp_dir` for a directory:
125
+
126
+ ```bash
127
+ function set_up() {
128
+ TEMP_FILE=$(bashunit::temp_file)
129
+ }
130
+
131
+ function tear_down() {
132
+ rm -f "$TEMP_FILE"
133
+ }
134
+ ```
135
+
136
+ For one-time resources (e.g. test databases, service instances), use `set_up_before_script()` and `tear_down_after_script()` instead.
137
+
138
+ ### Scripts Using External Commands
139
+
140
+ When a function calls external tools (`curl`, `git`, `docker`, etc.), replace them with `bashunit::mock`. The simplest form feeds a fixed response:
141
+
142
+ ```bash
143
+ bashunit::mock curl <<< '{"status":"ok"}'
144
+ ```
145
+
146
+ For conditional mocks, multi-line heredoc mocks, and the full pattern set, see `references/assertions.md` -- Mocking External Commands.
147
+
148
+ ### Scripts with Environment Variables
149
+
150
+ Functions that read environment variables should be tested with both set and unset states. Set variables in `set_up()` and unset in `tear_down()` to prevent state leaking between tests:
151
+
152
+ ```bash
153
+ function set_up() {
154
+ export AWS_REGION="eu-west-1"
155
+ }
156
+
157
+ function tear_down() {
158
+ unset AWS_REGION
159
+ }
160
+
161
+ function test_deploy_uses_custom_region() {
162
+ local result
163
+ result=$(deploy_function)
164
+ assert_contains "eu-west-1" "$result"
165
+ }
166
+
167
+ function test_deploy_defaults_to_us_east_1() {
168
+ unset AWS_REGION
169
+ local result
170
+ result=$(deploy_function)
171
+ assert_contains "us-east-1" "$result"
172
+ }
173
+ ```
174
+
175
+ ### Coverage and Subshells
176
+
177
+ bashunit's coverage tracks the main shell only. A function called inside `$(...)`, a pipeline, or `( )` runs in a subshell — its body lines are **not** recorded, so public-function coverage reads 0% even when the test passes. Since Step 4 enforces public-function coverage, call every function under test in the main shell and capture its output through a file:
178
+
179
+ ```bash
180
+ function test_myapp_add() {
181
+ myapp::add 2 3 > "$TEMP_FILE" # main-shell call -> body lines covered
182
+ assert_equals "5" "$(<"$TEMP_FILE")"
183
+ }
184
+ ```
185
+
186
+ `$(<file)` reads the file without re-running the function. Functions you are *not* measuring coverage on may still use `result=$(fn)`.
187
+
188
+ ## Test Naming Conventions
189
+
190
+ Follow a consistent naming pattern to make test intent clear:
191
+
192
+ | Pattern | Use For | Example |
193
+ |---------|---------|---------|
194
+ | `test_[fn]_returns_[expected]` | Happy path output | `test_square_returns_product` |
195
+ | `test_[fn]_defaults_to_[value]` | Default/fallback behaviour | `test_greet_defaults_to_world` |
196
+ | `test_[fn]_handles_[edge_case]` | Boundary or unusual input | `test_parse_handles_empty_input` |
197
+ | `test_[fn]_fails_on_[condition]` | Error/invalid input | `test_square_fails_on_missing_arg` |
198
+ | `test_[fn]_with_[setup]` | When a specific state is needed | `test_deploy_with_custom_region` |
199
+
200
+ For descriptive test output, use `set_test_title` inside the test function:
201
+
202
+ ```bash
203
+ function test_parse_handles_empty_input() {
204
+ set_test_title "Parser gracefully handles empty input string"
205
+ parse ""
206
+ assert_general_error
207
+ }
208
+ ```
209
+
210
+ ## Assertions
211
+
212
+ For the complete assertion reference with usage patterns, edge-case testing, and coverage CLI flags, consult `references/assertions.md`.
213
+
214
+ ## Additional Resources
215
+
216
+ ### Reference Files
217
+
218
+ - `references/test-template.md` -- Complete test file structure with setup/teardown
219
+ - `references/assertions.md` -- Full assertion reference with usage patterns and edge-case table
220
+ - `references/advanced-patterns.md` -- Data providers, spies, and snapshot testing
221
+
222
+ Always read all references and examples before generating tests.
223
+
224
+ ### Example Files
225
+
226
+ - `examples/test-example.md` -- End-to-end example showing input script, generated tests, and execution
227
+
228
+ ### Scripts
229
+
230
+ - `scripts/public-coverage.sh` -- Measures line coverage scoped to public (`<namespace>::`) functions, excluding private `_` helpers and non-function code. Usage: `scripts/public-coverage.sh [--min N] [bashunit args...]`
231
+
232
+ ## Integration
233
+
234
+ - **`/shell-test-run`** command — Run generated tests
235
+ - **`shell-expert`** agent — Complex test scenarios
236
+ - **`shell-review`** skill — Test quality review
237
+ - **`shell-debugging`** skill — Debug failing tests
@@ -0,0 +1,74 @@
1
+ # Example: Generating Tests
2
+
3
+ ## Input Script
4
+
5
+ ```bash
6
+ #!/usr/bin/env bash
7
+ # greeter.sh
8
+
9
+ myapp::greet() {
10
+ local name="${1:-World}"
11
+ echo "Hello, ${name}!"
12
+ }
13
+
14
+ myapp::square() {
15
+ local num="$1"
16
+ if [[ -z "$num" ]]; then
17
+ echo "Error: argument required" >&2
18
+ return 1
19
+ fi
20
+ echo "$((num * num))"
21
+ }
22
+ ```
23
+
24
+ ## Generated Test File
25
+
26
+ Public functions are called in the main shell with output redirected to a temp file, so bashunit records their body coverage. A function run inside `$(...)` or a pipe does not register coverage — see **Coverage and Subshells** in the skill.
27
+
28
+ ```bash
29
+ #!/usr/bin/env bash
30
+ # tests/greeter-test.sh
31
+
32
+ function set_up() {
33
+ OUT=$(bashunit::temp_file)
34
+ }
35
+
36
+ function set_up_before_script() {
37
+ source src/greeter.sh
38
+ }
39
+
40
+ function test_greet_with_name() {
41
+ myapp::greet "Alice" > "$OUT"
42
+ assert_equals "Hello, Alice!" "$(<"$OUT")"
43
+ }
44
+
45
+ function test_greet_defaults_to_world() {
46
+ myapp::greet > "$OUT"
47
+ assert_equals "Hello, World!" "$(<"$OUT")"
48
+ }
49
+
50
+ function test_square_positive_number() {
51
+ myapp::square 5 > "$OUT"
52
+ assert_equals "25" "$(<"$OUT")"
53
+ }
54
+
55
+ function test_square_zero() {
56
+ myapp::square 0 > "$OUT"
57
+ assert_equals "0" "$(<"$OUT")"
58
+ }
59
+
60
+ function test_square_fails_without_argument() {
61
+ myapp::square
62
+ assert_general_error
63
+ }
64
+ ```
65
+
66
+ ## Running Tests
67
+
68
+ ```bash
69
+ # Verify public-function coverage meets the 80% target (Step 4)
70
+ scripts/public-coverage.sh tests/ --coverage-paths src/
71
+
72
+ # Run the full suite
73
+ /shell-test-run tests/greeter-test.sh
74
+ ```
@@ -0,0 +1,52 @@
1
+ # Advanced Testing Patterns
2
+
3
+ Data providers, spies, and snapshots for richer tests. Verified against bashunit 0.40.0 — confirm against your installed version with `bashunit doc assert`.
4
+
5
+ ## Data Providers (table-driven tests)
6
+
7
+ Run a test function once per row of data; each row's words become `$1`, `$2`, …:
8
+
9
+ ```bash
10
+ function dp_add() {
11
+ echo "2 3 5" # $1=2 $2=3 expected=$3
12
+ echo "10 10 20"
13
+ echo "0 0 0"
14
+ }
15
+
16
+ # data_provider dp_add
17
+ function test_add() {
18
+ assert_equals "$3" "$(( $1 + $2 ))"
19
+ }
20
+ ```
21
+
22
+ The `# data_provider <fn>` annotation sits directly above the test function. Each line the provider echoes is one test run.
23
+
24
+ ## Spies (verify a function was called)
25
+
26
+ `bashunit::spy` wraps a function to track its calls without changing its behaviour; the `assert_have_been_called*` assertions then inspect the calls:
27
+
28
+ ```bash
29
+ function test_deploy_calls_notify() {
30
+ bashunit::spy notify
31
+ myapp::deploy
32
+ assert_have_been_called notify
33
+ assert_have_been_called_times 1 notify
34
+ assert_have_been_called_with "success" notify
35
+ }
36
+ ```
37
+
38
+ - `bashunit::mock` **replaces** a command with fixed output; `bashunit::spy` **observes** a function while keeping its behaviour.
39
+ - The spy command is `bashunit::spy` (a bare `spy` is not available in user tests).
40
+
41
+ ## Snapshot Testing
42
+
43
+ For stable multi-line output (help text, generated reports), snapshot it once and assert future runs match:
44
+
45
+ ```bash
46
+ function test_help_output() {
47
+ myapp::print_help > "$TEMP_FILE"
48
+ assert_match_snapshot "$(<"$TEMP_FILE")"
49
+ }
50
+ ```
51
+
52
+ After intentional output changes, refresh the stored snapshot: `bashunit --update-snapshots tests/`.