Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b9dea2118 | |||
| 35eb90a58b |
@@ -644,9 +644,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||
| `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location |
|
||||
| `bin/pos-network-scan` | 272 | Parallel ping sweep of CIDR |
|
||||
| `bin/pos-share-nfs-client` | 504 | Mount NFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-nfs-client` | 511 | Mount NFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-nfs-server` | 245 | Manage the NFS kernel server (status, share/unshare exports, enable/disable) |
|
||||
| `bin/pos-share-smb-client` | 766 | Mount SMB/CIFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-smb-client` | 773 | Mount SMB/CIFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-smb-server` | 441 | Manage the Samba server (status, share/unshare exports, users, enable/disable) |
|
||||
| `bin/pos-share-usb-server` | 362 | USB Redirector server control (--ls, --share; prompts when args omitted) |
|
||||
| `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent |
|
||||
|
||||
+12
-9
@@ -155,12 +155,13 @@ up — a down/unreachable NFS server can't break boot (with fstab it could).
|
||||
(mount / persist / unmount / unpersist / list). Mountpoints are offered from
|
||||
existing mount-layout candidates with manual entry as fallback — the picker
|
||||
also accepts the server-side export path as a "(as on server)" pick when it
|
||||
differs from your local layout, and `n=new` creates a fresh directory in
|
||||
place (y/N confirmed; a failure just returns to the picker). Unmount lists
|
||||
the active NFS mounts as `<mountpoint> ← <source>` picks and asks for
|
||||
confirmation before unmounting (with a typed fallback when nothing is
|
||||
mounted); unmount and unpersist tolerate already-absent targets instead of
|
||||
erroring.
|
||||
differs from your local layout, and `t=type` lets you type an absolute
|
||||
mountpoint — an existing directory is used as-is, a non-existent path
|
||||
creates it in place (y/N confirmed; a failure just returns to the picker).
|
||||
Unmount lists the active NFS mounts as `<mountpoint> ← <source>` picks and
|
||||
asks for confirmation before unmounting (with a typed fallback when nothing
|
||||
is mounted); unmount and unpersist tolerate already-absent targets instead
|
||||
of erroring.
|
||||
|
||||
**Troubleshooting:**
|
||||
- "mount.nfs not found" → `nfs-common` isn't installed; `sudo apt install nfs-common`
|
||||
@@ -292,9 +293,11 @@ fstab it could). `enable --now` arms the automount immediately.
|
||||
an empty user tries guest enumeration first (with an auth retry on denial),
|
||||
then shares and mountpoints are offered as pickers with manual fallback —
|
||||
the account you authenticated with is reused for the mount. The mountpoint
|
||||
picker accepts `n=new` to create a fresh directory in place (y/N confirmed;
|
||||
a failure just returns to the picker); when the server is this machine, its
|
||||
underlying share directory is offered as a "(as on server)" pick too.
|
||||
picker accepts `t=type` to type an absolute mountpoint — an existing
|
||||
directory is used as-is, a non-existent path creates it in place (y/N
|
||||
confirmed; a failure just returns to the picker); when the server is this
|
||||
machine, its underlying share directory is offered as a "(as on server)"
|
||||
pick too.
|
||||
Unmount lists the active CIFS mounts as `<mountpoint> ← <source>` picks and
|
||||
asks for confirmation before unmounting (typed fallback when nothing is
|
||||
mounted).
|
||||
|
||||
+20
-13
@@ -223,8 +223,8 @@ cmd_unpersist() {
|
||||
# ── Interactive menu flows ─────────────────────────────────────
|
||||
|
||||
# Mountpoint picker — local port of the share_pick primitive with exactly
|
||||
# two deltas: the hint line offers `n=new`, and typing n runs the
|
||||
# create-new-dir flow below. share_pick cannot intercept `n` (it filters on
|
||||
# two deltas: the hint line offers `t=type`, and typing t runs the
|
||||
# manual-entry flow below. share_pick cannot intercept `t` (it filters on
|
||||
# it) and lib/menu-lib.sh is shared, so the fork lives here. Rendering of
|
||||
# numbered picks / text filter / 0=back is byte-identical to menu_pick.
|
||||
# stdout: chosen item text (or the freshly created dir) · rc 1 = back/cancel.
|
||||
@@ -263,16 +263,16 @@ pick_mountpoint() {
|
||||
done
|
||||
fi
|
||||
} >&2
|
||||
if ! read -rp "${prompt} [1-${n}], n=new, text=filter, 0=back " ans; then
|
||||
if ! read -rp "${prompt} [1-${n}], t=type, text=filter, 0=back " ans; then
|
||||
return 1 # EOF — cancel
|
||||
fi
|
||||
case "$ans" in
|
||||
"") [ -z "$filter" ] || filter="" ; continue ;;
|
||||
"/") filter="" ; continue ;;
|
||||
0 | q | Q | b | B) return 1 ;;
|
||||
n | N)
|
||||
made="$(ask_new_mountpoint)" && { echo "$made"; return 0; }
|
||||
continue # declined/invalid/mkdir-failed → redraw
|
||||
t | T)
|
||||
made="$(ask_mountpoint)" && { echo "$made"; return 0; }
|
||||
continue # cancelled/invalid/mkdir-failed → redraw
|
||||
;;
|
||||
*[!0-9]*)
|
||||
filter="$ans"
|
||||
@@ -289,15 +289,16 @@ pick_mountpoint() {
|
||||
done
|
||||
}
|
||||
|
||||
# Create-new-dir flow behind the picker's `n` key. Validates the shape
|
||||
# (absolute, no trailing slash), confirm-gates the creation, then mkdir -p.
|
||||
# Manual entry flow behind the picker's `t` key. Validates the shape
|
||||
# (absolute, no trailing slash, system-path refusal), then branches:
|
||||
# existing dir → use as-is; new dir → confirm-gate the creation + mkdir -p.
|
||||
# Any decline, invalid input, EOF or mkdir failure is a warning + rc 1 —
|
||||
# the picker redraws, the tool never aborts.
|
||||
ask_new_mountpoint() { # stdout: created dir · rc 1 = cancelled/failed
|
||||
ask_mountpoint() { # stdout: absolute path (existing or newly created) · rc 1 = cancelled/failed
|
||||
# NOTE: runs inside $( ) from the picker — every display line MUST go to
|
||||
# stderr (menu-lib contract: display → stderr, result → stdout).
|
||||
local dir
|
||||
dir="$(share_ask_value "New mountpoint (absolute path)")" || return 1
|
||||
dir="$(share_ask_value "Mountpoint (absolute path)")" || return 1
|
||||
case "$dir" in
|
||||
/*) ;;
|
||||
*) warn "'$dir' is not an absolute path — must start with /" >&2; return 1 ;;
|
||||
@@ -311,6 +312,14 @@ ask_new_mountpoint() { # stdout: created dir · rc 1 = cancelled/failed
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ -d "$dir" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
if [ -e "$dir" ]; then
|
||||
warn "$dir exists and is not a directory — pick another mount point" >&2
|
||||
return 1
|
||||
fi
|
||||
confirm "Create mountpoint ${dir}?" n || return 1
|
||||
if ! run sudo mkdir -p "$dir"; then
|
||||
warn "Could not create ${dir}" >&2
|
||||
@@ -371,9 +380,7 @@ menu_ask_mountpoint() { # [server_path] — stdout: absolute path · rc 1 cancel
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
dir="$(share_ask_value "Mountpoint (absolute path)")" || return 1
|
||||
[ -n "$dir" ] || { warn "No mountpoint given"; return 1; }
|
||||
echo "$dir"
|
||||
ask_mountpoint
|
||||
}
|
||||
|
||||
menu_mount() {
|
||||
|
||||
+20
-13
@@ -495,8 +495,8 @@ smb_server_path() { # <host> <share> — stdout: server-side dir · rc 1 = unres
|
||||
}
|
||||
|
||||
# Mountpoint picker — local port of the share_pick primitive with exactly
|
||||
# two deltas: the hint line offers `n=new`, and typing n runs the
|
||||
# create-new-dir flow below. share_pick cannot intercept `n` (it filters on
|
||||
# two deltas: the hint line offers `t=type`, and typing t runs the
|
||||
# manual-entry flow below. share_pick cannot intercept `t` (it filters on
|
||||
# it) and lib/menu-lib.sh is shared, so the fork lives here. Rendering of
|
||||
# numbered picks / text filter / 0=back is byte-identical to menu_pick.
|
||||
# stdout: chosen item text (or the freshly created dir) · rc 1 = back/cancel.
|
||||
@@ -535,16 +535,16 @@ pick_mountpoint() {
|
||||
done
|
||||
fi
|
||||
} >&2
|
||||
if ! read -rp "${prompt} [1-${n}], n=new, text=filter, 0=back " ans; then
|
||||
if ! read -rp "${prompt} [1-${n}], t=type, text=filter, 0=back " ans; then
|
||||
return 1 # EOF — cancel
|
||||
fi
|
||||
case "$ans" in
|
||||
"") [ -z "$filter" ] || filter="" ; continue ;;
|
||||
"/") filter="" ; continue ;;
|
||||
0 | q | Q | b | B) return 1 ;;
|
||||
n | N)
|
||||
made="$(ask_new_mountpoint)" && { echo "$made"; return 0; }
|
||||
continue # declined/invalid/mkdir-failed → redraw
|
||||
t | T)
|
||||
made="$(ask_mountpoint)" && { echo "$made"; return 0; }
|
||||
continue # cancelled/invalid/mkdir-failed → redraw
|
||||
;;
|
||||
*[!0-9]*)
|
||||
filter="$ans"
|
||||
@@ -561,15 +561,16 @@ pick_mountpoint() {
|
||||
done
|
||||
}
|
||||
|
||||
# Create-new-dir flow behind the picker's `n` key. Validates the shape
|
||||
# (absolute, no trailing slash), confirm-gates the creation, then mkdir -p.
|
||||
# Manual entry flow behind the picker's `t` key. Validates the shape
|
||||
# (absolute, no trailing slash, system-path refusal), then branches:
|
||||
# existing dir → use as-is; new dir → confirm-gate the creation + mkdir -p.
|
||||
# Any decline, invalid input, EOF or mkdir failure is a warning + rc 1 —
|
||||
# the picker redraws, the tool never aborts.
|
||||
ask_new_mountpoint() { # stdout: created dir · rc 1 = cancelled/failed
|
||||
ask_mountpoint() { # stdout: absolute path (existing or newly created) · rc 1 = cancelled/failed
|
||||
# NOTE: runs inside $( ) from the picker — every display line MUST go to
|
||||
# stderr (menu-lib contract: display → stderr, result → stdout).
|
||||
local dir
|
||||
dir="$(share_ask_value "New mountpoint (absolute path)")" || return 1
|
||||
dir="$(share_ask_value "Mountpoint (absolute path)")" || return 1
|
||||
case "$dir" in
|
||||
/*) ;;
|
||||
*) warn "'$dir' is not an absolute path — must start with /" >&2; return 1 ;;
|
||||
@@ -583,6 +584,14 @@ ask_new_mountpoint() { # stdout: created dir · rc 1 = cancelled/failed
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ -d "$dir" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
if [ -e "$dir" ]; then
|
||||
warn "$dir exists and is not a directory — pick another mount point" >&2
|
||||
return 1
|
||||
fi
|
||||
confirm "Create mountpoint ${dir}?" n || return 1
|
||||
if ! run sudo mkdir -p "$dir"; then
|
||||
warn "Could not create ${dir}" >&2
|
||||
@@ -624,9 +633,7 @@ menu_ask_mountpoint() { # [server_path] — stdout: absolute path · rc 1 cancel
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
dir="$(share_ask_value "Mountpoint (absolute path)")" || return 1
|
||||
[ -n "$dir" ] || { warn "No mountpoint given"; return 1; }
|
||||
echo "$dir"
|
||||
ask_mountpoint
|
||||
}
|
||||
|
||||
menu_mount() { # ephemeral|persist
|
||||
|
||||
+2
-1
@@ -51,4 +51,5 @@ silently.
|
||||
| `t-config-precedence.sh` | `load_env_file` contract + CLI > env > file > defaults across tools |
|
||||
| `t-uninstall-manifest.sh` | install.sh ↔ POS_LIBS symmetry, user-unit discovery, marker-driven plugin removal |
|
||||
| `t-gen-docs-drift.sh` | `make gen` idempotence on a pristine tracked tree (CI drift gate) |
|
||||
| `t-lint-gate.sh` | `make lint` green on the real tree; planted violations are caught and named |
|
||||
| `t-lint-gate.sh` | `make lint` green on the real tree; planted violations are caught and named |
|
||||
| `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 |
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# t-share-mountpoint.sh — permanent regression for the share-client
|
||||
# ask_mountpoint() manual-entry flow (2026-09-07 `t=type` UX change in
|
||||
# bin/pos-share-smb-client + bin/pos-share-nfs-client; byte-identical bodies).
|
||||
#
|
||||
# Behaviour contract under test (both clients):
|
||||
# existing dir → printed as-is, rc 0, NO confirm, NO mkdir
|
||||
# new dir + y → confirm gate passes, sudo mkdir -p, dir created, rc 0
|
||||
# new dir + n → rc 1, nothing created (confirm EOF also denies)
|
||||
# existing file → rc 1 + "not a directory" warning
|
||||
# relative / trailing-slash / system paths / empty-EOF → rc 1, no side effects
|
||||
# mkdir failure → rc 1 + "Could not create" warning
|
||||
# stream → display on stderr, result on stdout
|
||||
#
|
||||
# Strategy (production logic is never re-typed): the REAL ask_mountpoint body
|
||||
# is brace-extracted verbatim from each client file (extract_fn, same pattern
|
||||
# as t-menu-allow-empty.sh / t-ai-server-validate.sh) and sourced; the REAL
|
||||
# helper chain lib/common.sh (warn/confirm/run/log) + lib/share-lib.sh
|
||||
# (share_ask_value → menu_ask_value → menu_read_value) is exercised through
|
||||
# the deterministic NON-TTY stdin path (stty -g fails on a pipe → plain
|
||||
# IFS= read, common.sh:181-186). Input is fed as `printf '%b' | fn`, exactly
|
||||
# like t-menu-allow-empty.sh feeds menu_ask_value. A fake `sudo` on PATH
|
||||
# records every invocation and honors SUDO_FAIL, so the sandbox never touches
|
||||
# the real system. NOTE: bash suppresses read -rp prompt text on a pipe, so
|
||||
# the confirm prompt string is never asserted — the confirm gate is proven
|
||||
# behaviourally (feed y → created; feed n → rc 1; missing answer → EOF-deny
|
||||
# rc 1) and by SUDO_LOG (mkdir attempted or not).
|
||||
|
||||
# extract_fn <source-file> <fnname> — print one brace-delimited function body.
|
||||
extract_fn() {
|
||||
local file="$1" fn="$2"
|
||||
awk -v fn="$fn" '
|
||||
BEGIN { found=0; depth=0 }
|
||||
{
|
||||
if (!found && $0 ~ ("^" fn "\\(\\)")) { found=1; depth=0 }
|
||||
if (found) {
|
||||
n_open = gsub(/\{/, "{")
|
||||
n_close = gsub(/\}/, "}")
|
||||
depth = depth + n_open - n_close
|
||||
print
|
||||
if (depth <= 0) exit
|
||||
}
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
run_test() {
|
||||
source "$ROOT/lib/common.sh" # warn / confirm / run / log
|
||||
source "$ROOT/lib/share-lib.sh" # share_ask_value → REAL menu_ask_value
|
||||
|
||||
local sandbox stubs
|
||||
sandbox="$(mksandbox share-mountpoint)"
|
||||
stubs="$sandbox/stubs"
|
||||
mkdir -p "$stubs"
|
||||
|
||||
# Fake sudo: records every invocation to $SUDO_LOG; exits 1 (no side
|
||||
# effect) when SUDO_FAIL=1, otherwise `exec "$@"` (real mkdir) so the
|
||||
# confirmed-create case genuinely creates the dir inside the sandbox.
|
||||
cat > "$stubs/sudo" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
printf 'sudo %s\n' "$*" >> "${SUDO_LOG:?fake sudo needs SUDO_LOG}"
|
||||
if [ "${SUDO_FAIL:-0}" = "1" ]; then
|
||||
printf 'sudo: permission denied (fake)\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
[ $# -gt 0 ] && exec "$@"
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$stubs/sudo"
|
||||
export PATH="$stubs:$PATH"
|
||||
export SUDO_LOG="$sandbox/sudo.log"
|
||||
export SUDO_FAIL=0
|
||||
: > "$SUDO_LOG"
|
||||
|
||||
# ═══ Part A0: non-TTY stdin contract of the real reader chain ═══
|
||||
# The whole matrix below is only deterministic if menu_read_value falls
|
||||
# back to plain `IFS= read` on a pipe and confirm() also reads plain
|
||||
# stdin. Prove both against the REAL helpers.
|
||||
local probe_out probe_rc
|
||||
set +e
|
||||
probe_out="$(printf 'x\n' | menu_read_value "probe" 2>/dev/null)"; probe_rc=$?
|
||||
set -e
|
||||
check_rc "non-TTY: menu_read_value pipe → rc 0 (plain-read fallback)" 0 "$probe_rc"
|
||||
check_eq "non-TTY: menu_read_value pipe → value" "x" "$probe_out"
|
||||
|
||||
set +e
|
||||
printf '' | stty -g >/dev/null 2>&1; probe_rc=$?
|
||||
set -e
|
||||
check_rc "non-TTY: stty -g fails on a pipe (plain-read path active)" 1 "$probe_rc"
|
||||
|
||||
set +e
|
||||
printf 'y\n' | confirm probe n >/dev/null 2>&1; probe_rc=$?
|
||||
set -e
|
||||
check_rc "non-TTY: confirm y → rc 0" 0 "$probe_rc"
|
||||
set +e
|
||||
printf 'n\n' | confirm probe n >/dev/null 2>&1; probe_rc=$?
|
||||
set -e
|
||||
check_rc "non-TTY: confirm n → rc 1" 1 "$probe_rc"
|
||||
set +e
|
||||
printf '' | confirm probe n >/dev/null 2>&1; probe_rc=$?
|
||||
set -e
|
||||
check_rc "non-TTY: confirm EOF → rc 1 (fail-closed)" 1 "$probe_rc"
|
||||
|
||||
# ═══ Part A/B runner over the REAL extracted ask_mountpoint ═══
|
||||
# mp_case <prefix> <desc> <feed> <expect_rc> <expect_out>
|
||||
# [<stderr-needle> [<stderr-absent> [<sudo_fail>]]]
|
||||
# feed = literal bytes: line 1 = mountpoint, line 2 (when present) = the
|
||||
# confirm answer. SUDO_LOG resets per case; side effects asserted by the
|
||||
# caller. SUDO_FAIL applies to the ask_mountpoint call only (assignment
|
||||
# prefix on the pipeline element; exported attribute carries it to the
|
||||
# fake sudo child process).
|
||||
mp_case() {
|
||||
local prefix="$1" desc="$2" feed="$3" expect_rc="$4" expect_out="$5"
|
||||
local needle="${6:-}" absent="${7:-}" failflag="${8:-0}"
|
||||
local out rc errf="$sandbox/mp.err"
|
||||
: > "$SUDO_LOG"
|
||||
set +e
|
||||
out="$(printf '%b' "$feed" | SUDO_FAIL="$failflag" ask_mountpoint 2>"$errf")"
|
||||
rc=$?
|
||||
set -e
|
||||
check_rc "$prefix: $desc (rc)" "$expect_rc" "$rc"
|
||||
check_eq "$prefix: $desc (stdout)" "$expect_out" "$out"
|
||||
if [ -n "$needle" ]; then
|
||||
check_contains "$prefix: $desc (stderr)" "$needle" "$(cat "$errf")"
|
||||
fi
|
||||
if [ -n "$absent" ]; then
|
||||
check_not_contains "$prefix: $desc (stderr)" "$absent" "$(cat "$errf")"
|
||||
fi
|
||||
}
|
||||
|
||||
local client file
|
||||
for client in smb nfs; do
|
||||
case "$client" in
|
||||
smb) file="$ROOT/bin/pos-share-smb-client" ;;
|
||||
nfs) file="$ROOT/bin/pos-share-nfs-client" ;;
|
||||
esac
|
||||
|
||||
local fn_file="$sandbox/ask_mountpoint-$client.sh"
|
||||
extract_fn "$file" ask_mountpoint > "$fn_file"
|
||||
source "$fn_file" # REAL body — a syntax error here aborts (FAIL, never silent)
|
||||
|
||||
local cdir="$sandbox/$client"
|
||||
mkdir -p "$cdir/existing"
|
||||
touch "$cdir/file"
|
||||
|
||||
# 1. existing dir → as-is, rc 0. Feed has NO y/n line, so reaching the
|
||||
# confirm gate would hit EOF → rc 1; rc 0 + empty SUDO_LOG proves
|
||||
# the confirm/mkdir branch was never entered.
|
||||
mp_case "$client" "existing dir → used as-is, no create" \
|
||||
"$cdir/existing\n" 0 "$cdir/existing" "" "Created mount point"
|
||||
check_eq "$client: existing dir → sudo NOT attempted" "" "$(cat "$SUDO_LOG")"
|
||||
check_file_exists "$client: existing dir → fixture intact" "$cdir/existing"
|
||||
|
||||
# 2. new dir + confirm y → sudo mkdir -p, dir created, printed, logged
|
||||
mp_case "$client" "new dir confirm=y → rc 0 + created" \
|
||||
"$cdir/newdir\ny\n" 0 "$cdir/newdir" "Created mount point"
|
||||
check_contains "$client: new dir → sudo mkdir -p attempted" \
|
||||
"sudo mkdir -p $cdir/newdir" "$(cat "$SUDO_LOG")"
|
||||
check_file_exists "$client: new dir → created" "$cdir/newdir"
|
||||
|
||||
# 3. new dir + confirm n → rc 1, nothing created
|
||||
mp_case "$client" "new dir confirm=n → rc 1" \
|
||||
"$cdir/declined\nn\n" 1 "" "" ""
|
||||
check_eq "$client: declined → sudo NOT attempted" "" "$(cat "$SUDO_LOG")"
|
||||
check_file_absent "$client: declined → no dir created" "$cdir/declined"
|
||||
|
||||
# 4. new dir + confirm EOF → rc 1 (gate fails closed)
|
||||
mp_case "$client" "new dir confirm=EOF → rc 1 (fail-closed)" \
|
||||
"$cdir/eof\n" 1 "" "" ""
|
||||
check_eq "$client: confirm-EOF → sudo NOT attempted" "" "$(cat "$SUDO_LOG")"
|
||||
check_file_absent "$client: confirm-EOF → no dir created" "$cdir/eof"
|
||||
|
||||
# 5. existing non-directory → rc 1 + not-a-directory warning
|
||||
mp_case "$client" "existing file → rc 1 not-a-directory" \
|
||||
"$cdir/file\n" 1 "" "not a directory" ""
|
||||
check_eq "$client: file → sudo NOT attempted" "" "$(cat "$SUDO_LOG")"
|
||||
|
||||
# 6. relative path → rc 1, warned, no write
|
||||
mp_case "$client" "relative path → rc 1" \
|
||||
"relative/path\n" 1 "" "not an absolute path" ""
|
||||
check_eq "$client: relative → sudo NOT attempted" "" "$(cat "$SUDO_LOG")"
|
||||
|
||||
# 7. trailing slash → rc 1, warned, no write
|
||||
mp_case "$client" "trailing slash → rc 1" \
|
||||
"$cdir/existing/\n" 1 "" "must not end with a slash" ""
|
||||
check_eq "$client: trailing slash → sudo NOT attempted" "" "$(cat "$SUDO_LOG")"
|
||||
|
||||
# 8. system paths → rc 1 (shape glob fires before any FS access)
|
||||
mp_case "$client" "system path /etc → rc 1" "/etc\n" 1 "" "Refusing system path" ""
|
||||
mp_case "$client" "system path /root → rc 1" "/root\n" 1 "" "Refusing system path" ""
|
||||
mp_case "$client" "system path /home/*/.ssh* → rc 1" \
|
||||
"/home/ci-user/.ssh/authorized_keys\n" 1 "" "Refusing system path" ""
|
||||
check_eq "$client: system paths → sudo NEVER attempted" "" "$(cat "$SUDO_LOG")"
|
||||
|
||||
# 9. empty/EOF on the path input → rc 1 (share_ask_value cancel)
|
||||
mp_case "$client" "EOF on path → rc 1" "" 1 "" "" ""
|
||||
check_eq "$client: EOF → sudo NOT attempted" "" "$(cat "$SUDO_LOG")"
|
||||
|
||||
# 10. mkdir failure (sudo shim fails) → rc 1 + could-not-create warning
|
||||
mp_case "$client" "mkdir fails → rc 1" \
|
||||
"$cdir/failmkdir\ny\n" 1 "" "Could not create" "" 1
|
||||
check_file_absent "$client: mkdir-fail → no dir created" "$cdir/failmkdir"
|
||||
|
||||
# ═══ Part B: static guards — the old n=new flow must not regress ═══
|
||||
local thint=0 tarm=0
|
||||
grep -q 't=type' "$file" && thint=1
|
||||
grep -q 't | T)' "$file" && tarm=1
|
||||
check_eq "$client: t=type hint present" 1 "$thint"
|
||||
check_eq "$client: t | T arm present" 1 "$tarm"
|
||||
check_eq "$client: no n=new hint" 0 "$(grep -c 'n=new' "$file" || true)"
|
||||
check_eq "$client: no n | N arm" 0 "$(grep -c 'n | N)' "$file" || true)"
|
||||
check_eq "$client: no ask_new_mountpoint" 0 "$(grep -c 'ask_new_mountpoint' "$file" || true)"
|
||||
|
||||
# Behavioural contract: the existing-dir branch must precede the
|
||||
# confirm/mkdir create flow inside the extracted body, so an existing
|
||||
# dir can never reach confirm/mkdir.
|
||||
check_eq "$client: ask_mountpoint has -d branch" 1 \
|
||||
"$(grep -cF '[ -d "$dir" ]' "$fn_file" || true)"
|
||||
check_eq "$client: ask_mountpoint has create flow" 1 \
|
||||
"$(grep -cF 'confirm "Create mountpoint' "$fn_file" || true)"
|
||||
local dline cline order=0
|
||||
dline="$(grep -nF '[ -d "$dir" ]' "$fn_file" | head -1 | cut -d: -f1 || true)"
|
||||
cline="$(grep -nF 'confirm "Create mountpoint' "$fn_file" | head -1 | cut -d: -f1 || true)"
|
||||
[ -n "$dline" ] && [ -n "$cline" ] && [ "$dline" -lt "$cline" ] && order=1
|
||||
check_eq "$client: existing-dir branch precedes confirm/mkdir" 1 "$order"
|
||||
|
||||
unset -f ask_mountpoint
|
||||
done
|
||||
|
||||
# ═══ Part C: symmetry — the two shipped bodies must stay identical ═══
|
||||
local same=0
|
||||
cmp -s "$sandbox/ask_mountpoint-smb.sh" "$sandbox/ask_mountpoint-nfs.sh" && same=1
|
||||
check_eq "ask_mountpoint bodies byte-identical (smb == nfs)" 1 "$same"
|
||||
}
|
||||
Reference in New Issue
Block a user