Files
stack/packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh
T
549b6fbaae fix(tools/git): use tea comment, keep one principal, add a red-first regression
Addresses both blockers from review 127 (rev-security-02) and corrects the
severity claim in the original report.

Blocker 1 -- mixed principals. Routing the comment through the token-
authenticated gitea_issue_comment_api() attributed the comment to the token
holder while the close still used --login $GITEA_LOGIN_NAME: two principals for
one operation. `tea comment` accepts the same --repo/--login flags, so the tea
branch now uses it and both calls carry the same principal. The no-login branch
keeps the API helper for both, also a single principal.

Blocker 2 -- no regression test. Adds test-issue-close-fail-closed.sh on the
existing mocked-tea/sandboxed-git harness pattern. Asserts: a failed comment
does not close the issue and exits non-zero; a successful comment does close it;
the subcommand is top-level `tea comment`, never `tea issue comment`; and the
comment and close carry the same --login. GREEN on this branch, RED on main.

Severity correction. The original report said the issue closes anyway and the
audit trail is silently lost. It does not: set -e at line 5 aborts the script
when the comment fails, so the close is never reached. The real defect is that
issue-close.sh -c cannot succeed at all where a tea login resolves -- loud, not
silent. The explicit || guard is retained deliberately: a fail-closed property
that depends on set -e disappears the moment anyone adds `|| true` or wraps the
call in a conditional. Posted as a comment on #1081 and #1085.

Refs #1081

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Amf1Neca162odgcbCWMk1y
2026-08-07 00:33:57 -05:00

80 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Regression: issue-close.sh must NOT close an issue when the closing comment
# could not be posted, and comment+close must be made by ONE principal.
#
# Guards two defects fixed together (see #1081):
# 1. `tea issue comment` is not a subcommand -- tea exposes comments as the
# TOP-LEVEL `tea comment`. The old call always failed, was unchecked, and
# the issue closed anyway, losing the record of WHY it was closed.
# 2. Routing the comment through the token-authenticated API helper while the
# close used --login would attribute one operation to two principals.
#
# Fully offline: `tea` and `curl` are mocked onto PATH, the repo is a throwaway
# `git init`, everything lives under $SANDBOX, removed on EXIT.
set -uo pipefail
WORK_ROOT="${AGENT_WORK_ROOT:-${TMPDIR:-/tmp}}"
SANDBOX="$WORK_ROOT/issue-close-fail-closed-test-$$"
MOCK_BIN="$SANDBOX/bin"; REPO_DIR="$SANDBOX/repo"; CALLS="$SANDBOX/calls.log"
cleanup() { rm -rf "$SANDBOX"; }
trap cleanup EXIT
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET="$SCRIPT_DIR/issue-close.sh"
[ -f "$TARGET" ] || { echo "FAIL: issue-close.sh not found beside this test"; exit 1; }
fail() { echo "FAIL: $*"; exit 1; }
mkdir -p "$MOCK_BIN" "$REPO_DIR"; : > "$CALLS"
cd "$REPO_DIR"; git init -q
git remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
export PATH="$MOCK_BIN:$PATH" CALLS
export GITEA_LOGIN="git.mosaicstack.dev"
export GITEA_URL="https://git.mosaicstack.dev"
export GITEA_TOKEN="redacted-test-token"
cat > "$MOCK_BIN/curl" <<'EOF'
#!/bin/bash
printf 'curl %q ' "$@" >> "$CALLS"; printf '\n' >> "$CALLS"; exit 0
EOF
chmod +x "$MOCK_BIN/curl"
mk_tea() { # $1 = exit code for `tea comment`
cat > "$MOCK_BIN/tea" <<EOF
#!/bin/bash
printf 'tea %s\n' "\$*" >> "$CALLS"
if [[ "\$*" == *"login list"* ]]; then
echo '[{"name":"git.mosaicstack.dev","url":"https://git.mosaicstack.dev"}]'; exit 0
fi
# Fail ANY comment attempt -- both the correct top-level \`tea comment\` and the
# broken \`tea issue comment\` -- so the test exercises the DEFECT on an unfixed
# script rather than failing on a setup assertion.
if [[ "\$1" == "comment" || ( "\$1" == "issue" && "\$2" == "comment" ) ]]; then exit $1; fi
exit 0
EOF
chmod +x "$MOCK_BIN/tea"
}
# ── 1. NEGATIVE (the regression): comment fails => must NOT close, must exit non-zero
mk_tea 1; : > "$CALLS"
bash "$TARGET" -i 42 -c "closing note" >/dev/null 2>&1; rc=$?
grep -qE 'tea (issue )?comment' "$CALLS" || fail "no comment attempt at all -- setup did not reach the tea branch"
grep -q 'tea issue close' "$CALLS" && fail "ISSUE CLOSED AFTER THE COMMENT FAILED -- the regression"
[ "$rc" -ne 0 ] || fail "comment failed but issue-close exited 0 -- FAIL-OPEN"
# ── 2. POSITIVE (rule MET must pass): comment succeeds => close proceeds, exit 0
mk_tea 0; : > "$CALLS"
bash "$TARGET" -i 42 -c "closing note" >/dev/null 2>&1; rc=$?
[ "$rc" -eq 0 ] || fail "comment succeeded but issue-close exited $rc"
grep -q 'tea issue close' "$CALLS" || fail "issue not closed even though the comment succeeded"
# ── 3. must use top-level `tea comment`, never `tea issue comment`
grep -q 'tea issue comment' "$CALLS" && fail "used 'tea issue comment' -- not a valid subcommand"
# ── 4. ONE PRINCIPAL: comment and close must carry the SAME --login
c=$(grep -m1 '^tea comment' "$CALLS" | grep -o -- '--login [^ ]*' | awk '{print $2}')
k=$(grep -m1 '^tea issue close' "$CALLS" | grep -o -- '--login [^ ]*' | awk '{print $2}')
[ -n "$c" ] || fail "comment carried no --login"
[ "$c" = "$k" ] || fail "MIXED PRINCIPALS: comment=$c close=$k"
echo "issue-close.sh fail-closed + single-principal regression passed"