backend-manager 5.0.150 → 5.0.151
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/functions/core/actions/api/general/add-marketing-contact.js +3 -0
- package/src/manager/libraries/email/constants.js +1 -0
- package/src/manager/libraries/email/marketing/index.js +3 -1
- package/src/manager/libraries/email/providers/beehiiv.js +7 -2
- package/src/manager/libraries/email/providers/sendgrid.js +10 -1
- package/src/manager/libraries/infer-contact.js +11 -8
- package/src/manager/libraries/prompts/infer-contact.md +39 -9
- package/src/manager/routes/admin/infer-contact/post.js +34 -0
- package/src/manager/routes/marketing/contact/post.js +4 -1
- package/src/manager/schemas/admin/infer-contact/post.js +12 -0
- package/test/routes/admin/infer-contact.js +195 -0
package/package.json
CHANGED
|
@@ -95,10 +95,12 @@ Module.prototype.main = function () {
|
|
|
95
95
|
|
|
96
96
|
// Infer name if not provided
|
|
97
97
|
let nameInferred = null;
|
|
98
|
+
let company = '';
|
|
98
99
|
if (!firstName && !lastName) {
|
|
99
100
|
nameInferred = await inferContact(email, assistant);
|
|
100
101
|
firstName = nameInferred.firstName;
|
|
101
102
|
lastName = nameInferred.lastName;
|
|
103
|
+
company = nameInferred.company;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
// Add to providers
|
|
@@ -112,6 +114,7 @@ Module.prototype.main = function () {
|
|
|
112
114
|
email,
|
|
113
115
|
firstName,
|
|
114
116
|
lastName,
|
|
117
|
+
company,
|
|
115
118
|
source,
|
|
116
119
|
});
|
|
117
120
|
}
|
|
@@ -144,6 +144,7 @@ const FIELDS = {
|
|
|
144
144
|
|
|
145
145
|
// User identity
|
|
146
146
|
user_auth_uid: { source: 'user', path: 'auth.uid', type: 'text' },
|
|
147
|
+
user_personal_company: { source: 'user', path: 'personal.company.name', type: 'text' },
|
|
147
148
|
user_personal_country: { source: 'user', path: 'personal.location.country', type: 'text' },
|
|
148
149
|
user_metadata_signup_date: { source: 'user', path: 'metadata.created.timestamp', type: 'date' },
|
|
149
150
|
user_metadata_last_activity: { source: 'user', path: 'metadata.updated.timestamp', type: 'date' },
|
|
@@ -62,7 +62,7 @@ function Marketing(assistant) {
|
|
|
62
62
|
Marketing.prototype.add = async function (options) {
|
|
63
63
|
const self = this;
|
|
64
64
|
const assistant = self.assistant;
|
|
65
|
-
const { email, firstName, lastName, source, customFields } = options;
|
|
65
|
+
const { email, firstName, lastName, company, source, customFields } = options;
|
|
66
66
|
|
|
67
67
|
if (!email) {
|
|
68
68
|
assistant.warn('Marketing.add(): No email provided, skipping');
|
|
@@ -85,6 +85,7 @@ Marketing.prototype.add = async function (options) {
|
|
|
85
85
|
email,
|
|
86
86
|
firstName,
|
|
87
87
|
lastName,
|
|
88
|
+
company,
|
|
88
89
|
customFields,
|
|
89
90
|
}).then((r) => { results.sendgrid = r; })
|
|
90
91
|
);
|
|
@@ -96,6 +97,7 @@ Marketing.prototype.add = async function (options) {
|
|
|
96
97
|
email,
|
|
97
98
|
firstName,
|
|
98
99
|
lastName,
|
|
100
|
+
company,
|
|
99
101
|
source,
|
|
100
102
|
}).then((r) => { results.beehiiv = r; })
|
|
101
103
|
);
|
|
@@ -215,20 +215,25 @@ async function getPublicationId() {
|
|
|
215
215
|
* @param {Array<{name: string, value: string}>} [options.customFields] - Pre-built custom fields
|
|
216
216
|
* @returns {{ success: boolean, id?: string, error?: string }}
|
|
217
217
|
*/
|
|
218
|
-
async function addContact({ email, firstName, lastName, source, customFields }) {
|
|
218
|
+
async function addContact({ email, firstName, lastName, company, source, customFields }) {
|
|
219
219
|
const publicationId = await getPublicationId();
|
|
220
220
|
|
|
221
221
|
if (!publicationId) {
|
|
222
222
|
return { success: false, error: 'Publication not found' };
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
const fields = [...(customFields || [])];
|
|
226
|
+
if (company) {
|
|
227
|
+
fields.push({ name: 'company', value: company });
|
|
228
|
+
}
|
|
229
|
+
|
|
225
230
|
return addSubscriber({
|
|
226
231
|
email,
|
|
227
232
|
firstName,
|
|
228
233
|
lastName,
|
|
229
234
|
source,
|
|
230
235
|
publicationId,
|
|
231
|
-
customFields:
|
|
236
|
+
customFields: fields,
|
|
232
237
|
});
|
|
233
238
|
}
|
|
234
239
|
|
|
@@ -370,7 +370,7 @@ async function listSingleSends(options) {
|
|
|
370
370
|
* @param {object} [options.customFields] - Pre-built custom_fields object (keyed by SendGrid field IDs)
|
|
371
371
|
* @returns {{ success: boolean, jobId?: string, listId?: string, error?: string }}
|
|
372
372
|
*/
|
|
373
|
-
async function addContact({ email, firstName, lastName, customFields }) {
|
|
373
|
+
async function addContact({ email, firstName, lastName, company, customFields }) {
|
|
374
374
|
const contact = {
|
|
375
375
|
email: email.toLowerCase(),
|
|
376
376
|
first_name: firstName || undefined,
|
|
@@ -378,6 +378,15 @@ async function addContact({ email, firstName, lastName, customFields }) {
|
|
|
378
378
|
custom_fields: customFields || {},
|
|
379
379
|
};
|
|
380
380
|
|
|
381
|
+
// Add company to custom fields if provided (requires field provisioned via OMEGA)
|
|
382
|
+
if (company) {
|
|
383
|
+
const idMap = await resolveFieldIds();
|
|
384
|
+
const companyFieldId = idMap['user_personal_company'];
|
|
385
|
+
if (companyFieldId) {
|
|
386
|
+
contact.custom_fields[companyFieldId] = company;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
381
390
|
const listId = await getListId();
|
|
382
391
|
const result = await upsertContacts({
|
|
383
392
|
contacts: [contact],
|
|
@@ -31,12 +31,14 @@ const GENERIC_DOMAINS = new Set([
|
|
|
31
31
|
async function inferContact(email, assistant) {
|
|
32
32
|
if (process.env.BACKEND_MANAGER_OPENAI_API_KEY) {
|
|
33
33
|
const aiResult = await inferContactWithAI(email, assistant);
|
|
34
|
-
if (aiResult
|
|
34
|
+
if (aiResult) {
|
|
35
35
|
return aiResult;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// TODO: Re-enable regex fallback if needed
|
|
40
|
+
// return inferContactFromEmail(email);
|
|
41
|
+
return { firstName: '', lastName: '', company: '', confidence: 0, method: 'none' };
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
@@ -48,7 +50,7 @@ async function inferContact(email, assistant) {
|
|
|
48
50
|
*/
|
|
49
51
|
async function inferContactWithAI(email, assistant) {
|
|
50
52
|
try {
|
|
51
|
-
const ai = assistant.Manager.AI(assistant);
|
|
53
|
+
const ai = assistant.Manager.AI(assistant, process.env.BACKEND_MANAGER_OPENAI_API_KEY);
|
|
52
54
|
const result = await ai.request({
|
|
53
55
|
model: 'gpt-5-mini',
|
|
54
56
|
timeout: 30000,
|
|
@@ -63,12 +65,13 @@ async function inferContactWithAI(email, assistant) {
|
|
|
63
65
|
},
|
|
64
66
|
});
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
const parsed = result?.content;
|
|
69
|
+
if (parsed?.firstName !== undefined) {
|
|
67
70
|
return {
|
|
68
|
-
firstName: capitalize(
|
|
69
|
-
lastName: capitalize(
|
|
70
|
-
company: capitalize(
|
|
71
|
-
confidence: typeof
|
|
71
|
+
firstName: capitalize(parsed.firstName || ''),
|
|
72
|
+
lastName: capitalize(parsed.lastName || ''),
|
|
73
|
+
company: capitalize(parsed.company || ''),
|
|
74
|
+
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5,
|
|
72
75
|
method: 'ai',
|
|
73
76
|
};
|
|
74
77
|
}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
<identity>
|
|
2
|
-
You extract names and company from email addresses.
|
|
2
|
+
You extract real human names and company from email addresses.
|
|
3
3
|
</identity>
|
|
4
4
|
|
|
5
|
+
<rules>
|
|
6
|
+
- Only return a name if it is a REAL human name. If the local part contains placeholder text, gibberish, generic words, or fictional/brand names, return empty strings for firstName and lastName.
|
|
7
|
+
- Placeholder/test examples that are NOT real names: firstname.lastname, first.last, asdf.qwerty, test.user, mixed.case, bobs.burgers, john.doe (this is a well-known placeholder)
|
|
8
|
+
- Generic role words are NOT names: admin, info, support, hello, contact, sales, billing, noreply, webmaster, postmaster, dev, ceo
|
|
9
|
+
- Single letters or initials ARE acceptable (e.g., "j" from j@company.com → firstName: "J")
|
|
10
|
+
- ALWAYS infer company from the domain, even when there is no name. Generic email providers (gmail.com, yahoo.com, hotmail.com, outlook.com, aol.com, icloud.com, mail.com, protonmail.com, proton.me, zoho.com, yandex.com, gmx.com, live.com, msn.com, me.com) should return empty company.
|
|
11
|
+
- Hyphenated names should preserve capitalization on both parts (e.g., jean-pierre → Jean-Pierre)
|
|
12
|
+
- confidence should reflect how certain you are that the name is a REAL person's name. Placeholders and gibberish should never have confidence above 0.
|
|
13
|
+
</rules>
|
|
14
|
+
|
|
5
15
|
<format>
|
|
6
|
-
Return ONLY valid JSON
|
|
16
|
+
Return ONLY valid JSON:
|
|
7
17
|
{
|
|
8
18
|
"firstName": "...",
|
|
9
19
|
"lastName": "...",
|
|
@@ -11,12 +21,12 @@ Return ONLY valid JSON like so:
|
|
|
11
21
|
"confidence": "..."
|
|
12
22
|
}
|
|
13
23
|
|
|
14
|
-
- firstName: First name (string), capitalized
|
|
15
|
-
- lastName: Last name (string), capitalized
|
|
16
|
-
- company: Company name (string), capitalized
|
|
17
|
-
- confidence: Confidence level (number), 0-1 scale
|
|
24
|
+
- firstName: First name (string), properly capitalized
|
|
25
|
+
- lastName: Last name (string), properly capitalized
|
|
26
|
+
- company: Company name (string), capitalized. Infer from domain (not generic providers).
|
|
27
|
+
- confidence: Confidence level (number), 0-1 scale. How sure you are the name is real.
|
|
18
28
|
|
|
19
|
-
If you cannot determine a name, use empty strings.
|
|
29
|
+
If you cannot determine a real name, use empty strings for firstName and lastName.
|
|
20
30
|
</format>
|
|
21
31
|
|
|
22
32
|
<examples>
|
|
@@ -30,7 +40,7 @@ If you cannot determine a name, use empty strings.
|
|
|
30
40
|
</example>
|
|
31
41
|
<example>
|
|
32
42
|
<input>support@bigcorp.io</input>
|
|
33
|
-
<output>{"firstName": "", "lastName": "", "company": "Bigcorp", "confidence": 0
|
|
43
|
+
<output>{"firstName": "", "lastName": "", "company": "Bigcorp", "confidence": 0}</output>
|
|
34
44
|
</example>
|
|
35
45
|
<example>
|
|
36
46
|
<input>mary_jane_watson@stark-industries.com</input>
|
|
@@ -38,6 +48,26 @@ If you cannot determine a name, use empty strings.
|
|
|
38
48
|
</example>
|
|
39
49
|
<example>
|
|
40
50
|
<input>info@company.org</input>
|
|
41
|
-
<output>{"firstName": "", "lastName": "", "company": "Company", "confidence": 0
|
|
51
|
+
<output>{"firstName": "", "lastName": "", "company": "Company", "confidence": 0}</output>
|
|
52
|
+
</example>
|
|
53
|
+
<example>
|
|
54
|
+
<input>firstname.lastname@company.com</input>
|
|
55
|
+
<output>{"firstName": "", "lastName": "", "company": "Company", "confidence": 0}</output>
|
|
56
|
+
</example>
|
|
57
|
+
<example>
|
|
58
|
+
<input>asdf.qwerty@outlook.com</input>
|
|
59
|
+
<output>{"firstName": "", "lastName": "", "company": "", "confidence": 0}</output>
|
|
60
|
+
</example>
|
|
61
|
+
<example>
|
|
62
|
+
<input>jean-pierre.dupont@orange.fr</input>
|
|
63
|
+
<output>{"firstName": "Jean-Pierre", "lastName": "Dupont", "company": "Orange", "confidence": 0.95}</output>
|
|
64
|
+
</example>
|
|
65
|
+
<example>
|
|
66
|
+
<input>bobs.burgers@example.com</input>
|
|
67
|
+
<output>{"firstName": "", "lastName": "", "company": "Example", "confidence": 0}</output>
|
|
68
|
+
</example>
|
|
69
|
+
<example>
|
|
70
|
+
<input>j@company.com</input>
|
|
71
|
+
<output>{"firstName": "J", "lastName": "", "company": "Company", "confidence": 0.3}</output>
|
|
42
72
|
</example>
|
|
43
73
|
</examples>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /admin/infer-contact - Infer contact info from email addresses
|
|
3
|
+
* Admin-only endpoint for testing/debugging the inferContact pipeline
|
|
4
|
+
*/
|
|
5
|
+
const { inferContact } = require('../../../libraries/infer-contact.js');
|
|
6
|
+
|
|
7
|
+
module.exports = async ({ assistant, user, settings }) => {
|
|
8
|
+
|
|
9
|
+
// Require authentication
|
|
10
|
+
if (!user.authenticated) {
|
|
11
|
+
return assistant.respond('Authentication required', { code: 401 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Require admin
|
|
15
|
+
if (!user.roles.admin) {
|
|
16
|
+
return assistant.respond('Admin required.', { code: 403 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Accept single email or array of emails
|
|
20
|
+
const emails = Array.isArray(settings.emails)
|
|
21
|
+
? settings.emails
|
|
22
|
+
: [settings.email];
|
|
23
|
+
|
|
24
|
+
const results = await Promise.all(
|
|
25
|
+
emails
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map(async (email) => {
|
|
28
|
+
const result = await inferContact(email, assistant);
|
|
29
|
+
return { email, ...result };
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return assistant.respond({ results });
|
|
34
|
+
};
|
|
@@ -83,12 +83,14 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// Infer
|
|
86
|
+
// Infer contact info if name not provided
|
|
87
87
|
let nameInferred = null;
|
|
88
|
+
let company = '';
|
|
88
89
|
if (!firstName && !lastName) {
|
|
89
90
|
nameInferred = await inferContact(email, assistant);
|
|
90
91
|
firstName = nameInferred.firstName;
|
|
91
92
|
lastName = nameInferred.lastName;
|
|
93
|
+
company = nameInferred.company;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
// Add to providers
|
|
@@ -102,6 +104,7 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
|
|
|
102
104
|
email,
|
|
103
105
|
firstName,
|
|
104
106
|
lastName,
|
|
107
|
+
company,
|
|
105
108
|
source,
|
|
106
109
|
});
|
|
107
110
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: POST /admin/infer-contact
|
|
3
|
+
* Tests the admin infer-contact endpoint for inferring names from email addresses
|
|
4
|
+
*
|
|
5
|
+
* AI inference tests only run when TEST_EXTENDED_MODE is set (requires BACKEND_MANAGER_OPENAI_API_KEY)
|
|
6
|
+
*/
|
|
7
|
+
module.exports = {
|
|
8
|
+
description: 'Admin infer contact',
|
|
9
|
+
type: 'group',
|
|
10
|
+
tests: [
|
|
11
|
+
// ─── Auth rejection ───
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
name: 'unauthenticated-rejected',
|
|
15
|
+
auth: 'none',
|
|
16
|
+
timeout: 15000,
|
|
17
|
+
|
|
18
|
+
async run({ http, assert }) {
|
|
19
|
+
const response = await http.post('admin/infer-contact', {
|
|
20
|
+
email: 'john.smith@gmail.com',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.isError(response, 401, 'Should reject unauthenticated requests');
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
name: 'non-admin-rejected',
|
|
29
|
+
auth: 'user',
|
|
30
|
+
timeout: 15000,
|
|
31
|
+
|
|
32
|
+
async run({ http, assert }) {
|
|
33
|
+
const response = await http.post('admin/infer-contact', {
|
|
34
|
+
email: 'john.smith@gmail.com',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
assert.isError(response, 403, 'Should reject non-admin users');
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// ─── Single email ───
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
name: 'single-email-returns-result',
|
|
45
|
+
auth: 'admin',
|
|
46
|
+
timeout: 30000,
|
|
47
|
+
|
|
48
|
+
async run({ http, assert }) {
|
|
49
|
+
const response = await http.post('admin/infer-contact', {
|
|
50
|
+
email: 'john.smith@gmail.com',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.isSuccess(response, 'Should succeed for admin');
|
|
54
|
+
assert.hasProperty(response, 'data.results', 'Should have results array');
|
|
55
|
+
assert.equal(response.data.results.length, 1, 'Should have 1 result');
|
|
56
|
+
|
|
57
|
+
const result = response.data.results[0];
|
|
58
|
+
assert.equal(result.email, 'john.smith@gmail.com', 'Should include email');
|
|
59
|
+
assert.ok(result.firstName, 'Should infer a first name');
|
|
60
|
+
assert.ok(result.lastName, 'Should infer a last name');
|
|
61
|
+
assert.hasProperty(result, 'method', 'Should include method');
|
|
62
|
+
assert.hasProperty(result, 'confidence', 'Should include confidence');
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// ─── Batch emails ───
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
name: 'batch-emails-returns-all-results',
|
|
70
|
+
auth: 'admin',
|
|
71
|
+
timeout: 60000,
|
|
72
|
+
|
|
73
|
+
async run({ http, assert }) {
|
|
74
|
+
const emails = [
|
|
75
|
+
'sarah.connor@skynet.io',
|
|
76
|
+
'bob-jones@hotmail.com',
|
|
77
|
+
'admin@acme.com',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const response = await http.post('admin/infer-contact', { emails });
|
|
81
|
+
|
|
82
|
+
assert.isSuccess(response, 'Should succeed for batch');
|
|
83
|
+
assert.equal(response.data.results.length, 3, 'Should have 3 results');
|
|
84
|
+
|
|
85
|
+
// Verify each email is in results
|
|
86
|
+
for (let i = 0; i < emails.length; i++) {
|
|
87
|
+
assert.equal(response.data.results[i].email, emails[i], `Result ${i} should match input email`);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// ─── Name parsing (regex) ───
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
name: 'regex-parses-dot-separated-names',
|
|
96
|
+
auth: 'admin',
|
|
97
|
+
timeout: 30000,
|
|
98
|
+
|
|
99
|
+
async run({ http, assert }) {
|
|
100
|
+
const response = await http.post('admin/infer-contact', {
|
|
101
|
+
email: 'alice.wonderland@example.com',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.isSuccess(response);
|
|
105
|
+
const result = response.data.results[0];
|
|
106
|
+
|
|
107
|
+
// AI or regex — either way should get the name right
|
|
108
|
+
assert.equal(result.firstName, 'Alice', 'Should parse first name');
|
|
109
|
+
assert.equal(result.lastName, 'Wonderland', 'Should parse last name');
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
name: 'infers-company-from-custom-domain',
|
|
115
|
+
auth: 'admin',
|
|
116
|
+
timeout: 30000,
|
|
117
|
+
|
|
118
|
+
async run({ http, assert }) {
|
|
119
|
+
const response = await http.post('admin/infer-contact', {
|
|
120
|
+
email: 'ceo@my-startup.com',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert.isSuccess(response);
|
|
124
|
+
const result = response.data.results[0];
|
|
125
|
+
assert.ok(result.company, 'Should infer company from custom domain');
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
name: 'no-company-from-generic-domain',
|
|
131
|
+
auth: 'admin',
|
|
132
|
+
timeout: 30000,
|
|
133
|
+
|
|
134
|
+
async run({ http, assert }) {
|
|
135
|
+
const response = await http.post('admin/infer-contact', {
|
|
136
|
+
email: 'someone@gmail.com',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
assert.isSuccess(response);
|
|
140
|
+
const result = response.data.results[0];
|
|
141
|
+
assert.equal(result.company, '', 'Should not infer company from gmail');
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// ─── AI inference (requires TEST_EXTENDED_MODE) ───
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
name: 'ai-inference',
|
|
149
|
+
auth: 'admin',
|
|
150
|
+
timeout: 60000,
|
|
151
|
+
skip: !process.env.TEST_EXTENDED_MODE
|
|
152
|
+
? 'TEST_EXTENDED_MODE not set (skipping AI inference test)'
|
|
153
|
+
: false,
|
|
154
|
+
|
|
155
|
+
async run({ http, assert }) {
|
|
156
|
+
const response = await http.post('admin/infer-contact', {
|
|
157
|
+
emails: [
|
|
158
|
+
'john.smith@microsoft.com',
|
|
159
|
+
'xkcd42@gmail.com',
|
|
160
|
+
'bobs.burgers@example.com',
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert.isSuccess(response, 'AI inference should succeed');
|
|
165
|
+
|
|
166
|
+
const results = response.data.results;
|
|
167
|
+
const aiResults = results.filter(r => r.method === 'ai');
|
|
168
|
+
|
|
169
|
+
assert.ok(aiResults.length > 0, 'At least one result should use AI method');
|
|
170
|
+
|
|
171
|
+
// john.smith should be parsed correctly regardless of method
|
|
172
|
+
const john = results.find(r => r.email === 'john.smith@microsoft.com');
|
|
173
|
+
assert.equal(john.firstName, 'John', 'Should infer John');
|
|
174
|
+
assert.equal(john.lastName, 'Smith', 'Should infer Smith');
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// ─── Edge cases ───
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
name: 'empty-emails-array-returns-empty',
|
|
182
|
+
auth: 'admin',
|
|
183
|
+
timeout: 15000,
|
|
184
|
+
|
|
185
|
+
async run({ http, assert }) {
|
|
186
|
+
const response = await http.post('admin/infer-contact', {
|
|
187
|
+
emails: [],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
assert.isSuccess(response);
|
|
191
|
+
assert.equal(response.data.results.length, 0, 'Empty input should return empty results');
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|