From a907e01daa2e9f57fb372b4efa3ef910f9addbd5 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 15:46:48 +1100 Subject: [PATCH] fix: validate parsed PR urls on session completion --- .../branches/branchCardHelpers.test.ts | 43 +++++++++++++++++++ .../features/branches/branchCardHelpers.ts | 24 ++++++++--- 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 apps/staged/src/lib/features/branches/branchCardHelpers.test.ts diff --git a/apps/staged/src/lib/features/branches/branchCardHelpers.test.ts b/apps/staged/src/lib/features/branches/branchCardHelpers.test.ts new file mode 100644 index 00000000..4e1357ae --- /dev/null +++ b/apps/staged/src/lib/features/branches/branchCardHelpers.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { extractPrNumber, extractPrUrl } from './branchCardHelpers'; + +describe('extractPrUrl', () => { + it('returns the canonical GitHub PR URL from a PR_URL marker', () => { + expect( + extractPrUrl([ + { + role: 'assistant', + content: 'PR_URL: https://github.com/block/builderbot/pull/123?expand=1', + }, + ]) + ).toBe('https://github.com/block/builderbot/pull/123'); + }); + + it('ignores placeholder PR_URL markers that do not contain a real PR number', () => { + expect( + extractPrUrl([ + { + role: 'assistant', + content: 'Return the required line exactly like this: PR_URL: https://github.com/...', + }, + ]) + ).toBeNull(); + }); + + it('strips surrounding markdown punctuation from fallback URL matches', () => { + expect( + extractPrUrl([ + { + role: 'assistant', + content: 'Created it successfully: .', + }, + ]) + ).toBe('https://github.com/block/builderbot/pull/456'); + }); +}); + +describe('extractPrNumber', () => { + it('reads the PR number from a canonical URL', () => { + expect(extractPrNumber('https://github.com/block/builderbot/pull/456')).toBe(456); + }); +}); diff --git a/apps/staged/src/lib/features/branches/branchCardHelpers.ts b/apps/staged/src/lib/features/branches/branchCardHelpers.ts index deed9276..d10c20c1 100644 --- a/apps/staged/src/lib/features/branches/branchCardHelpers.ts +++ b/apps/staged/src/lib/features/branches/branchCardHelpers.ts @@ -76,19 +76,33 @@ export function formatBaseBranch(baseBranch: string): string { return baseBranch.replace(/^origin\//, ''); } +function normalizePrUrl(candidate: string): string | null { + const trimmed = candidate.trim().replace(/^[<`'"\[(]+|[>`'"\]),.?!;:]+$/g, ''); + const match = trimmed.match( + /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)(?:[/?#].*)?$/ + ); + + if (!match) return null; + + return `https://github.com/${match[1]}/${match[2]}/pull/${match[3]}`; +} + export function extractPrUrl(messages: { content: string; role: string }[]): string | null { for (const msg of messages) { if (msg.role !== 'assistant' && msg.role !== 'tool_result') continue; - const markerMatch = msg.content.match(/PR_URL:\s*(https?:\/\/\S+)/); + const markerMatch = msg.content.match(/PR_URL:\s*(\S+)/); if (markerMatch) { - // Strip trailing markdown characters (*, ), ], etc.) from the URL - return markerMatch[1].replace(/[\*\)\]]+$/, ''); + const normalized = normalizePrUrl(markerMatch[1]); + if (normalized) return normalized; } } for (const msg of messages) { - const ghMatch = msg.content.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/); - if (ghMatch) return ghMatch[0]; + const urlMatches = msg.content.match(/https?:\/\/\S+/g) ?? []; + for (const url of urlMatches) { + const normalized = normalizePrUrl(url); + if (normalized) return normalized; + } } return null;