From a71bf2d4925501fa3577ce203f0e561f3924b28e Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Mon, 9 Mar 2026 15:56:09 -0700 Subject: [PATCH] Make webhook tests independent with isolated PRs and retry on merge timing issues Each test now creates its own PR and cleans up after itself. Tests no longer depend on shared state or ordering, making them resilient to failures. Added retry logic to mergePR for Gitea's 404/405 responses while computing mergeability. Used crypto.randomUUID() for unique branch names instead of Date.now() to eliminate collision risk. --- packages/spike/src/gitea/test/helpers.ts | 28 ++++++++++++--- .../spike/src/gitea/test/webhooks.test.ts | 35 ++++++++++++------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/packages/spike/src/gitea/test/helpers.ts b/packages/spike/src/gitea/test/helpers.ts index 44805fa..098fbb0 100644 --- a/packages/spike/src/gitea/test/helpers.ts +++ b/packages/spike/src/gitea/test/helpers.ts @@ -228,10 +228,30 @@ export async function commentOnPR(repo: string, prNumber: number, body: string, } export async function mergePR(repo: string, prNumber: number, user: User) { - return giteaFetch(`/repos/${repo}/pulls/${prNumber}/merge`, user, { - method: "POST", - body: JSON.stringify({ Do: "merge", merge_message_field: "Test merge" }), - }) + // Gitea returns 404/405 while computing mergeability after PR creation + const path = `/repos/${repo}/pulls/${prNumber}/merge` + + for (let attempt = 0; attempt < 10; attempt++) { + const response = await fetch(`${giteaUrl}/api/v1${path}`, { + method: "POST", + headers: { + Authorization: `token ${user.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ Do: "merge" }), + }) + + if (response.ok) return + if (response.status === 404 || response.status === 405) { + await Bun.sleep(1000) + continue + } + + const body = await response.text().catch(() => "") + throw new Error(`Gitea ${response.status}: POST ${path}\n${body}`) + } + + throw new Error(`Gitea merge failed after 10 retries for ${path}`) } export async function createReview(repo: string, prNumber: number, user: User, filePath: string) { diff --git a/packages/spike/src/gitea/test/webhooks.test.ts b/packages/spike/src/gitea/test/webhooks.test.ts index 419c4ec..6d31975 100644 --- a/packages/spike/src/gitea/test/webhooks.test.ts +++ b/packages/spike/src/gitea/test/webhooks.test.ts @@ -4,7 +4,6 @@ setDefaultTimeout(30_000) import { ensure } from "@workshop/shared/utils" import { type User, - type TestPR, setupWebhooks, expectPullRequestWebhook, expectIssueCommentWebhook, @@ -15,6 +14,10 @@ import { mergePR, } from "./helpers" +function testBranch(label: string) { + return `test-${crypto.randomUUID().slice(0, 8)}-${label}` +} + function getEnv(name: string): string { const value = process.env[name] ensure(value, `Missing env var: ${name}. See .env.example`) @@ -37,20 +40,19 @@ afterAll(() => teardown()) // --- Tests for Corey's repo (Spike opens PR, Corey comments/reviews/merges) --- describe(`${coreyRepo}`, () => { - let pr: TestPR - const branch = `test-${Date.now()}-corey` - test("opening a PR sends pull_request webhook with correct author and repo", async () => { - pr = await expectPullRequestWebhook({ action: "opened", branch }, async (webhook) => { - const result = await openTestPR(coreyRepo, spike, branch) + const branch = testBranch("open") + await expectPullRequestWebhook({ action: "opened", branch }, async (webhook) => { + await openTestPR(coreyRepo, spike, branch) const payload = await webhook expect(payload.pull_request.user.login).toBe(spike.username) expect(payload.repository.full_name).toBe(coreyRepo) - return result }) }) test("commenting sends issue_comment webhook with correct user", async () => { + const branch = testBranch("comment") + const pr = await openTestPR(coreyRepo, spike, branch) await expectIssueCommentWebhook({ action: "created", username: corey.username, prNumber: pr.number }, async (webhook) => { await commentOnPR(coreyRepo, pr.number, "Looks good!", corey) const payload = await webhook @@ -59,6 +61,8 @@ describe(`${coreyRepo}`, () => { }) test("review with line comment sends pull_request_comment webhook", async () => { + const branch = testBranch("review") + const pr = await openTestPR(coreyRepo, spike, branch) await expectPullRequestCommentWebhook({ action: "reviewed", branch }, async (webhook) => { await createReview(coreyRepo, pr.number, corey, pr.filename) const payload = await webhook @@ -68,6 +72,8 @@ describe(`${coreyRepo}`, () => { }) test("merging sends pull_request closed webhook", async () => { + const branch = testBranch("merge") + const pr = await openTestPR(coreyRepo, spike, branch) await expectPullRequestWebhook({ action: "closed", branch }, async (webhook) => { await mergePR(coreyRepo, pr.number, corey) const payload = await webhook @@ -79,20 +85,19 @@ describe(`${coreyRepo}`, () => { // --- Tests for Spike's repo (Corey opens PR, Spike comments/reviews/merges) --- describe(`${spikeRepo}`, () => { - let pr: TestPR - const branch = `test-${Date.now()}-spike` - test("opening a PR sends pull_request webhook with correct author and repo", async () => { - pr = await expectPullRequestWebhook({ action: "opened", branch }, async (webhook) => { - const result = await openTestPR(spikeRepo, corey, branch) + const branch = testBranch("open") + await expectPullRequestWebhook({ action: "opened", branch }, async (webhook) => { + await openTestPR(spikeRepo, corey, branch) const payload = await webhook expect(payload.pull_request.user.login).toBe(corey.username) expect(payload.repository.full_name).toBe(spikeRepo) - return result }) }) test("commenting sends issue_comment webhook with correct user", async () => { + const branch = testBranch("comment") + const pr = await openTestPR(spikeRepo, corey, branch) await expectIssueCommentWebhook({ action: "created", username: spike.username, prNumber: pr.number }, async (webhook) => { await commentOnPR(spikeRepo, pr.number, "On it!", spike) const payload = await webhook @@ -101,6 +106,8 @@ describe(`${spikeRepo}`, () => { }) test("review with line comment sends pull_request_comment webhook", async () => { + const branch = testBranch("review") + const pr = await openTestPR(spikeRepo, corey, branch) await expectPullRequestCommentWebhook({ action: "reviewed", branch }, async (webhook) => { await createReview(spikeRepo, pr.number, spike, pr.filename) const payload = await webhook @@ -110,6 +117,8 @@ describe(`${spikeRepo}`, () => { }) test("merging sends pull_request closed webhook", async () => { + const branch = testBranch("merge") + const pr = await openTestPR(spikeRepo, corey, branch) await expectPullRequestWebhook({ action: "closed", branch }, async (webhook) => { await mergePR(spikeRepo, pr.number, spike) const payload = await webhook