@elevasis/core 0.18.0 → 0.19.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.
Files changed (44) hide show
  1. package/dist/index.d.ts +82 -1
  2. package/dist/index.js +291 -171
  3. package/dist/knowledge/index.d.ts +43 -0
  4. package/dist/organization-model/index.d.ts +82 -1
  5. package/dist/organization-model/index.js +291 -171
  6. package/dist/test-utils/index.d.ts +41 -12
  7. package/dist/test-utils/index.js +291 -171
  8. package/package.json +2 -1
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +78 -65
  10. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -0
  11. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -128
  12. package/src/business/acquisition/api-schemas.test.ts +100 -2
  13. package/src/business/acquisition/api-schemas.ts +81 -43
  14. package/src/business/acquisition/build-templates.test.ts +212 -0
  15. package/src/business/acquisition/types.ts +21 -38
  16. package/src/execution/engine/index.ts +436 -434
  17. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -0
  18. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -0
  19. package/src/execution/engine/tools/lead-service-types.ts +51 -9
  20. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -6
  21. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -5
  22. package/src/execution/engine/tools/platform/acquisition/types.ts +20 -9
  23. package/src/execution/engine/tools/registry.ts +700 -698
  24. package/src/execution/engine/tools/tool-maps.ts +10 -0
  25. package/src/execution/external/__tests__/api-schemas.test.ts +127 -0
  26. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -6
  27. package/src/integrations/oauth/provider-registry.ts +74 -61
  28. package/src/integrations/oauth/server/credentials.ts +43 -39
  29. package/src/knowledge/__tests__/queries.test.ts +89 -0
  30. package/src/organization-model/__tests__/icons.test.ts +61 -0
  31. package/src/organization-model/__tests__/knowledge.test.ts +118 -1
  32. package/src/organization-model/__tests__/prospecting-ssot.test.ts +94 -0
  33. package/src/organization-model/defaults.ts +8 -0
  34. package/src/organization-model/domains/knowledge.ts +9 -0
  35. package/src/organization-model/domains/prospecting.ts +272 -226
  36. package/src/organization-model/domains/sales.ts +32 -25
  37. package/src/organization-model/icons.ts +3 -0
  38. package/src/organization-model/types.ts +9 -1
  39. package/src/platform/constants/versions.ts +1 -1
  40. package/src/platform/utils/__tests__/validation.test.ts +1084 -1083
  41. package/src/platform/utils/validation.ts +425 -425
  42. package/src/reference/_generated/contracts.md +78 -65
  43. package/src/server.ts +6 -0
  44. package/src/supabase/database.types.ts +6 -12
@@ -258,6 +258,8 @@ import type {
258
258
  RecordListExecutionParams,
259
259
  UpdateCompanyStageParams,
260
260
  UpdateContactStageParams,
261
+ ListPendingCompanyIdsParams,
262
+ ListPendingContactIdsParams,
261
263
  UpsertSocialPostParams,
262
264
  UpsertSocialPostsResult,
263
265
  GetDealByIdParams,
@@ -725,6 +727,14 @@ export type ListToolMap = {
725
727
  params: Omit<UpdateContactStageParams, 'organizationId'>
726
728
  result: void
727
729
  }
730
+ listPendingCompanyIds: {
731
+ params: Omit<ListPendingCompanyIdsParams, 'organizationId'>
732
+ result: string[]
733
+ }
734
+ listPendingContactIds: {
735
+ params: Omit<ListPendingContactIdsParams, 'organizationId'>
736
+ result: string[]
737
+ }
728
738
  }
729
739
 
730
740
  // ---------------------------------------------------------------------------
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { ExecuteAsyncEnvelopeSchema, GetExecutionsQuerySchema } from '../api-schemas'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // ExecuteAsyncEnvelopeSchema
6
+ // ---------------------------------------------------------------------------
7
+
8
+ describe('ExecuteAsyncEnvelopeSchema', () => {
9
+ it('accepts a minimal payload with only resourceId', () => {
10
+ const result = ExecuteAsyncEnvelopeSchema.safeParse({ resourceId: 'my-workflow' })
11
+ expect(result.success).toBe(true)
12
+ })
13
+
14
+ it('accepts resourceId with optional input passthrough', () => {
15
+ const result = ExecuteAsyncEnvelopeSchema.safeParse({
16
+ resourceId: 'my-workflow',
17
+ input: { foo: 'bar', nested: { count: 5 } }
18
+ })
19
+ expect(result.success).toBe(true)
20
+ if (result.success) {
21
+ expect(result.data.resourceId).toBe('my-workflow')
22
+ expect(result.data.input).toEqual({ foo: 'bar', nested: { count: 5 } })
23
+ }
24
+ })
25
+
26
+ it('accepts any arbitrary input shape (unknown passthrough)', () => {
27
+ const result = ExecuteAsyncEnvelopeSchema.safeParse({
28
+ resourceId: 'some-resource',
29
+ input: [1, 2, 3]
30
+ })
31
+ expect(result.success).toBe(true)
32
+ })
33
+
34
+ it('accepts input as null (unknown type allows null)', () => {
35
+ const result = ExecuteAsyncEnvelopeSchema.safeParse({ resourceId: 'r', input: null })
36
+ expect(result.success).toBe(true)
37
+ })
38
+
39
+ it('accepts payload without input field (input is optional)', () => {
40
+ const result = ExecuteAsyncEnvelopeSchema.safeParse({ resourceId: 'r' })
41
+ expect(result.success).toBe(true)
42
+ if (result.success) {
43
+ expect(result.data.input).toBeUndefined()
44
+ }
45
+ })
46
+
47
+ it('rejects an empty resourceId (minLength 1)', () => {
48
+ expect(ExecuteAsyncEnvelopeSchema.safeParse({ resourceId: '' }).success).toBe(false)
49
+ })
50
+
51
+ it('rejects a missing resourceId', () => {
52
+ expect(ExecuteAsyncEnvelopeSchema.safeParse({}).success).toBe(false)
53
+ expect(ExecuteAsyncEnvelopeSchema.safeParse({ input: { x: 1 } }).success).toBe(false)
54
+ })
55
+
56
+ it('rejects resourceId as a non-string type', () => {
57
+ expect(ExecuteAsyncEnvelopeSchema.safeParse({ resourceId: 42 }).success).toBe(false)
58
+ expect(ExecuteAsyncEnvelopeSchema.safeParse({ resourceId: null }).success).toBe(false)
59
+ })
60
+ })
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // GetExecutionsQuerySchema
64
+ // ---------------------------------------------------------------------------
65
+
66
+ describe('GetExecutionsQuerySchema', () => {
67
+ it('accepts an empty query and applies default limit of 50', () => {
68
+ const result = GetExecutionsQuerySchema.safeParse({})
69
+ expect(result.success).toBe(true)
70
+ if (result.success) {
71
+ expect(result.data.limit).toBe(50)
72
+ }
73
+ })
74
+
75
+ it('coerces limit from string "10" to number 10', () => {
76
+ const result = GetExecutionsQuerySchema.safeParse({ limit: '10' })
77
+ expect(result.success).toBe(true)
78
+ if (result.success) expect(result.data.limit).toBe(10)
79
+ })
80
+
81
+ it('coerces limit from string "100" to number 100 (upper boundary)', () => {
82
+ const result = GetExecutionsQuerySchema.safeParse({ limit: '100' })
83
+ expect(result.success).toBe(true)
84
+ if (result.success) expect(result.data.limit).toBe(100)
85
+ })
86
+
87
+ it('coerces limit from string "1" to number 1 (lower boundary)', () => {
88
+ const result = GetExecutionsQuerySchema.safeParse({ limit: '1' })
89
+ expect(result.success).toBe(true)
90
+ if (result.success) expect(result.data.limit).toBe(1)
91
+ })
92
+
93
+ it('rejects limit of 101 (above max 100)', () => {
94
+ expect(GetExecutionsQuerySchema.safeParse({ limit: '101' }).success).toBe(false)
95
+ })
96
+
97
+ it('rejects limit of 0 (below min 1)', () => {
98
+ expect(GetExecutionsQuerySchema.safeParse({ limit: '0' }).success).toBe(false)
99
+ })
100
+
101
+ it('rejects a negative limit', () => {
102
+ expect(GetExecutionsQuerySchema.safeParse({ limit: '-1' }).success).toBe(false)
103
+ })
104
+
105
+ it('rejects a non-numeric string for limit', () => {
106
+ expect(GetExecutionsQuerySchema.safeParse({ limit: 'abc' }).success).toBe(false)
107
+ })
108
+
109
+ it('accepts status as an optional string filter', () => {
110
+ const result = GetExecutionsQuerySchema.safeParse({ status: 'running' })
111
+ expect(result.success).toBe(true)
112
+ if (result.success) expect(result.data.status).toBe('running')
113
+ })
114
+
115
+ it('accepts status as undefined (optional field omitted)', () => {
116
+ const result = GetExecutionsQuerySchema.safeParse({})
117
+ expect(result.success).toBe(true)
118
+ if (result.success) expect(result.data.status).toBeUndefined()
119
+ })
120
+
121
+ it('note: GetExecutionsQuerySchema does not have an offset field', () => {
122
+ // The schema only defines limit and status -- no offset field exists.
123
+ // Passing offset is neither rejected nor coerced (schema is not strict).
124
+ const result = GetExecutionsQuerySchema.safeParse({ limit: '10', offset: '5' })
125
+ expect(result.success).toBe(true)
126
+ })
127
+ })
@@ -38,12 +38,13 @@ describe('OAuth Provider Registry', () => {
38
38
  })
39
39
 
40
40
  describe('listProviders', () => {
41
- it('should return all providers as array', () => {
42
- const providers = listProviders()
43
- expect(providers).toHaveLength(2)
44
- expect(providers).toContainEqual(OAUTH_PROVIDERS['google-sheets'])
45
- expect(providers).toContainEqual(OAUTH_PROVIDERS.dropbox)
46
- })
41
+ it('should return all providers as array', () => {
42
+ const providers = listProviders()
43
+ expect(providers).toHaveLength(Object.keys(OAUTH_PROVIDERS).length)
44
+ expect(providers).toContainEqual(OAUTH_PROVIDERS['google-sheets'])
45
+ expect(providers).toContainEqual(OAUTH_PROVIDERS['google-calendar'])
46
+ expect(providers).toContainEqual(OAUTH_PROVIDERS.dropbox)
47
+ })
47
48
 
48
49
  it('should return array with correct provider structure', () => {
49
50
  const providers = listProviders()
@@ -1,61 +1,74 @@
1
- import type { OAuthProviderConfig } from './types'
2
-
3
- /**
4
- * OAuth Provider Registry
5
- *
6
- * Add new integration = add config object here (no new code needed)
7
- *
8
- * Standard token exchange methods:
9
- * - basic-auth: client_id:client_secret in Authorization header
10
- * - form-encoded: credentials in URL-encoded body (Google)
11
- * - json-body: credentials in JSON body (most providers)
12
- *
13
- * Custom overrides for edge cases:
14
- * - customAuthFlow: Override authorization URL building
15
- * - customTokenExchange: Override token exchange logic
16
- */
17
- export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
18
- 'google-sheets': {
19
- id: 'google-sheets',
20
- name: 'Google Sheets',
21
- authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
22
- tokenUrl: 'https://oauth2.googleapis.com/token',
23
- authParams: {
24
- access_type: 'offline', // Required for refresh token
25
- prompt: 'consent' // Force consent to get refresh token on reconnect
26
- },
27
- tokenExchange: 'form-encoded',
28
- scopes: ['https://www.googleapis.com/auth/spreadsheets']
29
- },
30
-
31
- dropbox: {
32
- id: 'dropbox',
33
- name: 'Dropbox',
34
- authUrl: 'https://www.dropbox.com/oauth2/authorize',
35
- tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
36
- authParams: {
37
- token_access_type: 'offline' // Required for refresh token
38
- },
39
- tokenExchange: 'form-encoded',
40
- scopes: ['files.content.write', 'files.content.read', 'files.metadata.write']
41
- }
42
- }
43
-
44
- /**
45
- * Get provider config by ID
46
- * @throws Error if provider not found
47
- */
48
- export function getProviderConfig(providerId: string): OAuthProviderConfig {
49
- const config = OAUTH_PROVIDERS[providerId]
50
- if (!config) {
51
- throw new Error(`Unknown OAuth provider: ${providerId}`)
52
- }
53
- return config
54
- }
55
-
56
- /**
57
- * List all available providers (for frontend)
58
- */
59
- export function listProviders(): OAuthProviderConfig[] {
60
- return Object.values(OAUTH_PROVIDERS)
61
- }
1
+ import type { OAuthProviderConfig } from './types'
2
+
3
+ /**
4
+ * OAuth Provider Registry
5
+ *
6
+ * Add new integration = add config object here (no new code needed)
7
+ *
8
+ * Standard token exchange methods:
9
+ * - basic-auth: client_id:client_secret in Authorization header
10
+ * - form-encoded: credentials in URL-encoded body (Google)
11
+ * - json-body: credentials in JSON body (most providers)
12
+ *
13
+ * Custom overrides for edge cases:
14
+ * - customAuthFlow: Override authorization URL building
15
+ * - customTokenExchange: Override token exchange logic
16
+ */
17
+ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
18
+ 'google-sheets': {
19
+ id: 'google-sheets',
20
+ name: 'Google Sheets',
21
+ authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
22
+ tokenUrl: 'https://oauth2.googleapis.com/token',
23
+ authParams: {
24
+ access_type: 'offline', // Required for refresh token
25
+ prompt: 'consent' // Force consent to get refresh token on reconnect
26
+ },
27
+ tokenExchange: 'form-encoded',
28
+ scopes: ['https://www.googleapis.com/auth/spreadsheets']
29
+ },
30
+
31
+ 'google-calendar': {
32
+ id: 'google-calendar',
33
+ name: 'Google Calendar',
34
+ authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
35
+ tokenUrl: 'https://oauth2.googleapis.com/token',
36
+ authParams: {
37
+ access_type: 'offline', // Required for refresh token
38
+ prompt: 'consent' // Force consent to get refresh token on reconnect
39
+ },
40
+ tokenExchange: 'form-encoded',
41
+ scopes: ['https://www.googleapis.com/auth/calendar.readonly']
42
+ },
43
+
44
+ dropbox: {
45
+ id: 'dropbox',
46
+ name: 'Dropbox',
47
+ authUrl: 'https://www.dropbox.com/oauth2/authorize',
48
+ tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
49
+ authParams: {
50
+ token_access_type: 'offline' // Required for refresh token
51
+ },
52
+ tokenExchange: 'form-encoded',
53
+ scopes: ['files.content.write', 'files.content.read', 'files.metadata.write']
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get provider config by ID
59
+ * @throws Error if provider not found
60
+ */
61
+ export function getProviderConfig(providerId: string): OAuthProviderConfig {
62
+ const config = OAUTH_PROVIDERS[providerId]
63
+ if (!config) {
64
+ throw new Error(`Unknown OAuth provider: ${providerId}`)
65
+ }
66
+ return config
67
+ }
68
+
69
+ /**
70
+ * List all available providers (for frontend)
71
+ */
72
+ export function listProviders(): OAuthProviderConfig[] {
73
+ return Object.values(OAUTH_PROVIDERS)
74
+ }
@@ -1,39 +1,43 @@
1
- /**
2
- * OAuth Provider Credentials
3
- * Maps provider IDs to environment variable names
4
- * Single source of truth for credential lookup
5
- */
6
-
7
- const PROVIDER_ENV_VARS: Record<string, { clientIdEnv: string; clientSecretEnv: string }> = {
8
- 'google-sheets': {
9
- clientIdEnv: 'GOOGLE_OAUTH_CLIENT_ID',
10
- clientSecretEnv: 'GOOGLE_OAUTH_CLIENT_SECRET'
11
- },
12
- dropbox: {
13
- clientIdEnv: 'DROPBOX_APP_KEY',
14
- clientSecretEnv: 'DROPBOX_APP_SECRET'
15
- }
16
- }
17
-
18
- /**
19
- * Get OAuth client credentials for a provider
20
- * @throws Error if provider unknown or credentials missing
21
- */
22
- export function getOAuthCredentials(providerId: string): { clientId: string; clientSecret: string } {
23
- const config = PROVIDER_ENV_VARS[providerId]
24
- if (!config) {
25
- throw new Error(`Unknown OAuth provider: ${providerId}`)
26
- }
27
-
28
- const clientId = process.env[config.clientIdEnv]
29
- const clientSecret = process.env[config.clientSecretEnv]
30
-
31
- if (!clientId) {
32
- throw new Error(`Missing environment variable: ${config.clientIdEnv}`)
33
- }
34
- if (!clientSecret) {
35
- throw new Error(`Missing environment variable: ${config.clientSecretEnv}`)
36
- }
37
-
38
- return { clientId, clientSecret }
39
- }
1
+ /**
2
+ * OAuth Provider Credentials
3
+ * Maps provider IDs to environment variable names
4
+ * Single source of truth for credential lookup
5
+ */
6
+
7
+ const PROVIDER_ENV_VARS: Record<string, { clientIdEnv: string; clientSecretEnv: string }> = {
8
+ 'google-sheets': {
9
+ clientIdEnv: 'GOOGLE_OAUTH_CLIENT_ID',
10
+ clientSecretEnv: 'GOOGLE_OAUTH_CLIENT_SECRET'
11
+ },
12
+ 'google-calendar': {
13
+ clientIdEnv: 'GOOGLE_OAUTH_CLIENT_ID',
14
+ clientSecretEnv: 'GOOGLE_OAUTH_CLIENT_SECRET'
15
+ },
16
+ dropbox: {
17
+ clientIdEnv: 'DROPBOX_APP_KEY',
18
+ clientSecretEnv: 'DROPBOX_APP_SECRET'
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Get OAuth client credentials for a provider
24
+ * @throws Error if provider unknown or credentials missing
25
+ */
26
+ export function getOAuthCredentials(providerId: string): { clientId: string; clientSecret: string } {
27
+ const config = PROVIDER_ENV_VARS[providerId]
28
+ if (!config) {
29
+ throw new Error(`Unknown OAuth provider: ${providerId}`)
30
+ }
31
+
32
+ const clientId = process.env[config.clientIdEnv]
33
+ const clientSecret = process.env[config.clientSecretEnv]
34
+
35
+ if (!clientId) {
36
+ throw new Error(`Missing environment variable: ${config.clientIdEnv}`)
37
+ }
38
+ if (!clientSecret) {
39
+ throw new Error(`Missing environment variable: ${config.clientSecretEnv}`)
40
+ }
41
+
42
+ return { clientId, clientSecret }
43
+ }
@@ -384,6 +384,95 @@ describe('parsePath — invalid inputs', () => {
384
384
  })
385
385
  })
386
386
 
387
+ // ---------------------------------------------------------------------------
388
+ // parsePath — edge cases
389
+ // ---------------------------------------------------------------------------
390
+
391
+ describe('parsePath edge cases', () => {
392
+ // Whitespace handling — the parser does NOT trim the incoming string.
393
+ // A path with leading whitespace does not start with '/' so it throws.
394
+ it('throws on path with leading whitespace', () => {
395
+ expect(() => parsePath(' /by-kind/playbook')).toThrow('parsePath: path must start with "/"')
396
+ })
397
+
398
+ // Trailing whitespace becomes part of the last segment (not stripped).
399
+ // The segment is non-empty so parsing proceeds and the whitespace is
400
+ // included verbatim in the returned arg.
401
+ it('preserves trailing whitespace inside the last segment', () => {
402
+ const result = parsePath('/by-kind/playbook ')
403
+ expect(result.mount).toBe('by-kind')
404
+ expect(result.args[0]).toBe('playbook ')
405
+ })
406
+
407
+ // Multiple trailing slashes are all stripped (regex /\/+$/).
408
+ it('strips multiple trailing slashes', () => {
409
+ expect(parsePath('/by-kind/playbook///')).toEqual({ mount: 'by-kind', args: ['playbook'] })
410
+ })
411
+
412
+ // Lone-slash variants — a path of only slashes normalises to an empty
413
+ // segments list and must throw the root-with-no-mount error.
414
+ it('throws on path consisting only of slashes', () => {
415
+ expect(() => parsePath('///')).toThrow('parsePath: path resolves to root with no mount')
416
+ })
417
+
418
+ // Case sensitivity — parsePath does not normalise case.
419
+ // 'Playbook' (capitalised) is passed through as-is to the caller.
420
+ it('preserves node-id case (no case normalisation)', () => {
421
+ // /by-kind takes rest[0] verbatim
422
+ const result = parsePath('/by-kind/Playbook')
423
+ expect(result.args[0]).toBe('Playbook')
424
+ })
425
+
426
+ // Feature ID: dot notation passes through unchanged.
427
+ it('/by-feature/sales.crm preserves dot notation', () => {
428
+ expect(parsePath('/by-feature/sales.crm')).toEqual({ mount: 'by-feature', args: ['sales.crm'] })
429
+ })
430
+
431
+ // Feature ID: slash notation joins rest segments with '/'.
432
+ // The parser does NOT convert slashes to dots — the caller must normalise.
433
+ it('/by-feature/sales/crm joins segments with slash (no dot conversion)', () => {
434
+ const result = parsePath('/by-feature/sales/crm')
435
+ expect(result.mount).toBe('by-feature')
436
+ // args[0] is 'sales/crm', NOT 'sales.crm'
437
+ expect(result.args[0]).toBe('sales/crm')
438
+ })
439
+
440
+ // /by-kind silently ignores extra path segments beyond the kind argument.
441
+ it('/by-kind/playbook/extra silently uses only first kind segment', () => {
442
+ // First segment after 'by-kind' is taken; extra is ignored.
443
+ const result = parsePath('/by-kind/playbook/extra')
444
+ expect(result.mount).toBe('by-kind')
445
+ expect(result.args[0]).toBe('playbook')
446
+ expect(result.args).toHaveLength(1)
447
+ })
448
+
449
+ // /by-owner joins rest with '/' (same behaviour as /by-feature).
450
+ it('/by-owner/role.ops-lead/sub joins segments with slash', () => {
451
+ const result = parsePath('/by-owner/role.ops-lead/sub')
452
+ expect(result.mount).toBe('by-owner')
453
+ expect(result.args[0]).toBe('role.ops-lead/sub')
454
+ })
455
+
456
+ // Graph node ID may contain a colon prefix (e.g. knowledge:knowledge.foo).
457
+ // The parser accepts it — the rest before the verb is joined with '/'.
458
+ it('/graph/<prefixed-nodeId>/governs round-trips the prefixed id', () => {
459
+ const result = parsePath('/graph/knowledge:knowledge.test-node/governs')
460
+ expect(result.mount).toBe('graph')
461
+ expect(result.args[0]).toBe('knowledge:knowledge.test-node')
462
+ expect(result.args[1]).toBe('governs')
463
+ })
464
+
465
+ // /graph with no arguments at all (just '/graph').
466
+ it('throws on /graph with no nodeId or verb', () => {
467
+ expect(() => parsePath('/graph')).toThrow('/graph requires <nodeId>/<verb>')
468
+ })
469
+
470
+ // /node mount — single-segment path works for any non-reserved first segment.
471
+ it('single-segment non-reserved path is treated as node mount', () => {
472
+ expect(parsePath('/knowledge.my-doc')).toEqual({ mount: 'node', args: ['knowledge.my-doc'] })
473
+ })
474
+ })
475
+
387
476
  // ---------------------------------------------------------------------------
388
477
  // formatText
389
478
  // ---------------------------------------------------------------------------
@@ -24,4 +24,65 @@ describe('OrganizationModelIconTokenSchema', () => {
24
24
  expect(OrganizationModelIconTokenSchema.safeParse(token).success).toBe(false)
25
25
  }
26
26
  })
27
+
28
+ // max-length — CustomIconTokenSchema allows at most 80 characters
29
+ it('rejects custom token exceeding 80 characters', () => {
30
+ // 'custom.' is 7 chars; pad with 'a' so total = 81
31
+ const token = 'custom.' + 'a'.repeat(74)
32
+ expect(token.length).toBe(81)
33
+ expect(OrganizationModelIconTokenSchema.safeParse(token).success).toBe(false)
34
+ })
35
+
36
+ it('accepts custom token at exactly 80 characters', () => {
37
+ // 'custom.' = 7 chars, fill rest with 'a' to reach exactly 80
38
+ const token = 'custom.' + 'a'.repeat(73)
39
+ expect(token.length).toBe(80)
40
+ expect(OrganizationModelIconTokenSchema.safeParse(token).success).toBe(true)
41
+ })
42
+
43
+ // invalid charset — custom token regex only allows [a-z0-9] with [-._] separators
44
+ it('rejects custom token with uppercase letters in the suffix', () => {
45
+ expect(OrganizationModelIconTokenSchema.safeParse('custom.MyIcon').success).toBe(false)
46
+ })
47
+
48
+ it('rejects custom token with spaces', () => {
49
+ expect(OrganizationModelIconTokenSchema.safeParse('custom.my icon').success).toBe(false)
50
+ })
51
+
52
+ it('rejects custom token with a leading separator (double-dot)', () => {
53
+ expect(OrganizationModelIconTokenSchema.safeParse('custom..icon').success).toBe(false)
54
+ })
55
+
56
+ it('rejects custom token with a trailing separator', () => {
57
+ expect(OrganizationModelIconTokenSchema.safeParse('custom.icon-').success).toBe(false)
58
+ })
59
+
60
+ // missing prefix — tokens without the 'custom.' prefix that are not built-ins
61
+ it('rejects token that looks like a custom slug but lacks the custom. prefix', () => {
62
+ expect(OrganizationModelIconTokenSchema.safeParse('my-icon').success).toBe(false)
63
+ expect(OrganizationModelIconTokenSchema.safeParse('org.my-icon').success).toBe(false)
64
+ })
65
+ })
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // OrganizationModelBuiltinIconTokenSchema — only accepts built-ins
69
+ // ---------------------------------------------------------------------------
70
+
71
+ describe('OrganizationModelBuiltinIconTokenSchema', () => {
72
+ it('accepts every entry in ORGANIZATION_MODEL_ICON_TOKENS', () => {
73
+ for (const token of ORGANIZATION_MODEL_ICON_TOKENS) {
74
+ expect(OrganizationModelBuiltinIconTokenSchema.safeParse(token).success).toBe(true)
75
+ }
76
+ })
77
+
78
+ it('rejects custom.* tokens (only built-ins allowed)', () => {
79
+ expect(OrganizationModelBuiltinIconTokenSchema.safeParse('custom.my-icon').success).toBe(false)
80
+ expect(OrganizationModelBuiltinIconTokenSchema.safeParse('custom.partner-portal').success).toBe(false)
81
+ })
82
+
83
+ it('rejects arbitrary strings not in the enum', () => {
84
+ expect(OrganizationModelBuiltinIconTokenSchema.safeParse('book').success).toBe(false)
85
+ expect(OrganizationModelBuiltinIconTokenSchema.safeParse('').success).toBe(false)
86
+ expect(OrganizationModelBuiltinIconTokenSchema.safeParse('nav.unknown-item').success).toBe(false)
87
+ })
27
88
  })