From bc301de7be8c3246e8e65507cf2e889d7e9fc0ce Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:43:14 -0700 Subject: [PATCH] fix(worktree-palette): guard undefined review title in matcher (crash c5d87873) (#10024) Co-authored-by: Orca --- .../lib/worktree-palette-review-match.test.ts | 102 ++++++++++++++++++ .../src/lib/worktree-palette-review-match.ts | 9 +- 2 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 src/renderer/src/lib/worktree-palette-review-match.test.ts diff --git a/src/renderer/src/lib/worktree-palette-review-match.test.ts b/src/renderer/src/lib/worktree-palette-review-match.test.ts new file mode 100644 index 000000000..c72710d29 --- /dev/null +++ b/src/renderer/src/lib/worktree-palette-review-match.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest' +import { matchWorktreePaletteReview } from './worktree-palette-review-match' +import { searchWorktrees } from './worktree-palette-search' +import type { Repo, Worktree } from '../../../shared/types' +import type { HostedReviewInfo } from '../../../shared/hosted-review' + +// Regression tests for the production crash (report c5d87873, macOS, Orca 1.4.147): +// TypeError: Cannot read properties of undefined (reading 'toLowerCase') +// at matchWorktreePaletteReview -> searchWorktrees -> WorktreeJumpPalette useMemo +// A rehydrated review/PR cache entry can carry an undefined `title` even though the +// type declares it non-optional, so the Cmd+J worktree palette crashed on any text query. + +type MatcherReview = Parameters[0] + +function makeWorktree(overrides: Partial = {}): Worktree { + return { + id: 'wt-1', + repoId: 'repo-1', + path: '/tmp/wt-1', + head: 'abc123', + branch: 'refs/heads/feature/worktree-jump', + isBare: false, + isMainWorktree: false, + displayName: 'Jump Palette', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + ...overrides + } +} + +const repoMap = new Map([ + [ + 'repo-1', + { + id: 'repo-1', + path: '/repo/orca', + displayName: 'stablyai/orca', + badgeColor: '#22c55e', + addedAt: 0 + } + ] +]) + +describe('matchWorktreePaletteReview title null-safety', () => { + it('returns null instead of throwing when a cached review has no title', () => { + const review = { number: 42, provider: 'github' } as unknown as MatcherReview + expect(() => matchWorktreePaletteReview(review, 'feature', 'feature')).not.toThrow() + expect(matchWorktreePaletteReview(review, 'feature', 'feature')).toBeNull() + }) + + it('still matches on the title text when a title is present', () => { + const review = { + number: 42, + title: 'Fix the thing', + provider: 'github' + } as unknown as MatcherReview + const match = matchWorktreePaletteReview(review, 'thing', 'thing') + expect(match).not.toBeNull() + expect(match?.text).toBe('Fix the thing') + expect(match?.matchRange).toEqual({ start: 8, end: 13 }) + }) + + it('still matches on the PR number even when the title is missing', () => { + const review = { number: 42, provider: 'github' } as unknown as MatcherReview + const match = matchWorktreePaletteReview(review, '42', '42') + expect(match?.text).toBe('PR #42') + }) +}) + +describe('searchWorktrees with a titleless cached review (Cmd+J palette crash path)', () => { + it('does not throw when the checks-review cache entry has no title', () => { + const worktree = makeWorktree() + const titlelessReview = { + number: 7, + provider: 'github', + state: 'open', + url: 'https://example.test/pr/7' + } as unknown as HostedReviewInfo + const checksReviewByWorktree = new Map([ + [worktree, titlelessReview] + ]) + + expect(() => + searchWorktrees( + [worktree], + 'nonmatchingtext', + repoMap, + null, + null, + undefined, + checksReviewByWorktree + ) + ).not.toThrow() + }) +}) diff --git a/src/renderer/src/lib/worktree-palette-review-match.ts b/src/renderer/src/lib/worktree-palette-review-match.ts index 9945722a3..726ec1a64 100644 --- a/src/renderer/src/lib/worktree-palette-review-match.ts +++ b/src/renderer/src/lib/worktree-palette-review-match.ts @@ -1,6 +1,7 @@ import type { HostedReviewInfo } from '../../../shared/hosted-review' -type SearchableReview = Pick +// title is intentionally optional: rehydrated review/PR caches can hold entries without one. +type SearchableReview = Pick & { title?: string } type WorktreePaletteReviewMatch = { labelKind: 'pr' | 'mr' text: string @@ -35,13 +36,15 @@ export function matchWorktreePaletteReview( } } - const titleIndex = review.title.toLowerCase().indexOf(query) + // Null-safe: a cached review may have no title, so fall back to '' (query is non-empty, so it won't match). + const title = review.title ?? '' + const titleIndex = title.toLowerCase().indexOf(query) if (titleIndex === -1) { return null } return { labelKind: isMergeRequest ? 'mr' : 'pr', - text: review.title, + text: title, matchRange: { start: titleIndex, end: titleIndex + query.length } } }