thinkwork-cli 0.12.16 → 0.13.0
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/dist/cli.js +517 -10403
- package/dist/commands/enterprise/templates/deploy-repo/docs/runbook.md +3 -8
- package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +1 -184
- package/dist/drizzle/0050_brain_v0_entity_subtype.sql +2 -2
- package/dist/drizzle/0051_brain_v0_tenant_entity_pages.sql +6 -6
- package/dist/drizzle/0052_brain_v0_external_refs.sql +2 -2
- package/dist/drizzle/0053_brain_v0_unresolved_mentions_subtype.sql +2 -2
- package/dist/drizzle/0145_knowledge_graph_thread_ingest.sql +14 -14
- package/dist/drizzle/0146_knowledge_graph_source_scope.sql +1 -1
- package/dist/drizzle/0157_knowledge_graph_observations_source.sql +2 -2
- package/dist/drizzle/0158_wiki_tenant_scope.sql +1 -1
- package/dist/drizzle/0165_plugin_premium_entitlements.sql +2 -2
- package/dist/drizzle/{0166_company_brain_substrate_contract.sql → 0166_brain_substrate_contract.sql} +9 -9
- package/dist/drizzle/{0167_company_brain_artifact_manifest_runtime.sql → 0167_brain_artifact_manifest_runtime.sql} +5 -5
- package/dist/drizzle/0168_eval_skill_gate.sql +1 -1
- package/dist/drizzle/0183_okf_artifact_manifests.sql +1 -1
- package/dist/drizzle/0200_eval_results_execution_tier.sql +40 -0
- package/dist/drizzle/0201_brain_dream_runs.sql +72 -0
- package/dist/drizzle/0202_backfill_thread_participants_from_senders.sql +110 -0
- package/dist/drizzle/0203_threads_mode_override.sql +61 -0
- package/dist/drizzle/0204_thread_turns_triggering_message_id.sql +48 -0
- package/dist/drizzle/0205_routines_git_python.sql +185 -0
- package/dist/drizzle/0206_agent_loop_versions_routine_actions.sql +40 -0
- package/dist/drizzle/0207_tenant_credentials_github_repo_kind.sql +52 -0
- package/dist/drizzle/0208_knowledge_graph_neutral_identifiers.sql +253 -0
- package/dist/drizzle/0209_living_artifacts_schema.sql +109 -0
- package/dist/drizzle/0210_think137_automations_additive_schema.sql +113 -0
- package/dist/drizzle/0211_backfill_agent_loop_versions_target_spec.sql +120 -0
- package/dist/drizzle/0212_fold_webhooks_into_automations.sql +312 -0
- package/dist/drizzle/0213_agent_loop_versions_judge_evidence_nullable.sql +63 -0
- package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +99 -0
- package/dist/drizzle/0215_agent_loop_versions_goal_worker_policy_nullable.sql +70 -0
- package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +81 -0
- package/dist/drizzle/0217_agents_capability_folder_dispatch.sql +12 -0
- package/dist/drizzle/0218_document_plates.sql +30 -0
- package/dist/drizzle/0219_document_section_waivers.sql +29 -0
- package/dist/drizzle/0220_artifacts_created_by_user.sql +21 -0
- package/dist/drizzle/0221_artifact_shares.sql +29 -0
- package/dist/drizzle/0221_workflow_interpreter_thin_slice.sql +71 -0
- package/dist/drizzle/0222_compliance_drainer_select_audit_events.sql +21 -0
- package/dist/drizzle/0223_document_conformance_reports.sql +38 -0
- package/dist/drizzle/0224_artifact_refresh_state.sql +11 -0
- package/dist/drizzle/0225_think216_agent_loop_convergence.sql +21 -0
- package/dist/drizzle/0226_drop_n8n_agent_step_runs.sql +6 -0
- package/dist/drizzle/0227_analyst_reader_role.sql +1055 -0
- package/dist/drizzle/0228_tenant_credentials_rds_iam_kind.sql +62 -0
- package/dist/drizzle/0229_analyst_reader_rds_iam_grant.sql +79 -0
- package/dist/drizzle/0230_analyst_rls.sql +1613 -0
- package/dist/drizzle/0231_cost_events_cache_write_enforcement_exempt.sql +11 -0
- package/dist/plugins/catalog/package.json +0 -1
- package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +0 -1
- package/dist/plugins/catalog/src/__tests__/catalog.test.ts +16 -8
- package/dist/plugins/catalog/src/__tests__/contracts.test.ts +56 -0
- package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +0 -2
- package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -5
- package/dist/plugins/catalog/src/contracts.ts +112 -0
- package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -6
- package/dist/plugins/n8n/README.md +4 -36
- package/dist/plugins/n8n/src/deployment/managed-app.ts +0 -17
- package/dist/plugins/n8n/src/manifest.ts +0 -5
- package/dist/plugins/n8n/terraform/n8n/main.tf +0 -26
- package/dist/plugins/n8n/terraform/n8n/outputs.tf +0 -5
- package/dist/plugins/n8n/terraform/n8n/variables.tf +0 -6
- package/dist/plugins/n8n/test/manifest.test.ts +0 -1
- package/dist/plugins/twenty/package.json +5 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +232 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +489 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +199 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +43 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +230 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/twenty-client.test.ts +207 -0
- package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +282 -0
- package/dist/plugins/twenty/scripts/lib/load-attachments.ts +220 -0
- package/dist/plugins/twenty/scripts/lib/load-records.ts +680 -0
- package/dist/plugins/twenty/scripts/lib/mappers.ts +559 -0
- package/dist/plugins/twenty/scripts/lib/members-ensure.ts +381 -0
- package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +191 -0
- package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +425 -0
- package/dist/plugins/twenty/scripts/lib/twenty-client.ts +246 -0
- package/dist/plugins/twenty/scripts/migrate-lastmile.ts +610 -0
- package/dist/plugins/twenty/scripts/provision-twenty-members.ts +214 -0
- package/dist/plugins/twenty/src/manifest.ts +99 -0
- package/dist/plugins/twenty/terraform/twenty/README.md +1 -1
- package/dist/plugins/twenty/test/manifest.test.ts +12 -2
- package/dist/terraform/examples/greenfield/main.tf +90 -380
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +0 -1
- package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
- package/dist/terraform/modules/app/agentcore-pi/main.tf +29 -38
- package/dist/terraform/modules/app/agentcore-pi/variables.tf +23 -15
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +3 -3
- package/dist/terraform/modules/app/customer-domain/main.tf +16 -2
- package/dist/terraform/modules/app/deployment-control-plane/main.tf +5 -0
- package/dist/terraform/modules/app/deployment-control-plane/runner.py +85 -207
- package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +56 -2
- package/dist/terraform/modules/app/hindsight-memory/main.tf +52 -4
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +7 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +493 -89
- package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +215 -96
- package/dist/terraform/modules/app/lambda-api/main.tf +24 -5
- package/dist/terraform/modules/app/lambda-api/outputs.tf +19 -11
- package/dist/terraform/modules/app/lambda-api/variables.tf +94 -67
- package/dist/terraform/modules/app/ses-email/main.tf +14 -2
- package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/main.tf +493 -0
- package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/outputs.tf +34 -0
- package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/variables.tf +32 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +55 -0
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +15 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +26 -0
- package/dist/terraform/modules/thinkwork/README.md +0 -1
- package/dist/terraform/modules/thinkwork/main.tf +167 -446
- package/dist/terraform/modules/thinkwork/outputs.tf +2 -142
- package/dist/terraform/modules/thinkwork/variables.tf +55 -309
- package/dist/terraform/schema.graphql +4 -0
- package/dist/workspace-defaults/files/AGENTS.md +45 -31
- package/dist/workspace-defaults/files/CONTEXT.md +48 -4
- package/dist/workspace-defaults/files/GUARDRAILS.md +18 -0
- package/dist/workspace-defaults/files/MEMORY_GUIDE.md +32 -4
- package/dist/workspace-defaults/files/SPACE.md +34 -19
- package/dist/workspace-defaults/files/TOOLS.md +30 -0
- package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +154 -0
- package/dist/workspace-defaults/files/skills/document-composer/references/authoring-rules.md +101 -0
- package/package.json +1 -1
- package/dist/plugins/company-brain/README.md +0 -37
- package/dist/plugins/company-brain/package.json +0 -33
- package/dist/plugins/company-brain/runtime/cognee/Dockerfile +0 -11
- package/dist/plugins/company-brain/smoke/cognee-managed-app-smoke.mjs +0 -458
- package/dist/plugins/company-brain/smoke/cognee-memory-cutover-smoke.mjs +0 -539
- package/dist/plugins/company-brain/smoke/company-brain-context-engine-smoke.mjs +0 -312
- package/dist/plugins/company-brain/smoke/company-brain-operations-smoke.mjs +0 -449
- package/dist/plugins/company-brain/smoke/company-brain-plugin-smoke.mjs +0 -655
- package/dist/plugins/company-brain/smoke/hindsight-memory-isolation-smoke.mjs +0 -474
- package/dist/plugins/company-brain/src/api/cognee-client.ts +0 -896
- package/dist/plugins/company-brain/src/api/cognee-cluster-identity.ts +0 -49
- package/dist/plugins/company-brain/src/api/cognee-memory-scope.ts +0 -84
- package/dist/plugins/company-brain/src/api/context-engine-provider.ts +0 -888
- package/dist/plugins/company-brain/src/api/migration.ts +0 -665
- package/dist/plugins/company-brain/src/deployment/cognee-managed-app.ts +0 -396
- package/dist/plugins/company-brain/src/index.ts +0 -62
- package/dist/plugins/company-brain/src/manifest.ts +0 -112
- package/dist/plugins/company-brain/terraform/cognee/README.md +0 -263
- package/dist/plugins/company-brain/terraform/cognee/main.tf +0 -766
- package/dist/plugins/company-brain/terraform/cognee/outputs.tf +0 -144
- package/dist/plugins/company-brain/terraform/cognee/variables.tf +0 -402
- package/dist/plugins/company-brain/test/api/cognee-client.test.ts +0 -562
- package/dist/plugins/company-brain/test/api/cognee-memory-scope.test.ts +0 -74
- package/dist/plugins/company-brain/test/api/context-engine-provider.test.ts +0 -447
- package/dist/plugins/company-brain/test/api/migration.test.ts +0 -465
- package/dist/plugins/company-brain/test/manifest.test.ts +0 -95
- package/dist/plugins/company-brain/tsconfig.json +0 -8
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
LastmileContact,
|
|
5
|
+
LastmileCustomerNote,
|
|
6
|
+
LastmileLead,
|
|
7
|
+
LastmileOpportunity,
|
|
8
|
+
} from "../lastmile-reader";
|
|
9
|
+
import {
|
|
10
|
+
buildOwnerIndex,
|
|
11
|
+
contentHash,
|
|
12
|
+
deriveRepEmail,
|
|
13
|
+
isMobilBrand,
|
|
14
|
+
mapAccount,
|
|
15
|
+
mapContact,
|
|
16
|
+
mapCrmComment,
|
|
17
|
+
mapCustomerNote,
|
|
18
|
+
mapLead,
|
|
19
|
+
mapLeadStatusToStage,
|
|
20
|
+
mapOpportunity,
|
|
21
|
+
mapOpportunityProduct,
|
|
22
|
+
mapOpportunityStage,
|
|
23
|
+
normalizeEmail,
|
|
24
|
+
normalizePhone,
|
|
25
|
+
resolveOwner,
|
|
26
|
+
sourceId,
|
|
27
|
+
stableStringify,
|
|
28
|
+
toAmountMicros,
|
|
29
|
+
toQuantity,
|
|
30
|
+
} from "../mappers";
|
|
31
|
+
|
|
32
|
+
const ownerMap = new Map([["rep_jane", "member-jane-uuid"]]);
|
|
33
|
+
const companyMap = new Map([[sourceId("account", "acct_1"), "company-uuid-1"]]);
|
|
34
|
+
|
|
35
|
+
function lead(overrides: Partial<LastmileLead> = {}): LastmileLead {
|
|
36
|
+
return {
|
|
37
|
+
id: "lead_1",
|
|
38
|
+
status: "00-New",
|
|
39
|
+
companyName: "Acme Fuel",
|
|
40
|
+
firstName: "Ann",
|
|
41
|
+
lastName: "Lee",
|
|
42
|
+
email: null,
|
|
43
|
+
phone: null,
|
|
44
|
+
source: null,
|
|
45
|
+
description: null,
|
|
46
|
+
ownerRepId: null,
|
|
47
|
+
dateCreated: null,
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function opportunity(
|
|
53
|
+
overrides: Partial<LastmileOpportunity> = {},
|
|
54
|
+
): LastmileOpportunity {
|
|
55
|
+
return {
|
|
56
|
+
id: "opp_1",
|
|
57
|
+
name: "Bulk diesel contract",
|
|
58
|
+
stage: "10-Prospect",
|
|
59
|
+
amount: "1234.56",
|
|
60
|
+
quantity: "100",
|
|
61
|
+
productType: "Diesel",
|
|
62
|
+
brand: "Mobil",
|
|
63
|
+
closed: null,
|
|
64
|
+
won: null,
|
|
65
|
+
accountId: "acct_1",
|
|
66
|
+
ownerRepId: "rep_jane",
|
|
67
|
+
expectedCloseDate: "2026-08-01",
|
|
68
|
+
description: null,
|
|
69
|
+
dateCreated: null,
|
|
70
|
+
...overrides,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("toAmountMicros", () => {
|
|
75
|
+
it("converts $1,234.56 to 1234560000 micros", () => {
|
|
76
|
+
expect(toAmountMicros("$1,234.56")).toBe(1_234_560_000);
|
|
77
|
+
expect(toAmountMicros("1234.56")).toBe(1_234_560_000);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns null for missing or garbage amounts", () => {
|
|
81
|
+
expect(toAmountMicros(null)).toBeNull();
|
|
82
|
+
expect(toAmountMicros("")).toBeNull();
|
|
83
|
+
expect(toAmountMicros("n/a")).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("toQuantity", () => {
|
|
88
|
+
it("parses numeric text and rejects garbage", () => {
|
|
89
|
+
expect(toQuantity("1,500")).toBe(1500);
|
|
90
|
+
expect(toQuantity("about 12")).toBeNull();
|
|
91
|
+
expect(toQuantity(null)).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("normalizePhone", () => {
|
|
96
|
+
it("normalizes US national formats to number + calling/country code", () => {
|
|
97
|
+
expect(normalizePhone("512-825-8875")).toEqual({
|
|
98
|
+
primaryPhoneNumber: "5128258875",
|
|
99
|
+
primaryPhoneCallingCode: "+1",
|
|
100
|
+
primaryPhoneCountryCode: "US",
|
|
101
|
+
});
|
|
102
|
+
expect(normalizePhone("(361) 664-9106")?.primaryPhoneNumber).toBe(
|
|
103
|
+
"3616649106",
|
|
104
|
+
);
|
|
105
|
+
expect(normalizePhone("1-512-825-8875")?.primaryPhoneNumber).toBe(
|
|
106
|
+
"5128258875",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("drops unparseable or non-US-shaped numbers", () => {
|
|
111
|
+
expect(normalizePhone("call the office")).toBeNull();
|
|
112
|
+
expect(normalizePhone("12345")).toBeNull();
|
|
113
|
+
expect(normalizePhone("011-44-20-7946-0958")).toBeNull();
|
|
114
|
+
expect(normalizePhone(null)).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("normalizeEmail", () => {
|
|
119
|
+
it("lowercases and validates", () => {
|
|
120
|
+
expect(normalizeEmail(" Jane@TEI.com ")).toBe("jane@tei.com");
|
|
121
|
+
expect(normalizeEmail("not-an-email")).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("stage mapping", () => {
|
|
126
|
+
it("maps messy lead statuses into the lead band", () => {
|
|
127
|
+
expect(mapLeadStatusToStage("00-New").stage).toBe("LEAD");
|
|
128
|
+
expect(mapLeadStatusToStage("new").stage).toBe("LEAD");
|
|
129
|
+
expect(mapLeadStatusToStage(null).stage).toBe("LEAD");
|
|
130
|
+
expect(mapLeadStatusToStage("20-Working").stage).toBe("LEAD_WORKING");
|
|
131
|
+
expect(mapLeadStatusToStage("30-Nurturing").stage).toBe("LEAD_WORKING");
|
|
132
|
+
expect(mapLeadStatusToStage("50-Qualified").stage).toBe("LEAD_QUALIFIED");
|
|
133
|
+
expect(mapLeadStatusToStage("90-Unqualified").stage).toBe(
|
|
134
|
+
"LEAD_UNQUALIFIED",
|
|
135
|
+
);
|
|
136
|
+
expect(mapLeadStatusToStage("Converted").stage).toBe("LEAD_QUALIFIED");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("flags unknown lead statuses", () => {
|
|
140
|
+
expect(mapLeadStatusToStage("13-Mystery")).toEqual({
|
|
141
|
+
stage: "LEAD",
|
|
142
|
+
unknown: true,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("maps opportunity stages including both Negotiate spellings", () => {
|
|
147
|
+
expect(
|
|
148
|
+
mapOpportunityStage({ stage: "60-Won", closed: null, won: null }).stage,
|
|
149
|
+
).toBe("WON");
|
|
150
|
+
expect(
|
|
151
|
+
mapOpportunityStage({ stage: "90-Lost", closed: null, won: null }).stage,
|
|
152
|
+
).toBe("LOST");
|
|
153
|
+
expect(
|
|
154
|
+
mapOpportunityStage({
|
|
155
|
+
stage: "40-Negotiate to Close",
|
|
156
|
+
closed: null,
|
|
157
|
+
won: null,
|
|
158
|
+
}).stage,
|
|
159
|
+
).toBe("NEGOTIATE");
|
|
160
|
+
expect(
|
|
161
|
+
mapOpportunityStage({
|
|
162
|
+
stage: "40-Negotiation to Close",
|
|
163
|
+
closed: null,
|
|
164
|
+
won: null,
|
|
165
|
+
}).stage,
|
|
166
|
+
).toBe("NEGOTIATE");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("falls back to closed/won flags for blank stages", () => {
|
|
170
|
+
expect(
|
|
171
|
+
mapOpportunityStage({ stage: null, closed: "true", won: "true" }).stage,
|
|
172
|
+
).toBe("WON");
|
|
173
|
+
expect(
|
|
174
|
+
mapOpportunityStage({ stage: null, closed: "true", won: "false" }).stage,
|
|
175
|
+
).toBe("LOST");
|
|
176
|
+
expect(
|
|
177
|
+
mapOpportunityStage({ stage: null, closed: null, won: null }).stage,
|
|
178
|
+
).toBe("NEW");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("mapAccount", () => {
|
|
183
|
+
it("maps to a company with owner and namespaced sourceId", () => {
|
|
184
|
+
const mapped = mapAccount(
|
|
185
|
+
{ id: "acct_1", name: "Acme", ownerRepId: "rep_jane" },
|
|
186
|
+
ownerMap,
|
|
187
|
+
);
|
|
188
|
+
expect(mapped.input).toMatchObject({
|
|
189
|
+
name: "Acme",
|
|
190
|
+
accountOwnerId: "member-jane-uuid",
|
|
191
|
+
sourceId: "account:acct_1",
|
|
192
|
+
});
|
|
193
|
+
expect(mapped.warnings).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("warns when the owner is unprovisioned", () => {
|
|
197
|
+
const mapped = mapAccount(
|
|
198
|
+
{ id: "acct_2", name: "Beta", ownerRepId: "rep_gone" },
|
|
199
|
+
ownerMap,
|
|
200
|
+
);
|
|
201
|
+
expect(mapped.input).not.toHaveProperty("accountOwnerId");
|
|
202
|
+
expect(mapped.warnings[0]).toMatch(/rep_gone/);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("mapContact", () => {
|
|
207
|
+
function contact(overrides: Partial<LastmileContact> = {}): LastmileContact {
|
|
208
|
+
return {
|
|
209
|
+
id: "ct_1",
|
|
210
|
+
accountId: "acct_1",
|
|
211
|
+
firstName: "Ann",
|
|
212
|
+
lastName: "Lee",
|
|
213
|
+
email: "Ann@Acme.com",
|
|
214
|
+
phone: "210-555-0101",
|
|
215
|
+
phoneCellular: null,
|
|
216
|
+
title: "Buyer",
|
|
217
|
+
...overrides,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
it("maps FullName split and company link", () => {
|
|
222
|
+
const mapped = mapContact(contact(), companyMap);
|
|
223
|
+
expect(mapped.input).toMatchObject({
|
|
224
|
+
name: { firstName: "Ann", lastName: "Lee" },
|
|
225
|
+
emails: { primaryEmail: "ann@acme.com" },
|
|
226
|
+
phones: {
|
|
227
|
+
primaryPhoneNumber: "2105550101",
|
|
228
|
+
primaryPhoneCallingCode: "+1",
|
|
229
|
+
primaryPhoneCountryCode: "US",
|
|
230
|
+
},
|
|
231
|
+
jobTitle: "Buyer",
|
|
232
|
+
companyId: "company-uuid-1",
|
|
233
|
+
sourceId: "contact:ct_1",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("keeps a contact without an account as a person with no company", () => {
|
|
238
|
+
const mapped = mapContact(contact({ accountId: null }), companyMap);
|
|
239
|
+
expect(mapped.input).not.toHaveProperty("companyId");
|
|
240
|
+
expect(mapped.warnings).toEqual([]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("drops invalid emails with a warning", () => {
|
|
244
|
+
const mapped = mapContact(contact({ email: "garbage" }), companyMap);
|
|
245
|
+
expect(mapped.input).not.toHaveProperty("emails");
|
|
246
|
+
expect(mapped.warnings[0]).toMatch(/invalid email/);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("mapOpportunity", () => {
|
|
251
|
+
it("maps stage, micros, custom fields, links, and owner (AE1 shape)", () => {
|
|
252
|
+
const mapped = mapOpportunity(opportunity(), ownerMap, companyMap);
|
|
253
|
+
expect(mapped.input).toMatchObject({
|
|
254
|
+
name: "Bulk diesel contract",
|
|
255
|
+
stage: "PROSPECT",
|
|
256
|
+
amount: { amountMicros: 1_234_560_000, currencyCode: "USD" },
|
|
257
|
+
ownerId: "member-jane-uuid",
|
|
258
|
+
companyId: "company-uuid-1",
|
|
259
|
+
product: "Diesel",
|
|
260
|
+
quantity: 100,
|
|
261
|
+
isMobil: true,
|
|
262
|
+
sourceId: "opportunity:opp_1",
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("is not Mobil when neither product nor brand mentions Mobil", () => {
|
|
267
|
+
const mapped = mapOpportunity(
|
|
268
|
+
opportunity({ brand: "Shell", productType: "Unleaded" }),
|
|
269
|
+
ownerMap,
|
|
270
|
+
companyMap,
|
|
271
|
+
);
|
|
272
|
+
expect(mapped.input.isMobil).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("flags an unresolvable company and unprovisioned owner", () => {
|
|
276
|
+
const mapped = mapOpportunity(
|
|
277
|
+
opportunity({ accountId: "acct_missing", ownerRepId: "rep_gone" }),
|
|
278
|
+
ownerMap,
|
|
279
|
+
companyMap,
|
|
280
|
+
);
|
|
281
|
+
expect(mapped.input).not.toHaveProperty("companyId");
|
|
282
|
+
expect(mapped.input).not.toHaveProperty("ownerId");
|
|
283
|
+
expect(mapped.warnings).toHaveLength(2);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("mapLead", () => {
|
|
288
|
+
it("maps a lead row to the early Lead stage in the opportunity pipeline (AE3)", () => {
|
|
289
|
+
const mapped = mapLead(lead(), ownerMap);
|
|
290
|
+
expect(mapped.input).toMatchObject({
|
|
291
|
+
name: "Acme Fuel",
|
|
292
|
+
stage: "LEAD",
|
|
293
|
+
sourceId: "lead:lead_1",
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("names person-only leads by their person name", () => {
|
|
298
|
+
const mapped = mapLead(lead({ companyName: null }), ownerMap);
|
|
299
|
+
expect(mapped.input.name).toBe("Ann Lee");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("notes mapping", () => {
|
|
304
|
+
it("maps a CRM comment onto its lead/opportunity target", () => {
|
|
305
|
+
const mapped = mapCrmComment({
|
|
306
|
+
id: "tc_1",
|
|
307
|
+
entityType: "opportunity",
|
|
308
|
+
entityId: "opp_1",
|
|
309
|
+
content: "Called the buyer.\nFollow up Friday.",
|
|
310
|
+
isDeleted: false,
|
|
311
|
+
createdAt: null,
|
|
312
|
+
});
|
|
313
|
+
expect(mapped).toMatchObject({
|
|
314
|
+
sourceId: "task_comment:tc_1",
|
|
315
|
+
title: "Called the buyer.",
|
|
316
|
+
targetSourceId: "opportunity:opp_1",
|
|
317
|
+
targetKind: "opportunity",
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("maps a customer note only when the customer uniquely matches an account", () => {
|
|
322
|
+
const base: LastmileCustomerNote = {
|
|
323
|
+
id: "n_1",
|
|
324
|
+
noteText: "Tank inspection notes",
|
|
325
|
+
customerId: "cust_1",
|
|
326
|
+
customerName: "Acme",
|
|
327
|
+
matchedAccountId: "acct_1",
|
|
328
|
+
dateCreated: null,
|
|
329
|
+
};
|
|
330
|
+
expect(mapCustomerNote(base)?.targetSourceId).toBe("account:acct_1");
|
|
331
|
+
expect(mapCustomerNote({ ...base, matchedAccountId: null })).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("contentHash", () => {
|
|
336
|
+
it("is stable across key order and ignores sourceHash itself", () => {
|
|
337
|
+
const a = contentHash({ name: "X", stage: "LEAD" });
|
|
338
|
+
const b = contentHash({ stage: "LEAD", name: "X" });
|
|
339
|
+
const c = contentHash({ stage: "LEAD", name: "X", sourceHash: "whatever" });
|
|
340
|
+
expect(a).toBe(b);
|
|
341
|
+
expect(a).toBe(c);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("changes when content changes (AE1: source edit → update)", () => {
|
|
345
|
+
expect(contentHash({ amount: 1 })).not.toBe(contentHash({ amount: 2 }));
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("stableStringify sorts keys deterministically", () => {
|
|
349
|
+
expect(stableStringify({ b: 1, a: [{ d: 2, c: 3 }] })).toBe(
|
|
350
|
+
'{"a":[{"c":3,"d":2}],"b":1}',
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("buildOwnerIndex / resolveOwner", () => {
|
|
356
|
+
const reps = [
|
|
357
|
+
{ id: "rep_1", alias: "jbake", firstName: "Jane", lastName: "Baker" },
|
|
358
|
+
{ id: "rep_2", alias: null, firstName: "Daniel", lastName: "Emblen" },
|
|
359
|
+
{ id: "rep_3", alias: null, firstName: "Jose", lastName: "Vazquez" },
|
|
360
|
+
{ id: "rep_4", alias: null, firstName: "Jose", lastName: "Vazquez" },
|
|
361
|
+
];
|
|
362
|
+
const memberIds = new Map([
|
|
363
|
+
["rep_1", "wm-1"],
|
|
364
|
+
["rep_2", "wm-2"],
|
|
365
|
+
["rep_3", "wm-3"],
|
|
366
|
+
["rep_4", "wm-4"],
|
|
367
|
+
]);
|
|
368
|
+
const index = buildOwnerIndex(reps, memberIds);
|
|
369
|
+
|
|
370
|
+
it("resolves rep ids, aliases, full names, and derived aliases", () => {
|
|
371
|
+
expect(resolveOwner("rep_1", index)).toBe("wm-1");
|
|
372
|
+
expect(resolveOwner("jbake", index)).toBe("wm-1");
|
|
373
|
+
expect(resolveOwner("Jane Baker", index)).toBe("wm-1");
|
|
374
|
+
expect(resolveOwner("dembl", index)).toBe("wm-2"); // derived: D + embl
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("refuses ambiguous names shared by two reps", () => {
|
|
378
|
+
expect(resolveOwner("Jose Vazquez", index)).toBeNull();
|
|
379
|
+
expect(resolveOwner("rep_3", index)).toBe("wm-3"); // ids stay exact
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("unknown refs resolve to null", () => {
|
|
383
|
+
expect(resolveOwner("Data Migration", index)).toBeNull();
|
|
384
|
+
expect(resolveOwner(null, index)).toBeNull();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("deriveRepEmail", () => {
|
|
389
|
+
const domain = "texasenterprises.com";
|
|
390
|
+
|
|
391
|
+
it("derives first-initial + lastname for real people", () => {
|
|
392
|
+
expect(deriveRepEmail("Daniel", "Emblen", domain)).toBe(
|
|
393
|
+
"demblen@texasenterprises.com",
|
|
394
|
+
);
|
|
395
|
+
expect(deriveRepEmail("Sal", "Carrizales", domain)).toBe(
|
|
396
|
+
"scarrizales@texasenterprises.com",
|
|
397
|
+
);
|
|
398
|
+
expect(deriveRepEmail("Aaron", "Anderson", domain)).toBe(
|
|
399
|
+
"aanderson@texasenterprises.com",
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("strips hyphens and apostrophes from the last name", () => {
|
|
404
|
+
expect(deriveRepEmail("Mary", "O'Brien", domain)).toBe(
|
|
405
|
+
"mobrien@texasenterprises.com",
|
|
406
|
+
);
|
|
407
|
+
expect(deriveRepEmail("Jean", "Smith-Jones", domain)).toBe(
|
|
408
|
+
"jsmithjones@texasenterprises.com",
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("refuses house, intercompany, and placeholder rows", () => {
|
|
413
|
+
expect(deriveRepEmail("House", "Mighty", domain)).toBeNull();
|
|
414
|
+
expect(deriveRepEmail("House", "Golden West Laredo", domain)).toBeNull();
|
|
415
|
+
expect(deriveRepEmail("Hotsy Austin", "House", domain)).toBeNull();
|
|
416
|
+
expect(deriveRepEmail("Intercompany", "GWPP", domain)).toBeNull();
|
|
417
|
+
expect(deriveRepEmail("TBD", "TBD", domain)).toBeNull();
|
|
418
|
+
expect(deriveRepEmail("undefined", "undefined", domain)).toBeNull();
|
|
419
|
+
expect(deriveRepEmail("UNKNOWN", "SALES REP", domain)).toBeNull();
|
|
420
|
+
expect(deriveRepEmail("Buyback", "Unassigned", domain)).toBeNull();
|
|
421
|
+
expect(deriveRepEmail("Oil", "Hauling", domain)).toBeNull();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("refuses names with digits or underscores", () => {
|
|
425
|
+
expect(deriveRepEmail("Chelsea", "Ervi_2", domain)).toBeNull();
|
|
426
|
+
expect(deriveRepEmail("Bob", "Loa2", domain)).toBeNull();
|
|
427
|
+
expect(deriveRepEmail(null, "Smith", domain)).toBeNull();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("mapOpportunityProduct (multiple products per opportunity)", () => {
|
|
432
|
+
const item = (overrides = {}) => ({
|
|
433
|
+
opportunityId: "opp_1",
|
|
434
|
+
index: 0,
|
|
435
|
+
brand: "MOBIL",
|
|
436
|
+
quantity: "6500",
|
|
437
|
+
amount: "50000",
|
|
438
|
+
...overrides,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("maps a line to product, quantity, currency micros, and its opportunity", () => {
|
|
442
|
+
const mapped = mapOpportunityProduct(item());
|
|
443
|
+
expect(mapped.sourceId).toBe("opportunity_item:opp_1#0");
|
|
444
|
+
expect(mapped.opportunitySourceId).toBe("opportunity:opp_1");
|
|
445
|
+
expect(mapped.input).toMatchObject({
|
|
446
|
+
name: "MOBIL",
|
|
447
|
+
product: "MOBIL",
|
|
448
|
+
quantity: 6500,
|
|
449
|
+
amount: { amountMicros: 50_000_000_000, currencyCode: "USD" },
|
|
450
|
+
isMobil: true,
|
|
451
|
+
lineNumber: 1,
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("gives each line on one opportunity a distinct, stable sourceId", () => {
|
|
456
|
+
const lines = [
|
|
457
|
+
mapOpportunityProduct(item({ index: 0, brand: "DEF" })),
|
|
458
|
+
mapOpportunityProduct(item({ index: 1, brand: "GOLDEN WEST" })),
|
|
459
|
+
mapOpportunityProduct(item({ index: 2, brand: "MOBIL" })),
|
|
460
|
+
];
|
|
461
|
+
expect(lines.map((l) => l.sourceId)).toEqual([
|
|
462
|
+
"opportunity_item:opp_1#0",
|
|
463
|
+
"opportunity_item:opp_1#1",
|
|
464
|
+
"opportunity_item:opp_1#2",
|
|
465
|
+
]);
|
|
466
|
+
expect(new Set(lines.map((l) => l.opportunitySourceId)).size).toBe(1);
|
|
467
|
+
expect(lines.map((l) => l.input.isMobil)).toEqual([false, false, true]);
|
|
468
|
+
expect(lines.map((l) => l.input.lineNumber)).toEqual([1, 2, 3]);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("names an unbranded line by its position and flags unparseable numbers", () => {
|
|
472
|
+
const mapped = mapOpportunityProduct(
|
|
473
|
+
item({ brand: null, quantity: "n/a", amount: "" }),
|
|
474
|
+
);
|
|
475
|
+
expect(mapped.input.name).toBe("Line 1");
|
|
476
|
+
expect(mapped.input).not.toHaveProperty("product");
|
|
477
|
+
expect(mapped.input).not.toHaveProperty("quantity");
|
|
478
|
+
expect(mapped.input).not.toHaveProperty("amount");
|
|
479
|
+
expect(mapped.input.isMobil).toBe(false);
|
|
480
|
+
expect(mapped.warnings).toContain("unparseable line quantity n/a dropped");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("treats MOBIL - CVL as Mobil, GOLDEN WEST as not", () => {
|
|
484
|
+
expect(isMobilBrand("MOBIL - CVL")).toBe(true);
|
|
485
|
+
expect(isMobilBrand("Mobil")).toBe(true);
|
|
486
|
+
expect(isMobilBrand("GOLDEN WEST")).toBe(false);
|
|
487
|
+
expect(isMobilBrand(null)).toBe(false);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { ensureMembers, type RepToProvision } from "../members-ensure";
|
|
4
|
+
import type { TwentyClient } from "../twenty-client";
|
|
5
|
+
|
|
6
|
+
function rep(overrides: Partial<RepToProvision> = {}): RepToProvision {
|
|
7
|
+
return {
|
|
8
|
+
repId: "rep_1",
|
|
9
|
+
email: "jane@tei.com",
|
|
10
|
+
firstName: "Jane",
|
|
11
|
+
lastName: "Doe",
|
|
12
|
+
archived: false,
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function dataClientWithMembers(
|
|
18
|
+
members: Array<{ id: string; userEmail: string }>,
|
|
19
|
+
): TwentyClient {
|
|
20
|
+
return {
|
|
21
|
+
requestWithRetry: vi.fn(async () => ({
|
|
22
|
+
workspaceMembers: {
|
|
23
|
+
edges: members.map((member) => ({ node: member })),
|
|
24
|
+
pageInfo: { hasNextPage: false, endCursor: null },
|
|
25
|
+
},
|
|
26
|
+
})),
|
|
27
|
+
endpoint: (path: string) => `https://crm.example.com${path}`,
|
|
28
|
+
} as unknown as TwentyClient;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("ensureMembers (dry run)", () => {
|
|
32
|
+
it("three reps, one already a member → one existing + two planned, map holds the existing", async () => {
|
|
33
|
+
const dataClient = dataClientWithMembers([
|
|
34
|
+
{ id: "wm-1", userEmail: "Jane@TEI.com" },
|
|
35
|
+
]);
|
|
36
|
+
const result = await ensureMembers({
|
|
37
|
+
dataClient,
|
|
38
|
+
adminClient: null,
|
|
39
|
+
reps: [
|
|
40
|
+
rep(),
|
|
41
|
+
rep({ repId: "rep_2", email: "bob@tei.com" }),
|
|
42
|
+
rep({ repId: "rep_3", email: "amy@tei.com" }),
|
|
43
|
+
],
|
|
44
|
+
repPassword: "test",
|
|
45
|
+
workspaceId: null,
|
|
46
|
+
dryRun: true,
|
|
47
|
+
});
|
|
48
|
+
expect(result.ownerMap.get("rep_1")).toBe("wm-1");
|
|
49
|
+
expect(
|
|
50
|
+
result.report.filter((row) => row.action === "planned"),
|
|
51
|
+
).toHaveLength(2);
|
|
52
|
+
expect(result.hadFailures).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("reps without an email are unprovisionable and the run continues", async () => {
|
|
56
|
+
const dataClient = dataClientWithMembers([]);
|
|
57
|
+
const result = await ensureMembers({
|
|
58
|
+
dataClient,
|
|
59
|
+
adminClient: null,
|
|
60
|
+
reps: [
|
|
61
|
+
rep({ email: null }),
|
|
62
|
+
rep({ repId: "rep_2", email: "bob@tei.com" }),
|
|
63
|
+
],
|
|
64
|
+
repPassword: "test",
|
|
65
|
+
workspaceId: null,
|
|
66
|
+
dryRun: true,
|
|
67
|
+
});
|
|
68
|
+
expect(result.report[0]).toMatchObject({
|
|
69
|
+
repId: "rep_1",
|
|
70
|
+
action: "unprovisionable",
|
|
71
|
+
});
|
|
72
|
+
expect(result.report[1]).toMatchObject({
|
|
73
|
+
repId: "rep_2",
|
|
74
|
+
action: "planned",
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("duplicate emails collapse to one login; both rep ids map to the same member", async () => {
|
|
79
|
+
// Observed live in LastMile: a "house" sales_rep row reusing a real rep's
|
|
80
|
+
// email. Email is the login identity, so the reps merge — reported, never
|
|
81
|
+
// silent.
|
|
82
|
+
const dataClient = dataClientWithMembers([
|
|
83
|
+
{ id: "wm-jane", userEmail: "jane@tei.com" },
|
|
84
|
+
]);
|
|
85
|
+
const result = await ensureMembers({
|
|
86
|
+
dataClient,
|
|
87
|
+
adminClient: null,
|
|
88
|
+
reps: [
|
|
89
|
+
rep(),
|
|
90
|
+
rep({ repId: "rep_2", firstName: "Employee", lastName: "Accounts" }),
|
|
91
|
+
],
|
|
92
|
+
repPassword: "test",
|
|
93
|
+
workspaceId: null,
|
|
94
|
+
dryRun: true,
|
|
95
|
+
});
|
|
96
|
+
expect(result.ownerMap.get("rep_1")).toBe("wm-jane");
|
|
97
|
+
expect(result.ownerMap.get("rep_2")).toBe("wm-jane");
|
|
98
|
+
expect(result.report.find((row) => row.repId === "rep_2")).toMatchObject({
|
|
99
|
+
action: "merged-duplicate-email",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("ensureMembers (apply)", () => {
|
|
105
|
+
it("requires the admin client when new members must be provisioned", async () => {
|
|
106
|
+
const dataClient = dataClientWithMembers([]);
|
|
107
|
+
await expect(
|
|
108
|
+
ensureMembers({
|
|
109
|
+
dataClient,
|
|
110
|
+
adminClient: null,
|
|
111
|
+
reps: [rep()],
|
|
112
|
+
repPassword: "test",
|
|
113
|
+
workspaceId: "ws-1",
|
|
114
|
+
dryRun: false,
|
|
115
|
+
}),
|
|
116
|
+
).rejects.toThrow(/TWENTY_ADMIN_EMAIL/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a signUpInWorkspace rejection is reported per-user and the run continues", async () => {
|
|
120
|
+
const membersAfterSignup = [{ id: "wm-bob", userEmail: "bob@tei.com" }];
|
|
121
|
+
let listCalls = 0;
|
|
122
|
+
const dataClient = {
|
|
123
|
+
requestWithRetry: vi.fn(async () => {
|
|
124
|
+
listCalls += 1;
|
|
125
|
+
return {
|
|
126
|
+
workspaceMembers: {
|
|
127
|
+
// First list: empty; lists after signup include bob only.
|
|
128
|
+
edges: (listCalls === 1 ? [] : membersAfterSignup).map(
|
|
129
|
+
(member) => ({
|
|
130
|
+
node: member,
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
pageInfo: { hasNextPage: false, endCursor: null },
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
137
|
+
endpoint: (path: string) => `https://crm.example.com${path}`,
|
|
138
|
+
} as unknown as TwentyClient;
|
|
139
|
+
|
|
140
|
+
const adminClient = {
|
|
141
|
+
requestOnce: vi.fn(async () => ({
|
|
142
|
+
sendInvitations: { success: true, result: [] },
|
|
143
|
+
})),
|
|
144
|
+
requestWithRetry: vi.fn(async (_path: string, query: string) => {
|
|
145
|
+
if (query.includes("findWorkspaceInvitations")) {
|
|
146
|
+
return {
|
|
147
|
+
findWorkspaceInvitations: [
|
|
148
|
+
{ id: "inv-jane", email: "jane@tei.com" },
|
|
149
|
+
{ id: "inv-bob", email: "bob@tei.com" },
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { getWorkspaceInvitationToken: "token-123" };
|
|
154
|
+
}),
|
|
155
|
+
} as unknown as TwentyClient;
|
|
156
|
+
|
|
157
|
+
// jane's signup fails (fetch mock), bob's succeeds.
|
|
158
|
+
const fetchMock = vi.fn(
|
|
159
|
+
async (_url: RequestInfo | URL, init?: RequestInit) => {
|
|
160
|
+
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
|
161
|
+
variables?: { email?: string };
|
|
162
|
+
};
|
|
163
|
+
if (body.variables?.email === "jane@tei.com") {
|
|
164
|
+
return new Response(
|
|
165
|
+
JSON.stringify({ errors: [{ message: "invite token invalid" }] }),
|
|
166
|
+
{
|
|
167
|
+
status: 200,
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return new Response(
|
|
172
|
+
JSON.stringify({ data: { signUpInWorkspace: {} } }),
|
|
173
|
+
{ status: 200 },
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
178
|
+
try {
|
|
179
|
+
const result = await ensureMembers({
|
|
180
|
+
dataClient,
|
|
181
|
+
adminClient,
|
|
182
|
+
reps: [rep(), rep({ repId: "rep_2", email: "bob@tei.com" })],
|
|
183
|
+
repPassword: "test",
|
|
184
|
+
workspaceId: "ws-1",
|
|
185
|
+
dryRun: false,
|
|
186
|
+
});
|
|
187
|
+
expect(result.hadFailures).toBe(true);
|
|
188
|
+
expect(result.report.find((row) => row.repId === "rep_1")).toMatchObject({
|
|
189
|
+
action: "failed",
|
|
190
|
+
});
|
|
191
|
+
expect(result.report.find((row) => row.repId === "rep_2")).toMatchObject({
|
|
192
|
+
action: "invited+signedUp",
|
|
193
|
+
});
|
|
194
|
+
expect(result.ownerMap.get("rep_2")).toBe("wm-bob");
|
|
195
|
+
} finally {
|
|
196
|
+
vi.unstubAllGlobals();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|