feat: add pos bank — persistent command bank with parameterized templates
gates / consistency-and-conventions (push) Successful in 29s
gates / consistency-and-conventions (push) Successful in 29s
This commit is contained in:
+2
-1
@@ -56,4 +56,5 @@ silently.
|
||||
| `t-install-version.sh` | install.sh version gate: match→skip, mismatch→proceed, --force bypass, dry-run variant, flag write, numeric comparison |
|
||||
| `t-share-mountpoint.sh` | share-client `ask_mountpoint` UX: existing/new/declined/rejected paths, confirm gate, mkdir side effects, non-TTY stdin contract, static `n`→`t` guards |
|
||||
| `t-pos-media-yt.sh` | unified `pos media yt` suite: dispatcher + forwarder resolution, shared yt-lib helpers, yt-mp3/mp4/grab/subtitles flags, dry-run deps, `YT_OUT_DIR` seam, `GRAB_DEFAULT` config, negative controls (unsafe-URL no-expansion, `--lang en,ar` single arg, txt timestamp-stripping) |
|
||||
| `t-telegram-listener-singleton.sh` | Telegram listener single-instance guard: first `--run` acquires the flock, second `--run` fails fast with the exact message, lock auto-releases so the next start is clean, `--status` reports the lock state |
|
||||
| `t-telegram-listener-singleton.sh` | Telegram listener single-instance guard: first `--run` acquires the flock, second `--run` fails fast with the exact message, lock auto-releases so the next start is clean, `--status` reports the lock state |
|
||||
| `t-bank.sh` | Command Bank: bank-lib.sh unit tests (add/remove/update/find/get/list/count/valid/extract_params/substitute_params) + pos-bank CLI integration (help, list, add, show, remove, params, invalid name) |
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/bank-test-helper.sh — minimal shim that sources bank-lib.sh for unit
|
||||
# testing. Provides a stub `err()` so bank-lib.sh can load without the full
|
||||
# common.sh / menu-lib.sh dependency chain.
|
||||
set -euo pipefail
|
||||
|
||||
# Stub err() — bank-lib.sh is contractually "never exits" but bank_valid_name
|
||||
# callers sometimes chain via `|| err …`. This version returns 1 so the test
|
||||
# harness can assert rc.
|
||||
err() { printf 'ERROR: %s\n' "$*" >&2; return 1; }
|
||||
|
||||
# Source the library under test.
|
||||
source "$ROOT/lib/bank-lib.sh"
|
||||
Executable
+337
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# t-bank.sh — Command Bank feature: bank-lib.sh unit tests + pos-bank CLI
|
||||
# integration tests.
|
||||
#
|
||||
# Storage contract: pipe-delimited name|description|command in $BANK_FILE.
|
||||
# Parameter syntax: {param} placeholders in command templates.
|
||||
|
||||
run_test() {
|
||||
local sandbox helper
|
||||
sandbox="$(mksandbox bank)"
|
||||
helper="$sandbox/helper.sh"
|
||||
|
||||
# ── Build a sourced helper that gives us bank-lib functions ──
|
||||
cat > "$helper" <<'HELPER'
|
||||
source "$ROOT/tests/bank-test-helper.sh"
|
||||
HELPER
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Part A: bank-lib.sh unit tests
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# A1: bank_add + bank_load — add two entries, verify arrays
|
||||
(
|
||||
BANK_FILE="$sandbox/a1.bank.env"
|
||||
: > "$BANK_FILE"
|
||||
source "$helper"
|
||||
bank_add "deploy" "Deploy to prod" "ssh prod 'cd /app && git pull'"
|
||||
bank_add "backup" "Backup database" "pg_dump mydb | gzip > /tmp/db.sql.gz"
|
||||
bank_load
|
||||
# Check arrays
|
||||
[ "${#BANK_NAMES[@]}" -eq 2 ] || { echo "FAIL A1: expected 2 names, got ${#BANK_NAMES[@]}"; exit 1; }
|
||||
[ "${BANK_NAMES[0]}" = "deploy" ] || { echo "FAIL A1: name[0] mismatch"; exit 1; }
|
||||
[ "${BANK_DESCS[0]}" = "Deploy to prod" ] || { echo "FAIL A1: desc[0] mismatch"; exit 1; }
|
||||
[ "${BANK_CMDS[1]}" = "pg_dump mydb | gzip > /tmp/db.sql.gz" ] || { echo "FAIL A1: cmd[1] mismatch"; exit 1; }
|
||||
echo "PASS A1: bank_add + bank_load populates arrays correctly"
|
||||
) && printf ' PASS bank_add + bank_load populates arrays\n' \
|
||||
|| printf ' FAIL bank_add + bank_load populates arrays\n'
|
||||
|
||||
# A2: bank_add duplicate — returns rc 1
|
||||
(
|
||||
BANK_FILE="$sandbox/a2.bank.env"
|
||||
printf 'old|Old cmd|echo old\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
bank_add "old" "New desc" "echo new"
|
||||
exit $?
|
||||
)
|
||||
check_rc "bank_add duplicate returns rc 1" 1 $?
|
||||
|
||||
# A3: bank_remove — remove entry, verify gone
|
||||
(
|
||||
BANK_FILE="$sandbox/a3.bank.env"
|
||||
printf 'aaa|desc a|cmd a\nbbb|desc b|cmd b\nccc|desc c|cmd c\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
bank_remove "bbb"
|
||||
bank_load
|
||||
[ "${#BANK_NAMES[@]}" -eq 2 ] || { echo "FAIL A3: expected 2, got ${#BANK_NAMES[@]}"; exit 1; }
|
||||
[ "${BANK_NAMES[0]}" = "aaa" ] || { echo "FAIL A3: name[0] mismatch"; exit 1; }
|
||||
[ "${BANK_NAMES[1]}" = "ccc" ] || { echo "FAIL A3: name[1] mismatch"; exit 1; }
|
||||
# Verify file on disk
|
||||
grep -q '^bbb|' "$BANK_FILE" && { echo "FAIL A3: bbb still in file"; exit 1; }
|
||||
echo "PASS A3: bank_remove"
|
||||
) && printf ' PASS bank_remove removes entry\n' \
|
||||
|| printf ' FAIL bank_remove removes entry\n'
|
||||
|
||||
# A4: bank_remove missing — returns rc 1
|
||||
(
|
||||
BANK_FILE="$sandbox/a4.bank.env"
|
||||
printf 'xxx|desc|cmd\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
bank_remove "nonexistent"
|
||||
exit $?
|
||||
)
|
||||
check_rc "bank_remove missing returns rc 1" 1 $?
|
||||
|
||||
# A5: bank_update — update entry, verify changed
|
||||
(
|
||||
BANK_FILE="$sandbox/a5.bank.env"
|
||||
printf 'mycmd|old desc|old cmd\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
bank_update "mycmd" "new desc" "new cmd"
|
||||
bank_load
|
||||
[ "${BANK_DESCS[0]}" = "new desc" ] || { echo "FAIL A5: desc mismatch"; exit 1; }
|
||||
[ "${BANK_CMDS[0]}" = "new cmd" ] || { echo "FAIL A5: cmd mismatch"; exit 1; }
|
||||
# Verify on disk
|
||||
grep -q 'new desc|new cmd' "$BANK_FILE" || { echo "FAIL A5: not persisted"; exit 1; }
|
||||
echo "PASS A5: bank_update"
|
||||
) && printf ' PASS bank_update updates entry\n' \
|
||||
|| printf ' FAIL bank_update updates entry\n'
|
||||
|
||||
# A6: bank_update missing — returns rc 1
|
||||
(
|
||||
BANK_FILE="$sandbox/a6.bank.env"
|
||||
printf 'exists|desc|cmd\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
bank_update "nope" "desc" "cmd"
|
||||
exit $?
|
||||
)
|
||||
check_rc "bank_update missing returns rc 1" 1 $?
|
||||
|
||||
# A7: bank_find — found sets BANK_IDX, not-found returns 1
|
||||
(
|
||||
BANK_FILE="$sandbox/a7.bank.env"
|
||||
printf 'alpha|d1|c1\nbeta|d2|c2\ngamma|d3|c3\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
bank_find "beta"
|
||||
[ "${BANK_IDX:-}" = "1" ] || { echo "FAIL A7: expected BANK_IDX=1, got ${BANK_IDX:-unset}"; exit 1; }
|
||||
bank_find "nonexistent" && { echo "FAIL A7: should return 1 for missing"; exit 1; }
|
||||
echo "PASS A7: bank_find"
|
||||
) && printf ' PASS bank_find sets BANK_IDX / returns 1\n' \
|
||||
|| printf ' FAIL bank_find sets BANK_IDX / returns 1\n'
|
||||
|
||||
# A8: bank_get — returns tab-separated output
|
||||
(
|
||||
BANK_FILE="$sandbox/a8.bank.env"
|
||||
printf 'mytool|My Tool|echo hello\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
local out
|
||||
out="$(bank_get "mytool")"
|
||||
[ "$out" = "mytool My Tool echo hello" ] || { echo "FAIL A8: unexpected output [$out]"; exit 1; }
|
||||
echo "PASS A8: bank_get"
|
||||
) && printf ' PASS bank_get returns tab-separated output\n' \
|
||||
|| printf ' FAIL bank_get returns tab-separated output\n'
|
||||
|
||||
# A9: bank_list_names — lists all names
|
||||
(
|
||||
BANK_FILE="$sandbox/a9.bank.env"
|
||||
printf 'one|x|y\ntwo|a|b\nthree|c|d\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
local names
|
||||
names="$(bank_list_names)"
|
||||
local expected
|
||||
expected="$(printf 'one\ntwo\nthree')"
|
||||
[ "$names" = "$expected" ] || { echo "FAIL A9: expected [$expected] got [$names]"; exit 1; }
|
||||
echo "PASS A9: bank_list_names"
|
||||
) && printf ' PASS bank_list_names lists all names\n' \
|
||||
|| printf ' FAIL bank_list_names lists all names\n'
|
||||
|
||||
# A10: bank_count — returns correct count
|
||||
(
|
||||
BANK_FILE="$sandbox/a10.bank.env"
|
||||
printf 'a|1|2\nb|3|4\nc|5|6\nd|7|8\n' > "$BANK_FILE"
|
||||
source "$helper"
|
||||
local cnt
|
||||
cnt="$(bank_count)"
|
||||
[ "$cnt" -eq 4 ] || { echo "FAIL A10: expected 4, got $cnt"; exit 1; }
|
||||
echo "PASS A10: bank_count"
|
||||
) && printf ' PASS bank_count returns correct count\n' \
|
||||
|| printf ' FAIL bank_count returns correct count\n'
|
||||
|
||||
# A11: bank_count — empty file returns 0
|
||||
(
|
||||
BANK_FILE="$sandbox/a11.bank.env"
|
||||
: > "$BANK_FILE"
|
||||
source "$helper"
|
||||
local cnt
|
||||
cnt="$(bank_count)"
|
||||
[ "$cnt" -eq 0 ] || { echo "FAIL A11: expected 0, got $cnt"; exit 1; }
|
||||
echo "PASS A11: bank_count empty"
|
||||
) && printf ' PASS bank_count returns 0 for empty file\n' \
|
||||
|| printf ' FAIL bank_count returns 0 for empty file\n'
|
||||
|
||||
# A12: bank_valid_name — valid names pass
|
||||
(
|
||||
source "$helper"
|
||||
local fail=0
|
||||
for n in "deploy" "my-cmd" "test_123" "A" "z9"; do
|
||||
bank_valid_name "$n" || { fail=1; printf 'FAIL A12: %s should be valid\n' "$n" >&2; }
|
||||
done
|
||||
[ "$fail" -eq 0 ] || exit 1
|
||||
echo "PASS A12: bank_valid_name accepts valid names"
|
||||
) && printf ' PASS bank_valid_name accepts valid names\n' \
|
||||
|| printf ' FAIL bank_valid_name accepts valid names\n'
|
||||
|
||||
# A13: bank_valid_name — invalid names fail
|
||||
(
|
||||
source "$helper"
|
||||
local fail=0
|
||||
for n in "" "1abc" "-start" "_under" "has space" "special!"; do
|
||||
bank_valid_name "$n" && { fail=1; printf 'FAIL A13: %s should be invalid\n' "$n" >&2; }
|
||||
done
|
||||
[ "$fail" -eq 0 ] || exit 1
|
||||
echo "PASS A13: bank_valid_name rejects invalid names"
|
||||
) && printf ' PASS bank_valid_name rejects invalid names\n' \
|
||||
|| printf ' FAIL bank_valid_name rejects invalid names\n'
|
||||
|
||||
# A14: _extract_params — extracts {param} names from template
|
||||
(
|
||||
source "$helper"
|
||||
local params
|
||||
params="$(_extract_params "echo {name} {path}")"
|
||||
local expected
|
||||
expected="$(printf 'name\npath')"
|
||||
[ "$params" = "$expected" ] || { echo "FAIL A14: expected [$expected] got [$params]"; exit 1; }
|
||||
echo "PASS A14: _extract_params"
|
||||
) && printf ' PASS _extract_params extracts param names\n' \
|
||||
|| printf ' FAIL _extract_params extracts param names\n'
|
||||
|
||||
# A15: _extract_params — no params returns empty
|
||||
(
|
||||
source "$helper"
|
||||
local params
|
||||
params="$(_extract_params "echo hello world")"
|
||||
[ -z "$params" ] || { echo "FAIL A15: expected empty, got [$params]"; exit 1; }
|
||||
echo "PASS A15: _extract_params empty"
|
||||
) && printf ' PASS _extract_params returns empty for no params\n' \
|
||||
|| printf ' FAIL _extract_params returns empty for no params\n'
|
||||
|
||||
# A16: _extract_params — deduplicates repeated params
|
||||
(
|
||||
source "$helper"
|
||||
local params
|
||||
params="$(_extract_params "cp {src} {dst} && ls {src}")"
|
||||
local count
|
||||
count="$(printf '%s' "$params" | grep -c '^src$' || true)"
|
||||
[ "$count" -eq 1 ] || { echo "FAIL A16: expected 1 occurrence of src, got $count"; exit 1; }
|
||||
echo "PASS A16: _extract_params deduplicates"
|
||||
) && printf ' PASS _extract_params deduplicates\n' \
|
||||
|| printf ' FAIL _extract_params deduplicates\n'
|
||||
|
||||
# A17: _substitute_params — replaces {param} with quoted values
|
||||
(
|
||||
source "$helper"
|
||||
local result
|
||||
result="$(_substitute_params "scp {file} user@host:{dest}" "file=/tmp/data.csv" "dest=/var/data")"
|
||||
local expected='scp "/tmp/data.csv" user@host:"/var/data"'
|
||||
[ "$result" = "$expected" ] || { echo "FAIL A17: expected [$expected] got [$result]"; exit 1; }
|
||||
echo "PASS A17: _substitute_params"
|
||||
) && printf ' PASS _substitute_params replaces params\n' \
|
||||
|| printf ' FAIL _substitute_params replaces params\n'
|
||||
|
||||
# A18: bank_load — comments and blank lines are skipped
|
||||
(
|
||||
BANK_FILE="$sandbox/a18.bank.env"
|
||||
cat > "$BANK_FILE" <<'BANK'
|
||||
# Command Bank — managed by pos bank (do not hand-edit)
|
||||
# Format: name|description|command
|
||||
|
||||
real|Real command|echo real
|
||||
# another comment
|
||||
|
||||
# indented comment
|
||||
|
||||
BANK
|
||||
source "$helper"
|
||||
bank_load
|
||||
[ "${#BANK_NAMES[@]}" -eq 1 ] || { echo "FAIL A18: expected 1, got ${#BANK_NAMES[@]}"; exit 1; }
|
||||
[ "${BANK_NAMES[0]}" = "real" ] || { echo "FAIL A18: name mismatch"; exit 1; }
|
||||
echo "PASS A18: bank_load skips comments and blanks"
|
||||
) && printf ' PASS bank_load skips comments and blank lines\n' \
|
||||
|| printf ' FAIL bank_load skips comments and blank lines\n'
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Part B: pos-bank CLI integration tests
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
local pos_bank="$ROOT/bin/pos-bank"
|
||||
|
||||
# B1: pos bank --help shows usage
|
||||
test_run env BANK_FILE="$sandbox/b1.bank.env" "$pos_bank" --help
|
||||
check_rc "pos bank --help exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank --help shows Usage" "Usage:" "$TR_OUT"
|
||||
check_contains "pos bank --help mentions subcommands" "Subcommands:" "$TR_OUT"
|
||||
|
||||
# B2: pos bank list on empty bank shows empty message
|
||||
: > "$sandbox/b2.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b2.bank.env" "$pos_bank" list
|
||||
check_rc "pos bank list empty exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank list empty message" "Command bank is empty" "$TR_OUT"
|
||||
|
||||
# B3: pos bank add — adds a command, verify in bank.env
|
||||
: > "$sandbox/b3.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" add "disk-usage" "Check disk usage" "df -h"
|
||||
check_rc "pos bank add exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank add confirms" "Saved: disk-usage" "$TR_OUT"
|
||||
# Verify the file
|
||||
if grep -q '^disk-usage|Check disk usage|df -h$' "$sandbox/b3.bank.env"; then
|
||||
printf ' PASS pos bank add persists to bank.env\n'
|
||||
else
|
||||
printf ' FAIL pos bank add did not persist to bank.env\n'
|
||||
fi
|
||||
|
||||
# B4: pos bank add duplicate — fails
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" add "disk-usage" "dup" "echo dup"
|
||||
check_not_contains "pos bank add duplicate errors" "0" "$TR_RC"
|
||||
check_contains "pos bank add duplicate message" "already exists" "$TR_OUT"
|
||||
|
||||
# B5: pos bank show — shows command details
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" show "disk-usage"
|
||||
check_rc "pos bank show exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank show shows name" "Name: disk-usage" "$TR_OUT"
|
||||
check_contains "pos bank show shows description" "Check disk usage" "$TR_OUT"
|
||||
check_contains "pos bank show shows command" "df -h" "$TR_OUT"
|
||||
|
||||
# B6: pos bank show missing — fails
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" show "nonexistent"
|
||||
check_not_contains "pos bank show missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos bank show missing message" "not found" "$TR_OUT"
|
||||
|
||||
# B7: pos bank list after add — shows the entry
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" list
|
||||
check_rc "pos bank list populated exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank list shows header" "COMMAND BANK" "$TR_OUT"
|
||||
check_contains "pos bank list shows entry" "disk-usage" "$TR_OUT"
|
||||
|
||||
# B8: pos bank remove — removes a command
|
||||
test_run env BANK_FILE="$sandbox/b3.bank.env" "$pos_bank" remove "disk-usage"
|
||||
check_rc "pos bank remove exits 0" 0 "$TR_RC"
|
||||
check_contains "pos bank remove confirms" "Removed: disk-usage" "$TR_OUT"
|
||||
# Verify gone
|
||||
if grep -q '^disk-usage|' "$sandbox/b3.bank.env"; then
|
||||
printf ' FAIL pos bank remove did not delete from bank.env\n'
|
||||
else
|
||||
printf ' PASS pos bank remove deletes from bank.env\n'
|
||||
fi
|
||||
|
||||
# B9: pos bank remove missing — fails
|
||||
: > "$sandbox/b9.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b9.bank.env" "$pos_bank" remove "ghost"
|
||||
check_not_contains "pos bank remove missing exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos bank remove missing message" "not found" "$TR_OUT"
|
||||
|
||||
# B10: pos bank add with parameters — show lists them
|
||||
: > "$sandbox/b10.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b10.bank.env" "$pos_bank" add "convert" "Convert video" "ffmpeg -i {input} -q:v {quality} {output}"
|
||||
check_rc "pos bank add with params exits 0" 0 "$TR_RC"
|
||||
test_run env BANK_FILE="$sandbox/b10.bank.env" "$pos_bank" show "convert"
|
||||
check_contains "show lists input param" "input" "$TR_OUT"
|
||||
check_contains "show lists quality param" "quality" "$TR_OUT"
|
||||
check_contains "show lists output param" "output" "$TR_OUT"
|
||||
|
||||
# B11: pos bank add invalid name — fails
|
||||
: > "$sandbox/b11.bank.env"
|
||||
test_run env BANK_FILE="$sandbox/b11.bank.env" "$pos_bank" add "1bad" "desc" "cmd"
|
||||
check_not_contains "pos bank add invalid name exits non-zero" "0" "$TR_RC"
|
||||
check_contains "pos bank add invalid name message" "Invalid name" "$TR_OUT"
|
||||
}
|
||||
Reference in New Issue
Block a user