@elevasis/core 0.15.1 → 0.17.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/index.d.ts +1662 -23
- package/dist/index.js +171 -24
- package/dist/knowledge/index.d.ts +1340 -0
- package/dist/knowledge/index.js +138 -0
- package/dist/organization-model/index.d.ts +1662 -23
- package/dist/organization-model/index.js +171 -24
- package/dist/test-utils/index.d.ts +711 -10
- package/dist/test-utils/index.js +159 -16
- package/package.json +7 -3
- package/src/__tests__/publish.test.ts +14 -13
- package/src/__tests__/template-core-compatibility.test.ts +4 -4
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1265 -1154
- package/src/auth/multi-tenancy/index.ts +3 -0
- package/src/auth/multi-tenancy/theme-presets.ts +45 -0
- package/src/auth/multi-tenancy/types.ts +57 -83
- package/src/auth/multi-tenancy/users/api-schemas.ts +165 -194
- package/src/business/acquisition/activity-events.ts +1 -1
- package/src/business/acquisition/api-schemas.ts +1196 -1177
- package/src/business/acquisition/crm-state-actions.test.ts +139 -139
- package/src/business/acquisition/types.ts +381 -390
- package/src/business/crm/api-schemas.ts +40 -0
- package/src/business/crm/index.ts +1 -0
- package/src/business/deals/api-schemas.ts +79 -0
- package/src/business/deals/index.ts +1 -0
- package/src/business/projects/types.ts +124 -88
- package/src/execution/core/runner-types.ts +61 -80
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -104
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1473
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -102
- package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -179
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -309
- package/src/execution/engine/tools/integration/tool.ts +255 -253
- package/src/execution/engine/tools/lead-service-types.ts +895 -894
- package/src/execution/engine/tools/messages.ts +43 -0
- package/src/execution/engine/tools/platform/acquisition/types.ts +2 -1
- package/src/execution/engine/tools/platform/email/types.ts +97 -96
- package/src/execution/engine/tools/types.ts +234 -233
- package/src/execution/engine/workflow/types.ts +195 -193
- package/src/execution/external/api-schemas.ts +40 -0
- package/src/execution/external/index.ts +1 -0
- package/src/knowledge/README.md +32 -0
- package/src/knowledge/__tests__/queries.test.ts +504 -0
- package/src/knowledge/format.ts +99 -0
- package/src/knowledge/index.ts +5 -0
- package/src/knowledge/published.ts +5 -0
- package/src/knowledge/queries.ts +256 -0
- package/src/organization-model/__tests__/defaults.test.ts +172 -172
- package/src/organization-model/__tests__/foundation.test.ts +7 -7
- package/src/organization-model/__tests__/icons.test.ts +27 -0
- package/src/organization-model/__tests__/knowledge.test.ts +214 -0
- package/src/organization-model/contracts.ts +17 -15
- package/src/organization-model/defaults.ts +74 -19
- package/src/organization-model/domains/knowledge.ts +53 -0
- package/src/organization-model/domains/navigation.ts +416 -399
- package/src/organization-model/domains/shared.ts +6 -5
- package/src/organization-model/foundation.ts +10 -6
- package/src/organization-model/graph/build.ts +209 -182
- package/src/organization-model/graph/schema.ts +37 -34
- package/src/organization-model/graph/types.ts +47 -31
- package/src/organization-model/icons.ts +81 -0
- package/src/organization-model/index.ts +8 -3
- package/src/organization-model/organization-model.mdx +1 -1
- package/src/organization-model/published.ts +103 -86
- package/src/organization-model/schema.ts +90 -85
- package/src/organization-model/types.ts +40 -33
- package/src/platform/index.ts +23 -27
- package/src/platform/registry/index.ts +0 -4
- package/src/platform/registry/resource-registry.ts +0 -77
- package/src/platform/registry/serialized-types.ts +148 -219
- package/src/platform/registry/stats-types.ts +60 -60
- package/src/reference/_generated/contracts.md +1265 -1154
- package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +0 -393
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { OrganizationGraph } from '../organization-model/graph/types'
|
|
2
|
+
import type { OrgKnowledgeKind, OrgKnowledgeNode } from '../organization-model/domains/knowledge'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Internal helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Graph node ID prefix for knowledge nodes.
|
|
10
|
+
* Graph node IDs follow the convention: `knowledge:<om-node-id>`
|
|
11
|
+
* e.g. `knowledge:knowledge.outreach-playbook`
|
|
12
|
+
*/
|
|
13
|
+
function toGraphNodeId(omNodeId: string): string {
|
|
14
|
+
return `knowledge:${omNodeId}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the sourceId of every knowledge node in the graph, keyed by graph
|
|
19
|
+
* node ID. The `sourceId` on a knowledge graph node is the OM node id
|
|
20
|
+
* (e.g. `knowledge.outreach-playbook`).
|
|
21
|
+
*/
|
|
22
|
+
function buildKnowledgeSourceIdMap(graph: OrganizationGraph): Map<string, string> {
|
|
23
|
+
const map = new Map<string, string>()
|
|
24
|
+
for (const node of graph.nodes) {
|
|
25
|
+
if (node.kind === 'knowledge' && node.sourceId) {
|
|
26
|
+
map.set(node.id, node.sourceId)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return map
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Query functions
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns all knowledge nodes whose `governs` edges point to the given
|
|
38
|
+
* featureId (graph node ID: `feature:<featureId>`).
|
|
39
|
+
*
|
|
40
|
+
* @param graph - The built OrganizationGraph.
|
|
41
|
+
* @param featureId - The dotted feature id (e.g. `sales.crm`).
|
|
42
|
+
*/
|
|
43
|
+
export function byFeature(
|
|
44
|
+
graph: OrganizationGraph,
|
|
45
|
+
featureId: string,
|
|
46
|
+
knowledgeNodes: OrgKnowledgeNode[]
|
|
47
|
+
): OrgKnowledgeNode[] {
|
|
48
|
+
const targetGraphNodeId = `feature:${featureId}`
|
|
49
|
+
|
|
50
|
+
// Find all knowledge graph node IDs that have a `governs` edge to the target
|
|
51
|
+
const governingKnowledgeNodeIds = new Set<string>()
|
|
52
|
+
for (const edge of graph.edges) {
|
|
53
|
+
if (edge.kind === 'governs' && edge.targetId === targetGraphNodeId && edge.sourceId.startsWith('knowledge:')) {
|
|
54
|
+
governingKnowledgeNodeIds.add(edge.sourceId)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build a lookup from graph-node-id -> OM sourceId
|
|
59
|
+
const sourceIdMap = buildKnowledgeSourceIdMap(graph)
|
|
60
|
+
|
|
61
|
+
// Collect the OM node ids that correspond to those graph nodes
|
|
62
|
+
const matchingOmIds = new Set<string>()
|
|
63
|
+
for (const graphNodeId of governingKnowledgeNodeIds) {
|
|
64
|
+
const omId = sourceIdMap.get(graphNodeId)
|
|
65
|
+
if (omId) matchingOmIds.add(omId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return knowledgeNodes.filter((n) => matchingOmIds.has(n.id))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns all knowledge nodes whose `kind` matches the given kind.
|
|
73
|
+
*
|
|
74
|
+
* @param graph - The built OrganizationGraph (unused structurally; kept for API symmetry).
|
|
75
|
+
* @param kind - The knowledge kind (`'playbook' | 'strategy' | 'reference'`).
|
|
76
|
+
* @param knowledgeNodes - Flat array of OrgKnowledgeNode from the OrganizationModel.
|
|
77
|
+
*/
|
|
78
|
+
export function byKind(
|
|
79
|
+
_graph: OrganizationGraph,
|
|
80
|
+
kind: OrgKnowledgeKind,
|
|
81
|
+
knowledgeNodes: OrgKnowledgeNode[]
|
|
82
|
+
): OrgKnowledgeNode[] {
|
|
83
|
+
return knowledgeNodes.filter((n) => n.kind === kind)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns all knowledge nodes where `ownerIds` includes the given `ownerId`.
|
|
88
|
+
*
|
|
89
|
+
* @param _graph - The built OrganizationGraph (unused; kept for API symmetry).
|
|
90
|
+
* @param ownerId - The owner id string (matches values in `node.ownerIds`).
|
|
91
|
+
* @param knowledgeNodes - Flat array of OrgKnowledgeNode from the OrganizationModel.
|
|
92
|
+
*/
|
|
93
|
+
export function byOwner(
|
|
94
|
+
_graph: OrganizationGraph,
|
|
95
|
+
ownerId: string,
|
|
96
|
+
knowledgeNodes: OrgKnowledgeNode[]
|
|
97
|
+
): OrgKnowledgeNode[] {
|
|
98
|
+
return knowledgeNodes.filter((n) => n.ownerIds.includes(ownerId))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns the IDs of the graph nodes that the given knowledge node governs
|
|
103
|
+
* (outgoing `governs` edges from the knowledge graph node).
|
|
104
|
+
*
|
|
105
|
+
* The graph node ID for a knowledge OM node with id `X` is `knowledge:X`.
|
|
106
|
+
*
|
|
107
|
+
* @param graph - The built OrganizationGraph.
|
|
108
|
+
* @param nodeId - The OM knowledge node id (e.g. `knowledge.outreach-playbook`).
|
|
109
|
+
* Also accepts the graph node ID format (`knowledge:knowledge.outreach-playbook`).
|
|
110
|
+
* @returns Array of target graph node IDs (e.g. `['feature:sales.crm', ...]`).
|
|
111
|
+
*/
|
|
112
|
+
export function governs(graph: OrganizationGraph, nodeId: string): string[] {
|
|
113
|
+
const graphNodeId = nodeId.startsWith('knowledge:') ? nodeId : toGraphNodeId(nodeId)
|
|
114
|
+
|
|
115
|
+
const results: string[] = []
|
|
116
|
+
for (const edge of graph.edges) {
|
|
117
|
+
if (edge.kind === 'governs' && edge.sourceId === graphNodeId) {
|
|
118
|
+
results.push(edge.targetId)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return results
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns the IDs of the knowledge graph nodes that govern the given node
|
|
126
|
+
* (incoming `governs` edges pointing to `nodeId`).
|
|
127
|
+
*
|
|
128
|
+
* @param graph - The built OrganizationGraph.
|
|
129
|
+
* @param nodeId - The target graph node ID (e.g. `feature:sales.crm`) or a
|
|
130
|
+
* bare feature id (e.g. `sales.crm` — will be prefixed with `feature:`).
|
|
131
|
+
* @returns Array of source graph node IDs (e.g. `['knowledge:knowledge.outreach-playbook', ...]`).
|
|
132
|
+
*/
|
|
133
|
+
export function governedBy(graph: OrganizationGraph, nodeId: string): string[] {
|
|
134
|
+
// Accept both `feature:sales.crm` and bare `sales.crm`
|
|
135
|
+
const targetId =
|
|
136
|
+
nodeId.startsWith('feature:') || nodeId.startsWith('knowledge:') || nodeId.startsWith('resource:')
|
|
137
|
+
? nodeId
|
|
138
|
+
: `feature:${nodeId}`
|
|
139
|
+
|
|
140
|
+
const results: string[] = []
|
|
141
|
+
for (const edge of graph.edges) {
|
|
142
|
+
if (edge.kind === 'governs' && edge.targetId === targetId) {
|
|
143
|
+
results.push(edge.sourceId)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return results
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Path parser
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/** The recognized mount axes for Knowledge Map paths. */
|
|
154
|
+
export type KnowledgeMount = 'by-feature' | 'by-kind' | 'by-owner' | 'graph' | 'node'
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The result of parsing a Knowledge Map path string.
|
|
158
|
+
*
|
|
159
|
+
* Shape: `{ mount: KnowledgeMount, args: string[] }`
|
|
160
|
+
*
|
|
161
|
+
* Per-mount arg arrays:
|
|
162
|
+
* - `by-feature`: `[featureId]` (e.g. `['sales.crm']`)
|
|
163
|
+
* - `by-kind`: `[kind]` (e.g. `['playbook']`)
|
|
164
|
+
* - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
|
|
165
|
+
* - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
|
|
166
|
+
* - `node`: `[nodeId]` (single node lookup, no sub-path)
|
|
167
|
+
*/
|
|
168
|
+
export interface ParsedKnowledgePath {
|
|
169
|
+
mount: KnowledgeMount
|
|
170
|
+
args: string[]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parses a Knowledge Map path string into a `{ mount, args }` descriptor.
|
|
175
|
+
*
|
|
176
|
+
* Supported path patterns:
|
|
177
|
+
* `/by-feature/<featureId>` → `{ mount: 'by-feature', args: ['<featureId>'] }`
|
|
178
|
+
* `/by-kind/<kind>` → `{ mount: 'by-kind', args: ['<kind>'] }`
|
|
179
|
+
* `/by-owner/<ownerId>` → `{ mount: 'by-owner', args: ['<ownerId>'] }`
|
|
180
|
+
* `/graph/<nodeId>/governs` → `{ mount: 'graph', args: ['<nodeId>', 'governs'] }`
|
|
181
|
+
* `/graph/<nodeId>/governed-by` → `{ mount: 'graph', args: ['<nodeId>', 'governed-by'] }`
|
|
182
|
+
* `/<nodeId>` → `{ mount: 'node', args: ['<nodeId>'] }`
|
|
183
|
+
*
|
|
184
|
+
* The path MUST start with `/`. Trailing slashes are stripped before parsing.
|
|
185
|
+
*
|
|
186
|
+
* @throws {Error} If the path is empty, missing a leading slash, or does not
|
|
187
|
+
* match any supported mount pattern.
|
|
188
|
+
*/
|
|
189
|
+
export function parsePath(pathString: string): ParsedKnowledgePath {
|
|
190
|
+
if (!pathString || typeof pathString !== 'string') {
|
|
191
|
+
throw new Error('parsePath: path must be a non-empty string')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!pathString.startsWith('/')) {
|
|
195
|
+
throw new Error(`parsePath: path must start with "/", got: "${pathString}"`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Strip trailing slashes then split
|
|
199
|
+
const normalized = pathString.replace(/\/+$/, '')
|
|
200
|
+
const segments = normalized.split('/').filter((s) => s.length > 0)
|
|
201
|
+
|
|
202
|
+
if (segments.length === 0) {
|
|
203
|
+
throw new Error(`parsePath: path resolves to root with no mount: "${pathString}"`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const [first, ...rest] = segments
|
|
207
|
+
|
|
208
|
+
// /by-feature/<featureId>
|
|
209
|
+
if (first === 'by-feature') {
|
|
210
|
+
if (rest.length === 0) {
|
|
211
|
+
throw new Error(`parsePath: /by-feature requires a featureId argument, got: "${pathString}"`)
|
|
212
|
+
}
|
|
213
|
+
return { mount: 'by-feature', args: [rest.join('/')] }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// /by-kind/<kind>
|
|
217
|
+
if (first === 'by-kind') {
|
|
218
|
+
if (rest.length === 0) {
|
|
219
|
+
throw new Error(`parsePath: /by-kind requires a kind argument, got: "${pathString}"`)
|
|
220
|
+
}
|
|
221
|
+
return { mount: 'by-kind', args: [rest[0]] }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// /by-owner/<ownerId>
|
|
225
|
+
if (first === 'by-owner') {
|
|
226
|
+
if (rest.length === 0) {
|
|
227
|
+
throw new Error(`parsePath: /by-owner requires an ownerId argument, got: "${pathString}"`)
|
|
228
|
+
}
|
|
229
|
+
return { mount: 'by-owner', args: [rest.join('/')] }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// /graph/<nodeId>/governs or /graph/<nodeId>/governed-by
|
|
233
|
+
if (first === 'graph') {
|
|
234
|
+
if (rest.length < 2) {
|
|
235
|
+
throw new Error(`parsePath: /graph requires <nodeId>/<verb> (governs|governed-by), got: "${pathString}"`)
|
|
236
|
+
}
|
|
237
|
+
const graphNodeId = rest.slice(0, -1).join('/')
|
|
238
|
+
const verb = rest[rest.length - 1]
|
|
239
|
+
if (verb !== 'governs' && verb !== 'governed-by') {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`parsePath: /graph/<nodeId> verb must be "governs" or "governed-by", got: "${verb}" in "${pathString}"`
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
return { mount: 'graph', args: [graphNodeId, verb] }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// /<nodeId> (single node)
|
|
248
|
+
// first must not be a recognized mount prefix
|
|
249
|
+
if (segments.length === 1) {
|
|
250
|
+
return { mount: 'node', args: [first] }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw new Error(
|
|
254
|
+
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-feature/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>`
|
|
255
|
+
)
|
|
256
|
+
}
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
|
|
3
|
-
import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
|
|
4
|
-
import { OrganizationModelBrandingSchema } from '../domains/branding'
|
|
5
|
-
import { DEFAULT_ORGANIZATION_MODEL_SALES } from '../domains/sales'
|
|
6
|
-
import { OrganizationModelSalesSchema } from '../domains/sales'
|
|
7
|
-
import { DEFAULT_ORGANIZATION_MODEL_PROJECTS } from '../domains/projects'
|
|
8
|
-
import { OrganizationModelProjectsSchema } from '../domains/projects'
|
|
9
|
-
import { DEFAULT_ORGANIZATION_MODEL_PROSPECTING } from '../domains/prospecting'
|
|
10
|
-
import { OrganizationModelProspectingSchema } from '../domains/prospecting'
|
|
11
|
-
import { DEFAULT_ORGANIZATION_MODEL_NAVIGATION } from '../domains/navigation'
|
|
12
|
-
import { OrganizationModelNavigationSchema } from '../domains/navigation'
|
|
13
|
-
import { DEFAULT_ORGANIZATION_MODEL_OPERATIONS } from '../domains/operations'
|
|
14
|
-
import { OperationsDomainSchema } from '../domains/operations'
|
|
15
|
-
import { DEFAULT_ORGANIZATION_MODEL_STATUSES } from '../domains/statuses'
|
|
16
|
-
import { StatusesDomainSchema } from '../domains/statuses'
|
|
17
|
-
import { resolveOrganizationModel } from '../resolve'
|
|
18
|
-
import { OrganizationModelSchema } from '../schema'
|
|
19
|
-
|
|
20
|
-
// All DEFAULT_ORGANIZATION_MODEL* constants and the schemas used to validate them.
|
|
21
|
-
// The composite constant lives in defaults.ts; domain sub-constants live in their
|
|
22
|
-
// respective domain files. All are covered here for roundtrip integrity.
|
|
23
|
-
const domainCases = [
|
|
24
|
-
{
|
|
25
|
-
name: 'DEFAULT_ORGANIZATION_MODEL_BRANDING',
|
|
26
|
-
constant: DEFAULT_ORGANIZATION_MODEL_BRANDING,
|
|
27
|
-
schema: OrganizationModelBrandingSchema
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: 'DEFAULT_ORGANIZATION_MODEL_SALES',
|
|
31
|
-
constant: DEFAULT_ORGANIZATION_MODEL_SALES,
|
|
32
|
-
schema: OrganizationModelSalesSchema
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: 'DEFAULT_ORGANIZATION_MODEL_PROJECTS',
|
|
36
|
-
constant: DEFAULT_ORGANIZATION_MODEL_PROJECTS,
|
|
37
|
-
schema: OrganizationModelProjectsSchema
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: 'DEFAULT_ORGANIZATION_MODEL_PROSPECTING',
|
|
41
|
-
constant: DEFAULT_ORGANIZATION_MODEL_PROSPECTING,
|
|
42
|
-
schema: OrganizationModelProspectingSchema
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: 'DEFAULT_ORGANIZATION_MODEL_NAVIGATION',
|
|
46
|
-
constant: DEFAULT_ORGANIZATION_MODEL_NAVIGATION,
|
|
47
|
-
schema: OrganizationModelNavigationSchema
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: 'DEFAULT_ORGANIZATION_MODEL_STATUSES',
|
|
51
|
-
constant: DEFAULT_ORGANIZATION_MODEL_STATUSES,
|
|
52
|
-
schema: StatusesDomainSchema
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: 'DEFAULT_ORGANIZATION_MODEL_OPERATIONS',
|
|
56
|
-
constant: DEFAULT_ORGANIZATION_MODEL_OPERATIONS,
|
|
57
|
-
schema: OperationsDomainSchema
|
|
58
|
-
}
|
|
59
|
-
] as const
|
|
60
|
-
|
|
61
|
-
describe('organization-model defaults', () => {
|
|
62
|
-
describe('DEFAULT_ORGANIZATION_MODEL (composite)', () => {
|
|
63
|
-
it('passes OrganizationModelSchema.parse without throwing', () => {
|
|
64
|
-
expect(() => OrganizationModelSchema.parse(DEFAULT_ORGANIZATION_MODEL)).not.toThrow()
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL) returns a schema-valid model', () => {
|
|
68
|
-
const result = resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL)
|
|
69
|
-
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
70
|
-
})
|
|
71
|
-
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
|
|
3
|
+
import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
|
|
4
|
+
import { OrganizationModelBrandingSchema } from '../domains/branding'
|
|
5
|
+
import { DEFAULT_ORGANIZATION_MODEL_SALES } from '../domains/sales'
|
|
6
|
+
import { OrganizationModelSalesSchema } from '../domains/sales'
|
|
7
|
+
import { DEFAULT_ORGANIZATION_MODEL_PROJECTS } from '../domains/projects'
|
|
8
|
+
import { OrganizationModelProjectsSchema } from '../domains/projects'
|
|
9
|
+
import { DEFAULT_ORGANIZATION_MODEL_PROSPECTING } from '../domains/prospecting'
|
|
10
|
+
import { OrganizationModelProspectingSchema } from '../domains/prospecting'
|
|
11
|
+
import { DEFAULT_ORGANIZATION_MODEL_NAVIGATION } from '../domains/navigation'
|
|
12
|
+
import { OrganizationModelNavigationSchema } from '../domains/navigation'
|
|
13
|
+
import { DEFAULT_ORGANIZATION_MODEL_OPERATIONS } from '../domains/operations'
|
|
14
|
+
import { OperationsDomainSchema } from '../domains/operations'
|
|
15
|
+
import { DEFAULT_ORGANIZATION_MODEL_STATUSES } from '../domains/statuses'
|
|
16
|
+
import { StatusesDomainSchema } from '../domains/statuses'
|
|
17
|
+
import { resolveOrganizationModel } from '../resolve'
|
|
18
|
+
import { OrganizationModelSchema } from '../schema'
|
|
19
|
+
|
|
20
|
+
// All DEFAULT_ORGANIZATION_MODEL* constants and the schemas used to validate them.
|
|
21
|
+
// The composite constant lives in defaults.ts; domain sub-constants live in their
|
|
22
|
+
// respective domain files. All are covered here for roundtrip integrity.
|
|
23
|
+
const domainCases = [
|
|
24
|
+
{
|
|
25
|
+
name: 'DEFAULT_ORGANIZATION_MODEL_BRANDING',
|
|
26
|
+
constant: DEFAULT_ORGANIZATION_MODEL_BRANDING,
|
|
27
|
+
schema: OrganizationModelBrandingSchema
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'DEFAULT_ORGANIZATION_MODEL_SALES',
|
|
31
|
+
constant: DEFAULT_ORGANIZATION_MODEL_SALES,
|
|
32
|
+
schema: OrganizationModelSalesSchema
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'DEFAULT_ORGANIZATION_MODEL_PROJECTS',
|
|
36
|
+
constant: DEFAULT_ORGANIZATION_MODEL_PROJECTS,
|
|
37
|
+
schema: OrganizationModelProjectsSchema
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'DEFAULT_ORGANIZATION_MODEL_PROSPECTING',
|
|
41
|
+
constant: DEFAULT_ORGANIZATION_MODEL_PROSPECTING,
|
|
42
|
+
schema: OrganizationModelProspectingSchema
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'DEFAULT_ORGANIZATION_MODEL_NAVIGATION',
|
|
46
|
+
constant: DEFAULT_ORGANIZATION_MODEL_NAVIGATION,
|
|
47
|
+
schema: OrganizationModelNavigationSchema
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'DEFAULT_ORGANIZATION_MODEL_STATUSES',
|
|
51
|
+
constant: DEFAULT_ORGANIZATION_MODEL_STATUSES,
|
|
52
|
+
schema: StatusesDomainSchema
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'DEFAULT_ORGANIZATION_MODEL_OPERATIONS',
|
|
56
|
+
constant: DEFAULT_ORGANIZATION_MODEL_OPERATIONS,
|
|
57
|
+
schema: OperationsDomainSchema
|
|
58
|
+
}
|
|
59
|
+
] as const
|
|
60
|
+
|
|
61
|
+
describe('organization-model defaults', () => {
|
|
62
|
+
describe('DEFAULT_ORGANIZATION_MODEL (composite)', () => {
|
|
63
|
+
it('passes OrganizationModelSchema.parse without throwing', () => {
|
|
64
|
+
expect(() => OrganizationModelSchema.parse(DEFAULT_ORGANIZATION_MODEL)).not.toThrow()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL) returns a schema-valid model', () => {
|
|
68
|
+
const result = resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL)
|
|
69
|
+
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
72
|
it('resolveOrganizationModel with no override equals resolveOrganizationModel with DEFAULT_ORGANIZATION_MODEL', () => {
|
|
73
73
|
const fromUndefined = resolveOrganizationModel(undefined)
|
|
74
74
|
const fromDefault = resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL)
|
|
75
75
|
expect(fromDefault).toEqual(fromUndefined)
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
it('keeps Command View enabled but development-only in the default
|
|
78
|
+
it('keeps Command View enabled but development-only in the default Knowledge navigation', () => {
|
|
79
79
|
const result = resolveOrganizationModel(undefined)
|
|
80
|
-
const commandViewFeature = result.features.find((feature) => feature.id === '
|
|
80
|
+
const commandViewFeature = result.features.find((feature) => feature.id === 'knowledge.command-view')
|
|
81
81
|
const commandViewSurface = DEFAULT_ORGANIZATION_MODEL_NAVIGATION.surfaces.find(
|
|
82
|
-
(surface) => surface.id === '
|
|
82
|
+
(surface) => surface.id === 'knowledge.command-view'
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
expect(commandViewFeature?.enabled).toBe(true)
|
|
@@ -88,101 +88,101 @@ describe('organization-model defaults', () => {
|
|
|
88
88
|
expect(commandViewSurface?.devOnly).toBe(true)
|
|
89
89
|
})
|
|
90
90
|
})
|
|
91
|
-
|
|
92
|
-
describe.each(domainCases)('$name (domain sub-constant)', ({ name: _name, constant, schema }) => {
|
|
93
|
-
it('passes its domain schema parse without throwing', () => {
|
|
94
|
-
expect(() => schema.parse(constant)).not.toThrow()
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
describe('DEFAULT_ORGANIZATION_MODEL_BRANDING via resolveOrganizationModel', () => {
|
|
99
|
-
it('resolving with branding override produces a schema-valid model', () => {
|
|
100
|
-
const result = resolveOrganizationModel({ branding: DEFAULT_ORGANIZATION_MODEL_BRANDING })
|
|
101
|
-
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
describe('DEFAULT_ORGANIZATION_MODEL_SALES via resolveOrganizationModel', () => {
|
|
106
|
-
it('resolving with sales override produces a schema-valid model', () => {
|
|
107
|
-
const result = resolveOrganizationModel({ sales: DEFAULT_ORGANIZATION_MODEL_SALES })
|
|
108
|
-
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
describe('DEFAULT_ORGANIZATION_MODEL_PROJECTS via resolveOrganizationModel', () => {
|
|
113
|
-
it('resolving with projects override produces a schema-valid model', () => {
|
|
114
|
-
const result = resolveOrganizationModel({ projects: DEFAULT_ORGANIZATION_MODEL_PROJECTS })
|
|
115
|
-
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe('DEFAULT_ORGANIZATION_MODEL_PROSPECTING via resolveOrganizationModel', () => {
|
|
120
|
-
it('resolving with prospecting override produces a schema-valid model', () => {
|
|
121
|
-
const result = resolveOrganizationModel({ prospecting: DEFAULT_ORGANIZATION_MODEL_PROSPECTING })
|
|
122
|
-
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
describe('DEFAULT_ORGANIZATION_MODEL_NAVIGATION via resolveOrganizationModel', () => {
|
|
127
|
-
it('resolving with navigation override produces a schema-valid model', () => {
|
|
128
|
-
const result = resolveOrganizationModel({ navigation: DEFAULT_ORGANIZATION_MODEL_NAVIGATION })
|
|
129
|
-
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
130
|
-
})
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
describe('DEFAULT_ORGANIZATION_MODEL_OPERATIONS via resolveOrganizationModel', () => {
|
|
134
|
-
it('resolving with operations override produces a schema-valid model', () => {
|
|
135
|
-
const result = resolveOrganizationModel({ operations: DEFAULT_ORGANIZATION_MODEL_OPERATIONS })
|
|
136
|
-
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('default operations seed covers all 5 entity categories', () => {
|
|
140
|
-
expect(DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries).toHaveLength(5)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('every entry has a non-empty id, label, and semanticClass', () => {
|
|
144
|
-
for (const entry of DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries) {
|
|
145
|
-
expect(entry.id.length).toBeGreaterThan(0)
|
|
146
|
-
expect(entry.label.length).toBeGreaterThan(0)
|
|
147
|
-
expect(entry.semanticClass.length).toBeGreaterThan(0)
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('all entry ids are unique', () => {
|
|
152
|
-
const ids = DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.map((e) => e.id)
|
|
153
|
-
const uniqueIds = new Set(ids)
|
|
154
|
-
expect(uniqueIds.size).toBe(ids.length)
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
describe('DEFAULT_ORGANIZATION_MODEL_STATUSES via resolveOrganizationModel', () => {
|
|
159
|
-
it('resolving with statuses override produces a schema-valid model', () => {
|
|
160
|
-
const result = resolveOrganizationModel({ statuses: DEFAULT_ORGANIZATION_MODEL_STATUSES })
|
|
161
|
-
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('default statuses seed covers all 9 delivery.task values', () => {
|
|
165
|
-
const taskEntries = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.filter((e) => e.semanticClass === 'delivery.task')
|
|
166
|
-
expect(taskEntries).toHaveLength(9)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('default statuses seed covers all 5 queue values', () => {
|
|
170
|
-
const queueEntries = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.filter((e) => e.semanticClass === 'queue')
|
|
171
|
-
expect(queueEntries).toHaveLength(5)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('every entry has a non-empty id, label, and semanticClass', () => {
|
|
175
|
-
for (const entry of DEFAULT_ORGANIZATION_MODEL_STATUSES.entries) {
|
|
176
|
-
expect(entry.id.length).toBeGreaterThan(0)
|
|
177
|
-
expect(entry.label.length).toBeGreaterThan(0)
|
|
178
|
-
expect(entry.semanticClass.length).toBeGreaterThan(0)
|
|
179
|
-
}
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('all entry ids are unique', () => {
|
|
183
|
-
const ids = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.map((e) => e.id)
|
|
184
|
-
const uniqueIds = new Set(ids)
|
|
185
|
-
expect(uniqueIds.size).toBe(ids.length)
|
|
186
|
-
})
|
|
187
|
-
})
|
|
188
|
-
})
|
|
91
|
+
|
|
92
|
+
describe.each(domainCases)('$name (domain sub-constant)', ({ name: _name, constant, schema }) => {
|
|
93
|
+
it('passes its domain schema parse without throwing', () => {
|
|
94
|
+
expect(() => schema.parse(constant)).not.toThrow()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('DEFAULT_ORGANIZATION_MODEL_BRANDING via resolveOrganizationModel', () => {
|
|
99
|
+
it('resolving with branding override produces a schema-valid model', () => {
|
|
100
|
+
const result = resolveOrganizationModel({ branding: DEFAULT_ORGANIZATION_MODEL_BRANDING })
|
|
101
|
+
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('DEFAULT_ORGANIZATION_MODEL_SALES via resolveOrganizationModel', () => {
|
|
106
|
+
it('resolving with sales override produces a schema-valid model', () => {
|
|
107
|
+
const result = resolveOrganizationModel({ sales: DEFAULT_ORGANIZATION_MODEL_SALES })
|
|
108
|
+
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('DEFAULT_ORGANIZATION_MODEL_PROJECTS via resolveOrganizationModel', () => {
|
|
113
|
+
it('resolving with projects override produces a schema-valid model', () => {
|
|
114
|
+
const result = resolveOrganizationModel({ projects: DEFAULT_ORGANIZATION_MODEL_PROJECTS })
|
|
115
|
+
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('DEFAULT_ORGANIZATION_MODEL_PROSPECTING via resolveOrganizationModel', () => {
|
|
120
|
+
it('resolving with prospecting override produces a schema-valid model', () => {
|
|
121
|
+
const result = resolveOrganizationModel({ prospecting: DEFAULT_ORGANIZATION_MODEL_PROSPECTING })
|
|
122
|
+
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('DEFAULT_ORGANIZATION_MODEL_NAVIGATION via resolveOrganizationModel', () => {
|
|
127
|
+
it('resolving with navigation override produces a schema-valid model', () => {
|
|
128
|
+
const result = resolveOrganizationModel({ navigation: DEFAULT_ORGANIZATION_MODEL_NAVIGATION })
|
|
129
|
+
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('DEFAULT_ORGANIZATION_MODEL_OPERATIONS via resolveOrganizationModel', () => {
|
|
134
|
+
it('resolving with operations override produces a schema-valid model', () => {
|
|
135
|
+
const result = resolveOrganizationModel({ operations: DEFAULT_ORGANIZATION_MODEL_OPERATIONS })
|
|
136
|
+
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('default operations seed covers all 5 entity categories', () => {
|
|
140
|
+
expect(DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries).toHaveLength(5)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('every entry has a non-empty id, label, and semanticClass', () => {
|
|
144
|
+
for (const entry of DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries) {
|
|
145
|
+
expect(entry.id.length).toBeGreaterThan(0)
|
|
146
|
+
expect(entry.label.length).toBeGreaterThan(0)
|
|
147
|
+
expect(entry.semanticClass.length).toBeGreaterThan(0)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('all entry ids are unique', () => {
|
|
152
|
+
const ids = DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.map((e) => e.id)
|
|
153
|
+
const uniqueIds = new Set(ids)
|
|
154
|
+
expect(uniqueIds.size).toBe(ids.length)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('DEFAULT_ORGANIZATION_MODEL_STATUSES via resolveOrganizationModel', () => {
|
|
159
|
+
it('resolving with statuses override produces a schema-valid model', () => {
|
|
160
|
+
const result = resolveOrganizationModel({ statuses: DEFAULT_ORGANIZATION_MODEL_STATUSES })
|
|
161
|
+
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('default statuses seed covers all 9 delivery.task values', () => {
|
|
165
|
+
const taskEntries = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.filter((e) => e.semanticClass === 'delivery.task')
|
|
166
|
+
expect(taskEntries).toHaveLength(9)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('default statuses seed covers all 5 queue values', () => {
|
|
170
|
+
const queueEntries = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.filter((e) => e.semanticClass === 'queue')
|
|
171
|
+
expect(queueEntries).toHaveLength(5)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('every entry has a non-empty id, label, and semanticClass', () => {
|
|
175
|
+
for (const entry of DEFAULT_ORGANIZATION_MODEL_STATUSES.entries) {
|
|
176
|
+
expect(entry.id.length).toBeGreaterThan(0)
|
|
177
|
+
expect(entry.label.length).toBeGreaterThan(0)
|
|
178
|
+
expect(entry.semanticClass.length).toBeGreaterThan(0)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('all entry ids are unique', () => {
|
|
183
|
+
const ids = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.map((e) => e.id)
|
|
184
|
+
const uniqueIds = new Set(ids)
|
|
185
|
+
expect(uniqueIds.size).toBe(ids.length)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -59,13 +59,13 @@ describe('createFoundationOrganizationModel', () => {
|
|
|
59
59
|
branding: { organizationName: 'Acme', productName: 'Acme OS', shortName: 'Acme' }
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
const crmSurface = result.getOrganizationSurface('crm')
|
|
63
|
-
expect(crmSurface).toBeDefined()
|
|
64
|
-
expect(crmSurface?.id).toBe('crm')
|
|
65
|
-
expect(crmSurface?.icon).toBe('crm')
|
|
66
|
-
|
|
67
|
-
expect(result.getOrganizationSurface('nonexistent')).toBeUndefined()
|
|
68
|
-
})
|
|
62
|
+
const crmSurface = result.getOrganizationSurface('crm')
|
|
63
|
+
expect(crmSurface).toBeDefined()
|
|
64
|
+
expect(crmSurface?.id).toBe('crm')
|
|
65
|
+
expect(crmSurface?.icon).toBe('feature.crm')
|
|
66
|
+
|
|
67
|
+
expect(result.getOrganizationSurface('nonexistent')).toBeUndefined()
|
|
68
|
+
})
|
|
69
69
|
|
|
70
70
|
it('throws when a required core surface is missing', () => {
|
|
71
71
|
const override = {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
ORGANIZATION_MODEL_ICON_TOKENS,
|
|
4
|
+
OrganizationModelBuiltinIconTokenSchema,
|
|
5
|
+
OrganizationModelIconTokenSchema
|
|
6
|
+
} from '../icons'
|
|
7
|
+
import { IconNameSchema } from '../domains/shared'
|
|
8
|
+
|
|
9
|
+
describe('OrganizationModelIconTokenSchema', () => {
|
|
10
|
+
it('accepts built-in semantic icon tokens', () => {
|
|
11
|
+
for (const token of ORGANIZATION_MODEL_ICON_TOKENS) {
|
|
12
|
+
expect(OrganizationModelBuiltinIconTokenSchema.parse(token)).toBe(token)
|
|
13
|
+
expect(OrganizationModelIconTokenSchema.parse(token)).toBe(token)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('accepts custom extension icon tokens', () => {
|
|
18
|
+
expect(OrganizationModelIconTokenSchema.parse('custom.partner-portal')).toBe('custom.partner-portal')
|
|
19
|
+
expect(IconNameSchema.parse('custom.acme.ops_queue')).toBe('custom.acme.ops_queue')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('rejects random strings outside the built-in and custom namespaces', () => {
|
|
23
|
+
for (const token of ['crm', 'lead-gen', 'settings', 'random.icon', 'custom.', 'custom.Bad']) {
|
|
24
|
+
expect(OrganizationModelIconTokenSchema.safeParse(token).success).toBe(false)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
})
|