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 @@ -53,6 +53,7 @@ script-test:
$(call run-timed,bash scripts/post-review-test.sh)
$(call run-timed,bash scripts/post-fix-test.sh)
$(call run-timed,bash scripts/post-retro-test.sh)
$(call run-timed,bash scripts/pre-scribe-test.sh)
$(call run-timed,bash scripts/post-scribe-test.sh)
$(call run-timed,bash scripts/validate-output-schema-test.sh)
$(call run-timed,bash scripts/gitlint-forbidden-type-scope-test.sh)
Expand Down
334 changes: 334 additions & 0 deletions scripts/pre-scribe-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
#!/usr/bin/env bash
# pre-scribe-test.sh — Test the backlog fetch and metadata logic in
# pre-scribe.sh. Tests the paginated issue fetch pipeline in isolation
# (the full script requires Drive credentials that are unavailable in
# the test environment).
#
# Run from the repo root: bash scripts/pre-scribe-test.sh

set -euo pipefail

FAILURES=0

TMPDIR="$(mktemp -d)"
trap 'rm -rf "${TMPDIR}"' EXIT

MOCK_BIN="${TMPDIR}/bin"
mkdir -p "${MOCK_BIN}"

# --- Mock gh ---
# Simulates `gh api --paginate` for the issues endpoint. Returns fixture
# data from a file, applying the --jq filter via real jq to exercise
# the filter expression end-to-end.
build_mock_gh() {
local fixture_file="$1"

cat > "${MOCK_BIN}/gh" <<'MOCKEOF'
#!/usr/bin/env bash
FIXTURE="FIXTURE_PLACEHOLDER"

if [[ "$1" == "api" ]]; then
shift
JQ_EXPR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--paginate) shift ;;
--jq) JQ_EXPR="$2"; shift 2 ;;
*) shift ;;
esac
done

if [[ -n "${JQ_EXPR}" ]]; then
jq -r "${JQ_EXPR}" "${FIXTURE}"
else

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] test-robustness

build_mock_gh escapes only forward slashes in the fixture path before Perl regex substitution. Characters special to Perl regex (., +, $) in mktemp paths could theoretically cause failures, though in practice Linux mktemp paths are safe.

cat "${FIXTURE}"
fi
exit 0
fi

exit 0
MOCKEOF

local escaped_fixture="${fixture_file//\//\\/}"
perl -pi -e "s/FIXTURE_PLACEHOLDER/${escaped_fixture}/g" "${MOCK_BIN}/gh"
chmod +x "${MOCK_BIN}/gh"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] test-coverage-gap

The run_backlog_fetch function duplicates the gh api/jq pipeline from pre-scribe.sh rather than sourcing the actual script. This is a reasonable trade-off given Drive credential requirements, but future pipeline changes in pre-scribe.sh require a parallel update in the test.

}

# --- Backlog fetch pipeline ---
# Reproduces the exact command chain from pre-scribe.sh so we test the
# real pipeline without requiring Drive credentials.
run_backlog_fetch() {
local scribe_repo="$1"
local backlog_file="$2"

PATH="${MOCK_BIN}:${PATH}" \
gh api --paginate "repos/${scribe_repo}/issues?state=open&per_page=100" \
--jq '.[] | select(.pull_request == null) | {number, title, body, labels, milestone, url: .html_url}' \
| jq -s '[.[] | .body = ((.body // "")[:500] + if ((.body // "") | length) > 500 then "…" else "" end)]' \
> "${backlog_file}"
}

# --- Test helpers ---
assert_eq() {
local test_name="$1" expected="$2" actual="$3"
if [[ "${expected}" != "${actual}" ]]; then
echo "FAIL: ${test_name} — expected '${expected}', got '${actual}'"
FAILURES=$((FAILURES + 1))
return 1
fi
return 0
}

# ===================================================================
# Test 1: All issues included, PRs filtered out
# ===================================================================
test_pagination_filters_prs() {
local test_name="pagination-filters-prs"
local fixture="${TMPDIR}/fixture-${test_name}.json"
local backlog="${TMPDIR}/backlog-${test_name}.json"

# Fixture: 3 issues + 1 pull request (has pull_request field)
cat > "${fixture}" <<'EOF'
[
{"number": 1, "title": "Old bug", "body": "stale issue", "labels": [{"name": "bug"}], "milestone": null, "html_url": "https://github.com/o/r/issues/1"},
{"number": 2, "title": "Feature request", "body": "add dark mode", "labels": [], "milestone": {"title": "v2"}, "html_url": "https://github.com/o/r/issues/2"},
{"number": 3, "title": "A pull request", "body": "PR body", "labels": [], "milestone": null, "html_url": "https://github.com/o/r/pull/3", "pull_request": {"url": "https://api.github.com/repos/o/r/pulls/3"}},
{"number": 4, "title": "Config issue", "body": "assignee config", "labels": [{"name": "enhancement"}], "milestone": null, "html_url": "https://github.com/o/r/issues/4"}
]
EOF

build_mock_gh "${fixture}"
run_backlog_fetch "o/r" "${backlog}"

local count
count=$(jq 'length' "${backlog}")
if ! assert_eq "${test_name}: count" "3" "${count}"; then
echo " backlog: $(cat "${backlog}")"
return
fi

# Verify PR (number 3) is excluded
local has_pr
has_pr=$(jq '[.[] | select(.number == 3)] | length' "${backlog}")
if ! assert_eq "${test_name}: no PR" "0" "${has_pr}"; then return; fi

# Verify issues 1, 2, 4 are present
local has_1 has_2 has_4
has_1=$(jq '[.[] | select(.number == 1)] | length' "${backlog}")
has_2=$(jq '[.[] | select(.number == 2)] | length' "${backlog}")
has_4=$(jq '[.[] | select(.number == 4)] | length' "${backlog}")
if ! assert_eq "${test_name}: issue 1" "1" "${has_1}"; then return; fi
if ! assert_eq "${test_name}: issue 2" "1" "${has_2}"; then return; fi
if ! assert_eq "${test_name}: issue 4" "1" "${has_4}"; then return; fi

# Verify url field is mapped from html_url
local url_1
url_1=$(jq -r '.[] | select(.number == 1) | .url' "${backlog}")
if ! assert_eq "${test_name}: url mapped" "https://github.com/o/r/issues/1" "${url_1}"; then return; fi

echo "PASS: ${test_name}"
}

# ===================================================================
# Test 2: Body truncation at 500 chars
# ===================================================================
test_body_truncation() {
local test_name="body-truncation"
local fixture="${TMPDIR}/fixture-${test_name}.json"
local backlog="${TMPDIR}/backlog-${test_name}.json"

# Generate a body > 500 chars (600 'a' characters)
local long_body
long_body=$(printf 'a%.0s' $(seq 1 600))

jq -n --arg body "${long_body}" \
'[{"number": 10, "title": "Long body issue", "body": $body, "labels": [], "milestone": null, "html_url": "https://github.com/o/r/issues/10"}]' \
> "${fixture}"

build_mock_gh "${fixture}"
run_backlog_fetch "o/r" "${backlog}"

local body_len
body_len=$(jq -r '.[0].body | length' "${backlog}")
# 500 chars + 1 "…" character = 501
if ! assert_eq "${test_name}: truncated length" "501" "${body_len}"; then
echo " actual body length: ${body_len}"
return
fi

# Verify the truncation marker
local ends_with
ends_with=$(jq -r '.[0].body | .[-1:]' "${backlog}")
if ! assert_eq "${test_name}: ends with ellipsis" "…" "${ends_with}"; then return; fi

echo "PASS: ${test_name}"
}

# ===================================================================
# Test 3: Short body is NOT truncated
# ===================================================================
test_short_body_preserved() {
local test_name="short-body-preserved"
local fixture="${TMPDIR}/fixture-${test_name}.json"
local backlog="${TMPDIR}/backlog-${test_name}.json"

cat > "${fixture}" <<'EOF'
[
{"number": 20, "title": "Short issue", "body": "This is a short body.", "labels": [], "milestone": null, "html_url": "https://github.com/o/r/issues/20"}
]
EOF

build_mock_gh "${fixture}"
run_backlog_fetch "o/r" "${backlog}"

local body
body=$(jq -r '.[0].body' "${backlog}")
if ! assert_eq "${test_name}: body preserved" "This is a short body." "${body}"; then return; fi

echo "PASS: ${test_name}"
}

# ===================================================================
# Test 4: Empty result (no open issues)
# ===================================================================
test_empty_issues() {
local test_name="empty-issues"
local fixture="${TMPDIR}/fixture-${test_name}.json"
local backlog="${TMPDIR}/backlog-${test_name}.json"

echo '[]' > "${fixture}"
build_mock_gh "${fixture}"
run_backlog_fetch "o/r" "${backlog}"

local count
count=$(jq 'length' "${backlog}")
if ! assert_eq "${test_name}: empty array" "0" "${count}"; then
echo " backlog: $(cat "${backlog}")"
return
fi

echo "PASS: ${test_name}"
}

# ===================================================================
# Test 5: Null body handled gracefully
# ===================================================================
test_null_body() {
local test_name="null-body"
local fixture="${TMPDIR}/fixture-${test_name}.json"
local backlog="${TMPDIR}/backlog-${test_name}.json"

cat > "${fixture}" <<'EOF'
[
{"number": 30, "title": "No body issue", "body": null, "labels": [], "milestone": null, "html_url": "https://github.com/o/r/issues/30"}
]
EOF

build_mock_gh "${fixture}"
run_backlog_fetch "o/r" "${backlog}"

local body
body=$(jq -r '.[0].body' "${backlog}")
if ! assert_eq "${test_name}: null body becomes empty" "" "${body}"; then return; fi

echo "PASS: ${test_name}"
}

# ===================================================================
# Test 6: Metadata includes open_issue_total and backlog_truncated
# ===================================================================
test_metadata_fields() {
local test_name="metadata-fields"
local meta_file="${TMPDIR}/meta-${test_name}.json"
local issue_count=1879

# Run the same jq command from pre-scribe.sh to generate metadata
jq -n \
--arg cutoff "2026-08-06T09:00:00Z" \
--arg notes_url "https://docs.google.com/document/d/abc" \
--arg repo "mock-org/mock-repo" \
--argjson doc_count 1 \
--argjson issue_count "${issue_count}" \
--argjson closed_count 50 \
--argjson pr_count 10 \
--argjson doc_path_count 25 \
'{
cutoff_date: $cutoff,
notes_url: $notes_url,
repo: $repo,
docs_downloaded: $doc_count,
backlog_issues: $issue_count,
open_issue_total: $issue_count,
backlog_truncated: false,
closed_issues: $closed_count,
open_prs: $pr_count,
repo_docs: $doc_path_count
}' > "${meta_file}"

# Verify new fields exist and have correct values
local total truncated
total=$(jq '.open_issue_total' "${meta_file}")
truncated=$(jq '.backlog_truncated' "${meta_file}")
if ! assert_eq "${test_name}: open_issue_total" "1879" "${total}"; then return; fi
if ! assert_eq "${test_name}: backlog_truncated" "false" "${truncated}"; then return; fi

# Verify existing fields still present
local backlog_issues
backlog_issues=$(jq '.backlog_issues' "${meta_file}")
if ! assert_eq "${test_name}: backlog_issues" "1879" "${backlog_issues}"; then return; fi

echo "PASS: ${test_name}"
}

# ===================================================================
# Test 7: Labels and milestone are preserved from REST API format
# ===================================================================
test_labels_milestone_preserved() {
local test_name="labels-milestone-preserved"
local fixture="${TMPDIR}/fixture-${test_name}.json"
local backlog="${TMPDIR}/backlog-${test_name}.json"

cat > "${fixture}" <<'EOF'
[
{
"number": 40,
"title": "Labeled issue",
"body": "has labels and milestone",
"labels": [{"id": 1, "name": "bug", "color": "d73a4a"}, {"id": 2, "name": "high-priority", "color": "ff0000"}],
"milestone": {"id": 1, "title": "v2.0", "number": 3},
"html_url": "https://github.com/o/r/issues/40"
}
]
EOF

build_mock_gh "${fixture}"
run_backlog_fetch "o/r" "${backlog}"

local label_count label_name milestone_title
label_count=$(jq '.[0].labels | length' "${backlog}")
label_name=$(jq -r '.[0].labels[0].name' "${backlog}")
milestone_title=$(jq -r '.[0].milestone.title' "${backlog}")

if ! assert_eq "${test_name}: label count" "2" "${label_count}"; then return; fi
if ! assert_eq "${test_name}: label name" "bug" "${label_name}"; then return; fi
if ! assert_eq "${test_name}: milestone title" "v2.0" "${milestone_title}"; then return; fi

echo "PASS: ${test_name}"
}

# --- Run tests ---

test_pagination_filters_prs
test_body_truncation
test_short_body_preserved
test_empty_issues
test_null_body
test_metadata_fields
test_labels_milestone_preserved

echo ""
if [[ ${FAILURES} -gt 0 ]]; then
echo "${FAILURES} test(s) failed"
exit 1
fi
echo "All tests passed"
15 changes: 10 additions & 5 deletions scripts/pre-scribe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ CLOSED_ISSUES_FILE="${WORK_DIR}/closed-issues.json"
OPEN_PRS_FILE="${WORK_DIR}/open-prs.json"
REPO_DOCS_FILE="${WORK_DIR}/repo-docs-index.json"

# Open issues with bodies (truncated to 500 chars to keep context lean)
# Open issues with bodies (truncated to 500 chars to keep context lean).
# Use gh api --paginate to fetch ALL open issues, avoiding truncation on
# repos with >1000 open issues (see #705). The REST /issues endpoint
# includes pull requests, so we filter them out with --jq.
echo "Fetching open issues from ${SCRIBE_REPO}..."
gh issue list --repo "${SCRIBE_REPO}" --state open \
--json number,title,body,labels,milestone,url --limit 1000 \
| jq '[.[] | .body = ((.body // "")[:500] + if ((.body // "") | length) > 500 then "…" else "" end)]' \
gh api --paginate "repos/${SCRIBE_REPO}/issues?state=open&per_page=100" \
--jq '.[] | select(.pull_request == null) | {number, title, body, labels, milestone, url: .html_url}' \
| jq -s '[.[] | .body = ((.body // "")[:500] + if ((.body // "") | length) > 500 then "…" else "" end)]' \
> "${BACKLOG_FILE}"
ISSUE_COUNT=$(jq 'length' "${BACKLOG_FILE}")
echo "Fetched ${ISSUE_COUNT} open issues for backlog context."
Expand Down Expand Up @@ -176,7 +179,7 @@ if [[ "${DOC_COUNT}" -eq 0 ]]; then
--argjson closed_count "${CLOSED_COUNT}" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] incomplete-metadata-update

backlog_truncated is hardcoded to false and open_issue_total is set to the same value as backlog_issues. Since --paginate fetches all issues, truncation cannot occur, making these values correct. However, neither field can ever carry a different value — the new fields add no dynamic runtime information beyond what backlog_issues already provides.

--argjson pr_count "${PR_COUNT}" \
--argjson doc_path_count "${DOC_PATH_COUNT}" \
'{cutoff_date: $cutoff, notes_url: "", repo: $repo, docs_downloaded: $doc_count, backlog_issues: $issue_count, closed_issues: $closed_count, open_prs: $pr_count, repo_docs: $doc_path_count}' \
'{cutoff_date: $cutoff, notes_url: "", repo: $repo, docs_downloaded: $doc_count, backlog_issues: $issue_count, open_issue_total: $issue_count, backlog_truncated: false, closed_issues: $closed_count, open_prs: $pr_count, repo_docs: $doc_path_count}' \
> "${META_FILE}"
echo "Workspace: ${WORK_DIR}"
exit 0
Expand Down Expand Up @@ -316,6 +319,8 @@ jq -n \
repo: $repo,
docs_downloaded: $doc_count,
backlog_issues: $issue_count,
open_issue_total: $issue_count,
backlog_truncated: false,
closed_issues: $closed_count,
open_prs: $pr_count,
repo_docs: $doc_path_count
Expand Down
Loading