|
1 | 1 | import { describe, expect, it } from 'vitest'; |
2 | 2 |
|
| 3 | +import { campaignDraftViolations } from '../campaign/templates.js'; |
3 | 4 | import { renderFulfillmentTemplate } from './templates.js'; |
4 | 5 |
|
5 | 6 | const URL_PATTERN = /https:\/\/[^\s]+/gu; |
6 | 7 | const HTML_PATTERN = /<\/?[a-z][^>]*>/iu; |
| 8 | +const CONTRACTION_PATTERN = /\b\w+['’]\w+\b/u; |
| 9 | + |
| 10 | +function everyFulfillmentMessage() { |
| 11 | + return [ |
| 12 | + renderFulfillmentTemplate({ context: 'whitepaper', paper: 'overview' }), |
| 13 | + renderFulfillmentTemplate({ context: 'newsletter' }), |
| 14 | + renderFulfillmentTemplate({ context: 'contact' }), |
| 15 | + renderFulfillmentTemplate({ context: 'pricing' }), |
| 16 | + renderFulfillmentTemplate({ |
| 17 | + context: 'project-connect', |
| 18 | + claimedSignals: ['thread.persisted'], |
| 19 | + }), |
| 20 | + ]; |
| 21 | +} |
7 | 22 |
|
8 | 23 | describe('renderFulfillmentTemplate', () => { |
9 | 24 | it.each([ |
@@ -35,20 +50,24 @@ describe('renderFulfillmentTemplate', () => { |
35 | 50 | paper, |
36 | 51 | }); |
37 | 52 |
|
38 | | - expect(message).toEqual({ |
39 | | - subject, |
40 | | - body: `Here is the guide you requested:\n\n${url}`, |
41 | | - }); |
| 53 | + expect(message.subject).toBe(subject); |
| 54 | + expect( |
| 55 | + message.body.startsWith(`Here is the guide you requested:\n${url}\n\n`) |
| 56 | + ).toBe(true); |
| 57 | + expect(message.body.match(URL_PATTERN)).toEqual([url]); |
42 | 58 | } |
43 | 59 | ); |
44 | 60 |
|
45 | 61 | it('welcomes a newsletter signup without adding another request', () => { |
46 | | - expect(renderFulfillmentTemplate({ context: 'newsletter' })).toEqual({ |
47 | | - subject: 'Welcome to Threadplane', |
48 | | - body: expect.stringMatching( |
49 | | - /^Thanks for signing up\. I’ll keep these notes focused on practical engineering work with agent interfaces\.$/u |
50 | | - ), |
51 | | - }); |
| 62 | + const message = renderFulfillmentTemplate({ context: 'newsletter' }); |
| 63 | + |
| 64 | + expect(message.subject).toBe('Welcome to Threadplane'); |
| 65 | + expect(message.body.startsWith('You are on the list.')).toBe(true); |
| 66 | + expect(message.body).toContain( |
| 67 | + 'practical engineering work with agent interfaces' |
| 68 | + ); |
| 69 | + expect(message.body).not.toMatch(URL_PATTERN); |
| 70 | + expect(message.body).not.toContain('?'); |
52 | 71 | }); |
53 | 72 |
|
54 | 73 | it.each(['contact', 'pricing'] as const)( |
@@ -82,8 +101,10 @@ describe('renderFulfillmentTemplate', () => { |
82 | 101 | claimedSignals: [claim], |
83 | 102 | }); |
84 | 103 |
|
| 104 | + expect(message.body.startsWith('You connected your project.')).toBe(true); |
85 | 105 | expect(message.body).toContain(expectedFact); |
86 | 106 | expect(message.body).toContain('you shared'); |
| 107 | + expect(message.body).toContain('keep any follow-up to that context'); |
87 | 108 | expect(message.body).not.toMatch( |
88 | 109 | /I saw you|we noticed|based on your activity|tracking|telemetry/iu |
89 | 110 | ); |
@@ -131,19 +152,37 @@ describe('renderFulfillmentTemplate', () => { |
131 | 152 | expect(() => renderFulfillmentTemplate(input as never)).toThrow(); |
132 | 153 | }); |
133 | 154 |
|
134 | | - it('keeps every recipient message plain and compact', () => { |
135 | | - const messages = [ |
136 | | - renderFulfillmentTemplate({ context: 'whitepaper', paper: 'overview' }), |
137 | | - renderFulfillmentTemplate({ context: 'newsletter' }), |
138 | | - renderFulfillmentTemplate({ context: 'contact' }), |
139 | | - renderFulfillmentTemplate({ context: 'pricing' }), |
140 | | - renderFulfillmentTemplate({ |
141 | | - context: 'project-connect', |
142 | | - claimedSignals: ['thread.persisted'], |
143 | | - }), |
144 | | - ]; |
| 155 | + it('writes every recipient message in the campaign register', () => { |
| 156 | + for (const message of everyFulfillmentMessage()) { |
| 157 | + // No contractions, no "thanks for" openers, no greeting baked into the |
| 158 | + // body (send.ts adds "Hey <name>," at send time), and every line short. |
| 159 | + expect(message.body).not.toMatch(CONTRACTION_PATTERN); |
| 160 | + expect(message.body).not.toMatch(/^thanks/iu); |
| 161 | + expect(message.body).not.toMatch(/^hey\b/iu); |
| 162 | + expect(message.body).not.toMatch(/\blet['’]?s\b/iu); |
| 163 | + for (const line of message.body.split('\n')) { |
| 164 | + expect(line.trim().split(/\s+/u).filter(Boolean).length).toBeLessThan( |
| 165 | + 25 |
| 166 | + ); |
| 167 | + } |
| 168 | + } |
| 169 | + }); |
| 170 | + |
| 171 | + it('stays inside the recipient-copy checks shared with the campaign', () => { |
| 172 | + for (const message of everyFulfillmentMessage()) { |
| 173 | + expect(campaignDraftViolations(message)).toEqual([]); |
| 174 | + } |
| 175 | + for (const paper of ['angular', 'render', 'chat'] as const) { |
| 176 | + expect( |
| 177 | + campaignDraftViolations( |
| 178 | + renderFulfillmentTemplate({ context: 'whitepaper', paper }) |
| 179 | + ) |
| 180 | + ).toEqual([]); |
| 181 | + } |
| 182 | + }); |
145 | 183 |
|
146 | | - for (const message of messages) { |
| 184 | + it('keeps every recipient message plain and compact', () => { |
| 185 | + for (const message of everyFulfillmentMessage()) { |
147 | 186 | expect(typeof message.subject).toBe('string'); |
148 | 187 | expect(typeof message.body).toBe('string'); |
149 | 188 | expect(message.subject).not.toMatch(/[\r\n]/u); |
|
0 commit comments