Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/dev/seed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,17 @@ describe('seedDevData', () => {
supportAddress: SUPPORT_ADDRESS,
})

expect(result.conversationCount).toBe(6)
expect(result.conversationCount).toBe(7)

// Real sends went through the injected sender for every reply EXCEPT the
// one deliberately routed through the seed's own failing sender — see
// seed.ts's "failed" demo.
// seed.ts's "failed" demo. The HTML demo is inbound-only (no reply), so
// it adds a conversation without adding a send.
expect(sent.length).toBe(4)

const open = await store.listConversations({ limit: 50, folder: 'open' })
const closed = await store.listConversations({ limit: 50, folder: 'closed' })
expect(open).toHaveLength(5)
expect(open).toHaveLength(6)
expect(closed).toHaveLength(1)

const allThreads = await Promise.all(
Expand All @@ -83,5 +84,19 @@ describe('seedDevData', () => {
.find((t) => t.deliveryStatus === 'pending')
expect(pendingThread).toBeDefined()
expect(Date.now() - (pendingThread?.createdAt.getTime() ?? 0)).toBeGreaterThan(5 * 60_000)

// The HTML demo exists to exercise the inbox UI's sanitized-HTML path, so
// its inbound thread must actually carry a bodyHtml, stored verbatim
// (spec §5: the store returns untrusted HTML as-is; the renderer sanitizes)
// alongside a plain-text alternative. Assert the four things the UI must
// handle survived storage: formatting, a link, a remote <img>, a <script>.
const htmlThread = allThreads.flatMap((c) => c?.threads ?? []).find((t) => t.bodyHtml !== null)
expect(htmlThread).toBeDefined()
expect(htmlThread?.direction).toBe('inbound')
expect(htmlThread?.bodyText).not.toBeNull()
expect(htmlThread?.bodyHtml).toContain('<strong>')
expect(htmlThread?.bodyHtml).toContain('<a href=')
expect(htmlThread?.bodyHtml).toContain('<img ')
expect(htmlThread?.bodyHtml).toContain('<script>')
})
})
42 changes: 40 additions & 2 deletions src/dev/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
* boot for the upcoming Agent Inbox UI (HT-23) to exercise every state it
* must render: an inbound-only conversation, a threaded back-and-forth, one
* outbound thread in each delivery state (`sent`/`failed`/stale `pending`),
* and a closed conversation. Every name and message below is invented for
* this seed — never real customer data (CLAUDE.md).
* a closed conversation, and one inbound message with a rich HTML body (so
* the UI's sanitized-HTML path — spec §5's stored-XSS contract — is exercised
* against real data). Every name and message below is invented for this
* seed — never real customer data (CLAUDE.md).
*
* Reuses the real engine paths wherever practical rather than raw SQL, so
* seeding itself exercises the store and the send pipeline:
Expand Down Expand Up @@ -272,5 +274,41 @@ export async function seedDevData(deps: SeedDevDataDeps): Promise<SeedDevDataRes
}
await store.setConversationStatus(closedDemo.conversationId, 'closed')

// --- 7. Inbound-only with a rich HTML body. --------------------------------
// The one demo that exercises the inbox UI's sanitized-HTML path (spec
// §5's stored-XSS contract). The parser stores inbound HTML verbatim —
// `<script>` and all (specs/mail/threading.md §5, fixtures/mail/observed/
// html-body.json) — so this `bodyHtml` deliberately carries the four
// things the UI's sanitizer, its "HTML email · sanitized · external images
// blocked" caption, and its Show-original modal must all handle: formatting,
// a link, a remote `<img>` (a tracking pixel), and a `<script>`. The store
// returns it untouched (safe as JSON); sanitization is the renderer's job.
// `bodyText` is the plain-text alternative the same mail would carry.
await store.createConversation({
subject: 'Unexpected charge on my March invoice',
customerEmail: 'noah.feldman@example.test',
firstMessage: {
direction: 'inbound',
messageId: '<inbound-1@noah-feldman.example.test>',
fromAddress: 'noah.feldman@example.test',
bodyText:
'Hi there,\n\n' +
"My March invoice shows a charge I don't recognize — a line item for " +
'"Priority Support" that I never signed up for.\n\n' +
'Here is the invoice in question: https://billing.example.com/invoices/48213\n\n' +
'Could you take a look and let me know? Thanks,\nNoah',
bodyHtml:
'<p>Hi there,</p>' +
'<p>My <strong>March invoice</strong> shows a charge I don&rsquo;t recognize &mdash; ' +
'a line item for <em>&ldquo;Priority Support&rdquo;</em> that I never signed up for.</p>' +
'<p>Here is the invoice in question: ' +
'<a href="https://billing.example.com/invoices/48213">billing.example.com/invoices/48213</a></p>' +
'<p>Could you take a look and let me know? Thanks,<br>Noah</p>' +
'<img src="https://tracker.example.com/o.gif?u=48213" width="1" height="1" alt="">' +
'<script>document.title = "pwned"</script>',
},
})
conversationCount++

return { conversationCount }
}
Loading