@commandable/integration-data 0.0.1 → 0.0.5

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 (83) hide show
  1. package/dist/credentials-index.d.ts +4 -21
  2. package/dist/credentials-index.d.ts.map +1 -1
  3. package/dist/credentials-index.js +407 -215
  4. package/dist/credentials-index.js.map +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/loader.d.ts +38 -2
  10. package/dist/loader.d.ts.map +1 -1
  11. package/dist/loader.js +70 -16
  12. package/dist/loader.js.map +1 -1
  13. package/integrations/__tests__/liveHarness.ts +84 -0
  14. package/integrations/__tests__/usageParity.ts +54 -0
  15. package/integrations/airtable/__tests__/get_handlers.test.ts +43 -31
  16. package/integrations/airtable/__tests__/usage_parity.test.ts +3 -29
  17. package/integrations/airtable/__tests__/write_and_admin_handlers.test.ts +20 -17
  18. package/integrations/airtable/credentials.json +21 -16
  19. package/integrations/github/__tests__/get_handlers.test.ts +101 -108
  20. package/integrations/github/__tests__/usage_parity.test.ts +15 -27
  21. package/integrations/github/__tests__/write_handlers.test.ts +223 -306
  22. package/integrations/github/credentials.json +40 -15
  23. package/integrations/github/credentials_hint_classic_pat.md +8 -0
  24. package/integrations/github/credentials_hint_fine_grained_pat.md +9 -0
  25. package/integrations/github/handlers/create_commit.js +2 -17
  26. package/integrations/github/manifest.json +2 -2
  27. package/integrations/google-calendar/__tests__/get_handlers.test.ts +21 -13
  28. package/integrations/google-calendar/__tests__/usage_parity.test.ts +3 -28
  29. package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +24 -17
  30. package/integrations/google-calendar/credentials.json +50 -29
  31. package/integrations/google-calendar/credentials_hint_oauth_token.md +8 -0
  32. package/integrations/google-calendar/credentials_hint_service_account.md +10 -0
  33. package/integrations/google-docs/__tests__/get_handlers.test.ts +87 -61
  34. package/integrations/google-docs/__tests__/usage_parity.test.ts +3 -28
  35. package/integrations/google-docs/__tests__/write_handlers.test.ts +251 -245
  36. package/integrations/google-docs/credentials.json +50 -29
  37. package/integrations/google-docs/credentials_hint_oauth_token.md +8 -0
  38. package/integrations/google-docs/credentials_hint_service_account.md +10 -0
  39. package/integrations/google-docs/handlers/insert_inline_image_after_first_match.js +1 -1
  40. package/integrations/google-docs/schemas/insert_inline_image_after_first_match.json +0 -1
  41. package/integrations/google-drive/__tests__/handlers.test.ts +102 -0
  42. package/integrations/google-drive/credentials.json +57 -0
  43. package/integrations/google-drive/credentials_hint_oauth_token.md +8 -0
  44. package/integrations/google-drive/credentials_hint_service_account.md +10 -0
  45. package/integrations/google-drive/handlers/create_file.js +15 -0
  46. package/integrations/google-drive/handlers/create_folder.js +15 -0
  47. package/integrations/google-drive/handlers/delete_file.js +14 -0
  48. package/integrations/google-drive/handlers/get_file.js +7 -0
  49. package/integrations/google-drive/handlers/move_file.js +12 -0
  50. package/integrations/google-drive/manifest.json +42 -0
  51. package/integrations/google-drive/schemas/create_file.json +12 -0
  52. package/integrations/google-drive/schemas/create_folder.json +11 -0
  53. package/integrations/google-drive/schemas/delete_file.json +10 -0
  54. package/integrations/google-drive/schemas/get_file.json +10 -0
  55. package/integrations/google-drive/schemas/move_file.json +12 -0
  56. package/integrations/google-sheet/__tests__/get_handlers.test.ts +48 -55
  57. package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
  58. package/integrations/google-sheet/__tests__/write_handlers.test.ts +65 -63
  59. package/integrations/google-sheet/credentials.json +50 -29
  60. package/integrations/google-sheet/credentials_hint_oauth_token.md +8 -0
  61. package/integrations/google-sheet/credentials_hint_service_account.md +10 -0
  62. package/integrations/google-slides/__tests__/get_handlers.test.ts +38 -36
  63. package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
  64. package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -59
  65. package/integrations/google-slides/credentials.json +50 -29
  66. package/integrations/google-slides/credentials_hint_oauth_token.md +8 -0
  67. package/integrations/google-slides/credentials_hint_service_account.md +10 -0
  68. package/integrations/notion/__tests__/get_handlers.test.ts +18 -15
  69. package/integrations/notion/__tests__/usage_parity.test.ts +3 -28
  70. package/integrations/notion/__tests__/write_and_admin_handlers.test.ts +56 -60
  71. package/integrations/notion/credentials.json +22 -17
  72. package/integrations/trello/__tests__/get_handlers.test.ts +58 -73
  73. package/integrations/trello/__tests__/usage_parity.test.ts +3 -28
  74. package/integrations/trello/__tests__/write_and_admin_handlers.test.ts +49 -67
  75. package/integrations/trello/credentials.json +26 -21
  76. package/integrations/trello/handlers/close_board.js +6 -0
  77. package/integrations/trello/handlers/create_board.js +11 -0
  78. package/integrations/trello/handlers/delete_board.js +13 -0
  79. package/integrations/trello/manifest.json +21 -0
  80. package/integrations/trello/schemas/close_board.json +10 -0
  81. package/integrations/trello/schemas/create_board.json +12 -0
  82. package/integrations/trello/schemas/delete_board.json +10 -0
  83. package/package.json +1 -1
@@ -1,76 +1,68 @@
1
- import { beforeAll, describe, expect, it } from 'vitest'
2
- import { IntegrationProxy } from '../../../src/integrations/proxy.js'
3
- import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
2
+ import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, hasEnv, safeCleanup } from '../../__tests__/liveHarness.js'
4
3
 
5
- // LIVE Google Slides write tests using managed OAuth
4
+ // LIVE Google Slides write tests using credentials
6
5
  // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - GSLIDES_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-slides')
10
- // - GSLIDES_TEST_PRESENTATION_ID (a presentation ID with write access)
11
-
12
- interface Ctx { presentationId?: string }
13
-
14
- const env = process.env as Record<string, string>
15
- const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
16
- const suite = hasEnv(
17
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
18
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
19
- 'GSLIDES_TEST_CONNECTION_ID',
20
- 'GSLIDES_TEST_PRESENTATION_ID',
21
- )
6
+ // - Either GOOGLE_TOKEN, OR GOOGLE_SERVICE_ACCOUNT_JSON
7
+
8
+ interface Ctx { presentationId?: string, folderId?: string, anchorText?: string }
9
+
10
+ const suite = (hasEnv('GOOGLE_TOKEN') || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON'))
22
11
  ? describe
23
12
  : describe.skip
24
13
 
25
14
  suite('google-slides write handlers (live)', () => {
26
15
  const ctx: Ctx = {}
27
- let buildWriteHandler: (name: string) => ((input: any) => Promise<any>)
16
+ let slides: ReturnType<typeof createToolbox>
17
+ let drive: ReturnType<typeof createToolbox>
28
18
 
29
19
  beforeAll(async () => {
30
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GSLIDES_TEST_CONNECTION_ID, GSLIDES_TEST_PRESENTATION_ID } = env
31
-
32
- const proxy = new IntegrationProxy({
33
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
34
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
20
+ const env = process.env as Record<string, string | undefined>
21
+ const credentialStore = createCredentialStore(async () => ({
22
+ token: env.GOOGLE_TOKEN || '',
23
+ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
24
+ subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
25
+ }))
26
+ const proxy = createProxy(credentialStore)
27
+ slides = createToolbox('google-slides', proxy, createIntegrationNode('google-slides', { label: 'Google Slides', credentialId: 'google-slides-creds' }))
28
+ drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
29
+
30
+ const folder = await drive.write('create_folder')({ name: `CmdTest Slides Write ${Date.now()}` })
31
+ ctx.folderId = folder?.id
32
+ expect(ctx.folderId).toBeTruthy()
33
+
34
+ const created = await drive.write('create_file')({
35
+ name: `CmdTest Slides ${Date.now()}`,
36
+ mimeType: 'application/vnd.google-apps.presentation',
37
+ parentId: ctx.folderId,
35
38
  })
36
- const integrationNode = { id: 'node-gslides', type: 'google-slides', label: 'Google Slides', connectionId: GSLIDES_TEST_CONNECTION_ID } as any
37
-
38
- const tools = loadIntegrationTools('google-slides')
39
- expect(tools).toBeTruthy()
39
+ ctx.presentationId = created?.id
40
+ expect(ctx.presentationId).toBeTruthy()
40
41
 
41
- buildWriteHandler = (name: string) => {
42
- const tool = tools!.write.find(t => t.name === name)
43
- expect(tool, `write tool ${name} exists`).toBeTruthy()
44
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
45
- const build = new Function('integration', `return (${tool!.handlerCode});`)
46
- return build(integration) as (input: any) => Promise<any>
47
- }
48
-
49
- ctx.presentationId = GSLIDES_TEST_PRESENTATION_ID
42
+ ctx.anchorText = `CMD_ANCHOR_${Date.now()}`
43
+ const appendTitle = slides.write('append_text_to_title_of_slide_index')
44
+ await appendTitle({ presentationId: ctx.presentationId, slideIndex: 0, text: ` ${ctx.anchorText} ` })
50
45
  }, 60000)
51
46
 
47
+ afterAll(async () => {
48
+ await safeCleanup(async () => ctx.presentationId ? drive.write('delete_file')({ fileId: ctx.presentationId }) : Promise.resolve())
49
+ await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
50
+ }, 60_000)
51
+
52
52
  it('batch_update performs a trivial update (no-op replace)', async () => {
53
53
  if (!ctx.presentationId)
54
54
  return expect(true).toBe(true)
55
- const handler = buildWriteHandler('batch_update')
55
+ const handler = slides.write('batch_update')
56
56
  const res = await handler({ presentationId: ctx.presentationId, requests: [
57
57
  { replaceAllText: { containsText: { text: '___unlikely___', matchCase: true }, replaceText: '___unlikely___' } },
58
58
  ] })
59
59
  expect(res?.presentationId || Array.isArray(res?.replies) || res?.writeControl).toBeTruthy()
60
60
  }, 60000)
61
61
 
62
- it('create_presentation creates a presentation when allowed', async () => {
63
- if (!env.GSLIDES_ALLOW_CREATE)
64
- return expect(true).toBe(true)
65
- const handler = buildWriteHandler('create_presentation')
66
- const res = await handler({ title: `Cmd Slides ${Date.now()}` })
67
- expect(typeof res?.presentationId === 'string').toBe(true)
68
- }, 60000)
69
-
70
62
  it('append_text_to_title_of_slide_index appends to title', async () => {
71
63
  if (!ctx.presentationId)
72
64
  return expect(true).toBe(true)
73
- const handler = buildWriteHandler('append_text_to_title_of_slide_index')
65
+ const handler = slides.write('append_text_to_title_of_slide_index')
74
66
  const res = await handler({ presentationId: ctx.presentationId, slideIndex: 0, text: ` CmdTest ${Date.now()}` })
75
67
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
76
68
  }, 60000)
@@ -78,7 +70,7 @@ suite('google-slides write handlers (live)', () => {
78
70
  it('replace_text_first_match replaces text (no-op ok)', async () => {
79
71
  if (!ctx.presentationId)
80
72
  return expect(true).toBe(true)
81
- const handler = buildWriteHandler('replace_text_first_match')
73
+ const handler = slides.write('replace_text_first_match')
82
74
  const res = await handler({ presentationId: ctx.presentationId, findText: '___unlikely___', replaceText: '___unlikely___' })
83
75
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
84
76
  }, 60000)
@@ -86,40 +78,54 @@ suite('google-slides write handlers (live)', () => {
86
78
  it('style_text_first_match applies style', async () => {
87
79
  if (!ctx.presentationId)
88
80
  return expect(true).toBe(true)
89
- const handler = buildWriteHandler('style_text_first_match')
90
- const res = await handler({ presentationId: ctx.presentationId, findText: 'the', textStyle: { bold: true } })
81
+ if (!ctx.anchorText)
82
+ return expect(true).toBe(true)
83
+ const handler = slides.write('style_text_first_match')
84
+ const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, textStyle: { bold: true } })
91
85
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
92
86
  }, 60000)
93
87
 
94
88
  it('insert_shape_after_first_match inserts a shape', async () => {
95
89
  if (!ctx.presentationId)
96
90
  return expect(true).toBe(true)
97
- const handler = buildWriteHandler('insert_shape_after_first_match')
98
- const res = await handler({ presentationId: ctx.presentationId, findText: 'the', shapeType: 'RECTANGLE' })
91
+ if (!ctx.anchorText)
92
+ return expect(true).toBe(true)
93
+ const handler = slides.write('insert_shape_after_first_match')
94
+ const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, shapeType: 'RECTANGLE' })
99
95
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
100
96
  }, 60000)
101
97
 
102
98
  it('insert_image_after_first_match inserts an image when allowed', async () => {
103
- if (!ctx.presentationId || !env.GSLIDES_TEST_IMAGE_URI)
99
+ if (!ctx.presentationId || !ctx.anchorText)
104
100
  return expect(true).toBe(true)
105
- const handler = buildWriteHandler('insert_image_after_first_match')
106
- const res = await handler({ presentationId: ctx.presentationId, findText: 'the', uri: env.GSLIDES_TEST_IMAGE_URI })
101
+ const imageUri = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
102
+ const handler = slides.write('insert_image_after_first_match')
103
+ const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, uri: imageUri })
107
104
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
108
105
  }, 60000)
109
106
 
110
107
  it('create_slide_after_first_match creates a slide near anchor', async () => {
111
108
  if (!ctx.presentationId)
112
109
  return expect(true).toBe(true)
113
- const handler = buildWriteHandler('create_slide_after_first_match')
114
- const res = await handler({ presentationId: ctx.presentationId, findText: 'the', layout: 'BLANK' })
110
+ if (!ctx.anchorText)
111
+ return expect(true).toBe(true)
112
+ const handler = slides.write('create_slide_after_first_match')
113
+ const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, layout: 'BLANK' })
115
114
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
116
115
  }, 60000)
117
116
 
118
117
  it('set_background_color_for_slide_index sets color', async () => {
119
118
  if (!ctx.presentationId)
120
119
  return expect(true).toBe(true)
121
- const handler = buildWriteHandler('set_background_color_for_slide_index')
120
+ const handler = slides.write('set_background_color_for_slide_index')
122
121
  const res = await handler({ presentationId: ctx.presentationId, slideIndex: 0, rgbColor: { red: 0.9, green: 0.9, blue: 0.9 } })
123
122
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
124
123
  }, 60000)
124
+
125
+ it('create_presentation creates a presentation (self-cleaning)', async () => {
126
+ const created = await slides.write('create_presentation')({ title: `CmdTest Slides Tool ${Date.now()}` })
127
+ const id = created?.presentationId
128
+ expect(typeof id).toBe('string')
129
+ await safeCleanup(async () => id ? drive.write('delete_file')({ fileId: id }) : Promise.resolve())
130
+ }, 60000)
125
131
  })
@@ -1,36 +1,57 @@
1
1
  {
2
- "schema": {
3
- "type": "object",
4
- "properties": {
5
- "token": {
6
- "type": "string",
7
- "title": "OAuth Access Token (optional)",
8
- "description": "Google OAuth access token to use as a Bearer token (typically short-lived)."
2
+ "variants": {
3
+ "service_account": {
4
+ "label": "Service Account (recommended)",
5
+ "schema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "serviceAccountJson": {
9
+ "type": "string",
10
+ "title": "Service Account JSON",
11
+ "description": "Full service account key JSON (contents of the downloaded JSON file from Google Cloud)."
12
+ },
13
+ "subject": {
14
+ "type": "string",
15
+ "title": "Subject / impersonated user (optional)",
16
+ "description": "Optional user email to impersonate when using Google Workspace domain-wide delegation."
17
+ },
18
+ "scopes": {
19
+ "type": "array",
20
+ "title": "OAuth scopes (optional)",
21
+ "description": "Optional override for OAuth scopes. Defaults to presentations + drive.",
22
+ "items": { "type": "string" }
23
+ }
24
+ },
25
+ "required": ["serviceAccountJson"],
26
+ "additionalProperties": false
9
27
  },
10
- "serviceAccountJson": {
11
- "type": "string",
12
- "title": "Service Account JSON (recommended)",
13
- "description": "Full service account key JSON (contents of the downloaded JSON file)."
28
+ "injection": {
29
+ "headers": {
30
+ "Authorization": "Bearer {{token}}"
31
+ }
14
32
  },
15
- "subject": {
16
- "type": "string",
17
- "title": "Subject / impersonated user (optional)",
18
- "description": "Optional user email to impersonate when using Google Workspace domain-wide delegation."
33
+ "preprocess": "google_service_account"
34
+ },
35
+ "oauth_token": {
36
+ "label": "OAuth Access Token (short-lived)",
37
+ "schema": {
38
+ "type": "object",
39
+ "properties": {
40
+ "token": {
41
+ "type": "string",
42
+ "title": "OAuth Access Token",
43
+ "description": "Short-lived Google OAuth access token with presentations and drive scopes."
44
+ }
45
+ },
46
+ "required": ["token"],
47
+ "additionalProperties": false
19
48
  },
20
- "scopes": {
21
- "type": "array",
22
- "title": "OAuth scopes (optional)",
23
- "description": "Optional override for OAuth scopes.",
24
- "items": { "type": "string" }
49
+ "injection": {
50
+ "headers": {
51
+ "Authorization": "Bearer {{token}}"
52
+ }
25
53
  }
26
- },
27
- "required": [],
28
- "additionalProperties": false
29
- },
30
- "injection": {
31
- "headers": {
32
- "Authorization": "Bearer {{token}}"
33
54
  }
34
- }
55
+ },
56
+ "default": "service_account"
35
57
  }
36
-
@@ -0,0 +1,8 @@
1
+ Obtain a short-lived Google OAuth access token:
2
+
3
+ 1. Use the Google OAuth 2.0 Playground (`https://developers.google.com/oauthplayground/`) or your own OAuth flow
4
+ 2. Select the scopes: `https://www.googleapis.com/auth/presentations` and `https://www.googleapis.com/auth/drive`
5
+ 3. Exchange the authorization code for an access token
6
+ 4. Paste the access token here
7
+
8
+ Note: OAuth access tokens are short-lived (typically 1 hour). For long-running use, prefer the Service Account variant.
@@ -0,0 +1,10 @@
1
+ Set up a Google Cloud Service Account:
2
+
3
+ 1. Open the [Google Cloud Console](https://console.cloud.google.com/)
4
+ 2. Enable the **Google Slides API** and **Google Drive API** for your project
5
+ 3. Go to **IAM & Admin → Service Accounts** and create a new service account
6
+ 4. Under **Keys**, click **Add Key → Create new key → JSON** and download the file
7
+ 5. Paste the full contents of the JSON file here
8
+ 6. Share your target presentations with the service account's `client_email`
9
+
10
+ For Google Workspace users: optionally configure domain-wide delegation and set `subject` to the user's email.
@@ -1,12 +1,10 @@
1
1
  import { beforeAll, describe, expect, it } from 'vitest'
2
- import { IntegrationProxy } from '../../../src/integrations/proxy.js'
3
- import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
2
+ import { IntegrationProxy } from '../../../../server/src/integrations/proxy.js'
3
+ import { loadIntegrationTools } from '../../../../server/src/integrations/dataLoader.js'
4
4
 
5
- // LIVE Notion integration tests using managed OAuth
5
+ // LIVE Notion integration tests using credentials
6
6
  // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - NOTION_TEST_CONNECTION_ID (managed OAuth connection for provider 'notion')
7
+ // - NOTION_TOKEN
10
8
 
11
9
  interface Ctx {
12
10
  database_id?: string
@@ -17,9 +15,7 @@ interface Ctx {
17
15
  const env = process.env as Record<string, string>
18
16
  const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
19
17
  const suite = hasEnv(
20
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
21
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
22
- 'NOTION_TEST_CONNECTION_ID',
18
+ 'NOTION_TOKEN',
23
19
  )
24
20
  ? describe
25
21
  : describe.skip
@@ -29,13 +25,20 @@ suite('notion read handlers (live)', () => {
29
25
  let buildHandler: (name: string) => ((input: any) => Promise<any>)
30
26
 
31
27
  beforeAll(async () => {
32
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, NOTION_TEST_CONNECTION_ID } = env
28
+ const credentialStore = {
29
+ getCredentials: async () => ({ token: env.NOTION_TOKEN || '' }),
30
+ }
33
31
 
34
- const proxy = new IntegrationProxy({
35
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
36
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
37
- })
38
- const integrationNode = { id: 'node-notion', type: 'notion', label: 'Notion', connectionId: NOTION_TEST_CONNECTION_ID } as any
32
+ const proxy = new IntegrationProxy({ credentialStore })
33
+ const integrationNode = {
34
+ spaceId: 'ci',
35
+ id: 'node-notion',
36
+ referenceId: 'node-notion',
37
+ type: 'notion',
38
+ label: 'Notion',
39
+ connectionMethod: 'credentials',
40
+ credentialId: 'notion-creds',
41
+ } as any
39
42
 
40
43
  const tools = loadIntegrationTools('notion')
41
44
  expect(tools).toBeTruthy()
@@ -1,34 +1,9 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
1
  import { describe, expect, it } from 'vitest'
5
- import { loadIntegrationManifest } from '../../../src/integrations/dataLoader.js'
6
-
7
- function escapeRegExp(str: string): string {
8
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
9
- }
2
+ import { getMissingToolUsages } from '../../__tests__/usageParity.js'
10
3
 
11
4
  describe('notion static usage parity', () => {
12
- it('every manifest tool is referenced in tests via build*(name)', () => {
13
- const manifest = loadIntegrationManifest('notion')!
14
- const toolNames = (manifest.tools as any[]).map(t => t.name)
15
-
16
- const testsDir = fileURLToPath(new URL('.', import.meta.url))
17
- expect(existsSync(testsDir)).toBe(true)
18
- const testFiles = readdirSync(testsDir)
19
- .filter(f => /\.test\.(t|j)s$/.test(f) && !f.includes('usage_parity.test'))
20
- .map(f => resolve(testsDir, f))
21
-
22
- const fileContents = testFiles.map(f => readFileSync(f, 'utf8'))
23
-
24
- const missing: string[] = []
25
- for (const name of toolNames) {
26
- const nameRe = new RegExp(`build(?:Read|Write|Admin)?(?:Handler)?\\(\\s*['\"\`]${escapeRegExp(name)}['\"\`]\\s*\\)`, 'm')
27
- const found = fileContents.some(src => nameRe.test(src))
28
- if (!found)
29
- missing.push(name)
30
- }
31
-
5
+ it('every manifest tool is referenced in tests', () => {
6
+ const missing = getMissingToolUsages({ integrationName: 'notion', importMetaUrl: import.meta.url })
32
7
  expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
33
8
  })
34
9
  })
@@ -1,73 +1,69 @@
1
- import { beforeAll, describe, expect, it } from 'vitest'
2
- import { IntegrationProxy } from '../../../src/integrations/proxy.js'
3
- import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
2
+ import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, hasEnv, safeCleanup } from '../../__tests__/liveHarness.js'
4
3
 
5
4
  interface Ctx {
6
5
  createdPageId?: string
7
- parentDatabaseId?: string
6
+ testDatabaseId?: string
8
7
  createdDatabaseId?: string
9
8
  }
10
9
 
11
- const env = process.env as Record<string, string>
12
- const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
13
10
  const suite = hasEnv(
14
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
15
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
16
- 'NOTION_TEST_CONNECTION_ID',
11
+ 'NOTION_TOKEN',
12
+ 'NOTION_TEST_PARENT_PAGE_ID',
17
13
  )
18
14
  ? describe
19
15
  : describe.skip
20
16
 
21
17
  suite('notion write handlers (live)', () => {
22
18
  const ctx: Ctx = {}
23
- let buildWrite: (name: string) => ((input: any) => Promise<any>)
24
- let buildRead: (name: string) => ((input: any) => Promise<any>)
19
+ let notion: ReturnType<typeof createToolbox>
25
20
 
26
21
  beforeAll(async () => {
27
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, NOTION_TEST_CONNECTION_ID } = env
28
-
29
- const proxy = new IntegrationProxy({
30
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
31
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
22
+ const env = process.env as Record<string, string | undefined>
23
+ const credentialStore = createCredentialStore(async () => ({ token: env.NOTION_TOKEN || '' }))
24
+ const proxy = createProxy(credentialStore)
25
+ notion = createToolbox('notion', proxy, createIntegrationNode('notion', { label: 'Notion', credentialId: 'notion-creds' }))
26
+
27
+ // Create a dedicated database under a known parent page for this run
28
+ const create_database = notion.write('create_database')
29
+ const createdDb = await create_database({
30
+ parent: { page_id: env.NOTION_TEST_PARENT_PAGE_ID },
31
+ title: [{ type: 'text', text: { content: `CmdTest DB ${Date.now()}` } }],
32
+ properties: {
33
+ Name: { title: {} },
34
+ Status: { select: { options: [{ name: 'Open' }, { name: 'Done' }] } },
35
+ },
32
36
  })
33
- const integrationNode = { id: 'node-notion', type: 'notion', label: 'Notion', connectionId: NOTION_TEST_CONNECTION_ID } as any
34
-
35
- const tools = loadIntegrationTools('notion')
36
- expect(tools).toBeTruthy()
37
-
38
- buildWrite = (name: string) => {
39
- const tool = tools!.write.find(t => t.name === name)
40
- expect(tool, `write tool ${name} exists`).toBeTruthy()
41
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
42
- const build = new Function('integration', `return (${tool!.handlerCode});`)
43
- return build(integration) as (input: any) => Promise<any>
44
- }
45
-
46
- buildRead = (name: string) => {
47
- const tool = tools!.read.find(t => t.name === name)
48
- expect(tool, `read tool ${name} exists`).toBeTruthy()
49
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
50
- const build = new Function('integration', `return (${tool!.handlerCode});`)
51
- return build(integration) as (input: any) => Promise<any>
52
- }
53
-
54
- // Try to find a database to create a page in by searching
55
- try {
56
- const search = buildRead('search')
57
- const res = await search({ query: '', filter: { value: 'database', property: 'object' }, page_size: 1 })
58
- ctx.parentDatabaseId = res?.results?.[0]?.id
59
- }
60
- catch {}
37
+ ctx.testDatabaseId = createdDb?.id
38
+ expect(ctx.testDatabaseId).toBeTruthy()
61
39
  }, 60000)
62
40
 
41
+ afterAll(async () => {
42
+ // Archive any created page + any created databases from this run
43
+ await safeCleanup(async () => {
44
+ if (!ctx.createdPageId)
45
+ return
46
+ const update_page_properties = notion.write('update_page_properties')
47
+ await update_page_properties({ page_id: ctx.createdPageId, properties: {}, archived: true })
48
+ })
49
+
50
+ await safeCleanup(async () => {
51
+ const update_database = notion.write('update_database')
52
+ if (ctx.createdDatabaseId)
53
+ await update_database({ database_id: ctx.createdDatabaseId, archived: true })
54
+ if (ctx.testDatabaseId)
55
+ await update_database({ database_id: ctx.testDatabaseId, archived: true })
56
+ })
57
+ }, 60_000)
58
+
63
59
  it('create_page -> retrieve_page -> update_page_properties', async () => {
64
- if (!ctx.parentDatabaseId)
60
+ if (!ctx.testDatabaseId)
65
61
  return expect(true).toBe(true)
66
62
 
67
- const create_page = buildWrite('create_page')
63
+ const create_page = notion.write('create_page')
68
64
  const titleText = `CmdTest ${Date.now()}`
69
65
  const created = await create_page({
70
- parent: { database_id: ctx.parentDatabaseId },
66
+ parent: { database_id: ctx.testDatabaseId },
71
67
  properties: {
72
68
  Name: { title: [{ type: 'text', text: { content: titleText } }] },
73
69
  },
@@ -76,11 +72,11 @@ suite('notion write handlers (live)', () => {
76
72
  expect(pageId).toBeTruthy()
77
73
  ctx.createdPageId = pageId
78
74
 
79
- const retrieve_page = buildRead('retrieve_page')
75
+ const retrieve_page = notion.read('retrieve_page')
80
76
  const got = await retrieve_page({ page_id: pageId })
81
77
  expect(got?.id).toBe(pageId)
82
78
 
83
- const update_page_properties = buildWrite('update_page_properties')
79
+ const update_page_properties = notion.write('update_page_properties')
84
80
  const newTitle = `${titleText} Updated`
85
81
  const updated = await update_page_properties({ page_id: pageId, properties: { Name: { title: [{ type: 'text', text: { content: newTitle } }] } } })
86
82
  expect(updated?.id).toBe(pageId)
@@ -94,7 +90,7 @@ suite('notion write handlers (live)', () => {
94
90
  it('append_block_children on created page', async () => {
95
91
  if (!ctx.createdPageId)
96
92
  return expect(true).toBe(true)
97
- const append_block_children = buildWrite('append_block_children')
93
+ const append_block_children = notion.write('append_block_children')
98
94
  const contentText = 'Hello from test'
99
95
  const res = await append_block_children({
100
96
  block_id: ctx.createdPageId,
@@ -105,7 +101,7 @@ suite('notion write handlers (live)', () => {
105
101
  expect(res).toBeTruthy()
106
102
 
107
103
  // Verify via list_block_children
108
- const list_block_children = buildRead('list_block_children')
104
+ const list_block_children = notion.read('list_block_children')
109
105
  const listed = await list_block_children({ block_id: ctx.createdPageId })
110
106
  const found = (listed?.results || listed || []).some((b: any) => b?.paragraph?.rich_text?.some((t: any) => (t?.plain_text || t?.text?.content) === contentText))
111
107
  expect(found).toBe(true)
@@ -114,13 +110,13 @@ suite('notion write handlers (live)', () => {
114
110
  it('create_comment on created page', async () => {
115
111
  if (!ctx.createdPageId)
116
112
  return expect(true).toBe(true)
117
- const create_comment = buildWrite('create_comment')
113
+ const create_comment = notion.write('create_comment')
118
114
  const commentText = 'Test comment'
119
115
  const res = await create_comment({ parent: { block_id: ctx.createdPageId }, rich_text: [{ type: 'text', text: { content: commentText } }] })
120
116
  expect(res?.object === 'comment' || res?.results).toBeTruthy()
121
117
 
122
118
  // Verify via list_comments
123
- const list_comments = buildRead('list_comments')
119
+ const list_comments = notion.read('list_comments')
124
120
  const comments = await list_comments({ block_id: ctx.createdPageId })
125
121
  const hasComment = (comments?.results || comments || []).some((c: any) => c?.rich_text?.some((t: any) => (t?.plain_text || t?.text?.content)?.includes(commentText)))
126
122
  expect(hasComment).toBe(true)
@@ -130,7 +126,7 @@ suite('notion write handlers (live)', () => {
130
126
  if (!ctx.createdPageId)
131
127
  return expect(true).toBe(true)
132
128
  // Create a specific block to edit
133
- const append_block_children = buildWrite('append_block_children')
129
+ const append_block_children = notion.write('append_block_children')
134
130
  const appended = await append_block_children({
135
131
  block_id: ctx.createdPageId,
136
132
  children: [
@@ -141,18 +137,18 @@ suite('notion write handlers (live)', () => {
141
137
  if (!blockId)
142
138
  return expect(true).toBe(true)
143
139
 
144
- const update_block = buildWrite('update_block')
140
+ const update_block = notion.write('update_block')
145
141
  const editedText = 'Edited'
146
142
  const updated = await update_block({ block_id: blockId, body: { paragraph: { rich_text: [{ type: 'text', text: { content: editedText } }] } } })
147
143
  expect(updated?.id).toBe(blockId)
148
144
 
149
145
  // Verify via retrieve_block
150
- const retrieve_block = buildRead('retrieve_block')
146
+ const retrieve_block = notion.read('retrieve_block')
151
147
  const gotBlock = await retrieve_block({ block_id: blockId })
152
148
  const gotEdited = gotBlock?.paragraph?.rich_text?.some((t: any) => (t?.plain_text || t?.text?.content) === editedText)
153
149
  expect(gotEdited).toBe(true)
154
150
 
155
- const delete_block = buildWrite('delete_block')
151
+ const delete_block = notion.write('delete_block')
156
152
  const del = await delete_block({ block_id: blockId })
157
153
  expect(del?.archived === true || del?.id === blockId).toBe(true)
158
154
 
@@ -164,7 +160,7 @@ suite('notion write handlers (live)', () => {
164
160
  it('create_database then update_database under created page (optional)', async () => {
165
161
  if (!ctx.createdPageId)
166
162
  return expect(true).toBe(true)
167
- const create_database = buildWrite('create_database')
163
+ const create_database = notion.write('create_database')
168
164
  const created = await create_database({
169
165
  parent: { page_id: ctx.createdPageId },
170
166
  title: [{ type: 'text', text: { content: `CmdDB ${Date.now()}` } }],
@@ -177,12 +173,12 @@ suite('notion write handlers (live)', () => {
177
173
  ctx.createdDatabaseId = dbId
178
174
  expect(dbId).toBeTruthy()
179
175
 
180
- const update_database = buildWrite('update_database')
176
+ const update_database = notion.write('update_database')
181
177
  const updated = await update_database({ database_id: dbId, title: [{ type: 'text', text: { content: 'CmdDB Updated' } }] })
182
178
  expect(updated?.id).toBe(dbId)
183
179
 
184
180
  // Verify via retrieve_database
185
- const retrieve_database = buildRead('retrieve_database')
181
+ const retrieve_database = notion.read('retrieve_database')
186
182
  const gotDb = await retrieve_database({ database_id: dbId })
187
183
  const titleText = gotDb?.title?.[0]?.plain_text || gotDb?.title?.[0]?.text?.content
188
184
  expect(titleText?.includes('Updated')).toBe(true)