From f744f32214ff6d15c0ef84ef8c727c0c1e2eb333 Mon Sep 17 00:00:00 2001 From: Mos Date: Fri, 7 Aug 2026 05:07:40 +0000 Subject: [PATCH] feat(tools/git): explain tea's misleading `user does not exist` error (stale token, not a missing account) (#1086) --- .../framework/tools/git/detect-platform.sh | 15 +++++ .../framework/tools/git/issue-create.sh | 1 + .../mosaic/framework/tools/git/issue-view.sh | 1 + .../mosaic/framework/tools/git/pr-create.sh | 1 + .../test-explain-diagnostic-status-neutral.sh | 64 +++++++++++++++++++ packages/mosaic/package.json | 2 +- 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100755 packages/mosaic/framework/tools/git/test-explain-diagnostic-status-neutral.sh diff --git a/packages/mosaic/framework/tools/git/detect-platform.sh b/packages/mosaic/framework/tools/git/detect-platform.sh index a06452d1..5a78920c 100755 --- a/packages/mosaic/framework/tools/git/detect-platform.sh +++ b/packages/mosaic/framework/tools/git/detect-platform.sh @@ -246,6 +246,21 @@ PY } >&2 } +# Explain tea's most misleading failure. `user does not exist [uid: 0, name: ]` reads +# as a missing account; it almost always means a REVOKED OR STALE TOKEN. `tea login` +# keeps its OWN COPY of the token, so rotating the credential store does not update it. +# Diagnostic only -- stderr, no control flow, no exit. +explain_tea_user_does_not_exist() { + cat >&2 <<'MSG' +NOTE: `user does not exist [uid: 0, name: ]` from tea usually means a REVOKED OR STALE TOKEN, + not a missing account. A `tea login` stores its OWN COPY of the token; rotating the + credential store does NOT update it. + CHECK: the login's cached copy (`tea login list` -- read the FULL table, never `| head`), + then re-register that login against the current token. + DO NOT probe capability with a mutating request; a POST is the action, not a check. +MSG +} + get_gitea_login_for_host() { local host="${1:-}" local login diff --git a/packages/mosaic/framework/tools/git/issue-create.sh b/packages/mosaic/framework/tools/git/issue-create.sh index 96f890cf..d9c0bc24 100755 --- a/packages/mosaic/framework/tools/git/issue-create.sh +++ b/packages/mosaic/framework/tools/git/issue-create.sh @@ -156,6 +156,7 @@ case "$PLATFORM" in exit 0 fi echo "Warning: tea issue create failed, trying Gitea API fallback..." >&2 + { declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true fi gitea_issue_create_api ;; diff --git a/packages/mosaic/framework/tools/git/issue-view.sh b/packages/mosaic/framework/tools/git/issue-view.sh index eccab98e..81c55965 100755 --- a/packages/mosaic/framework/tools/git/issue-view.sh +++ b/packages/mosaic/framework/tools/git/issue-view.sh @@ -71,6 +71,7 @@ elif [[ "$PLATFORM" == "gitea" ]]; then exit 0 fi echo "Warning: tea issue view failed, trying Gitea API fallback..." >&2 + { declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true fi gitea_issue_view_api else diff --git a/packages/mosaic/framework/tools/git/pr-create.sh b/packages/mosaic/framework/tools/git/pr-create.sh index 46b82a64..bf60863e 100755 --- a/packages/mosaic/framework/tools/git/pr-create.sh +++ b/packages/mosaic/framework/tools/git/pr-create.sh @@ -219,6 +219,7 @@ case "$PLATFORM" in exit 0 fi echo "Warning: tea pr create failed, trying Gitea API fallback..." >&2 + { declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true gitea_pr_create_api ;; *) diff --git a/packages/mosaic/framework/tools/git/test-explain-diagnostic-status-neutral.sh b/packages/mosaic/framework/tools/git/test-explain-diagnostic-status-neutral.sh new file mode 100755 index 00000000..9cc831ef --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-explain-diagnostic-status-neutral.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Regression: the tea-failure diagnostic must be STATUS-NEUTRAL. +# +# Found by be-coder-08 reviewing PR #1086. At all three call sites the diagnostic is emitted +# immediately BEFORE the Gitea API fallback. Written as the last command of an && list: +# declare -F explain_... >/dev/null && explain_... +# under `set -e` a FAILING diagnostic exits and the fallback never runs -- a diagnostic that +# suppresses the recovery path it exists to explain. It misbehaves ONLY when the helper is +# PRESENT, so the helper-absent path (pre-#1086 behaviour) keeps working and reads as a +# passing control. +# +# TWO DEFECTS IN THE FIRST VERSION OF THIS TEST, both found by be-coder-08: +# 1. `out=$( ... ) 2>"$errto"` applies the redirection to the ASSIGNMENT, not to the +# command substitution, so the probe's stderr was never actually pointed at /dev/full +# and the /dev/full rows proved nothing. Verified: `out=$(echo x >&2) 2>/dev/full` +# leaks to the terminal and returns 0; the redirect must be INSIDE the substitution. +# 2. `eval "$CONSTRUCT"` changes `set -e` semantics for a bare && list, so the probe did +# not exercise the construct as the shipped file executes it. It now writes the line +# into a real script and runs it -- same parse, same set -e rules, no eval. +# The construct is still LIFTED FROM THE SHIPPED FILE: retyping the fixed form makes the +# probe pass on a build whose real call sites still carry the bare && form. +set -uo pipefail +fail=0 +GIT_DIR_UNDER_TEST="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT + +probe() { # $1=present|absent $2=stderr target $3=source file -> "rc:fallback" + local helper="$1" errto="$2" src="$3" construct script out rc + construct=$(grep -m1 'explain_tea_user_does_not_exist' "$GIT_DIR_UNDER_TEST/$src" | sed 's/^[[:space:]]*//') + [ -n "$construct" ] || { printf 'no-construct:no'; return; } + script="$TMP/probe.sh" + { + echo '#!/bin/bash' + echo 'set -e' + echo 'explain_tea_user_does_not_exist() { echo "diagnostic" >&2; }' + [ "$helper" = absent ] && echo 'unset -f explain_tea_user_does_not_exist' + echo "$construct" # the shipped line, parsed by a real shell + echo 'echo FALLBACK_REACHED' + } > "$script" + # redirect INSIDE the substitution so the subshell's stderr really is $errto + out=$( bash "$script" 2>"$errto" ); rc=$? + printf '%s:%s' "$rc" "$(grep -q FALLBACK_REACHED <<<"$out" && echo yes || echo no)" +} + +check() { if [ "$2" = "$3" ]; then echo " PASS $1 ($2)"; else echo " FAIL $1: got $2, want $3"; fail=1; fi; } + +echo "== diagnostic must not alter exit status or skip the fallback ==" +# /dev/full makes every stderr write fail -- the real-world shape is a closed or full fd. +for src in pr-create.sh issue-view.sh issue-create.sh; do + check "$src stderr OK / helper present" "$(probe present /dev/null "$src")" "0:yes" + check "$src stderr OK / helper absent " "$(probe absent /dev/null "$src")" "0:yes" + check "$src stderr FAILING / helper present" "$(probe present /dev/full "$src")" "0:yes" + check "$src stderr FAILING / helper absent " "$(probe absent /dev/full "$src")" "0:yes" +done + +echo "== all three call sites use the status-neutral form ==" +for f in pr-create.sh issue-view.sh issue-create.sh; do + p="$GIT_DIR_UNDER_TEST/$f" + grep -q '{ declare -F explain_tea_user_does_not_exist >/dev/null && explain_tea_user_does_not_exist; } || true' "$p" \ + && echo " PASS $f guarded" || { echo " FAIL $f: diagnostic is not status-neutral"; fail=1; } +done + +[ "$fail" -eq 0 ] && echo "OK diagnostic is status-neutral" || echo "FAILED" +exit "$fail" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 7096794d..631b4a88 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*",