inbox.test.ts
Language TypeScript31.2 KB727 lines
/*** The consolidated inbox tab.** The two things that would make this feature actively harmful, both tested* here: hiding mail that matters, and showing mail to someone who should not* see it.*/import { describe, expect, it } from 'vitest'import {INBOX_MAILBOXES,INBOX_READERS,KNOWN_NOT_MARKETING,MARKETING_SENDERS,canReadInbox,isOperational,matchesFilter,readableMailboxes,} from '../../../config/inbox.js'import {ALL_INBOXES_CAP,INBOX_CSS,INBOX_MAX_PER_MAILBOX,inboxPage,messagePage,prioritised,rowTime,type InboxData,} from '../src/inbox-view.js'import { COMPANY_VIEWER, viewerFor } from '../src/viewer.js'import type { Attachment, InboxMessage } from '@meridian/gmail-client'/*** The count a named section header is claiming.** It sits in its own <span> so the search script can rewrite it: a count* rendered once and never updated stops describing the list under it the* moment anything is filtered. These tests care about the number, not* where it sits, so they ask for it rather than matching a flat string.*/const sectionCount = (html: string, title: string): number | null => {const m = new RegExp(`${title} · <span class="msg-count">(\\d+)</span>`).exec(html)return m ? Number(m[1]) : null}const msg = (over: Partial<InboxMessage>): InboxMessage => ({id: 'm1',threadId: 't1',mailbox: 'hello@meridiandisplay.example',sender: 'news@upscaler.vendor.example',senderName: 'Upscaler Labs',subject: 'What is new this month',snippet: 'Release notes and a few tips for the latest version',date: '2026-08-31T18:00:00.000Z',labels: ['INBOX', 'CATEGORY_PROMOTIONS'],unread: false,...over,})/** Two healthy mailboxes and one dead one, so "2 of 3" is exercised. */const DATA: InboxData = {syncedAt: '2026-09-01T06:00:00.000Z',mailboxes: [{mailbox: 'hello@meridiandisplay.example',ok: true,error: null,messages: [msg({ id: 'a' }),msg({ id: 'b', subject: 'Last chance: annual plan discount' }),msg({ id: 'c', sender: 'trackingupdates@courier.vendor.example', senderName: 'Courier', subject: 'Shipment delivered' }),msg({ id: 'd', sender: 'failed-payments@stripe.com', senderName: '', subject: 'A payment failed' }),// NEW MARKETING, not in the seed and not operational. This is the row// the button exists for, and without it the fixture had no "filter this"// control at all, so the add path was untested while the tests passed.msg({ id: 'f', sender: 'hello@newvendor.test', senderName: 'New Vendor', subject: 'Introducing our thing' }),],},{mailbox: 'orderdesk@meridiandisplay.example',ok: true,error: null,messages: [msg({ id: 'e', mailbox: 'orderdesk@meridiandisplay.example' })],},{mailbox: 'accounts@meridiandisplay.example',ok: false,error: 'invalid_grant: the mailbox is suspended',messages: [],},],}const ALL = INBOX_MAILBOXES.map((m) => m.email)const page = (readable = ALL, filters = MARKETING_SENDERS) =>inboxPage(DATA, filters, INBOX_MAILBOXES, readable, 'Sam Whitaker')describe('the filter never hides mail that matters', () => {it('leaves every known-operational sender unfiltered', () => {// Failed payments, shipment tracking, meeting recordings, and two of this// board's own sources all land in a Gmail promotional category. Seeding from// the category alone would have hidden them on day one.for (const s of KNOWN_NOT_MARKETING) {expect(matchesFilter(s, MARKETING_SENDERS), s).toBe(false)}})it('does not let a domain rule catch a vendor that sends both kinds', () => {// Three pairs. A same-domain rule hides the half that matters in the// first and last; the middle pair shows cold outreach and a failed payment// must still land on opposite sides of the filter:// sweeps@message.courier.vendor.example vs TrackingUpdates@courier.vendor.example// rep@growth.vendor.example vs failed-payments@stripe.com// teamscheduler@send.scheduler.vendor.example vs notifications@scheduler.vendor.exampleexpect(matchesFilter('sweeps@message.courier.vendor.example', MARKETING_SENDERS)).toBe(true)expect(matchesFilter('trackingupdates@courier.vendor.example', MARKETING_SENDERS)).toBe(false)expect(matchesFilter('rep@growth.vendor.example', MARKETING_SENDERS)).toBe(true)expect(matchesFilter('failed-payments@stripe.com', MARKETING_SENDERS)).toBe(false)expect(matchesFilter('teamscheduler@send.scheduler.vendor.example', MARKETING_SENDERS)).toBe(true)expect(matchesFilter('notifications@scheduler.vendor.example', MARKETING_SENDERS)).toBe(false)})it('matches nothing by substring', () => {// A customer at a domain containing a vendor's name must not be swept up.expect(matchesFilter('someone@notupscaler.vendor.example', ['news@upscaler.vendor.example'])).toBe(false)expect(matchesFilter('news@upscaler.vendor.example.evil.test', ['news@upscaler.vendor.example'])).toBe(false)})it('honours an explicit @domain entry, which is the only wildcard there is', () => {expect(matchesFilter('anyone@spam.test', ['@spam.test'])).toBe(true)expect(matchesFilter('anyone@notspam.test', ['@spam.test'])).toBe(false)})})describe('an operational sender cannot be filtered, by anyone', () => {it('refuses to hide one even when it is on the filter list', () => {// Courier tracking is operational and must stay visible, so this is// enforced rather than documented: a stray entry in KV or in config is// inert, not obeyed.expect(matchesFilter('trackingupdates@courier.vendor.example', ['trackingupdates@courier.vendor.example'])).toBe(false)expect(matchesFilter('failed-payments@stripe.com', ['@stripe.com'])).toBe(false)})it('protects every sender on the operational list', () => {for (const s of KNOWN_NOT_MARKETING) expect(isOperational(s), s).toBe(true)})it('shows a label instead of a button, rather than a button that refuses', () => {const html = page()expect(html).toContain('operational')// The courier row must not carry a filter form.const row = html.slice(html.indexOf('trackingupdates@courier.vendor.example'))expect(row.slice(0, row.indexOf('</div>') + 6)).not.toContain('<form')})})describe('reading a message', () => {const body = {id: 'm1',mailbox: 'hello@meridiandisplay.example',sender: 'news@upscaler.vendor.example',senderName: 'Upscaler Labs',subject: 'What is new this month',date: '2026-08-31T18:00:00.000Z',to: 'hello@meridiandisplay.example',text: 'Read this.',kind: 'plain' as const,truncated: false,html: null as string | null,htmlTruncated: false,unread: false,attachments: [] as Attachment[],}/** One attachment part, overridable. Defaults to a downloadable PDF. */const att = (over: Partial<Attachment> = {}): Attachment => ({index: 0,name: 'flyer.pdf',mimeType: 'application/pdf',size: 240_000,attachmentId: 'a1',inline: false,...over,})it('renders the body as text and escapes it', () => {// A marketing email is written by someone outside the company. Rendering its// markup on a page behind Access would hand an injection surface to anyone// who can email us.const html = messagePage({ ...body, text: '<script>alert(1)</script>' }, 'Sam Whitaker')expect(html).not.toContain('<script>alert(1)</script>')expect(html).toContain('<script>')})it('says a failed read failed, rather than showing an empty message', () => {const html = messagePage({ error: 'invalid_grant' }, 'Client Success')expect(html).toContain('invalid_grant')expect(html).toContain('not an empty message')})it('says when a body was cut', () => {expect(messagePage({ ...body, truncated: true }, 'Sam')).toContain('Cut at')})it('distinguishes an image-only message from a blank one', () => {expect(messagePage({ ...body, kind: 'none', text: '' }, 'Sam')).toContain('images only')})it('offers each attachment as a download, with its size', () => {// The property: a name is never on the page without either a way to get// the file or the reason there is none.const html = messagePage({ ...body, attachments: [att()] }, 'Sam')expect(html).toContain('flyer.pdf')expect(html).toContain('234 KB')expect(html).toMatch(/href="\/inbox\/attachment\?mailbox=[^"]*&id=[^"]*&n=0"/)})it('says why an attachment has no download rather than offering a dead link', () => {// The two absences are different and neither may render as a link.const noHandle = messagePage({ ...body, attachments: [att({ attachmentId: null })] }, 'Sam')expect(noHandle).toContain('no handle')expect(noHandle).not.toContain('/inbox/attachment?')const tooBig = messagePage({ ...body, attachments: [att({ size: 40 * 1024 * 1024 })] }, 'Sam')expect(tooBig).toContain('download ceiling')expect(tooBig).not.toContain('/inbox/attachment?')})it('does not report an unmeasured size as zero', () => {// Gmail omits body.size on some parts. "0 KB" beside a file that plainly// has contents is the prime invariant broken in the smallest possible way.const html = messagePage({ ...body, attachments: [att({ size: null })] }, 'Sam')expect(html).toContain('size not reported')expect(html).not.toContain('0 B')})it('separates inline images from files somebody attached', () => {// A multi-party client thread can carry four inline signature logos.// Counted as attachments they read as "4 attachments" on a message with// none, which is true and useless.const html = messagePage({...body,attachments: [att({ name: 'image001.jpg', mimeType: 'image/jpeg', inline: true })],},'Sam',)expect(html).toContain('No attachments')expect(html).toContain('1 inline image')})it('renders an HTML message through the frame, never inline', () => {// Messages render as full HTML, but the sender's markup must never touch// the board's own document: it rides in a separate response framed by a// sandbox without allow-scripts. If either assertion// below fails, the XSS this design prevents is back.const evil = '<img src=x onerror=alert(1)>'const html = messagePage({ ...body, kind: 'html' as const, html: evil }, 'Sam')expect(html).not.toContain(evil)expect(html).toContain('/inbox/message/frame?mailbox=hello%40meridiandisplay.example&id=m1')expect(html).toMatch(/sandbox="[^"]*"/)expect(html).not.toMatch(/sandbox="[^"]*allow-scripts/)})it('says when the HTML was cut, and falls back to text when there is none', () => {expect(messagePage({ ...body, html: '<p>x</p>', htmlTruncated: true }, 'M')).toContain('HTML was cut')// No HTML part → the plain text renders, no frame.const plain = messagePage(body, 'M')expect(plain).toContain('Read this.')expect(plain).not.toContain('mail-frame')})it('offers Mark read for a Gmail-unread message and Mark unread for a board-read one', () => {const unreadPage = messagePage({ ...body, unread: true }, 'M', false)expect(unreadPage).toContain('action="/inbox/read"')expect(unreadPage).toContain('Mark read')const boardReadPage = messagePage({ ...body, unread: true }, 'M', true)expect(boardReadPage).toContain('Mark unread')// Read in Gmail itself: no board state exists to flip, so no button.const gmailRead = messagePage({ ...body, unread: false }, 'M', false)expect(gmailRead).not.toContain('action="/inbox/read"')})})describe('access fails closed', () => {it('grants exactly the addresses Sam named, and nobody else', () => {// The whole roster plus the support and service desks (whose send grants// would otherwise be inert). The property that must survive any future// edit: a grant exists only// where a human wrote one (explicit lines, not a loop over the roster or// over INBOX_MAILBOXES), and anyone not written there is still closed, the// company viewer included.expect(Object.keys(INBOX_READERS).sort()).toEqual(['accounts@meridiandisplay.example','frontdesk@meridiandisplay.example','hello@meridiandisplay.example','help@meridiandisplay.example','nora.l@meridiandisplay.example','orderdesk@meridiandisplay.example','projects@meridiandisplay.example','service@meridiandisplay.example','studio@meridiandisplay.example',])for (const email of Object.keys(INBOX_READERS)) {expect(readableMailboxes(viewerFor(email)), email).toHaveLength(INBOX_MAILBOXES.length)}expect(canReadInbox(viewerFor('someone-else@meridiandisplay.example'))).toBe(false)expect(canReadInbox(COMPANY_VIEWER)).toBe(false)})it('reads inbox mailboxes that are not roster people without inventing viewers', () => {// exec@ is a mailbox the tab reads, not a person who can read the tab. An// address that is only in INBOX_MAILBOXES must resolve to no grant: access// arrives only when a human writes the line.expect(canReadInbox(viewerFor('exec@meridiandisplay.example'))).toBe(false)expect(INBOX_MAILBOXES.some((m) => m.email === 'exec@meridiandisplay.example')).toBe(true)})it('gives the two granted desks the inbox but not a roster identity', () => {// They are shared desks, not people, so they read every mailbox and resolve// to the COMPANY viewer everywhere else: no personal briefing, no// Leadership. Being in INBOX_READERS is not being on the roster.for (const email of ['help@meridiandisplay.example', 'service@meridiandisplay.example']) {const v = viewerFor(email)expect(readableMailboxes(v), email).toHaveLength(INBOX_MAILBOXES.length)expect(v.person, email).toBeNull()expect(v.seesLeadership, email).toBe(false)expect(v.ownBriefing, email).toBeNull()}})it('gives an unrecognised address nothing', () => {expect(readableMailboxes(viewerFor('stranger@example.com'))).toEqual([])expect(readableMailboxes(COMPANY_VIEWER)).toEqual([])})it('cannot invent a mailbox from a typo in a grant', () => {const grant = { 'x@meridiandisplay.example': { mailboxes: ['typo@meridiandisplay.example'] } }const saved = { ...INBOX_READERS }Object.assign(INBOX_READERS, grant)try {expect(readableMailboxes({ ...COMPANY_VIEWER, email: 'x@meridiandisplay.example' })).toEqual([])} finally {for (const k of Object.keys(INBOX_READERS)) delete INBOX_READERS[k]Object.assign(INBOX_READERS, saved)}})})describe('rowTime: the Gmail time column, in Meridian time', () => {it('shows clock time for today and month-day for older, in America/Chicago', () => {const now = new Date('2026-09-01T20:00:00Z') // 3:00 PM in Chicagoexpect(rowTime('2026-09-01T18:42:00Z', now)).toBe('1:42 PM')// 03:00Z on Sep 1 is still Aug 31 EVENING in Chicago; the day boundary is// the reader's, not UTC's. A UTC boundary would stamp late-evening mail// "today" a day early.expect(rowTime('2026-09-01T03:00:00Z', now)).toBe('Aug 31')expect(rowTime('2026-08-20T12:00:00Z', now)).toBe('Aug 20')})it('renders a dash for garbage, never Jan 1 1970', () => {expect(rowTime('not a date')).toBe('–')})})describe('the page: Gmail anatomy, board identity', () => {it('renders one row per message, newest first', () => {const html = page()// Every message row carries data-search; sender-grouping now lives only in// the marketing bucket. Two Upscaler messages = two rows, not one group.const rows = html.match(/class="msg-row/g) ?? []const unfilteredCount = DATA.mailboxes.flatMap((m) => m.messages).filter((m) => !matchesFilter(m.sender, MARKETING_SENDERS)).lengthexpect(rows.length).toBe(unfilteredCount)})it('splits Unread from Everything else, and bolds unread via the row class', () => {const withUnread: InboxData = {...DATA,mailboxes: [{mailbox: 'hello@meridiandisplay.example',ok: true,error: null,messages: [msg({ id: 'u1', sender: 'a@x.test', subject: 'New thing', unread: true, labels: ['UNREAD', 'INBOX'] }),msg({ id: 'r1', sender: 'b@x.test', subject: 'Old thing' }),],},],}const html = inboxPage(withUnread, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker')expect(sectionCount(html, 'Unread')).toBe(1)expect(sectionCount(html, 'Everything else')).toBe(1)expect(html).toContain('msg-row unread')})it('a board read-mark moves a Gmail-unread message out of Unread, and offers the way back', () => {const withUnread: InboxData = {...DATA,mailboxes: [{mailbox: 'hello@meridiandisplay.example',ok: true,error: null,messages: [msg({ id: 'u1', sender: 'a@x.test', subject: 'New thing', unread: true, labels: ['UNREAD'] }),msg({ id: 'u2', sender: 'b@x.test', subject: 'Handled thing', unread: true, labels: ['UNREAD'] }),],},],}// Nobody marked anything: both unread, both rows offer Mark read.const before = inboxPage(withUnread, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker')expect(sectionCount(before, 'Unread')).toBe(2)expect((before.match(/>Mark read</g) ?? []).length).toBe(2)// u2 marked read on the board: it leaves Unread, loses the bold class, and// its row offers Mark unread, so the mark is reversible from the page// (rule 12). The rail count follows the same arithmetic.const after = inboxPage(withUnread, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', null,new Set(['hello@meridiandisplay.example/u2']))expect(sectionCount(after, 'Unread')).toBe(1)expect(sectionCount(after, 'Everything else')).toBe(1)expect(after).toContain('>Mark unread<')expect(after).toContain('name="op" value="unread"')})it('a message read in Gmail itself gets no mark button; there is no board state to flip', () => {const html = page()// Fixture messages are all unread:false with no board marks.expect(html).not.toContain('action="/inbox/read"')})it('the mark-read form posts the message its OWN mailbox and echoes the view for the 303', () => {const withUnread: InboxData = {...DATA,mailboxes: [{mailbox: 'orderdesk@meridiandisplay.example',ok: true,error: null,messages: [msg({ id: 'u9', mailbox: 'orderdesk@meridiandisplay.example', sender: 'a@x.test', unread: true })],},],}const html = inboxPage(withUnread, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', 'orderdesk@meridiandisplay.example')expect(html).toContain('action="/inbox/read"')expect(html).toContain('name="mailbox" value="orderdesk@meridiandisplay.example"')expect(html).toContain('name="id" value="u9"')expect(html).toContain('name="back" value="orderdesk@meridiandisplay.example"')})it('shows the snippet on the subject line, escaped', () => {const nasty: InboxData = {syncedAt: null,mailboxes: [{mailbox: 'hello@meridiandisplay.example',ok: true,error: null,messages: [msg({ snippet: '<img onerror=x> preview', senderName: '<script>alert(1)</script>' })],},],}const html = inboxPage(nasty, [], INBOX_MAILBOXES, ALL, 'Sam')expect(html).toContain('· <img onerror=x> preview')expect(html).not.toContain('<script>alert(1)</script>')})it('filters to one mailbox when selected, and marks it current in the rail', () => {const html = inboxPage(DATA, MARKETING_SENDERS, INBOX_MAILBOXES, ALL, 'Sam', 'orderdesk@meridiandisplay.example')// hello@ holds the only Stripe mail in the fixture; a filtered view must not show it.expect(html).not.toContain('A payment failed')expect(html).toMatch(/aria-current="true"[^>]*>\s*<span>Rafael Montero<\/span>/)})it('carries the selected mailbox through the filter form, so the 303 returns here', () => {const html = inboxPage(DATA, [], INBOX_MAILBOXES, ALL, 'Sam', 'hello@meridiandisplay.example')expect(html).toContain('<input type="hidden" name="mailbox" value="hello@meridiandisplay.example">')})it('names a failed mailbox in the rail AND the banner, never as an empty inbox', () => {const html = page()expect(html).toContain('invalid_grant')expect(html).toContain('missing from this list, not absent')// The rail shows "failed" where its unread count would be; a count it// cannot have would be a lie.expect(html).toMatch(/class="dead"[^>]*>failed</)})it('keeps filtered senders in the collapsed bucket with the way back', () => {const html = page()expect(html).toContain('Filtered marketing:')expect(html).toContain('Not marketing')expect(html).toContain('news@upscaler.vendor.example')})it('keeps operational senders in the list with a tag instead of a button', () => {const html = page()expect(html).toContain('A payment failed')expect(html).toContain('Shipment delivered')const stripe = html.slice(html.indexOf('failed-payments@stripe.com'))expect(stripe.slice(0, stripe.indexOf('</div>'))).not.toContain('<form')})it('shows only the mailboxes this viewer may read, in list and rail alike', () => {const html = page(['orderdesk@meridiandisplay.example'])expect(html).not.toContain('A payment failed')expect(html).not.toContain('>Sam Whitaker</span>')})it('says a dead read is a dead read rather than a quiet inbox', () => {const allDown: InboxData = {syncedAt: null,mailboxes: [{ mailbox: 'hello@meridiandisplay.example', ok: false, error: 'HTTP 403', messages: [] }],}const html = inboxPage(allDown, MARKETING_SENDERS, INBOX_MAILBOXES, ['hello@meridiandisplay.example'], 'Sam')expect(html).toContain('not because there is no mail')})it('works with JavaScript off: rail links, form POSTs, search hidden by default', () => {const html = page()expect(html).toContain('href="/inbox?mailbox=')expect(html).toContain('method="post"')expect(html).toContain('action="/inbox/filter"')expect(html).toContain('action="/inbox/unfilter"')expect(html).toMatch(/<div hidden[^>]*>\s*<input id="inbox-search"/)})})/*** Search must actually hide rows, not merely render its markup.** The `hidden` property depends on the user-agent stylesheet's* `[hidden]{display:none}`, and a UA rule loses to any author rule regardless* of specificity. `.msg-row` is `display:grid`, so a row hidden that way stays* on screen. The presence of a control is not evidence that the control works.*/describe('search actually hides what it filters', () => {it('re-asserts display:none for every element the script toggles', () => {// The script sets `hidden` on .msg-row, .msg-sect and .msg-none. Each needs// an author-level rule or the UA default is overridden by whatever display// that class already carries.for (const sel of ['.msg-row[hidden]', '.msg-sect[hidden]', '.msg-none[hidden]']) {expect(INBOX_CSS, `${sel} has no display:none rule`).toContain(sel)}expect(INBOX_CSS).toMatch(/\.msg-row\[hidden\][^{]*\{display:none\}/)})it('gives every class that the script hides an explicit display rule', () => {// Generalised so the next person who styles a filtered element gets caught:// any selector carrying `display:` whose class the script toggles must also// appear in the [hidden] override.const hiddenRule = INBOX_CSS.match(/^[^\n]*\[hidden\][^{]*\{display:none\}/m)?.[0] ?? ''for (const cls of ['msg-row', 'msg-sect', 'msg-none']) {expect(hiddenRule, `${cls} missing from the [hidden] override`).toContain(cls)}})it('wraps each section so its header hides with its rows', () => {// Left as siblings, filtering away every row in a section stranded its// header above nothing, still claiming a count.const html = inboxPage(DATA, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker')expect(html).toContain('class="msg-sect"')const sects = html.match(/class="msg-sect"/g) ?? []const heads = html.match(/class="msg-section"/g) ?? []expect(sects.length).toBe(heads.length)})it('puts the header count in its own element so the script can rewrite it', () => {// A count rendered once on the server and never updated is a number that// stops describing the list under it the moment anything is filtered.const html = inboxPage(DATA, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker')expect(html).toMatch(/<span class="msg-count">\d+<\/span>/)})it('carries a no-match line that is distinct from an empty inbox', () => {const html = inboxPage(DATA, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker')expect(html).toContain('id="msg-none"')expect(html).toMatch(/id="msg-none"[^>]*hidden/)// And it must not claim to have searched Gmail.expect(html).toContain('not a search of Gmail')})})/*** How much of each mailbox the page shows is a decision, not a fetch limit: the* combined view summarises, one mailbox shows everything held, and both say* which they are doing.*//** Collapse markup whitespace, so a re-wrapped sentence is not a test failure. */const flat = (html: string): string => html.replace(/\s+/g, ' ')const bulk = (n: number, over: Partial<InboxMessage> = {}): InboxMessage[] =>Array.from({ length: n }, (_, i) =>msg({id: `b${i}`,subject: `Message ${i}`,// Descending dates, so index order is newest-first like the real list.date: new Date(Date.parse('2026-09-04T18:00:00.000Z') - i * 60_000).toISOString(),...over,}),)describe('prioritised: the All-inboxes rule', () => {const unreadOf = (m: InboxMessage) => m.unreadit('takes unread first, newest first, and caps at 25', () => {const r = prioritised(bulk(40, { unread: true }), unreadOf)expect(r.unread).toHaveLength(ALL_INBOXES_CAP)expect(r.read).toHaveLength(0)expect(r.unread[0]!.id).toBe('b0')expect(r.heldBack).toBe(15)})it('fills the remainder with read messages only when unread does not fill it', () => {// Unread outranks read: with 25 unread waiting, nothing already-read earns// a place in a summary.const kept = [...bulk(3, { unread: true }), ...bulk(30, { unread: false })]const r = prioritised(kept, unreadOf)expect(r.unread).toHaveLength(3)expect(r.read).toHaveLength(ALL_INBOXES_CAP - 3)// 3 unread + 22 read = the cap; the other 8 read are held back.expect(r.heldBack).toBe(kept.length - ALL_INBOXES_CAP)})it('accounts for every message it does not show', () => {// Rule 9: shown plus held back equals what was loaded, exactly.const kept = [...bulk(30, { unread: true }), ...bulk(30, { unread: false })]const r = prioritised(kept, unreadOf)expect(r.unread.length + r.read.length + r.heldBack).toBe(kept.length)})it('holds nothing back when there is little mail', () => {const r = prioritised(bulk(4, { unread: true }), unreadOf)expect(r.heldBack).toBe(0)})})describe('the combined view summarises; one mailbox shows everything', () => {const many = (n: number): InboxData => ({syncedAt: '2026-09-04T18:00:00.000Z',mailboxes: [{mailbox: 'hello@meridiandisplay.example', ok: true, error: null, canSend: true,windowTotal: n, truncated: false, failedFetches: 0,messages: bulk(n, { unread: true }),}],})it('caps All inboxes and says how many are waiting behind it', () => {const html = inboxPage(many(60), [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', null)expect((html.match(/class="msg-row/g) ?? [])).toHaveLength(ALL_INBOXES_CAP)expect(flat(html)).toContain('35 more')// And it must not read as a fetch limit.expect(flat(html)).toContain('nothing here is a limit on what was fetched')})it('shows everything held when one mailbox is selected', () => {const html = inboxPage(many(60), [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', 'hello@meridiandisplay.example')expect((html.match(/class="msg-row/g) ?? [])).toHaveLength(60)expect(flat(html)).not.toContain('more messages</b> are loaded')})it('names a mailbox whose window held more than was fetched', () => {// Truncated is not the same as held back: one means open the mailbox, the// other means raise the cap. They must not share a sentence.const d = many(10)d.mailboxes[0]!.truncated = trued.mailboxes[0]!.windowTotal = 900const html = inboxPage(d, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', 'hello@meridiandisplay.example')expect(flat(html)).toContain('900 in the window')expect(flat(html)).toContain('not absent from Gmail')})it('says the count is unknown rather than inventing one', () => {// windowTotal is null when the id list could not be walked to the end. The// field it replaced reported Gmail's resultSizeEstimate, which came back// 501 for windows of larger, unknown size, a number that read as a fact.const d = many(10)d.mailboxes[0]!.truncated = trued.mailboxes[0]!.windowTotal = nullconst html = inboxPage(d, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', 'hello@meridiandisplay.example')expect(flat(html)).toContain('more than the board counted')})it('reports messages Gmail failed to return, distinctly from truncation', () => {const d = many(5)d.mailboxes[0]!.failedFetches = 3const html = inboxPage(d, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', 'hello@meridiandisplay.example')expect(flat(html)).toContain('failed fetch, not an empty inbox')})it('says nothing about shortfalls when there are none', () => {const html = inboxPage(many(5), [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', 'hello@meridiandisplay.example')expect(flat(html)).not.toContain('not absent from Gmail')expect(flat(html)).not.toContain('more messages</b> are loaded')})})describe('the footer does not lie about coverage', () => {it('states the per-mailbox depth from the constant, not a typed literal', () => {// The footer is built from the constant, not typed beside it: a hand-typed// number would go on claiming the old depth when the constant moved, a page// lying about its own coverage.const html = inboxPage(DATA, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker')expect(html).toContain(INBOX_MAX_PER_MAILBOX.toLocaleString())expect(html).not.toContain('newest 25 per mailbox')})})describe('the New message button', () => {it('is absent unless the viewer may send from something', () => {// Defaults to absent, like every permission on this board (rule 13). A// button that 403s when pressed is worse than one that was never there,// and the reason is on the message page, where the Reply control explains// it in full rather than in a tooltip here.expect(inboxPage(DATA, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker')).not.toContain('/inbox/new')})it('appears for a viewer who holds a send grant', () => {const html = inboxPage(DATA, [], INBOX_MAILBOXES, ALL, 'Sam Whitaker', null, new Set(), new Set(), true)expect(html).toContain('href="/inbox/new"')expect(html).toContain('New message')})})