mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

inbox-search.test.ts

Language TypeScript5.7 KB140 lines

apps/dashboard/test/inbox-search.test.ts

  1. /**
  2. * Live Gmail search.
  3. *
  4. * The 30-day window is several times what the board holds, with the busiest
  5. * mailbox alone in the thousands. Holding all of it is megabytes re-parsed on
  6. * every click, and nobody finds an email by scrolling thousands of rows. So the
  7. * board keeps the recent slice for browsing and lets the search box reach
  8. * everything.
  9. *
  10. * That makes the search the thing standing between "we might be missing an
  11. * important email" and not. What is tested here is the part that decides what a
  12. * typed query is allowed to reach, and the part that refuses to imply a result
  13. * set is complete when it is not.
  14. */
  15. import { describe, expect, it } from 'vitest'
  16. import { SEARCH_QUERY_CAP, buildSearchQuery } from '../../../config/inbox.js'
  17. import { INBOX_MAILBOXES } from '../../../config/inbox.js'
  18. import { SEARCH_RESULT_CAP, searchResultsPage, type SearchHit } from '../src/inbox-view.js'
  19. import type { InboxMessage } from '@meridian/gmail-client'
  20. const msg = (over: Partial<InboxMessage> = {}): InboxMessage => ({
  21. id: 'm1',
  22. threadId: 't1',
  23. mailbox: 'hello@meridiandisplay.example',
  24. sender: 'client@example.com',
  25. senderName: 'A Client',
  26. subject: 'Banner proof',
  27. snippet: 'preview',
  28. date: '2026-09-04T18:00:00.000Z',
  29. labels: ['INBOX'],
  30. unread: false,
  31. ...over,
  32. })
  33. const hit = (over: Partial<SearchHit> = {}): SearchHit => ({
  34. mailbox: 'hello@meridiandisplay.example',
  35. ok: true,
  36. error: null,
  37. messages: [msg()],
  38. truncated: false,
  39. ...over,
  40. })
  41. describe('buildSearchQuery', () => {
  42. it('drops the 30-day window, which is the whole point', () => {
  43. // The footer has always said "older mail is in Gmail, not gone". This is
  44. // the board finally reaching it.
  45. expect(buildSearchQuery('proof')).not.toContain('newer_than')
  46. })
  47. it('forces in:inbox, so a search cannot widen what the board shows', () => {
  48. // Archived mail, Sent, Spam and Trash have never been on this board, and a
  49. // search box is not where that should quietly change.
  50. expect(buildSearchQuery('proof')).toBe('in:inbox proof')
  51. })
  52. it('removes a typed in: or label:, rather than combining with it', () => {
  53. // `in:inbox in:anywhere` is Gmail's syntax for widening, and a reader
  54. // typing it would not know they had. Stripped, not merged.
  55. for (const q of ['in:anywhere proof', 'proof in:trash', 'label:spam proof', 'proof -in:inbox']) {
  56. const built = buildSearchQuery(q)
  57. expect(built.startsWith('in:inbox '), q).toBe(true)
  58. expect(built.slice('in:inbox '.length), q).not.toMatch(/\b(in|label):/)
  59. }
  60. })
  61. it('leaves every other Gmail operator alone, because they are the feature', () => {
  62. // These are the searcher's own words in Gmail's own language, not a second
  63. // query language invented here.
  64. const q = buildSearchQuery('from:jordan@expohall.example has:attachment "feather flag"')
  65. expect(q).toContain('from:jordan@expohall.example')
  66. expect(q).toContain('has:attachment')
  67. expect(q).toContain('"feather flag"')
  68. })
  69. it('caps the length rather than passing an unbounded string to Gmail', () => {
  70. expect(buildSearchQuery('x'.repeat(5_000)).length).toBeLessThanOrEqual(
  71. SEARCH_QUERY_CAP + 'in:inbox '.length,
  72. )
  73. })
  74. it('an empty query still scopes to the inbox rather than matching everything', () => {
  75. expect(buildSearchQuery(' ')).toBe('in:inbox')
  76. })
  77. })
  78. describe('the results page', () => {
  79. it('names a mailbox that could not be searched, and says matches are missing', () => {
  80. // Six results and a named failure, never a short list that looks complete.
  81. const html = searchResultsPage('proof', [
  82. hit(),
  83. hit({ mailbox: 'accounts@meridiandisplay.example', ok: false, error: 'invalid_grant', messages: [] }),
  84. ], INBOX_MAILBOXES, null)
  85. expect(html).toContain('invalid_grant')
  86. expect(html).toContain('could not be searched')
  87. expect(html).toContain('not absent')
  88. })
  89. it('says when more matched than it is showing', () => {
  90. // Rule 5: capped and labelled, never capped and silent.
  91. const html = searchResultsPage('proof', [hit({ truncated: true })], INBOX_MAILBOXES, null)
  92. expect(html).toContain(String(SEARCH_RESULT_CAP))
  93. expect(html).toContain('More matched than are shown')
  94. })
  95. it('distinguishes an empty RESULT from an unloaded one', () => {
  96. // The distinction the whole feature turns on: Gmail was asked and said no,
  97. // which is not the same as the board not holding it.
  98. const html = searchResultsPage('nothing', [hit({ messages: [] })], INBOX_MAILBOXES, null)
  99. expect(html).toContain('empty result rather than an unloaded one')
  100. })
  101. it('states the corpus, both halves of it', () => {
  102. const html = searchResultsPage('proof', [hit()], INBOX_MAILBOXES, null)
  103. expect(html).toContain('past the 30 days')
  104. expect(html).toContain('Sent, Spam and Trash are not searched')
  105. })
  106. it('carries no read, filter or replied controls', () => {
  107. // A row here that silently lacked a replied chip would read as "not
  108. // replied" rather than "not known"; the search reaches past the board's
  109. // own record of what it holds.
  110. const html = searchResultsPage('proof', [hit()], INBOX_MAILBOXES, null)
  111. expect(html).not.toContain('action="/inbox/read"')
  112. expect(html).not.toContain('msg-replied')
  113. expect(html).toContain('not known')
  114. })
  115. it('escapes the query, which is user input rendered back', () => {
  116. const html = searchResultsPage('<img src=x onerror=alert(1)>', [hit()], INBOX_MAILBOXES, null)
  117. expect(html).not.toContain('<img src=x')
  118. expect(html).toContain('&lt;img')
  119. })
  120. it('says which mailbox it searched when scoped to one', () => {
  121. const html = searchResultsPage('proof', [hit()], INBOX_MAILBOXES, 'hello@meridiandisplay.example')
  122. expect(html).toContain('Sam Whitaker')
  123. })
  124. })