feat(mosaic): install-ordering guard for lease-enforcement hook wiring (#869 Point-1 C2)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
mosaic-link-runtime-assets wired the PreToolUse mutator-gate.py and Stop
receipt-observer-client.py hooks into ~/.claude/settings.json unconditionally.
If the activation half (C1's leaseEnforcementActivatable()) can't be
confirmed on the host, the fail-closed gate then denies every tool call —
bricking it.
Add a TS install-ordering guard (guardClaudeSettingsWiring /
runInstallOrderingGuard, hidden CLI bridge `mosaic __link-claude-settings`)
that imports leaseEnforcementActivatable() directly and decides whether the
enforcement hooks get wired:
- activatable -> wire as-is.
- NOT activatable, no opt-out -> strip the enforcement hooks, exit
non-zero with an actionable message
(default, fail-loud).
- NOT activatable + explicit
--allow-inactive-enforcement -> wire anyway, loud warning logged.
--allow-inactive-enforcement is a real CLI flag threaded through
install.sh -> mosaic-link-runtime-assets -> the hidden subcommand —
deliberately never an environment variable, so it can't sit as a
silently-inherited default. Wire mosaic-link-runtime-assets's guarded
settings.json copy to call out to the CLI (with a python3 fallback if
`mosaic` isn't resolvable at all), and stop swallowing stderr in
install.sh/finalize.ts so the guard's message actually reaches the operator.
mutator-gate.py's own fail-closed-on-absent-identity runtime behavior is
untouched (this only gates the WIRING); runtime_tools_unittest.py and
fail-closed-regression.spec.ts remain green.
Part of #869 (Point-1 C2)
This commit is contained in:
@@ -18,12 +18,33 @@ set -Eeuo pipefail
|
||||
# MOSAIC_INSTALL_MODE — prompt|keep|overwrite (default: prompt)
|
||||
# MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING — 1 to bypass MCP check
|
||||
# MOSAIC_SKIP_SKILLS_SYNC — 1 to skip skill sync
|
||||
#
|
||||
# Flags (CLI args, NOT environment variables — see #869 Point-1 C2):
|
||||
# --allow-inactive-enforcement Explicit, per-invocation opt-out that lets the
|
||||
# lease-enforcement hooks (mutator-gate.py,
|
||||
# receipt-observer-client.py) be wired into
|
||||
# ~/.claude/settings.json even when this host
|
||||
# cannot confirm it can ACTIVATE them. Loud on
|
||||
# use (see mosaic-link-runtime-assets). Default
|
||||
# (flag absent) is fail-loud: the enforcement
|
||||
# hooks are NOT wired and the framework's
|
||||
# runtime-asset-link step reports a failure.
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
||||
|
||||
# Deliberately parsed from "$@" (a real, explicit, per-invocation argument) —
|
||||
# never an environment variable — so this opt-out can never sit silently
|
||||
# inherited in a shell profile. See #869 Point-1 C2.
|
||||
ALLOW_INACTIVE_ENFORCEMENT=0
|
||||
for _arg in "$@"; do
|
||||
case "$_arg" in
|
||||
--allow-inactive-enforcement) ALLOW_INACTIVE_ENFORCEMENT=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Shared framework path-ownership manifest reader (#791). Parity with
|
||||
# packages/mosaic/src/framework/manifest.ts — both consume framework-manifest.txt.
|
||||
# Sourcing does not run its CLI dispatch (guarded by BASH_SOURCE==$0).
|
||||
@@ -670,10 +691,15 @@ step "Post-install tasks"
|
||||
SCRIPTS="$TARGET_DIR/tools/_scripts"
|
||||
|
||||
if [[ -x "$SCRIPTS/mosaic-link-runtime-assets" ]]; then
|
||||
if "$SCRIPTS/mosaic-link-runtime-assets" >/dev/null 2>&1; then
|
||||
link_args=()
|
||||
[[ "$ALLOW_INACTIVE_ENFORCEMENT" == "1" ]] && link_args+=(--allow-inactive-enforcement)
|
||||
# stdout is suppressed as before, but stderr is left connected: the
|
||||
# install-ordering guard's FAIL LOUD message (#869 Point-1 C2) must reach
|
||||
# the operator, not be swallowed silently.
|
||||
if "$SCRIPTS/mosaic-link-runtime-assets" "${link_args[@]}" >/dev/null; then
|
||||
ok "Runtime assets linked"
|
||||
else
|
||||
warn "Runtime asset linking failed (non-fatal)"
|
||||
warn "Runtime asset linking failed (non-fatal) — see message above for details."
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -4,6 +4,22 @@ set -euo pipefail
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
backup_stamp="$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
# ─── Install-ordering guard opt-out (#869 Point-1 C2) ───────────────────────
|
||||
# Explicit, per-invocation CLI flag ONLY — deliberately NOT read from an
|
||||
# environment variable, so it can never sit as a silently-inherited default in
|
||||
# a shell profile or CI env. Absent (the default) => hard fail-loud path.
|
||||
allow_inactive_enforcement=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--allow-inactive-enforcement) allow_inactive_enforcement=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Tracks whether the Claude settings install-ordering guard (below) reported a
|
||||
# degraded (enforcement-not-wired) outcome, so this script's own exit status
|
||||
# reflects it even though the rest of the runtime-asset sync must still run.
|
||||
guard_degraded=0
|
||||
|
||||
copy_file_managed() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
@@ -24,6 +40,103 @@ copy_file_managed() {
|
||||
cp "$src" "$dst"
|
||||
}
|
||||
|
||||
# ─── Install-ordering guard for settings.json (#869 Point-1 C2) ─────────────
|
||||
#
|
||||
# settings.json is where #828's enforcement hooks (PreToolUse mutator-gate.py,
|
||||
# Stop receipt-observer-client.py) get wired unconditionally. Before copying
|
||||
# it, delegate to `mosaic __link-claude-settings` (packages/mosaic/src/commands/
|
||||
# install-ordering-guard.ts) so the wiring decision is made by importing the
|
||||
# C1 activation probe (`leaseEnforcementActivatable()`) directly, rather than
|
||||
# re-implementing the capability/supervisor checks in shell. That subcommand:
|
||||
# - activatable -> writes settings.json with hooks intact, exits 0
|
||||
# - NOT activatable -> writes settings.json with hooks STRIPPED,
|
||||
# prints an actionable message, exits 1
|
||||
# - NOT activatable + opt-out -> writes settings.json with hooks intact,
|
||||
# prints a loud warning, exits 0
|
||||
# The `mosaic` CLI is expected on PATH at this point ("No executables are
|
||||
# placed on PATH — the mosaic npm CLI is the only binary", per install.sh).
|
||||
# If it is not resolvable at all, that is itself strong evidence the
|
||||
# activation half is absent, so the same fail-loud default applies via a
|
||||
# minimal python3 fallback (this repo already depends on python3 for the
|
||||
# lease broker itself).
|
||||
copy_claude_settings_guarded() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
|
||||
local guard_args=(__link-claude-settings "$src" "$dst")
|
||||
if [[ "$allow_inactive_enforcement" == "1" ]]; then
|
||||
guard_args+=(--allow-inactive-enforcement)
|
||||
fi
|
||||
|
||||
if command -v mosaic >/dev/null 2>&1; then
|
||||
if mosaic "${guard_args[@]}"; then
|
||||
return 0
|
||||
fi
|
||||
echo "[mosaic-link] Enforcement hooks were NOT wired into $dst (see message above)." >&2
|
||||
guard_degraded=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[mosaic-link] ERROR: 'mosaic' CLI not found on PATH — cannot confirm lease-enforcement" >&2
|
||||
echo "[mosaic-link] activation capability. enforcement requested but activation half absent —" >&2
|
||||
echo "[mosaic-link] needs a published CLI carrying launch-runtime activation + a broker" >&2
|
||||
echo "[mosaic-link] supervisor; refusing to wire a dead gate (see #869)." >&2
|
||||
|
||||
if [[ "$allow_inactive_enforcement" == "1" ]]; then
|
||||
echo "[mosaic-link] WARNING: --allow-inactive-enforcement set — wiring $dst AS-IS (with" >&2
|
||||
echo "[mosaic-link] enforcement hooks) despite being unable to confirm activation." >&2
|
||||
copy_file_managed "$src" "$dst"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "$src" "$dst" <<'PYEOF'
|
||||
import json, sys
|
||||
|
||||
src, dest = sys.argv[1], sys.argv[2]
|
||||
with open(src) as f:
|
||||
data = json.load(f)
|
||||
|
||||
hooks = data.get("hooks", {})
|
||||
|
||||
pre = hooks.get("PreToolUse", [])
|
||||
hooks["PreToolUse"] = [
|
||||
t for t in pre
|
||||
if not any("mutator-gate.py" in h.get("command", "") for h in t.get("hooks", []))
|
||||
]
|
||||
if not hooks["PreToolUse"]:
|
||||
del hooks["PreToolUse"]
|
||||
|
||||
stop = hooks.get("Stop", [])
|
||||
new_stop = []
|
||||
for t in stop:
|
||||
kept = [h for h in t.get("hooks", []) if "receipt-observer-client.py" not in h.get("command", "")]
|
||||
if kept:
|
||||
t = dict(t)
|
||||
t["hooks"] = kept
|
||||
new_stop.append(t)
|
||||
if new_stop:
|
||||
hooks["Stop"] = new_stop
|
||||
elif "Stop" in hooks:
|
||||
del hooks["Stop"]
|
||||
|
||||
if hooks:
|
||||
data["hooks"] = hooks
|
||||
else:
|
||||
data.pop("hooks", None)
|
||||
|
||||
with open(dest, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
else
|
||||
cp "$src" "$dst"
|
||||
fi
|
||||
guard_degraded=1
|
||||
return 0
|
||||
}
|
||||
|
||||
remove_legacy_path() {
|
||||
local p="$1"
|
||||
|
||||
@@ -110,6 +223,13 @@ for runtime_file in \
|
||||
fi
|
||||
src="$MOSAIC_HOME/runtime/claude/$runtime_file"
|
||||
[[ -f "$src" ]] || continue
|
||||
if [[ "$runtime_file" == "settings.json" ]]; then
|
||||
# Install-ordering guard (#869 Point-1 C2): gate enforcement-hook wiring
|
||||
# on confirmed activation instead of the plain copy_file_managed used for
|
||||
# every other runtime file. See copy_claude_settings_guarded() above.
|
||||
copy_claude_settings_guarded "$src" "$HOME/.claude/$runtime_file"
|
||||
continue
|
||||
fi
|
||||
copy_file_managed "$src" "$HOME/.claude/$runtime_file"
|
||||
done
|
||||
|
||||
@@ -167,3 +287,12 @@ fi
|
||||
|
||||
echo "[mosaic-link] Runtime assets synced (non-symlink mode)"
|
||||
echo "[mosaic-link] Canonical source: $MOSAIC_HOME"
|
||||
|
||||
# Propagate the install-ordering guard's outcome (#869 Point-1 C2): every
|
||||
# other runtime asset above is best-effort/non-fatal, but a degraded
|
||||
# (enforcement-not-wired) settings.json must make THIS script's own exit
|
||||
# status non-zero so callers (framework/install.sh, finalize.ts) can surface
|
||||
# it — never silently.
|
||||
if [[ "$guard_degraded" == "1" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression harness for issue #869 Point-1 C2 — the install-ordering guard
|
||||
# wired into mosaic-link-runtime-assets.
|
||||
#
|
||||
# Root cause under test: mosaic-link-runtime-assets copies
|
||||
# runtime/claude/settings.json (which embeds the PreToolUse mutator-gate.py
|
||||
# hook and the Stop receipt-observer-client.py hook) straight into
|
||||
# ~/.claude/settings.json, unconditionally. If the lease-broker activation
|
||||
# half cannot be confirmed on this host, wiring those hooks bricks it with a
|
||||
# fail-closed gate that can never be satisfied.
|
||||
#
|
||||
# This harness never invokes a real `mosaic` CLI build — it stubs the
|
||||
# `__link-claude-settings` contract with a fake `mosaic` on PATH so the shell
|
||||
# WIRING (does mosaic-link-runtime-assets call out correctly? does it
|
||||
# propagate a degraded outcome? does it still copy every other runtime file?
|
||||
# does --allow-inactive-enforcement forward through?) is exercised
|
||||
# independently of the TS guard's own logic (already covered by
|
||||
# install-ordering-guard.spec.ts). It also exercises the no-mosaic-on-PATH
|
||||
# python3 fallback directly.
|
||||
#
|
||||
# Scenarios:
|
||||
# 1. probe=true (fake mosaic exits 0) -> settings.json copied, script exits 0.
|
||||
# 2. probe=false (fake mosaic exits 1) -> script exits 1 (guard_degraded
|
||||
# propagated), but every OTHER runtime file is still copied.
|
||||
# 3. probe=false + --allow-inactive-enforcement -> the flag is forwarded to
|
||||
# the fake mosaic stub.
|
||||
# 4. No `mosaic` on PATH at all (activation unconfirmable) -> the python3
|
||||
# fallback strips the enforcement hooks itself and the script exits 1.
|
||||
# 5. No `mosaic` on PATH + --allow-inactive-enforcement -> the python3
|
||||
# fallback wires the hooks AS-IS and the script exits 0.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LINK_SCRIPT="$SCRIPT_DIR/mosaic-link-runtime-assets"
|
||||
|
||||
TMP_ROOT=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_ROOT"' EXIT
|
||||
|
||||
fail=0
|
||||
fail_msg() {
|
||||
echo "FAIL: $*" >&2
|
||||
fail=1
|
||||
}
|
||||
|
||||
FIXTURE_SETTINGS='{
|
||||
"model": "opus",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{ "matcher": ".*", "hooks": [ { "type": "command", "command": "python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude" } ] },
|
||||
{ "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", "command": "~/.config/mosaic/tools/qa/prevent-memory-write.sh" } ] }
|
||||
],
|
||||
"Stop": [
|
||||
{ "hooks": [
|
||||
{ "type": "command", "command": "python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude" },
|
||||
{ "type": "command", "command": "~/.config/mosaic/tools/qa/reflect-stop-hook.sh" }
|
||||
] }
|
||||
]
|
||||
}
|
||||
}'
|
||||
|
||||
# Sets up a fresh $MOSAIC_HOME/runtime/claude/{settings.json,CLAUDE.md,
|
||||
# hooks-config.json,context7-integration.md} + fresh $HOME, echoes both paths
|
||||
# space-separated for the caller to `read`.
|
||||
new_scenario_dirs() {
|
||||
local scenario="$1"
|
||||
local base="$TMP_ROOT/$scenario"
|
||||
local mosaic_home="$base/mosaic-home"
|
||||
local home="$base/home"
|
||||
mkdir -p "$mosaic_home/runtime/claude" "$home"
|
||||
printf '%s' "$FIXTURE_SETTINGS" > "$mosaic_home/runtime/claude/settings.json"
|
||||
echo "claude.md fixture" > "$mosaic_home/runtime/claude/CLAUDE.md"
|
||||
echo '{"hooks":{}}' > "$mosaic_home/runtime/claude/hooks-config.json"
|
||||
echo "context7 fixture" > "$mosaic_home/runtime/claude/context7-integration.md"
|
||||
echo "$mosaic_home" "$home"
|
||||
}
|
||||
|
||||
settings_has_marker() {
|
||||
local file="$1" marker="$2"
|
||||
[[ -f "$file" ]] && grep -q "$marker" "$file"
|
||||
}
|
||||
|
||||
# A fake `mosaic` binary implementing only the __link-claude-settings contract
|
||||
# this harness needs: writes dest verbatim (fixture is unmodified either way —
|
||||
# this stub only exercises the CALL CONTRACT, not the TS strip logic, which
|
||||
# has its own vitest coverage) and exits with the code the scenario wants.
|
||||
# Records the args it was called with so the harness can assert forwarding.
|
||||
make_fake_mosaic() {
|
||||
local bin_dir="$1" exit_code="$2"
|
||||
mkdir -p "$bin_dir"
|
||||
cat > "$bin_dir/mosaic" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "\$@" > "$bin_dir/mosaic.args"
|
||||
if [[ "\$1" == "__link-claude-settings" ]]; then
|
||||
cp "\$2" "\$3"
|
||||
exit $exit_code
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$bin_dir/mosaic"
|
||||
}
|
||||
|
||||
# --- Scenario 1: probe=true (fake mosaic exits 0) ---------------------------
|
||||
read -r MOSAIC_HOME_1 HOME_1 < <(new_scenario_dirs scenario1)
|
||||
BIN_1="$TMP_ROOT/scenario1/bin"
|
||||
make_fake_mosaic "$BIN_1" 0
|
||||
|
||||
OUTPUT=$(MOSAIC_HOME="$MOSAIC_HOME_1" HOME="$HOME_1" PATH="$BIN_1:$PATH" "$LINK_SCRIPT" 2>&1)
|
||||
STATUS=$?
|
||||
[[ "$STATUS" -eq 0 ]] || fail_msg "scenario1 (probe=true): expected exit 0, got $STATUS. Output: $OUTPUT"
|
||||
[[ -f "$HOME_1/.claude/settings.json" ]] || fail_msg "scenario1: settings.json was not copied"
|
||||
|
||||
# --- Scenario 2: probe=false (fake mosaic exits 1) --------------------------
|
||||
read -r MOSAIC_HOME_2 HOME_2 < <(new_scenario_dirs scenario2)
|
||||
BIN_2="$TMP_ROOT/scenario2/bin"
|
||||
make_fake_mosaic "$BIN_2" 1
|
||||
|
||||
OUTPUT=$(MOSAIC_HOME="$MOSAIC_HOME_2" HOME="$HOME_2" PATH="$BIN_2:$PATH" "$LINK_SCRIPT" 2>&1)
|
||||
STATUS=$?
|
||||
[[ "$STATUS" -ne 0 ]] || fail_msg "scenario2 (probe=false, default): expected non-zero exit, got 0. Output: $OUTPUT"
|
||||
[[ -f "$HOME_2/.claude/CLAUDE.md" ]] || fail_msg "scenario2: CLAUDE.md was NOT copied even though it is independent of the settings.json guard"
|
||||
[[ -f "$HOME_2/.claude/hooks-config.json" ]] || fail_msg "scenario2: hooks-config.json was NOT copied"
|
||||
[[ -f "$HOME_2/.claude/context7-integration.md" ]] || fail_msg "scenario2: context7-integration.md was NOT copied"
|
||||
case "$OUTPUT" in
|
||||
*"NOT be wired"*|*"NOT wired"*) ;;
|
||||
*) fail_msg "scenario2: expected an actionable degraded-wiring message in output, got: $OUTPUT" ;;
|
||||
esac
|
||||
|
||||
# --- Scenario 3: probe=false + --allow-inactive-enforcement forwards the flag
|
||||
read -r MOSAIC_HOME_3 HOME_3 < <(new_scenario_dirs scenario3)
|
||||
BIN_3="$TMP_ROOT/scenario3/bin"
|
||||
make_fake_mosaic "$BIN_3" 0
|
||||
|
||||
MOSAIC_HOME="$MOSAIC_HOME_3" HOME="$HOME_3" PATH="$BIN_3:$PATH" "$LINK_SCRIPT" --allow-inactive-enforcement >/dev/null 2>&1
|
||||
RECORDED_ARGS="$(cat "$BIN_3/mosaic.args" 2>/dev/null || true)"
|
||||
case "$RECORDED_ARGS" in
|
||||
*"--allow-inactive-enforcement"*) ;;
|
||||
*) fail_msg "scenario3: --allow-inactive-enforcement was not forwarded to the mosaic CLI invocation (got: '$RECORDED_ARGS')" ;;
|
||||
esac
|
||||
|
||||
# --- Scenario 4: no `mosaic` on PATH at all -> python3 fallback strips hooks
|
||||
read -r MOSAIC_HOME_4 HOME_4 < <(new_scenario_dirs scenario4)
|
||||
EMPTY_BIN="$TMP_ROOT/scenario4/empty-bin"
|
||||
mkdir -p "$EMPTY_BIN"
|
||||
# A PATH containing only python3 (for the fallback) + core utils, no mosaic.
|
||||
FALLBACK_PATH="$EMPTY_BIN:/usr/bin:/bin"
|
||||
|
||||
OUTPUT=$(MOSAIC_HOME="$MOSAIC_HOME_4" HOME="$HOME_4" PATH="$FALLBACK_PATH" "$LINK_SCRIPT" 2>&1)
|
||||
STATUS=$?
|
||||
[[ "$STATUS" -ne 0 ]] || fail_msg "scenario4 (no mosaic on PATH, default): expected non-zero exit, got 0. Output: $OUTPUT"
|
||||
if settings_has_marker "$HOME_4/.claude/settings.json" "mutator-gate.py"; then
|
||||
fail_msg "scenario4: mutator-gate.py hook was wired even though mosaic could not be resolved (activation unconfirmable)"
|
||||
fi
|
||||
if settings_has_marker "$HOME_4/.claude/settings.json" "receipt-observer-client.py"; then
|
||||
fail_msg "scenario4: receipt-observer-client.py hook was wired even though mosaic could not be resolved"
|
||||
fi
|
||||
if ! settings_has_marker "$HOME_4/.claude/settings.json" "prevent-memory-write.sh"; then
|
||||
fail_msg "scenario4: the unrelated prevent-memory-write.sh hook was incorrectly dropped too"
|
||||
fi
|
||||
|
||||
# --- Scenario 5: no `mosaic` on PATH + --allow-inactive-enforcement --------
|
||||
read -r MOSAIC_HOME_5 HOME_5 < <(new_scenario_dirs scenario5)
|
||||
|
||||
OUTPUT=$(MOSAIC_HOME="$MOSAIC_HOME_5" HOME="$HOME_5" PATH="$FALLBACK_PATH" "$LINK_SCRIPT" --allow-inactive-enforcement 2>&1)
|
||||
STATUS=$?
|
||||
[[ "$STATUS" -eq 0 ]] || fail_msg "scenario5 (no mosaic, opt-out): expected exit 0, got $STATUS. Output: $OUTPUT"
|
||||
if ! settings_has_marker "$HOME_5/.claude/settings.json" "mutator-gate.py"; then
|
||||
fail_msg "scenario5: mutator-gate.py hook should have been wired (explicit opt-out set)"
|
||||
fi
|
||||
case "$OUTPUT" in
|
||||
*"WARNING"*"--allow-inactive-enforcement"*) ;;
|
||||
*) fail_msg "scenario5: expected a loud WARNING mentioning --allow-inactive-enforcement, got: $OUTPUT" ;;
|
||||
esac
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "install-ordering-guard regression passed (5/5 scenarios)"
|
||||
fi
|
||||
|
||||
exit "$fail"
|
||||
Reference in New Issue
Block a user