Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ go-tidy:
script-test:
bash internal/scaffold/fullsend-repo/scripts/post-triage-test.sh
bash internal/scaffold/fullsend-repo/scripts/validate-output-schema-test.sh
bash internal/scaffold/fullsend-repo/scripts/pre-code-test.sh

test: lint go-vet go-test script-test

Expand Down
241 changes: 241 additions & 0 deletions internal/scaffold/fullsend-repo/scripts/pre-code-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
#!/usr/bin/env bash
# pre-code-test.sh — Test pre-code.sh with mock gh to verify existing-PR check.
#
# Uses a mock gh command to capture calls without hitting GitHub.
# Run from the repo root: bash internal/scaffold/fullsend-repo/scripts/pre-code-test.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRE_SCRIPT="${SCRIPT_DIR}/pre-code.sh"
FAILURES=0

# Create a temp directory for mock state.
TMPDIR="$(mktemp -d)"
trap 'rm -rf "${TMPDIR}"' EXIT

# --- Helpers ---

# build_mock creates a mock gh binary that returns preconfigured responses.
# Arguments:
# $1 — output to return for "gh pr list" calls (tab-separated lines
# matching what gh --jq would produce, or empty for no PRs)
build_mock() {
local pr_list_output="$1"
local mock_bin="${TMPDIR}/bin"
local gh_log="${TMPDIR}/gh-calls.log"

rm -rf "${mock_bin}"
mkdir -p "${mock_bin}"
> "${gh_log}"

# Write the pr list output to a file so the mock can read it.
printf '%s' "${pr_list_output}" > "${TMPDIR}/pr-list-output.txt"

cat > "${mock_bin}/gh" <<'MOCKEOF'
#!/usr/bin/env bash
CALL_LOG="LOGFILE_PLACEHOLDER"
PR_OUTPUT="OUTPUT_PLACEHOLDER"

echo "gh $*" >> "${CALL_LOG}"

# Route by subcommand
if [[ "$1" == "pr" && "$2" == "list" ]]; then
cat "${PR_OUTPUT}"
elif [[ "$1" == "label" ]]; then
exit 0
elif [[ "$1" == "api" ]]; then
exit 0
elif [[ "$1" == "issue" && "$2" == "comment" ]]; then
# Consume stdin (body-file reads from stdin)
cat > /dev/null
exit 0
fi
MOCKEOF

# Patch placeholders with actual paths (avoid sed on source files,
# but this is a generated mock — not repo source code).
local escaped_log="${gh_log//\//\\/}"
local escaped_out="${TMPDIR//\//\\/}\/pr-list-output.txt"
perl -pi -e "s/LOGFILE_PLACEHOLDER/${escaped_log}/g" "${mock_bin}/gh"
perl -pi -e "s/OUTPUT_PLACEHOLDER/${escaped_out}/g" "${mock_bin}/gh"

chmod +x "${mock_bin}/gh"

echo "${mock_bin}"
}

run_test() {
local test_name="$1"
local pr_list_output="$2"
local expected_pattern="$3"
local expect_exit="$4" # 0 = success, 1 = failure
local extra_env="${5:-}" # additional env vars (KEY=VAL KEY2=VAL2)

local mock_bin
mock_bin="$(build_mock "${pr_list_output}")"
local gh_log="${TMPDIR}/gh-calls.log"

# Set base env vars for the script.
local env_cmd=(
env
PATH="${mock_bin}:${PATH}"
ISSUE_NUMBER="42"
REPO_FULL_NAME="test-org/test-repo"
GITHUB_ISSUE_URL="https://github.com/test-org/test-repo/issues/42"
GH_TOKEN="fake-token"
)

# Add extra env vars if provided.
if [[ -n "${extra_env}" ]]; then
for kv in ${extra_env}; do
env_cmd+=("${kv}")
done
fi

local exit_code=0
"${env_cmd[@]}" bash "${PRE_SCRIPT}" > "${TMPDIR}/stdout.log" 2>&1 || exit_code=$?

# Check exit code.
if [[ ${exit_code} -ne ${expect_exit} ]]; then
echo "FAIL: ${test_name} — expected exit ${expect_exit}, got ${exit_code}"
cat "${TMPDIR}/stdout.log"
FAILURES=$((FAILURES + 1))
return
fi

# Check expected pattern in gh calls (if provided).
if [[ -n "${expected_pattern}" ]]; then
if ! grep -qF "${expected_pattern}" "${gh_log}" 2>/dev/null; then
echo "FAIL: ${test_name} — expected gh call pattern '${expected_pattern}' not found"
echo "Actual calls:"
cat "${gh_log}" 2>/dev/null || echo "(no calls)"
FAILURES=$((FAILURES + 1))
return
fi
fi

echo "PASS: ${test_name}"
}

# Check stdout contains a specific string.
run_test_stdout() {
local test_name="$1"
local pr_list_output="$2"
local expected_stdout="$3"
local expect_exit="$4"
local extra_env="${5:-}"

local mock_bin
mock_bin="$(build_mock "${pr_list_output}")"

local env_cmd=(
env
PATH="${mock_bin}:${PATH}"
ISSUE_NUMBER="42"
REPO_FULL_NAME="test-org/test-repo"
GITHUB_ISSUE_URL="https://github.com/test-org/test-repo/issues/42"
GH_TOKEN="fake-token"
)

if [[ -n "${extra_env}" ]]; then
for kv in ${extra_env}; do
env_cmd+=("${kv}")
done
fi

local exit_code=0
"${env_cmd[@]}" bash "${PRE_SCRIPT}" > "${TMPDIR}/stdout.log" 2>&1 || exit_code=$?

if [[ ${exit_code} -ne ${expect_exit} ]]; then
echo "FAIL: ${test_name} — expected exit ${expect_exit}, got ${exit_code}"
cat "${TMPDIR}/stdout.log"
FAILURES=$((FAILURES + 1))
return
fi

if ! grep -qF "${expected_stdout}" "${TMPDIR}/stdout.log" 2>/dev/null; then
echo "FAIL: ${test_name} — expected stdout '${expected_stdout}' not found"
echo "Actual stdout:"
cat "${TMPDIR}/stdout.log"
FAILURES=$((FAILURES + 1))
return
fi

echo "PASS: ${test_name}"
}

# --- Test cases ---

# Tab character for readability.
TAB=$'\t'

# No existing PRs → agent proceeds (exit 0, no label/comment).
run_test_stdout "no-existing-prs-proceeds" \
"" \
"No existing human PRs found" \
0

# Human PR exists → should apply label and comment, then exit 0.
run_test "human-pr-applies-label" \
"99${TAB}human-dev${TAB}https://github.com/test-org/test-repo/pull/99" \
"gh api repos/test-org/test-repo/issues/42/labels -f labels[]=pr-open --silent" \
0

run_test "human-pr-posts-comment" \
"99${TAB}human-dev${TAB}https://github.com/test-org/test-repo/pull/99" \
"gh issue comment 42 --repo test-org/test-repo --body-file -" \
0

run_test_stdout "human-pr-skips-agent" \
"99${TAB}human-dev${TAB}https://github.com/test-org/test-repo/pull/99" \
"Skipping code agent" \
0

# Bot PR only → gh --jq filters it out, so pr list returns empty → proceeds.
run_test_stdout "bot-pr-does-not-block" \
"" \
"No existing human PRs found" \
0

# CODE_FORCE=true → should skip check even with human PR.
run_test_stdout "force-override-skips-check" \
"99${TAB}human-dev${TAB}https://github.com/test-org/test-repo/pull/99" \
"CODE_FORCE=true" \
0 \
"CODE_FORCE=true"

# No GH_TOKEN → skips check entirely, exits 0.
run_test_stdout "no-gh-token-skips-check" \
"" \
"GH_TOKEN not set" \
0 \
"GH_TOKEN="

# Multiple human PRs → should block and apply label.
run_test "multiple-human-prs-block" \
"50${TAB}dev-a${TAB}https://github.com/test-org/test-repo/pull/50
51${TAB}dev-b${TAB}https://github.com/test-org/test-repo/pull/51" \
"gh api repos/test-org/test-repo/issues/42/labels -f labels[]=pr-open --silent" \
0

run_test_stdout "multiple-human-prs-notice" \
"50${TAB}dev-a${TAB}https://github.com/test-org/test-repo/pull/50
51${TAB}dev-b${TAB}https://github.com/test-org/test-repo/pull/51" \
"Found existing human PR #50 by @dev-a" \
0

# PR label gets created.
run_test "pr-label-created" \
"99${TAB}human-dev${TAB}https://github.com/test-org/test-repo/pull/99" \
"gh label create pr-open --repo test-org/test-repo" \
0

# --- Summary ---

echo ""
if [[ ${FAILURES} -gt 0 ]]; then
echo "${FAILURES} test(s) failed"
exit 1
fi
echo "All tests passed"
65 changes: 65 additions & 0 deletions internal/scaffold/fullsend-repo/scripts/pre-code.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,68 @@ echo "Input validation passed:"
echo " ISSUE_NUMBER=${ISSUE_NUMBER}"
echo " REPO_FULL_NAME=${REPO_FULL_NAME}"
echo " GITHUB_ISSUE_URL=${GITHUB_ISSUE_URL}"

# ---------------------------------------------------------------------------
# Check for existing human PRs linked to this issue
# ---------------------------------------------------------------------------
# Skip if GH_TOKEN is not available (best-effort check).
if [[ -z "${GH_TOKEN:-}" ]]; then
echo "GH_TOKEN not set — skipping existing-PR check"
exit 0
fi

# Allow override via CODE_FORCE (set when /code --force is used).
if [[ "${CODE_FORCE:-}" == "true" ]]; then
echo "CODE_FORCE=true — skipping existing-PR check"
exit 0
fi

BOT_LOGIN="${FULLSEND_BOT_LOGIN:-fullsend-ai[bot]}"

echo "Checking for existing open PRs linked to issue #${ISSUE_NUMBER}..."

# Search for open PRs in the repo that mention the issue number.
# This catches PRs with "Closes #N", "Fixes #N", or "#N" in the body/title.
# Use gh's built-in --jq to filter out bot-authored PRs in one call.
HUMAN_PR_LINES="$(gh pr list --repo "${REPO_FULL_NAME}" --state open \
--search "${ISSUE_NUMBER} in:body,title" \
--json number,url,author \
--jq "[.[] | select(.author.login != \"${BOT_LOGIN}\")] | .[] | \"\(.number)\t\(.author.login)\t\(.url)\"" \
2>/dev/null || true)"

if [[ -n "${HUMAN_PR_LINES}" ]]; then
# Parse the first PR for the notice.
FIRST_PR_NUM="$(echo "${HUMAN_PR_LINES}" | head -1 | cut -f1)"
FIRST_PR_AUTHOR="$(echo "${HUMAN_PR_LINES}" | head -1 | cut -f2)"

echo "::notice::Found existing human PR #${FIRST_PR_NUM} by @${FIRST_PR_AUTHOR}"

# Apply pr-open label to signal work is already underway.
gh label create "pr-open" --repo "${REPO_FULL_NAME}" \
--description "An open PR already addresses this issue" --color "D4C5F9" \
--force 2>/dev/null || true
gh api "repos/${REPO_FULL_NAME}/issues/${ISSUE_NUMBER}/labels" \
-f "labels[]=pr-open" --silent 2>/dev/null || true

# Build a markdown list of existing PRs.
PR_LIST_MD=""
while IFS=$'\t' read -r pr_num pr_author pr_url; do
PR_LIST_MD="${PR_LIST_MD}
- #${pr_num} by @${pr_author}"
done <<< "${HUMAN_PR_LINES}"

COMMENT_BODY="An open PR already addresses this issue — skipping automated implementation.
${PR_LIST_MD}

To override, comment \`/code --force\` on this issue.

<sub>Posted by <a href=\"https://github.com/fullsend-ai/fullsend\">fullsend</a> pre-code check</sub>"

printf '%s' "${COMMENT_BODY}" | gh issue comment "${ISSUE_NUMBER}" \
--repo "${REPO_FULL_NAME}" --body-file - 2>/dev/null || true

echo "Skipping code agent — existing PR(s) found for issue #${ISSUE_NUMBER}"
exit 0
fi

echo "No existing human PRs found — proceeding with code agent"
Loading