From 43edc3d2e569ea0cfa917fda176f506f729f1893 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 29 Oct 2026 12:52:54 +0000 Subject: [PATCH 1/3] test(web): reproduce #26950, host text search drops locally matched pull requests These tests reproduce #26950: once a searching host answers, rows from the text-free list that match the typed text locally vanish from the list. Co-Authored-By: Claude Opus 5.5 --- .../pullRequestListGroups.logic.test.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts diff --git a/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts b/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts new file mode 100644 index 000000000000..f6237fb04a18 --- /dev/null +++ b/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts @@ -0,0 +1,76 @@ +import { SourceControlProviderKind } from "@t3tools/contracts"; +import type { PullRequestListEntry, PullRequestListResult } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import type { EnvironmentPullRequestEntry } from "./pullRequestList.logic"; +import { pullRequestGroups } from "./pullRequestListGroups.logic"; + +const VIEWERS = { "github.com": "Bilal" } as const; + +const GITHUB: PullRequestListResult["providers"][number] = { + host: "github.com", + kind: SourceControlProviderKind.make("github"), + searchesOnHost: true, + projectCount: 1, + configured: true, + detail: null, +}; + +function entry( + overrides: Partial & Pick, +) { + return { + environmentId: "env-1", + provider: SourceControlProviderKind.make("github"), + host: "github.com", + projectId: "project-1", + projectTitle: "t3code", + repository: "pingdotgg/t3code", + title: "Add the pull requests page", + url: `https://github.com/pingdotgg/t3code/pull/${overrides.number}`, + author: { login: "octocat", name: null, avatarUrl: null }, + headBranch: `feat/branch-${overrides.number}`, + baseBranch: "main", + state: "open", + isDraft: false, + mergeability: "mergeable", + additions: 1, + deletions: 0, + createdAt: "2026-07-01T00:00:00Z", + updatedAt: "2026-07-02T00:00:00Z", + viewerReviewRequested: false, + labels: [], + ...overrides, + } as EnvironmentPullRequestEntry; +} + +describe("pullRequestGroups text search", () => { + it("keeps loaded rows that match the text locally when the host search misses them", () => { + const toast = entry({ number: 26895, title: "Threads show a completed toast" }); + const toastFixture = entry({ number: 26897, title: "Toast fixtures cover retries" }); + const cursor = entry({ number: 26908, title: "Cursor no longer adds attribution" }); + const hostOnly = entry({ number: 26777, title: "Notifications match the body text" }); + + const { entries } = pullRequestGroups({ + // GitHub matches whole tokens, so its answer for "toa" omits the "Toast fixtures" row. + known: [toast, hostOnly], + baseline: [toast, toastFixture, cursor], + providers: [GITHUB], + viewers: VIEWERS, + involvement: "all", + localFilters: {}, + hasLocalFilters: false, + text: "toa", + answeredLocally: true, + baselineEmpty: false, + held: undefined, + authored: undefined, + reviewing: undefined, + partitionsWanted: false, + }); + + expect(entries.map((row) => row.number).toSorted((left, right) => left - right)).toEqual([ + 26777, 26895, 26897, + ]); + }); +}); From 5c1c8b0004ee52542418bfc9f22c3a6f07f57b56 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 29 Oct 2026 12:59:06 +0000 Subject: [PATCH 2/3] fix(web): host text search no longer drops pull requests the local filter matches GitHub's search matches whole words, so a partial word like `toa` came back empty and replaced the rows the local filter had just shown. The list is now the host's ranked answer followed by the loaded text-free rows that match the text locally and pass the same involvement and typed-filter narrowing. Fixes pingdotgg/t3code#26950 Co-Authored-By: Claude Opus 5.5 --- .../pullRequest/PullRequestsPage.tsx | 2 ++ .../pullRequestListGroups.logic.test.ts | 33 +++++++++++++++++++ .../pullRequestListGroups.logic.ts | 27 +++++++++++---- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestsPage.tsx b/apps/web/src/components/pullRequest/PullRequestsPage.tsx index 359d86b4fc76..0b5625c2f141 100644 --- a/apps/web/src/components/pullRequest/PullRequestsPage.tsx +++ b/apps/web/src/components/pullRequest/PullRequestsPage.tsx @@ -685,6 +685,8 @@ export function PullRequestsPage() { heldPullRequestsBySurface, } = pullRequestGroups({ known: ordered?.key === filterKey ? ordered.entries : (listData?.entries ?? []), + // The baseline was read without typed qualifiers, and rows cannot be checked for `checks:`. + baseline: typedParsed.filters.checks === undefined ? baselineQuery.data?.entries : undefined, providers: baselineQuery.data?.providers ?? listData?.providers ?? [], viewers, involvement: search.involvement, diff --git a/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts b/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts index f6237fb04a18..89ff953d0a79 100644 --- a/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts +++ b/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts @@ -73,4 +73,37 @@ describe("pullRequestGroups text search", () => { 26777, 26895, 26897, ]); }); + + it("lists loaded matches after the host's answer, narrowed by the typed filters", () => { + const hosted = entry({ + number: 1, + title: "Toast", + author: { login: "bilal", name: null, avatarUrl: null }, + }); + const mine = entry({ + number: 2, + title: "Toaster", + author: { login: "bilal", name: null, avatarUrl: null }, + }); + const theirs = entry({ number: 3, title: "Toasted" }); + + const { entries } = pullRequestGroups({ + known: [hosted], + baseline: [theirs, mine, hosted], + providers: [GITHUB], + viewers: VIEWERS, + involvement: "all", + localFilters: { author: "bilal" }, + hasLocalFilters: true, + text: "toa", + answeredLocally: true, + baselineEmpty: false, + held: undefined, + authored: undefined, + reviewing: undefined, + partitionsWanted: false, + }); + + expect(entries.map((row) => row.number)).toEqual([1, 2]); + }); }); diff --git a/apps/web/src/components/pullRequest/pullRequestListGroups.logic.ts b/apps/web/src/components/pullRequest/pullRequestListGroups.logic.ts index 0ea592ba008d..a403c1a7ccd0 100644 --- a/apps/web/src/components/pullRequest/pullRequestListGroups.logic.ts +++ b/apps/web/src/components/pullRequest/pullRequestListGroups.logic.ts @@ -14,6 +14,7 @@ import { partitionPullRequestsWithPriority, pullRequestEntryKey, pullRequestEntryViewer, + rankPullRequestMatches, type EnvironmentPullRequestEntry, type PullRequestPartitionsSnapshot, } from "./pullRequestList.logic"; @@ -24,6 +25,7 @@ export const pullRequestListEntryId = (target: Parameters; + /** The text-free rows already loaded, or undefined when the typed filters cannot vet them. */ + baseline: ReadonlyArray | undefined; providers: PullRequestListResult["providers"]; viewers: PullRequestListResult["viewers"]; involvement: PullRequestInvolvement; @@ -55,24 +59,35 @@ export function pullRequestGroups({ providers.flatMap((provider) => (provider.searchesOnHost ? [provider.host] : [])), ); - const entries = (() => { - const involvementEntries = filterPullRequestsByInvolvement(known, viewers, involvement); - // Host text searches cover fields absent from rows, so do not narrow their matches locally. - // Filter text locally while results are pending or when the host has no text search. - const narrowedEntries = hasLocalFilters + const narrowRows = (rows: ReadonlyArray) => { + const involvementEntries = filterPullRequestsByInvolvement(rows, viewers, involvement); + return hasLocalFilters ? involvementEntries.filter((entry) => matchesPullRequestFilters(entry, localFilters, pullRequestEntryViewer(entry, viewers)), ) : involvementEntries; + }; + + const entries = (() => { + const narrowedEntries = narrowRows(known); if (text.length === 0) { return narrowedEntries.toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt), ); } - return narrowedEntries.filter( + // Host text searches cover fields absent from rows, so do not narrow their matches locally. + // Filter text locally while results are pending or when the host has no text search. + const matched = narrowedEntries.filter( (entry) => (answeredLocally && searchingHosts.has(entry.host)) || matchesPullRequestQuery(entry, text), ); + // Host search matches whole words, so it can miss loaded rows a partial word matches here. + // Those follow the host's ranked answer rather than reshuffling it. + const listed = new Set(matched.map(pullRequestEntryKey)); + const missed = narrowRows(baseline ?? []).filter( + (entry) => !listed.has(pullRequestEntryKey(entry)) && matchesPullRequestQuery(entry, text), + ); + return missed.length === 0 ? matched : [...matched, ...rankPullRequestMatches(missed, text)]; })(); const listedPullRequestTabStatuses: Record = Object.fromEntries( From 408dc208561cfc7cdbd5b466ba96d1d500f61917 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 00:32:54 +0000 Subject: [PATCH 3/3] test(web): fold the partial-word search case into the list logic tests AI-assisted; verified with the tests and tools listed above. --- .../pullRequest/pullRequestList.logic.test.ts | 38 ++++++ .../pullRequestListGroups.logic.test.ts | 109 ------------------ 2 files changed, 38 insertions(+), 109 deletions(-) delete mode 100644 apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts diff --git a/apps/web/src/components/pullRequest/pullRequestList.logic.test.ts b/apps/web/src/components/pullRequest/pullRequestList.logic.test.ts index 123111ff6a35..fd52cfe7ede4 100644 --- a/apps/web/src/components/pullRequest/pullRequestList.logic.test.ts +++ b/apps/web/src/components/pullRequest/pullRequestList.logic.test.ts @@ -38,6 +38,7 @@ import { reusePullRequestEntries, settlePullRequestOverrides, } from "./pullRequestList.logic"; +import { pullRequestGroups } from "./pullRequestListGroups.logic"; import { readPullRequestListPreferences, writePullRequestListPreferences, @@ -177,6 +178,43 @@ describe("ranking what a search found", () => { rankPullRequestMatches([mentioned, titled], "welcome wizard").map((entry) => entry.number), ).toEqual([1, 2]); }); + + it("keeps loaded partial-word matches the host's whole-word search missed, after its answer", () => { + const bilal = { login: "bilal", name: null, avatarUrl: null }; + const hosted = row({ number: 1, title: "Notifications match the body text", author: bilal }); + const toast = row({ number: 2, title: "Threads show a completed toast", author: bilal }); + const toaster = row({ number: 3, title: "Toaster fixtures cover retries", author: bilal }); + const theirs = row({ number: 4, title: "Toasted sidebar" }); + const unrelated = row({ number: 5, title: "Cursor no longer adds attribution", author: bilal }); + + const { entries } = pullRequestGroups({ + known: [hosted, toast], + baseline: [theirs, unrelated, toaster, toast], + providers: [ + { + host: "github.com", + kind: SourceControlProviderKind.make("github"), + searchesOnHost: true, + projectCount: 1, + configured: true, + detail: null, + }, + ], + viewers: VIEWERS, + involvement: "all", + localFilters: { author: "bilal" }, + hasLocalFilters: true, + text: "toa", + answeredLocally: true, + baselineEmpty: false, + held: undefined, + authored: undefined, + reviewing: undefined, + partitionsWanted: false, + }); + + expect(entries.map((entry) => entry.number)).toEqual([1, 2, 3]); + }); }); describe("default merge-readiness ranking", () => { diff --git a/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts b/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts deleted file mode 100644 index 89ff953d0a79..000000000000 --- a/apps/web/src/components/pullRequest/pullRequestListGroups.logic.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { SourceControlProviderKind } from "@t3tools/contracts"; -import type { PullRequestListEntry, PullRequestListResult } from "@t3tools/contracts"; -import { describe, expect, it } from "vite-plus/test"; - -import type { EnvironmentPullRequestEntry } from "./pullRequestList.logic"; -import { pullRequestGroups } from "./pullRequestListGroups.logic"; - -const VIEWERS = { "github.com": "Bilal" } as const; - -const GITHUB: PullRequestListResult["providers"][number] = { - host: "github.com", - kind: SourceControlProviderKind.make("github"), - searchesOnHost: true, - projectCount: 1, - configured: true, - detail: null, -}; - -function entry( - overrides: Partial & Pick, -) { - return { - environmentId: "env-1", - provider: SourceControlProviderKind.make("github"), - host: "github.com", - projectId: "project-1", - projectTitle: "t3code", - repository: "pingdotgg/t3code", - title: "Add the pull requests page", - url: `https://github.com/pingdotgg/t3code/pull/${overrides.number}`, - author: { login: "octocat", name: null, avatarUrl: null }, - headBranch: `feat/branch-${overrides.number}`, - baseBranch: "main", - state: "open", - isDraft: false, - mergeability: "mergeable", - additions: 1, - deletions: 0, - createdAt: "2026-07-01T00:00:00Z", - updatedAt: "2026-07-02T00:00:00Z", - viewerReviewRequested: false, - labels: [], - ...overrides, - } as EnvironmentPullRequestEntry; -} - -describe("pullRequestGroups text search", () => { - it("keeps loaded rows that match the text locally when the host search misses them", () => { - const toast = entry({ number: 26895, title: "Threads show a completed toast" }); - const toastFixture = entry({ number: 26897, title: "Toast fixtures cover retries" }); - const cursor = entry({ number: 26908, title: "Cursor no longer adds attribution" }); - const hostOnly = entry({ number: 26777, title: "Notifications match the body text" }); - - const { entries } = pullRequestGroups({ - // GitHub matches whole tokens, so its answer for "toa" omits the "Toast fixtures" row. - known: [toast, hostOnly], - baseline: [toast, toastFixture, cursor], - providers: [GITHUB], - viewers: VIEWERS, - involvement: "all", - localFilters: {}, - hasLocalFilters: false, - text: "toa", - answeredLocally: true, - baselineEmpty: false, - held: undefined, - authored: undefined, - reviewing: undefined, - partitionsWanted: false, - }); - - expect(entries.map((row) => row.number).toSorted((left, right) => left - right)).toEqual([ - 26777, 26895, 26897, - ]); - }); - - it("lists loaded matches after the host's answer, narrowed by the typed filters", () => { - const hosted = entry({ - number: 1, - title: "Toast", - author: { login: "bilal", name: null, avatarUrl: null }, - }); - const mine = entry({ - number: 2, - title: "Toaster", - author: { login: "bilal", name: null, avatarUrl: null }, - }); - const theirs = entry({ number: 3, title: "Toasted" }); - - const { entries } = pullRequestGroups({ - known: [hosted], - baseline: [theirs, mine, hosted], - providers: [GITHUB], - viewers: VIEWERS, - involvement: "all", - localFilters: { author: "bilal" }, - hasLocalFilters: true, - text: "toa", - answeredLocally: true, - baselineEmpty: false, - held: undefined, - authored: undefined, - reviewing: undefined, - partitionsWanted: false, - }); - - expect(entries.map((row) => row.number)).toEqual([1, 2]); - }); -});