molecule-core/workspace-template/tests/test_gh_wrapper.sh
rabbitblood 067a8333ce feat(workspace): gh-wrapper — auto-tag agent PRs + issues with role
Every agent in the template currently uses the same GitHub PAT, so
\`gh pr list\` shows every PR as authored by the CEO's account with
no signal which agent opened each one. Commits already carry
per-agent authors (GIT_AUTHOR_NAME from #402). This wrapper extends
the identity split to the PR/issue metadata surface layer that
commit attribution can't reach.

## How it works

A tiny bash script installed at \`/usr/local/bin/gh\`, which sits
earlier in PATH than the real binary at \`/usr/bin/gh\`. For \`gh pr
create\` and \`gh issue create\`:

- Title gets prefixed with \`[Role Name]\` — e.g. \`[Frontend Engineer]
  fix: canvas grid index\`
- Body gets \`\n\n---\n_Opened by: Molecule AI <Role>_\` appended

Role is read from \`GIT_AUTHOR_NAME\` which the platform provisioner
sets to \`Molecule AI <Role>\` (shipped with #402). Accepts both
\`--title X\` and \`--title=X\` forms. Same for \`--body\`.

Anything that isn't \`gh pr create\` or \`gh issue create\` (e.g.
\`gh pr list\`, \`gh issue view\`, \`gh run watch\`) passes through
untouched. No behaviour change for read-side operations.

## Idempotent

- If the title already starts with \`[...]\` the wrapper does not
  re-prefix. \`gh pr edit\` flows that resubmit title won't layer
  multiple tags.
- If the body already contains \`Opened by: Molecule AI\` the footer
  is not re-appended.

## Fail-open

When \`GIT_AUTHOR_NAME\` is absent or doesn't start with \`Molecule
AI \`, the wrapper exec's the real gh with unchanged args. No call
is ever blocked by this script.

## Test coverage

\`tests/test_gh_wrapper.sh\` — 12 cases, no network, no Docker:
- Passthrough for non-create subcommands (pr list)
- pr create title prefix + body footer
- issue create with \`--title=X\` \`--body=X\` equals-form
- Idempotent title re-prefix
- Idempotent body footer (count = 1 after two applies)
- Missing GIT_AUTHOR_NAME → passthrough, title preserved
- Malformed GIT_AUTHOR_NAME (not "Molecule AI ...") → passthrough

All 12 pass. Test script is standalone bash + a temp fake gh binary
that echoes argv; safe to run in CI's Python Lint & Test job via
subprocess shell-out.

## Deployment note

This lands in the workspace image. Existing containers keep their
old /usr/bin/gh until the image is rebuilt and they're re-provisioned
(POST /workspaces/:id/restart {}). No migration required; the wrapper
just starts tagging PRs once the new image is rolled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 03:10:46 -07:00

115 lines
4.3 KiB
Bash

#!/usr/bin/env bash
# Smoke-test the gh-wrapper behaviour with a fake gh binary that echoes
# back its argv. Runs entirely in-process (no Docker), so it's cheap to
# run per-CI-job. Tests the behaviour table in scripts/gh-wrapper.sh.
#
# Invoked by CI's Python Lint & Test job via a subprocess shell-out, or
# locally via `bash tests/test_gh_wrapper.sh`.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WRAPPER="$HERE/../scripts/gh-wrapper.sh"
if [[ ! -x "$WRAPPER" ]]; then
echo "FAIL: wrapper not executable: $WRAPPER" >&2
exit 1
fi
# Fake gh: prints every arg on its own line, prefixed by "ARG:". Lets
# tests introspect what the wrapper passed through.
FAKE_GH_DIR=$(mktemp -d)
trap 'rm -rf "$FAKE_GH_DIR"' EXIT
cat > "$FAKE_GH_DIR/gh" <<'EOF'
#!/usr/bin/env bash
for a in "$@"; do
printf 'ARG:%s\n' "$a"
done
EOF
chmod +x "$FAKE_GH_DIR/gh"
# Make the wrapper use the fake gh by overriding the hardcoded path via
# a temporary symlink trick: copy the wrapper to a temp location and
# sed-replace the REAL_GH default with our fake.
WRAPPER_UNDER_TEST=$(mktemp)
trap 'rm -f "$WRAPPER_UNDER_TEST"' EXIT
sed "s|REAL_GH=/usr/bin/gh|REAL_GH=$FAKE_GH_DIR/gh|" "$WRAPPER" > "$WRAPPER_UNDER_TEST"
chmod +x "$WRAPPER_UNDER_TEST"
pass=0
fail=0
assert_contains() {
local name="$1" haystack="$2" needle="$3"
if [[ "$haystack" == *"$needle"* ]]; then
pass=$((pass + 1))
echo " PASS: $name"
else
fail=$((fail + 1))
echo " FAIL: $name" >&2
echo " expected to contain: $needle" >&2
echo " got: $haystack" >&2
fi
}
assert_not_contains() {
local name="$1" haystack="$2" needle="$3"
if [[ "$haystack" == *"$needle"* ]]; then
fail=$((fail + 1))
echo " FAIL: $name — should not contain: $needle" >&2
echo " got: $haystack" >&2
else
pass=$((pass + 1))
echo " PASS: $name"
fi
}
echo "--- passthrough (no subcommand transform) ---"
out=$(GIT_AUTHOR_NAME="Molecule AI Frontend Engineer" "$WRAPPER_UNDER_TEST" pr list --state open)
assert_contains "pr list passthrough" "$out" "ARG:list"
assert_not_contains "pr list no prefix" "$out" "[Frontend"
echo "--- pr create with role ---"
out=$(GIT_AUTHOR_NAME="Molecule AI Backend Engineer" "$WRAPPER_UNDER_TEST" pr create --title "fix: auth" --body "Short description")
assert_contains "pr create title prefix" "$out" "ARG:[Backend Engineer] fix: auth"
assert_contains "pr create body footer" "$out" "_Opened by: Molecule AI Backend Engineer_"
echo "--- issue create with = form ---"
out=$(GIT_AUTHOR_NAME="Molecule AI PM" "$WRAPPER_UNDER_TEST" issue create --title="bug: foo" --body="details")
assert_contains "issue create --title= prefix" "$out" "ARG:--title=[PM] bug: foo"
assert_contains "issue create --body= footer" "$out" "_Opened by: Molecule AI PM_"
echo "--- idempotent title re-prefix ---"
out=$(GIT_AUTHOR_NAME="Molecule AI DevRel Engineer" "$WRAPPER_UNDER_TEST" pr create --title "[DevRel Engineer] already prefixed")
assert_not_contains "no double prefix" "$out" "[DevRel Engineer] [DevRel Engineer]"
echo "--- idempotent body footer ---"
already="original body
---
_Opened by: Molecule AI UIUX Designer_"
out=$(GIT_AUTHOR_NAME="Molecule AI UIUX Designer" "$WRAPPER_UNDER_TEST" pr create --title "x" --body "$already")
# Count how many times the footer marker appears — should be exactly 1.
count=$(echo "$out" | grep -c "_Opened by: Molecule AI UIUX Designer_" || true)
if [[ "$count" -eq 1 ]]; then
pass=$((pass + 1)); echo " PASS: footer not double-appended"
else
fail=$((fail + 1)); echo " FAIL: footer count=$count (want 1)" >&2
fi
echo "--- missing GIT_AUTHOR_NAME — passes through ---"
out=$(unset GIT_AUTHOR_NAME; "$WRAPPER_UNDER_TEST" pr create --title "fix: foo")
assert_not_contains "no role means no prefix" "$out" "[M"
assert_contains "raw title survives" "$out" "ARG:fix: foo"
echo "--- wrong prefix in GIT_AUTHOR_NAME — passes through ---"
out=$(GIT_AUTHOR_NAME="Some Random Human" "$WRAPPER_UNDER_TEST" pr create --title "fix: foo")
assert_not_contains "non-Molecule author means no prefix" "$out" "[S"
assert_contains "raw title survives (wrong prefix)" "$out" "ARG:fix: foo"
echo
echo "================================"
echo "gh-wrapper: $pass passed, $fail failed"
echo "================================"
[[ $fail -eq 0 ]]