backend-manager 5.11.4 → 5.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/manager/events/cron/frequent/marketing-campaigns.js +6 -3
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +4 -1
- package/src/manager/index.js +4 -4
- package/src/manager/libraries/prompts/infer-contact.md +5 -0
- package/src/manager/routes/marketing/contact/post.js +4 -1
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-24-21/metadata.json +14 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-24-21/newsletter.html +486 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-24-21/newsletter.md +41 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-24-21/newsletter.mjml +97 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-24-21/structure.json +42 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-24-21/summary.md +1 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-25-51/metadata.json +14 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-25-51/newsletter.html +486 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-25-51/newsletter.md +41 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-25-51/newsletter.mjml +97 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-25-51/structure.json +42 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-25-51/summary.md +1 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-27-47/metadata.json +14 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-27-47/newsletter.html +486 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-27-47/newsletter.md +41 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-27-47/newsletter.mjml +97 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-27-47/structure.json +42 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-07-03T23-27-47/summary.md +1 -0
- package/src/test/fixtures/firebase-project/.temp/test-mode.json +1 -1
- package/src/test/fixtures/firebase-project/database-debug.log +14 -8
- package/src/test/fixtures/firebase-project/firestore-debug.log +1345 -54
- package/src/test/fixtures/firebase-project/pubsub-debug.log +6 -4
- package/test/email/campaign-cron-pipeline.js +5 -1
- package/test/email/marketing-lifecycle.js +5 -0
- package/test/email/validation.js +42 -7
- package/test/events/auth-delete-race.js +11 -2
- package/test/functions/admin/edit-post.js +14 -10
- package/test/functions/general/add-marketing-contact.js +56 -16
- package/test/routes/admin/post.js +14 -10
- package/test/routes/marketing/contact.js +43 -10
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
This is the Google Pub/Sub fake.
|
|
2
2
|
Implementation may be incomplete or differ from the real system.
|
|
3
|
-
Jul
|
|
3
|
+
Jul 03, 2026 4:27:25 PM com.google.cloud.pubsub.testing.v1.Main main
|
|
4
4
|
INFO: IAM integration is disabled. IAM policy methods and ACL checks are not supported
|
|
5
5
|
SLF4J(W): No SLF4J providers were found.
|
|
6
6
|
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
|
|
7
7
|
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
|
|
8
8
|
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
|
9
|
-
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/Users/ian/.cache/firebase/emulators/pubsub-emulator-0.8.
|
|
9
|
+
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/Users/ian/.cache/firebase/emulators/pubsub-emulator-0.8.34/pubsub-emulator/lib/cloud-pubsub-emulator-0.8.34-all.jar)
|
|
10
10
|
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
|
|
11
11
|
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
|
|
12
|
-
Jul
|
|
12
|
+
Jul 03, 2026 4:27:25 PM com.google.cloud.pubsub.testing.v1.Main main
|
|
13
13
|
INFO: Server started, listening on 8085
|
|
14
|
-
Jul
|
|
14
|
+
Jul 03, 2026 4:27:31 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
15
|
+
INFO: Detected HTTP/2 connection.
|
|
16
|
+
Jul 03, 2026 4:27:45 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
15
17
|
INFO: Detected HTTP/2 connection.
|
|
16
18
|
*** shutting down gRPC server since JVM is shutting down
|
|
17
19
|
*** server shut down
|
|
@@ -255,7 +255,11 @@ module.exports = {
|
|
|
255
255
|
const unknownGenerator = await firestore.get('marketing-campaigns/_test-unknown-generator');
|
|
256
256
|
const unknownType = await firestore.get('marketing-campaigns/_test-unknown-type');
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
// 'sent'/'failed' — NOT merely !== 'pending': the claim flips the
|
|
259
|
+
// doc to 'processing' first, and proceeding on that mid-flight
|
|
260
|
+
// state races the send (oneoff-campaign-processed then reads
|
|
261
|
+
// 'processing' and fails).
|
|
262
|
+
return ['sent', 'failed'].includes(oneoff?.status)
|
|
259
263
|
&& recurring?.sendAt !== state.pastSendAt
|
|
260
264
|
&& recurringStale?.sendAt !== state.staleSendAt
|
|
261
265
|
&& ['sent', 'failed'].includes(staleProcessing?.status)
|
|
@@ -37,6 +37,11 @@ module.exports = {
|
|
|
37
37
|
firstName: 'Lifecycle',
|
|
38
38
|
lastName: 'Test',
|
|
39
39
|
source: 'bem-test-lifecycle',
|
|
40
|
+
// skipValidation bypasses the mailbox verification check — the lifecycle
|
|
41
|
+
// address has no real mailbox so the provider (correctly) marks it
|
|
42
|
+
// undeliverable. This suite tests add → sync → remove against the real
|
|
43
|
+
// providers, not deliverability (covered by email/validation + contact tests).
|
|
44
|
+
skipValidation: true,
|
|
40
45
|
});
|
|
41
46
|
|
|
42
47
|
assert.isSuccess(response, 'Add contact should succeed');
|
package/test/email/validation.js
CHANGED
|
@@ -679,8 +679,10 @@ module.exports = {
|
|
|
679
679
|
async run({ assert, skip }) {
|
|
680
680
|
const result = await validate('disposable@gmail.com', { checks: ALL_CHECKS });
|
|
681
681
|
|
|
682
|
-
|
|
683
|
-
|
|
682
|
+
// Provider errors fail open as { valid: true, error } with NO status field —
|
|
683
|
+
// out of credits, bad key, timeouts. Nothing assertable; skip.
|
|
684
|
+
if (result.checks.mailbox?.error) {
|
|
685
|
+
skip(`Mailbox verification unavailable: ${result.checks.mailbox.error}`);
|
|
684
686
|
}
|
|
685
687
|
|
|
686
688
|
assert.hasProperty(result, 'checks.mailbox', 'Should have mailbox check');
|
|
@@ -689,7 +691,7 @@ module.exports = {
|
|
|
689
691
|
},
|
|
690
692
|
|
|
691
693
|
{
|
|
692
|
-
name: 'mailbox-
|
|
694
|
+
name: 'mailbox-not-called-when-dns-fails',
|
|
693
695
|
timeout: 15000,
|
|
694
696
|
skip: !process.env.TEST_EXTENDED_MODE || !HAS_MAILBOX_API_KEY
|
|
695
697
|
? 'TEST_EXTENDED_MODE or mailbox API key not set'
|
|
@@ -698,12 +700,45 @@ module.exports = {
|
|
|
698
700
|
async run({ assert, skip }) {
|
|
699
701
|
const result = await validate('rachel.greene@thisfakedomain99999.com', { checks: ALL_CHECKS });
|
|
700
702
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
+
// Transient resolver errors fail open (dns: { valid: true, skipped: true })
|
|
704
|
+
// and let mailbox run — the short-circuit can't be observed that run.
|
|
705
|
+
if (result.checks.dns?.skipped) {
|
|
706
|
+
skip(`DNS lookup errored (${result.checks.dns.reason}) — cannot observe short-circuit`);
|
|
703
707
|
}
|
|
704
708
|
|
|
705
|
-
assert.
|
|
706
|
-
assert.
|
|
709
|
+
assert.equal(result.valid, false, 'Fake domain should be invalid');
|
|
710
|
+
assert.propertyEquals(result, 'checks.dns.valid', false, 'DNS check should fail (NXDOMAIN)');
|
|
711
|
+
assert.equal(
|
|
712
|
+
result.checks.mailbox,
|
|
713
|
+
undefined,
|
|
714
|
+
'Mailbox must NOT run after dns fails — the short-circuit saves a paid API call'
|
|
715
|
+
);
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
{
|
|
720
|
+
name: 'mailbox-nonexistent-mailbox-not-valid',
|
|
721
|
+
timeout: 15000,
|
|
722
|
+
skip: !process.env.TEST_EXTENDED_MODE || !HAS_MAILBOX_API_KEY
|
|
723
|
+
? 'TEST_EXTENDED_MODE or mailbox API key not set'
|
|
724
|
+
: false,
|
|
725
|
+
|
|
726
|
+
async run({ assert, skip }) {
|
|
727
|
+
// Real-MX domain (gmail passes dns) with a fabricated local part — this is
|
|
728
|
+
// the case that actually reaches the paid mailbox provider.
|
|
729
|
+
const result = await validate('rachel.greene.zk84qx@gmail.com', { checks: ALL_CHECKS });
|
|
730
|
+
|
|
731
|
+
assert.hasProperty(result, 'checks.mailbox', 'Should have mailbox check');
|
|
732
|
+
|
|
733
|
+
const mb = result.checks.mailbox;
|
|
734
|
+
|
|
735
|
+
// Provider errors fail open as { valid: true, error } with NO status; skip.
|
|
736
|
+
if (mb.error) {
|
|
737
|
+
skip(`Mailbox verification unavailable: ${mb.error}`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
assert.hasProperty(mb, 'status', 'Should have status');
|
|
741
|
+
assert.notEqual(mb.status, 'valid', 'Fabricated gmail mailbox should not verify as valid');
|
|
707
742
|
},
|
|
708
743
|
},
|
|
709
744
|
],
|
|
@@ -47,7 +47,7 @@ module.exports = {
|
|
|
47
47
|
|
|
48
48
|
{
|
|
49
49
|
name: 'no-wait-gets-clobbered',
|
|
50
|
-
async run({ Manager, assert }) {
|
|
50
|
+
async run({ Manager, assert, skip }) {
|
|
51
51
|
const admin = Manager.libraries.admin;
|
|
52
52
|
const testUid = '_test-race-no-wait';
|
|
53
53
|
const testEmail = '_test.race-no-wait@test.com';
|
|
@@ -76,7 +76,16 @@ module.exports = {
|
|
|
76
76
|
const doc = await userRef.get();
|
|
77
77
|
const survived = doc.exists && !!(doc.data()?.api?.clientId);
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
// The race resolves either way: when the emulator happens to finish
|
|
80
|
+
// the on-delete BEFORE the re-create's onCreate writes the doc, the
|
|
81
|
+
// dangerous late-clobber ordering never materializes that run —
|
|
82
|
+
// scheduler luck, not a regression. Only the clobber outcome is
|
|
83
|
+
// assertable; the benign ordering skips.
|
|
84
|
+
if (survived) {
|
|
85
|
+
await admin.auth().deleteUser(testUid).catch(() => {});
|
|
86
|
+
await pollUntilGone(userRef);
|
|
87
|
+
return skip('on-delete completed before the re-create this run — race did not manifest');
|
|
88
|
+
}
|
|
80
89
|
|
|
81
90
|
await admin.auth().deleteUser(testUid).catch(() => {});
|
|
82
91
|
await pollUntilGone(userRef);
|
|
@@ -243,10 +243,10 @@ module.exports = {
|
|
|
243
243
|
{
|
|
244
244
|
name: 'verify-edit',
|
|
245
245
|
auth: 'admin',
|
|
246
|
-
timeout:
|
|
246
|
+
timeout: 90000, // waitFor polls up to 60s for GitHub content propagation
|
|
247
247
|
skip: !process.env.GH_TOKEN ? 'GH_TOKEN env var not set' : false,
|
|
248
248
|
|
|
249
|
-
async run({ assert, state }) {
|
|
249
|
+
async run({ assert, state, waitFor }) {
|
|
250
250
|
if (!state.postPath || state.editSkipped) {
|
|
251
251
|
assert.ok(true, 'Skipping verify - edit was not performed');
|
|
252
252
|
return;
|
|
@@ -254,15 +254,19 @@ module.exports = {
|
|
|
254
254
|
|
|
255
255
|
const octokit = new Octokit({ auth: process.env.GH_TOKEN });
|
|
256
256
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
// GitHub's contents API is eventually consistent after createOrUpdateFileContents —
|
|
258
|
+
// an immediate read-back can serve the pre-edit blob. Poll until the edit lands.
|
|
259
|
+
const content = await waitFor(async () => {
|
|
260
|
+
const { data: fileData } = await octokit.rest.repos.getContent({
|
|
261
|
+
owner: state.owner,
|
|
262
|
+
repo: state.repo,
|
|
263
|
+
path: state.postPath,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const decoded = Buffer.from(fileData.content, 'base64').toString();
|
|
263
267
|
|
|
264
|
-
|
|
265
|
-
|
|
268
|
+
return decoded.includes('EDITED by the BEM test suite') ? decoded : false;
|
|
269
|
+
}, 60000, 3000);
|
|
266
270
|
|
|
267
271
|
assert.ok(
|
|
268
272
|
content.includes('EDITED by the BEM test suite'),
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Test email patterns - look like real emails but +bem suffix identifies them for cleanup.
|
|
10
|
-
// Fixed `
|
|
11
|
-
// state contamination in SendGrid/Beehiiv
|
|
12
|
-
|
|
10
|
+
// Fixed `itwcreativeworks.com` test domain — deterministic across brands (avoids cross-brand
|
|
11
|
+
// state contamination in SendGrid/Beehiiv) and we control it: mailbox verdicts are stable
|
|
12
|
+
// and the AI won't flag it as a placeholder (which `acme.com` now is — empty names,
|
|
13
|
+
// confidence 0 — breaking inference).
|
|
14
|
+
const TEST_DOMAIN = 'itwcreativeworks.com';
|
|
13
15
|
const TEST_EMAILS = {
|
|
14
16
|
valid: () => `sarah.martinez+bem@${TEST_DOMAIN}`, // Should infer: Sarah Martinez
|
|
15
17
|
invalid: () => `nonexistent.user+bem@${TEST_DOMAIN}`, // No such mailbox — mailbox verification should flag invalid
|
|
@@ -32,7 +34,11 @@ module.exports = {
|
|
|
32
34
|
const response = await http.command('general:add-marketing-contact', {
|
|
33
35
|
email: testEmail,
|
|
34
36
|
source: 'bem-test',
|
|
35
|
-
// No firstName/lastName - should be inferred
|
|
37
|
+
// No firstName/lastName - should be inferred
|
|
38
|
+
// skipValidation bypasses the mailbox verification check — the test email
|
|
39
|
+
// doesn't have a real mailbox so the provider (correctly) marks it as not
|
|
40
|
+
// deliverable. We're testing the command flow, not the deliverability check.
|
|
41
|
+
skipValidation: true,
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
assert.isSuccess(response, 'Add marketing contact should succeed for admin');
|
|
@@ -146,6 +152,10 @@ module.exports = {
|
|
|
146
152
|
email: testEmail,
|
|
147
153
|
source: 'bem-test',
|
|
148
154
|
// No firstName/lastName - should be inferred
|
|
155
|
+
// skipValidation bypasses the mailbox verification check — this test's concern
|
|
156
|
+
// is AI name inference (which runs after validation); without it the mailbox
|
|
157
|
+
// check 400s the fabricated address before the inference asserts run.
|
|
158
|
+
skipValidation: true,
|
|
149
159
|
});
|
|
150
160
|
|
|
151
161
|
assert.isSuccess(response, 'Add marketing contact should succeed');
|
|
@@ -227,6 +237,27 @@ module.exports = {
|
|
|
227
237
|
source: 'bem-test',
|
|
228
238
|
});
|
|
229
239
|
|
|
240
|
+
// Outcome A: the provider rejected the fabricated mailbox → admin 400. Every
|
|
241
|
+
// earlier check passes deterministically for this address (format/localPart/
|
|
242
|
+
// disposable/corporate return their own dedicated messages, typo can't match,
|
|
243
|
+
// and the domain has real MX), so reaching the generic validation-failed 400
|
|
244
|
+
// proves the paid mailbox pipeline ran and rejected. Nothing was added, so
|
|
245
|
+
// cleanup is a no-op.
|
|
246
|
+
if (response.status === 400) {
|
|
247
|
+
assert.ok(
|
|
248
|
+
response.error?.includes('Email validation failed'),
|
|
249
|
+
`400 should be the validation-failed branch, got: ${response.error}`
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (response.error.includes('(dns)')) {
|
|
253
|
+
skip('Transient DNS failure — mailbox check never ran');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
assert.ok(response.error.includes('(mailbox)'), `Failed check should be mailbox, got: ${response.error}`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Outcome B: provider accepted (valid/catch-all/unknown) or failed open.
|
|
230
261
|
assert.isSuccess(response, 'Add marketing contact should succeed');
|
|
231
262
|
|
|
232
263
|
// Check that validation info is included
|
|
@@ -238,9 +269,10 @@ module.exports = {
|
|
|
238
269
|
|
|
239
270
|
const mbResult = response.data.validation.checks.mailbox;
|
|
240
271
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
272
|
+
// Provider errors fail open as { valid: true, error, provider } with NO status
|
|
273
|
+
// field — out of credits, bad key, timeouts. Nothing assertable; skip.
|
|
274
|
+
if (mbResult.error) {
|
|
275
|
+
skip(`Mailbox verification unavailable: ${mbResult.error}`);
|
|
244
276
|
}
|
|
245
277
|
|
|
246
278
|
assert.hasProperty(mbResult, 'status', 'Mailbox check should return status');
|
|
@@ -268,7 +300,8 @@ module.exports = {
|
|
|
268
300
|
: false,
|
|
269
301
|
|
|
270
302
|
async run({ http, assert, skip }) {
|
|
271
|
-
//
|
|
303
|
+
// Email that should reach the mailbox provider and be flagged as undeliverable.
|
|
304
|
+
// Must NOT trip earlier checks (localPart blocklist, disposable, corporate).
|
|
272
305
|
const testEmail = TEST_EMAILS.invalid();
|
|
273
306
|
|
|
274
307
|
const response = await http.command('general:add-marketing-contact', {
|
|
@@ -276,20 +309,27 @@ module.exports = {
|
|
|
276
309
|
source: 'bem-test',
|
|
277
310
|
});
|
|
278
311
|
|
|
279
|
-
// Should still succeed (we fail open) but mailbox should report invalid
|
|
280
|
-
assert.isSuccess(response, 'Request should succeed even with invalid email');
|
|
281
|
-
|
|
282
312
|
const mbResult = response.data?.validation?.checks?.mailbox;
|
|
283
313
|
|
|
284
|
-
//
|
|
285
|
-
if (mbResult?.error
|
|
286
|
-
skip(
|
|
314
|
+
// Provider errors fail open with no status — the test can't exercise rejection; skip.
|
|
315
|
+
if (mbResult?.error) {
|
|
316
|
+
skip(`Mailbox verification unavailable: ${mbResult.error}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If the response was a 400, that's the legitimate rejection path — done.
|
|
320
|
+
if (response.status === 400) {
|
|
321
|
+
if (response.error?.includes('(dns)')) {
|
|
322
|
+
skip('Transient DNS failure — mailbox check never ran');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
assert.ok(response.error?.includes('(mailbox)'), `Failed check should be mailbox, got: ${response.error}`);
|
|
326
|
+
return;
|
|
287
327
|
}
|
|
288
328
|
|
|
289
|
-
//
|
|
329
|
+
// Otherwise expect a 200 with a non-"valid" mailbox status.
|
|
330
|
+
assert.isSuccess(response, 'Request should succeed (fail-open) or error 400');
|
|
290
331
|
if (mbResult) {
|
|
291
332
|
assert.hasProperty(mbResult, 'status', 'Should have status');
|
|
292
|
-
// Status should NOT be 'valid' for this fake email
|
|
293
333
|
assert.notEqual(mbResult.status, 'valid', 'Fake email should not be marked valid');
|
|
294
334
|
}
|
|
295
335
|
},
|
|
@@ -245,10 +245,10 @@ module.exports = {
|
|
|
245
245
|
{
|
|
246
246
|
name: 'verify-edit',
|
|
247
247
|
auth: 'admin',
|
|
248
|
-
timeout:
|
|
248
|
+
timeout: 90000, // waitFor polls up to 60s for GitHub content propagation
|
|
249
249
|
skip: !process.env.GH_TOKEN ? 'GH_TOKEN env var not set' : false,
|
|
250
250
|
|
|
251
|
-
async run({ assert, state }) {
|
|
251
|
+
async run({ assert, state, waitFor }) {
|
|
252
252
|
if (!state.postPath || state.editSkipped) {
|
|
253
253
|
assert.ok(true, 'Skipping verify - edit was not performed');
|
|
254
254
|
return;
|
|
@@ -256,15 +256,19 @@ module.exports = {
|
|
|
256
256
|
|
|
257
257
|
const octokit = new Octokit({ auth: process.env.GH_TOKEN });
|
|
258
258
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
259
|
+
// GitHub's contents API is eventually consistent after createOrUpdateFileContents —
|
|
260
|
+
// an immediate read-back can serve the pre-edit blob. Poll until the edit lands.
|
|
261
|
+
const content = await waitFor(async () => {
|
|
262
|
+
const { data: fileData } = await octokit.rest.repos.getContent({
|
|
263
|
+
owner: state.owner,
|
|
264
|
+
repo: state.repo,
|
|
265
|
+
path: state.postPath,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const decoded = Buffer.from(fileData.content, 'base64').toString();
|
|
265
269
|
|
|
266
|
-
|
|
267
|
-
|
|
270
|
+
return decoded.includes('EDITED by the BEM test suite') ? decoded : false;
|
|
271
|
+
}, 60000, 3000);
|
|
268
272
|
|
|
269
273
|
assert.ok(
|
|
270
274
|
content.includes('EDITED by the BEM test suite'),
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
// Test email patterns - look like real emails but +bem suffix identifies them for cleanup.
|
|
10
10
|
// Names should be inferred by AI from the email local part.
|
|
11
11
|
//
|
|
12
|
-
// Fixed test domain (`
|
|
13
|
-
// domain caused cross-brand state divergence in SendGrid/Beehiiv and
|
|
14
|
-
// company inference
|
|
12
|
+
// Fixed test domain (`itwcreativeworks.com`) — deterministic across brands (using the
|
|
13
|
+
// running brand's domain caused cross-brand state divergence in SendGrid/Beehiiv and
|
|
14
|
+
// non-deterministic company inference), and we control it: mailbox verdicts are stable
|
|
15
|
+
// and any double-opt-in email lands on our own domain. Was `acme.com`, but the AI now
|
|
16
|
+
// treats that domain as a placeholder (empty names, confidence 0), breaking inference.
|
|
15
17
|
//
|
|
16
18
|
// `valid`: use a name that won't be flagged as fictional/placeholder by the AI prompt.
|
|
17
19
|
// (The infer-contact prompt rejects fictional names — e.g. "rachel.greene" sometimes
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
// NOT start with "test"/"example" which are in BLOCKED_LOCAL_PATTERNS, NOT be on
|
|
22
24
|
// a corporate/disposable domain). Real-looking name on a real domain with no actual
|
|
23
25
|
// mailbox there is the safest pick.
|
|
24
|
-
const TEST_DOMAIN = '
|
|
26
|
+
const TEST_DOMAIN = 'itwcreativeworks.com';
|
|
25
27
|
const TEST_EMAILS = {
|
|
26
28
|
valid: () => `sarah.martinez+bem@${TEST_DOMAIN}`, // Should infer: Sarah Martinez
|
|
27
29
|
invalid: () => `nonexistent.user+bem@${TEST_DOMAIN}`, // No such mailbox — mailbox verification should flag as invalid
|
|
@@ -164,6 +166,10 @@ module.exports = {
|
|
|
164
166
|
email: testEmail,
|
|
165
167
|
source: 'bem-test',
|
|
166
168
|
// No firstName/lastName - should be inferred
|
|
169
|
+
// skipValidation bypasses the mailbox verification check — this test's concern
|
|
170
|
+
// is AI name inference (which runs after validation); without it the mailbox
|
|
171
|
+
// check 400s the fabricated address before the inference asserts run.
|
|
172
|
+
skipValidation: true,
|
|
167
173
|
});
|
|
168
174
|
|
|
169
175
|
assert.isSuccess(response, 'Add marketing contact should succeed');
|
|
@@ -245,6 +251,27 @@ module.exports = {
|
|
|
245
251
|
source: 'bem-test',
|
|
246
252
|
});
|
|
247
253
|
|
|
254
|
+
// Outcome A: the provider rejected the fabricated mailbox → admin 400. Every
|
|
255
|
+
// earlier check passes deterministically for this address (format/localPart/
|
|
256
|
+
// disposable/corporate return their own dedicated messages, typo can't match,
|
|
257
|
+
// and the domain has real MX), so reaching the generic validation-failed 400
|
|
258
|
+
// proves the paid mailbox pipeline ran and rejected. Nothing was added, so
|
|
259
|
+
// cleanup is a no-op.
|
|
260
|
+
if (response.status === 400) {
|
|
261
|
+
assert.ok(
|
|
262
|
+
response.error?.includes('Email validation failed'),
|
|
263
|
+
`400 should be the validation-failed branch, got: ${response.error}`
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (response.error.includes('(dns)')) {
|
|
267
|
+
skip('Transient DNS failure — mailbox check never ran');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
assert.ok(response.error.includes('(mailbox)'), `Failed check should be mailbox, got: ${response.error}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Outcome B: provider accepted (valid/catch-all/unknown) or failed open.
|
|
248
275
|
assert.isSuccess(response, 'Add marketing contact should succeed');
|
|
249
276
|
|
|
250
277
|
// Check that validation info is included
|
|
@@ -256,9 +283,10 @@ module.exports = {
|
|
|
256
283
|
|
|
257
284
|
const mbResult = response.data.validation.checks.mailbox;
|
|
258
285
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
286
|
+
// Provider errors fail open as { valid: true, error, provider } with NO status
|
|
287
|
+
// field — out of credits, bad key, timeouts. Nothing assertable; skip.
|
|
288
|
+
if (mbResult.error) {
|
|
289
|
+
skip(`Mailbox verification unavailable: ${mbResult.error}`);
|
|
262
290
|
}
|
|
263
291
|
|
|
264
292
|
assert.hasProperty(mbResult, 'status', 'Mailbox check should return status');
|
|
@@ -301,13 +329,18 @@ module.exports = {
|
|
|
301
329
|
// verify here is that mailbox check ran and didn't mark the email as `valid`.
|
|
302
330
|
const mbResult = response.data?.validation?.checks?.mailbox;
|
|
303
331
|
|
|
304
|
-
//
|
|
305
|
-
if (mbResult?.error
|
|
306
|
-
skip(
|
|
332
|
+
// Provider errors fail open with no status — the test can't exercise rejection; skip.
|
|
333
|
+
if (mbResult?.error) {
|
|
334
|
+
skip(`Mailbox verification unavailable: ${mbResult.error}`);
|
|
307
335
|
}
|
|
308
336
|
|
|
309
337
|
// If the response was a 400, that's the legitimate rejection path — done.
|
|
310
338
|
if (response.status === 400) {
|
|
339
|
+
if (response.error?.includes('(dns)')) {
|
|
340
|
+
skip('Transient DNS failure — mailbox check never ran');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
assert.ok(response.error?.includes('(mailbox)'), `Failed check should be mailbox, got: ${response.error}`);
|
|
311
344
|
return;
|
|
312
345
|
}
|
|
313
346
|
|