inbox-send.test.ts
Language TypeScript37.5 KB926 lines
/*** The send route, exercised end to end without a network and without Gmail.** The handlers take their collaborators as arguments because this is the one* action on the board that cannot be undone, and it is behind a Cloudflare* Access JWT these tests cannot mint. Injected deps are what let the* nonce claim, the write-before-send ordering and the per-mailbox thread rule* be tested at all, rather than being "covered by the deploy".** Nothing here sends anything: `sendMessage` is a fake in every test.*/import { describe, expect, it } from 'vitest'import {REPLIED_MARK_MAX_AGE_MS,SEND_KEY,decideNew,decideReply,handleCompose,handleSend,handleSignaturesPost,listSent,parseRecipients,parseStaged,readUploads,replyAllCc,stagedToOutgoing,readReplied,readSignatures,type SendDeps,type SendEnv,} from '../src/inbox-send.js'import { viewerFor } from '../../dashboard/src/viewer.js'import { SIGNATURE_CAP, type SendRecord } from '../../dashboard/src/inbox-view.js'import type { MessageBody, OutgoingMessage, SendResult } from '@meridian/gmail-client'/*** `decideReply` can refuse now: a reply-all whose derived Cc is over the* recipient cap comes back as an error rather than a trimmed message. This* unwraps the success case so the assertions below stay about the message,* and throws loudly if a test ever accidentally asserts against a refusal.*/const ok = (r: OutgoingMessage | { error: string }): OutgoingMessage => {if ('error' in r) throw new Error(`expected a message, got: ${r.error}`)return r}const ALL = ['hello@meridiandisplay.example','orderdesk@meridiandisplay.example','studio@meridiandisplay.example','accounts@meridiandisplay.example','help@meridiandisplay.example','service@meridiandisplay.example','exec@meridiandisplay.example',]/** A KV that behaves like the real one for get/put/delete/list-with-metadata. */function fakeKv(seed: Record<string, string> = {}) {const store = new Map<string, { value: string; metadata?: unknown }>()for (const [k, v] of Object.entries(seed)) store.set(k, { value: v })return {store,async get(k: string) {return store.get(k)?.value ?? null},async put(k: string, v: string, opts?: { metadata?: unknown }) {store.set(k, { value: v, metadata: opts?.metadata })},async delete(k: string) {store.delete(k)},async list({ prefix, limit = 1000 }: { prefix: string; limit?: number }) {const keys = [...store.entries()].filter(([k]) => k.startsWith(prefix)).map(([name, v]) => ({ name, metadata: v.metadata }))return { keys: keys.slice(0, limit), list_complete: keys.length <= limit }},}}const envOf = (seed: Record<string, string> = {}) => {const kv = fakeKv(seed)return { env: { DASHBOARD: kv } as unknown as SendEnv, kv }}const ORIG = (over: Partial<MessageBody> = {}): MessageBody => ({id: 'm1',mailbox: 'hello@meridiandisplay.example',sender: 'client@example.com',senderName: 'A Client',subject: 'Banner proof',date: '2026-09-04T10:00:00.000Z',to: 'hello@meridiandisplay.example',text: 'Where is my proof?',kind: 'plain',truncated: false,html: null,htmlTruncated: false,unread: true,attachments: [],threadId: 'thread-1',messageId: '<abc@example.com>',references: null,replyTo: '',cc: '',...over,})function deps(over: Partial<SendDeps> = {},sendResult: SendResult = { ok: true, id: 'gm1', threadId: 'gt1' },): SendDeps & { sent: OutgoingMessage[] } {const sent: OutgoingMessage[] = []return {sent,fetchBody: async () => ORIG(),sendMessage: async (_m, _c, msg) => {sent.push(msg)return sendResult},credentialFor: () => ({ clientId: 'i', clientSecret: 's', refreshToken: 'r' }),now: () => Date.parse('2026-09-04T12:00:00.000Z'),...over,}}const form = (o: Record<string, string>) => {const f = new FormData()for (const [k, v] of Object.entries(o)) f.set(k, v)return f}const GOOD = { mailbox: 'hello@meridiandisplay.example', id: 'm1', from: 'hello@meridiandisplay.example', body: 'Here it is.', nonce: 'n-11111111' }describe('decideReply', () => {it('replies to Reply-To when there is one, and to From when there is not', () => {expect(ok(decideReply(ORIG(), 'hello@meridiandisplay.example', 'x', null)).to).toBe('client@example.com')expect(ok(decideReply(ORIG({ replyTo: 'Billing <billing@example.com>' }), 'hello@meridiandisplay.example', 'x', null)).to,).toBe('billing@example.com')})it('falls back to From when Reply-To is unusable rather than sending to nothing', () => {expect(ok(decideReply(ORIG({ replyTo: 'not an address' }), 'hello@meridiandisplay.example', 'x', null)).to).toBe('client@example.com',)})it('sends the thread id only when replying from the mailbox it arrived in', () => {// Gmail thread ids are per-mailbox. Passing hello@'s id through accounts@'s// token is rejected outright, and the recipient's client threads on// In-Reply-To either way, so nothing is lost by omitting it.expect(ok(decideReply(ORIG(), 'hello@meridiandisplay.example', 'x', null)).threadId).toBe('thread-1')expect(ok(decideReply(ORIG(), 'accounts@meridiandisplay.example', 'x', null)).threadId).toBeNull()})it('builds References from the original chain plus its Message-ID', () => {expect(ok(decideReply(ORIG({ references: '<r1@x>' }), 'hello@meridiandisplay.example', 'x', null)).references).toBe('<r1@x> <abc@example.com>',)expect(ok(decideReply(ORIG({ references: null, messageId: null }), 'hello@meridiandisplay.example', 'x', null)).references).toBeNull()})it('puts NOTHING identifying the sender in the message', () => {// Attribution only if truly invisible to the client, and an X- header is// not. So there is no header at all, and this// pins that nobody adds one back without changing this test.const out = ok(decideReply(ORIG(), 'accounts@meridiandisplay.example', 'x', null))expect(out.extraHeaders).toBeUndefined()expect(JSON.stringify(out)).not.toContain('meridiandisplay.example>')})})describe('parseRecipients', () => {it('keeps the address and drops the display name', () => {// The name is cosmetic and it is the half that can contain anything. What// goes into the header is built out of the parse, never copied from input.const r = parseRecipients('Lena Ortiz <lena@example.org>')expect(r.ok && r.header).toBe('lena@example.org')})it('splits on a comma OUTSIDE quotes, which is the multi-party client case', () => {// A client thread Cc'd to `"Ortiz, Lena" <Lena.Ortiz@lakeside-expo.example>`// is the shape. A naive split turns that one recipient into `"Ortiz`, which// is not an address, and `Lena" <...>`, which is one that would be sent to.const r = parseRecipients('"Ortiz, Lena" <Lena.Ortiz@lakeside-expo.example>, sasha@lakeside-expo.example')expect(r.ok && r.addresses).toEqual(['Lena.Ortiz@lakeside-expo.example', 'sasha@lakeside-expo.example'])})it('cannot be made to inject a header', () => {// CR/LF is how one header becomes two, and a forged Bcc: is the payload// that matters. This is refused at the parse, so nothing downstream has to// be careful.expect(parseRecipients('a@b.com\r\nBcc: victim@elsewhere.com').ok).toBe(false)expect(parseRecipients('a@b.com\nBcc: victim@elsewhere.com').ok).toBe(false)})it('refuses anything that is not plainly an address, and says which', () => {const r = parseRecipients('not-an-address')expect(r.ok).toBe(false)expect(!r.ok && r.error).toContain('not-an-address')})it('collapses duplicates by normalized address but sends what was typed', () => {const r = parseRecipients('Lena@Example.org, lena@example.org')expect(r.ok && r.addresses).toEqual(['Lena@Example.org'])})it('refuses over the cap with the count, instead of trimming to fit', () => {const many = Array.from({ length: 20 }, (_, i) => `p${i}@example.com`).join(', ')const r = parseRecipients(many)expect(r.ok).toBe(false)expect(!r.ok && r.error).toContain('20')})it('is not satisfied by an empty list', () => {expect(parseRecipients(' ').ok).toBe(false)})})describe('reply-all', () => {const THREAD = ORIG({sender: 'sasha@lakeside-expo.example',to: 'hello@meridiandisplay.example',cc: '"Ortiz, Lena" <Lena.Ortiz@lakeside-expo.example>, help@meridiandisplay.example',})it('is off unless asked for', () => {expect(ok(decideReply(THREAD, 'hello@meridiandisplay.example', 'x', null)).cc).toBeNull()})it('adds everyone on the original except us and the person being answered', () => {const out = ok(decideReply(THREAD, 'hello@meridiandisplay.example', 'x', null, { replyAll: true }))expect(out.to).toBe('sasha@lakeside-expo.example')// hello@ was in To and is the sender here, so it goes; Lena and support stay.expect(out.cc).toBe('Lena.Ortiz@lakeside-expo.example, help@meridiandisplay.example')})it('keeps other Meridian mailboxes on the thread', () => {// A colleague following a client thread is not noise to be tidied away;// dropping them is the silent omission reply-all exists to avoid.const out = ok(decideReply(THREAD, 'hello@meridiandisplay.example', 'x', null, { replyAll: true }))expect(out.cc).toContain('help@meridiandisplay.example')})it('drops the mailbox it arrived in even when replying as another', () => {const out = ok(decideReply(THREAD, 'accounts@meridiandisplay.example', 'x', null, { replyAll: true }))expect(out.cc).not.toContain('hello@meridiandisplay.example')})it('is null, not empty, when there is nobody left to add', () => {const solo = ORIG({ to: 'hello@meridiandisplay.example', cc: '' })expect(ok(decideReply(solo, 'hello@meridiandisplay.example', 'x', null, { replyAll: true })).cc).toBeNull()})it('REFUSES rather than trimming when the derived list is over the cap', () => {const crowd = ORIG({cc: Array.from({ length: 30 }, (_, i) => `p${i}@example.com`).join(', '),})const out = decideReply(crowd, 'hello@meridiandisplay.example', 'x', null, { replyAll: true })expect('error' in out).toBe(true)if ('error' in out) expect(out.error).toContain('ceiling')})it('ignores a malformed entry on somebody else\'s header rather than failing the send', () => {// A broken address in an incoming Cc is not ours to fix and not a reason// to refuse a reply; it is dropped from the Cc and the rest goes.const messy = ORIG({ cc: 'undisclosed-recipients:;, real@example.com' })const out = ok(decideReply(messy, 'hello@meridiandisplay.example', 'x', null, { replyAll: true }))expect(out.cc).toBe('real@example.com')})it('replyAllCc is null when the original names nobody', () => {expect(replyAllCc(ORIG({ to: '', cc: '' }), 'hello@meridiandisplay.example', 'client@example.com')).toBeNull()})})describe('decideNew', () => {const NEW = {from: 'hello@meridiandisplay.example',to: 'client@example.com',cc: null,subject: 'Your banner',text: 'Hello',signatureHtml: null,attachments: [],}it('carries NO threading headers: a new message must not join a conversation', () => {const out = decideNew(NEW)expect(out.inReplyTo).toBeNull()expect(out.references).toBeNull()expect(out.threadId).toBeNull()})it('labels an empty subject rather than sending a blank one', () => {expect(decideNew({ ...NEW, subject: ' ' }).subject).toBe('(no subject)')})it('does not prefix Re:, since it is answering nothing', () => {expect(decideNew(NEW).subject).toBe('Your banner')})})describe('attachments on the way out', () => {/*** REAL `File` OBJECTS, not duck-typed stand-ins.** FormData.append stringifies anything that is not a Blob: an object* literal arrives as "[object Object]" and is silently dropped by* readUploads' filter, so a hand-rolled fake would make every cap test pass* by attaching nothing at all. Node has had File since 20.*/const file = (name: string, size: number, type = 'application/pdf') =>new File([new Uint8Array(size)], name, { type })const withFiles = (...files: File[]) => {const f = new FormData()for (const [k, v] of Object.entries(GOOD)) f.set(k, v)for (const x of files) f.append('files', x)return f}it('drops the empty entry a browser posts for an untouched file input', async () => {// WITHOUT THIS every reply would carry one nameless zero-byte attachment.const r = await readUploads(withFiles(file('', 0)))expect(r.ok && r.files).toEqual([])})it('refuses a file over the per-file cap, naming it', async () => {const r = await readUploads(withFiles(file('huge.pdf', 5 * 1024 * 1024)))expect(r.ok).toBe(false)expect(!r.ok && r.error).toContain('huge.pdf')})it('refuses a set over the total cap', async () => {const r = await readUploads(withFiles(file('a.pdf', 2_500_000), file('b.pdf', 2_500_000)))expect(r.ok).toBe(false)expect(!r.ok && r.error).toContain('total')})it('refuses more files than the count cap', async () => {const r = await readUploads(withFiles(...Array.from({ length: 9 }, (_, i) => file(`f${i}.pdf`, 10))))expect(r.ok).toBe(false)expect(!r.ok && r.error).toContain('at most')})it('round-trips a small file to base64 and back to the same bytes', async () => {const bytes = new Uint8Array([0, 1, 2, 250, 255])const up = await readUploads(withFiles(new File([bytes], 'x.bin', { type: 'application/octet-stream' })),)expect(up.ok).toBe(true)if (!up.ok) returnconst out = stagedToOutgoing(up.files)expect(out.ok).toBe(true)if (!out.ok) returnexpect([...out.files[0]!.bytes]).toEqual([...bytes])})it('re-applies the caps to what the confirmation page hands back', () => {// The compose step checked them, and then the payload sat in a form field// on somebody's browser. What comes back is input.const big = { name: 'x.bin', mimeType: 'application/octet-stream', b64: btoa('x'.repeat(4 * 1024 * 1024)) }const out = stagedToOutgoing([big])expect(out.ok).toBe(false)})it('refuses a malformed staged field rather than sending without the files', () => {expect(parseStaged('not json').ok).toBe(false)expect(parseStaged('{"n":1}').ok).toBe(false)expect(parseStaged('[{"name":"a"}]').ok).toBe(false)// Absent is legitimate: a message with no attachments.expect(parseStaged('').ok).toBe(true)})})describe('handleSend refuses what it should', () => {it('refuses a viewer who may read everything but holds no send grant', async () => {// The mistake this guards: reusing the read grant as the send check. The// whole roster reads all seven mailboxes; only some may send.const { env } = envOf()const out = await handleSend(viewerFor('projects@meridiandisplay.example'), env, form(GOOD), ALL, deps())expect(out.kind).toBe('denied')})it('refuses sending AS a mailbox outside this viewer’s grant, even one they read', async () => {const { env, kv } = envOf()const d = deps()const out = await handleSend(viewerFor('studio@meridiandisplay.example'),env,form({ ...GOOD, from: 'accounts@meridiandisplay.example' }),ALL,d,)expect(out.kind).toBe('denied')expect(d.sent).toHaveLength(0)expect(kv.store.size).toBe(0)})it('refuses a mailbox this viewer cannot READ, even if they could send as it', async () => {const { env } = envOf()const out = await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ['orderdesk@meridiandisplay.example'], deps())expect(out.kind).toBe('denied')})it('sends nothing for an empty body', async () => {const { env } = envOf()const d = deps()const out = await handleSend(viewerFor('hello@meridiandisplay.example'), env, form({ ...GOOD, body: ' ' }), ALL, d)expect(out.kind).toBe('error')expect(d.sent).toHaveLength(0)})it('refuses a missing or malformed nonce rather than sending un-deduplicated', async () => {const { env } = envOf()const d = deps()for (const nonce of ['', 'short', 'has spaces in it']) {const out = await handleSend(viewerFor('hello@meridiandisplay.example'), env, form({ ...GOOD, nonce }), ALL, d)expect(out.kind).toBe('error')}expect(d.sent).toHaveLength(0)})})describe('a new message, which is the one send that chooses its own recipients', () => {const NEWFORM = {mode: 'new',from: 'hello@meridiandisplay.example',to: 'client@example.com',subject: 'Your proof',body: 'Attached.',nonce: 'n-22222222',}it('still needs the SEND grant; reading seven mailboxes is not enough', async () => {// The same guard as a reply, and worth its own test because the new-message// route touches no mailbox at all: there is no readable() check to hide// behind here, so canSendFrom is the only thing standing in the way.const { env } = envOf()const out = await handleSend(viewerFor('projects@meridiandisplay.example'), env, form(NEWFORM), ALL, deps())expect(out.kind).toBe('denied')})it('refuses sending AS a mailbox outside the viewer’s grant', async () => {const { env } = envOf()const d = deps()const out = await handleSend(viewerFor('studio@meridiandisplay.example'),env,form({ ...NEWFORM, from: 'accounts@meridiandisplay.example' }),ALL,d,)expect(out.kind).toBe('denied')expect(d.sent).toHaveLength(0)})it('sends to the typed address and reads no original at all', async () => {const { env } = envOf()const d = deps({fetchBody: async () => {throw new Error('a new message must not fetch a message')},})await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(NEWFORM), ALL, d)expect(d.sent).toHaveLength(1)expect(d.sent[0]!.to).toBe('client@example.com')expect(d.sent[0]!.threadId).toBeNull()})it('refuses a bad recipient before the nonce is claimed, so it can be fixed and resent', async () => {// A submission refused for its contents must not burn the one-time token,// otherwise correcting a typo means the resend is treated as a duplicate// and silently redirected to a record of something that never went.const { env, kv } = envOf()const d = deps()const out = await handleSend(viewerFor('hello@meridiandisplay.example'),env,form({ ...NEWFORM, to: 'nonsense' }),ALL,d,)expect(out.kind).toBe('error')expect(d.sent).toHaveLength(0)expect(kv.store.get(`${SEND_KEY.sentPrefix}n-22222222`)).toBeUndefined()})it('records mode, cc and attachments, and NULL for the original it does not have', async () => {const { env, kv } = envOf()await handleSend(viewerFor('hello@meridiandisplay.example'),env,form({ ...NEWFORM, cc: 'boss@example.com' }),ALL,deps(),)const rec = JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-22222222`)!.value) as SendRecordexpect(rec.mode).toBe('new')expect(rec.cc).toBe('boss@example.com')// null, not '': there is no original, and an empty string in a field that// holds a mailbox reads as one that could not be read.expect(rec.originalMailbox).toBeNull()expect(rec.originalId).toBeNull()expect(rec.attachments).toEqual([])})it('leaves no replied mark; it answered nothing', async () => {const { env } = envOf()await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(NEWFORM), ALL, deps())expect(await readReplied(env)).toEqual({})})it('caps To and Cc together, not one each', async () => {const { env } = envOf()const eight = (p: string) => Array.from({ length: 8 }, (_, i) => `${p}${i}@example.com`).join(', ')const out = await handleSend(viewerFor('hello@meridiandisplay.example'),env,form({ ...NEWFORM, to: eight('a'), cc: eight('b') }),ALL,deps(),)expect(out.kind).toBe('error')if (out.kind === 'error') expect(out.message).toContain('16 recipients')})it('an unknown mode is a reply, never a new message', async () => {// The reply path is the one that cannot address a stranger, so it is the// one an unrecognised value falls back to. Here the reply fields are// missing, so falling back means denied, not "send to whatever `to` says".const { env } = envOf()const d = deps()const out = await handleSend(viewerFor('hello@meridiandisplay.example'),env,form({ ...NEWFORM, mode: 'NEW ' }),ALL,d,)expect(out.kind).toBe('denied')expect(d.sent).toHaveLength(0)})})describe('a reply that carries files, and one that carries everyone', () => {const staged = (files: { name: string; mimeType: string; b64: string }[]) => {const f = new FormData()for (const [k, v] of Object.entries(GOOD)) f.set(k, v)f.set('staged', JSON.stringify(files))return f}it('hands the decoded bytes to the sender and records name and size only', async () => {const { env, kv } = envOf()const d = deps()await handleSend(viewerFor('hello@meridiandisplay.example'),env,staged([{ name: 'proof.pdf', mimeType: 'application/pdf', b64: btoa('hello') }]),ALL,d,)expect(d.sent[0]!.attachments).toHaveLength(1)expect(d.sent[0]!.attachments[0]!.bytes.byteLength).toBe(5)const rec = JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value) as SendRecordexpect(rec.attachments).toEqual([{ name: 'proof.pdf', size: 5 }])// The bytes are not in the audit row. It records that a file went, not the// file: the same line the row already draws around the message text.expect(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value).not.toContain(btoa('hello'))})it('refuses an over-cap staged payload WITHOUT claiming the nonce', async () => {const { env, kv } = envOf()const d = deps()const out = await handleSend(viewerFor('hello@meridiandisplay.example'),env,staged([{ name: 'big.bin', mimeType: 'application/octet-stream', b64: btoa('x'.repeat(4 * 1024 * 1024)) }]),ALL,d,)expect(out.kind).toBe('error')expect(d.sent).toHaveLength(0)expect(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)).toBeUndefined()})it('sends the reply-all Cc that the original justifies', async () => {const { env, kv } = envOf()const thread = ORIG({ sender: 'sasha@lakeside-expo.example', to: 'hello@meridiandisplay.example', cc: 'Lena.Ortiz@lakeside-expo.example' })const d = deps({ fetchBody: async () => thread })await handleSend(viewerFor('hello@meridiandisplay.example'), env, form({ ...GOOD, all: '1' }), ALL, d)expect(d.sent[0]!.cc).toBe('Lena.Ortiz@lakeside-expo.example')const rec = JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value) as SendRecordexpect(rec.cc).toBe('Lena.Ortiz@lakeside-expo.example')})it('records cc as null (not absent) when reply-all was not asked for', async () => {// Absent means "this row predates the feature" and the page says so. A// send made today must record which of the two it was.const { env, kv } = envOf()await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, deps())const rec = JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value) as SendRecordexpect(rec.cc).toBeNull()expect('cc' in rec).toBe(true)})it('keeps the KV metadata inside its 1,024-byte ceiling with five long filenames', async () => {// The listing renders from metadata alone. Over the cap the metadata write// fails and the row vanishes from the list while the record still exists:// a send that happened and cannot be seen.const { env, kv } = envOf()const long = (i: number) => ({name: `${'a-very-long-client-filename'.repeat(4)}-${i}.pdf`,mimeType: 'application/pdf',b64: btoa('x'),})await handleSend(viewerFor('hello@meridiandisplay.example'),env,staged([1, 2, 3, 4, 5].map(long)),ALL,deps(),)const meta = kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.metadataexpect(new TextEncoder().encode(JSON.stringify(meta)).byteLength).toBeLessThan(1024)// And the count survives, because that is what the listing prints.expect((meta as { attachmentCount: number }).attachmentCount).toBe(5)})})describe('handleCompose, the confirmation step', () => {it('previews a reply-all with the list the SEND will use, not the header it came from', async () => {const { env } = envOf()const thread = ORIG({sender: 'sasha@lakeside-expo.example',to: 'hello@meridiandisplay.example',cc: 'Lena.Ortiz@lakeside-expo.example',})const out = await handleCompose(viewerFor('hello@meridiandisplay.example'),env,form({ ...GOOD, all: '1' }),ALL,deps({ fetchBody: async () => thread }),)expect(out.kind).toBe('confirm')if (out.kind !== 'confirm') returnexpect(out.outgoing.to).toBe('sasha@lakeside-expo.example')expect(out.outgoing.cc).toBe('Lena.Ortiz@lakeside-expo.example')expect(out.mode).toBe('reply')})it('sends nothing and writes nothing', async () => {const { env, kv } = envOf()const d = deps()await handleCompose(viewerFor('hello@meridiandisplay.example'), env, form({ ...GOOD, all: '1' }), ALL, d)expect(d.sent).toHaveLength(0)expect([...kv.store.keys()]).toEqual([])})it('carries the staged files forward for the confirmation page to show', async () => {const { env } = envOf()const f = new FormData()for (const [k, v] of Object.entries(GOOD)) f.set(k, v)f.append('files', new File([new Uint8Array([1, 2, 3])], 'proof.pdf', { type: 'application/pdf' }))const out = await handleCompose(viewerFor('hello@meridiandisplay.example'), env, f, ALL, deps())expect(out.kind).toBe('confirm')if (out.kind !== 'confirm') returnexpect(out.staged).toHaveLength(1)expect(out.staged[0]!.name).toBe('proof.pdf')expect(out.outgoing.attachments[0]!.bytes.byteLength).toBe(3)})it('refuses an over-cap upload at the compose step, before any confirmation', async () => {const { env } = envOf()const f = new FormData()for (const [k, v] of Object.entries(GOOD)) f.set(k, v)f.append('files', new File([new Uint8Array(5 * 1024 * 1024)], 'big.pdf', { type: 'application/pdf' }))const out = await handleCompose(viewerFor('hello@meridiandisplay.example'), env, f, ALL, deps())expect(out.kind).toBe('error')if (out.kind === 'error') expect(out.message).toContain('big.pdf')})})describe('handleSend, the happy path and the record', () => {it('writes "sending" BEFORE Gmail is called and overwrites it after', async () => {// A Worker torn down mid-flight must leave evidence rather than silence:// the outcome page then says "submitted, outcome not recorded; check the// Sent folder", which is the honest answer and not "failed".const { env, kv } = envOf()const seen: string[] = []const d = deps({sendMessage: async () => {seen.push(JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value).outcome)return { ok: true, id: 'gm1', threadId: 'gt1' }},})const out = await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, d)expect(seen).toEqual(['sending'])const rec = JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value) as SendRecordexpect(rec.outcome).toBe('sent')expect(rec.gmailId).toBe('gm1')expect(out).toEqual({ kind: 'redirect', location: '/inbox/sent?ref=n-11111111' })})it('records a failed send too, with its candidates and no cause picked', async () => {const { env, kv } = envOf()const d = deps({}, { ok: false, status: 403, error: 'insufficient scopes', candidates: ['a', 'b', 'c'] })await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, d)const rec = JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value) as SendRecordexpect(rec.outcome).toBe('failed')expect(rec.error).toBe('insufficient scopes')expect(rec.candidates).toHaveLength(3)// An attempt that left no trace would be the one worth hiding.expect(rec.at).toBeTruthy()})it('holds no message text: subject and addresses only', async () => {const { env, kv } = envOf()await handleSend(viewerFor('hello@meridiandisplay.example'),env,form({ ...GOOD, body: 'SECRET-REPLY-TEXT' }),ALL,deps(),)const raw = kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.valueexpect(raw).not.toContain('SECRET-REPLY-TEXT')expect(JSON.stringify(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.metadata)).not.toContain('SECRET-REPLY-TEXT')})it('a second post of the same nonce does not send again', async () => {const { env } = envOf()const d = deps()await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, d)const again = await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, d)expect(d.sent).toHaveLength(1)expect(again).toEqual({ kind: 'redirect', location: '/inbox/sent?ref=n-11111111&dup=1' })})it('applies the mailbox’s signature, and records that it did', async () => {const { env, kv } = envOf({[SEND_KEY.signatures]: JSON.stringify({'hello@meridiandisplay.example': { html: '<p>Sam</p>', by: 'Sam', at: '2026-09-04T00:00:00.000Z' },}),})const d = deps()await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, d)expect(d.sent[0]!.signatureHtml).toBe('<p>Sam</p>')expect((JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value) as SendRecord).signatureApplied).toBe(true)})it('sends unsigned rather than failing when the signature store is unreadable', async () => {const { env, kv } = envOf({ [SEND_KEY.signatures]: 'not json at all' })const d = deps()await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, d)expect(d.sent).toHaveLength(1)expect(d.sent[0]!.signatureHtml).toBeNull()expect((JSON.parse(kv.store.get(`${SEND_KEY.sentPrefix}n-11111111`)!.value) as SendRecord).signatureApplied).toBe(false)})})describe('the replied mark is persistent board state', () => {it('is written on a successful send, keyed like a read mark', async () => {const { env } = envOf()await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, deps())const marks = await readReplied(env)expect(marks['hello@meridiandisplay.example/m1']).toMatchObject({ from: 'hello@meridiandisplay.example', by: 'Sam Whitaker' })})it('is NOT written when the send failed', async () => {// A "replied" chip on a message nobody answered is worse than no chip: in a// shared mailbox it is what makes the next person not answer it either.const { env } = envOf()const d = deps({}, { ok: false, status: 500, error: 'boom', candidates: ['x'] })await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, d)expect(await readReplied(env)).toEqual({})})it('survives a rewrite of the message list, because it is not derived from it', async () => {// The 15-minute sync replaces inbox:messages wholesale; the mark lives in// its own key and is untouched.const { env, kv } = envOf()await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, deps())await kv.put('inbox:messages', JSON.stringify({ syncedAt: 'later', mailboxes: [] }))expect(Object.keys(await readReplied(env))).toEqual(['hello@meridiandisplay.example/m1'])})it('prunes past the list window, so it cannot grow without bound', async () => {const stale = Date.parse('2026-09-04T12:00:00.000Z') - REPLIED_MARK_MAX_AGE_MS - 1const { env } = envOf({[SEND_KEY.replied]: JSON.stringify({ 'x@y/old': { at: stale, by: 'Someone', from: 'x@y' } }),})await handleSend(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, deps())const marks = await readReplied(env)expect(marks['x@y/old']).toBeUndefined()expect(marks['hello@meridiandisplay.example/m1']).toBeTruthy()// 35 days, past the 30-day list window: a mark can never vanish from under// a message that is still on the page.expect(REPLIED_MARK_MAX_AGE_MS).toBeGreaterThan(30 * 24 * 60 * 60 * 1000)})it('degrades to no chips, not to broken, when the key is malformed', async () => {const { env } = envOf({ [SEND_KEY.replied]: '{{{' })expect(await readReplied(env)).toEqual({})})})describe('handleCompose', () => {it('confirms without sending or writing anything', async () => {const { env, kv } = envOf()const d = deps()const out = await handleCompose(viewerFor('hello@meridiandisplay.example'), env, form(GOOD), ALL, d)expect(out.kind).toBe('confirm')expect(d.sent).toHaveLength(0)expect(kv.store.size).toBe(0)})it('refuses the same viewers handleSend refuses', async () => {const { env } = envOf()expect((await handleCompose(viewerFor('nora.l@meridiandisplay.example'), env, form(GOOD), ALL, deps())).kind).toBe('denied')})it('reports a failed read of the original rather than composing a blank reply', async () => {const { env } = envOf()const out = await handleCompose(viewerFor('hello@meridiandisplay.example'),env,form(GOOD),ALL,deps({ fetchBody: async () => ({ error: 'Gmail returned HTTP 404 for that message.' }) }),)expect(out).toMatchObject({ kind: 'error' })})})describe('signatures', () => {const sam = viewerFor('hello@meridiandisplay.example')it('round-trips set, then clear', async () => {const { env } = envOf()await handleSignaturesPost(sam, env, form({ mailbox: 'hello@meridiandisplay.example', op: 'set', html: '<p>Hi</p>' }), Date.now)let store = await readSignatures(env)expect(store.ok && store.map['hello@meridiandisplay.example']?.html).toBe('<p>Hi</p>')await handleSignaturesPost(sam, env, form({ mailbox: 'hello@meridiandisplay.example', op: 'clear' }), Date.now)store = await readSignatures(env)expect(store.ok && store.map['hello@meridiandisplay.example']).toBeUndefined()})it('refuses a viewer who may read the mailbox but not send as it', async () => {const { env, kv } = envOf()const out = await handleSignaturesPost(viewerFor('studio@meridiandisplay.example'),env,form({ mailbox: 'accounts@meridiandisplay.example', op: 'set', html: '<p>x</p>' }),Date.now,)expect(out.kind).toBe('denied')expect(kv.store.size).toBe(0)})it('writes NOTHING when over the cap; a truncated signature would go out broken', async () => {const { env, kv } = envOf()const out = await handleSignaturesPost(sam,env,form({ mailbox: 'hello@meridiandisplay.example', op: 'set', html: 'x'.repeat(SIGNATURE_CAP + 1) }),Date.now,)expect(out).toMatchObject({ kind: 'redirect' })expect(out.kind === 'redirect' && out.location).toContain('rejected=')expect(kv.store.has(SEND_KEY.signatures)).toBe(false)})it('treats empty-after-trim as a clear, like the board’s notes', async () => {const { env } = envOf()await handleSignaturesPost(sam, env, form({ mailbox: 'hello@meridiandisplay.example', op: 'set', html: '<p>Hi</p>' }), Date.now)await handleSignaturesPost(sam, env, form({ mailbox: 'hello@meridiandisplay.example', op: 'set', html: ' ' }), Date.now)const store = await readSignatures(env)expect(store.ok && Object.keys(store.map)).toEqual([])})it('will not overwrite a store it could not read', async () => {// Otherwise one transient bad read permanently loses everyone's signature.const { env, kv } = envOf({ [SEND_KEY.signatures]: 'not json' })await handleSignaturesPost(sam, env, form({ mailbox: 'hello@meridiandisplay.example', op: 'set', html: '<p>x</p>' }), Date.now)expect(kv.store.get(SEND_KEY.signatures)!.value).toBe('not json')})it('surfaces a malformed store as failed rather than as an empty one', async () => {const { env } = envOf({ [SEND_KEY.signatures]: '["not","an","object"]' })const store = await readSignatures(env)expect(store.ok).toBe(false)})it('a missing key is ok and empty: nobody has set one yet', async () => {const { env } = envOf()expect(await readSignatures(env)).toEqual({ ok: true, map: {} })})})describe('listSent', () => {it('reads the listing from metadata, newest first, and labels truncation', async () => {// Rule 5: a capped list returns what it has and SAYS it is capped.const { env } = envOf()const d = deps()for (const n of ['n-aaaaaaaa', 'n-bbbbbbbb', 'n-cccccccc']) {await handleSend(viewerFor('hello@meridiandisplay.example'),env,form({ ...GOOD, nonce: n }),ALL,{ ...d, now: () => Date.parse(`2026-09-0${n === 'n-aaaaaaaa' ? 1 : n === 'n-bbbbbbbb' ? 2 : 3}T12:00:00.000Z`) },)}const all = await listSent(env)expect(all.rows.map((r) => r.nonce)).toEqual(['n-cccccccc', 'n-bbbbbbbb', 'n-aaaaaaaa'])expect(all.truncated).toBe(false)const capped = await listSent(env, 2)expect(capped.rows).toHaveLength(2)expect(capped.truncated).toBe(true)})})