@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.
- package/dist/credentials-index.d.ts +4 -21
- package/dist/credentials-index.d.ts.map +1 -1
- package/dist/credentials-index.js +407 -215
- package/dist/credentials-index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +38 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +70 -16
- package/dist/loader.js.map +1 -1
- package/integrations/__tests__/liveHarness.ts +84 -0
- package/integrations/__tests__/usageParity.ts +54 -0
- package/integrations/airtable/__tests__/get_handlers.test.ts +43 -31
- package/integrations/airtable/__tests__/usage_parity.test.ts +3 -29
- package/integrations/airtable/__tests__/write_and_admin_handlers.test.ts +20 -17
- package/integrations/airtable/credentials.json +21 -16
- package/integrations/github/__tests__/get_handlers.test.ts +101 -108
- package/integrations/github/__tests__/usage_parity.test.ts +15 -27
- package/integrations/github/__tests__/write_handlers.test.ts +223 -306
- package/integrations/github/credentials.json +40 -15
- package/integrations/github/credentials_hint_classic_pat.md +8 -0
- package/integrations/github/credentials_hint_fine_grained_pat.md +9 -0
- package/integrations/github/handlers/create_commit.js +2 -17
- package/integrations/github/manifest.json +2 -2
- package/integrations/google-calendar/__tests__/get_handlers.test.ts +21 -13
- package/integrations/google-calendar/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +24 -17
- package/integrations/google-calendar/credentials.json +50 -29
- package/integrations/google-calendar/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-calendar/credentials_hint_service_account.md +10 -0
- package/integrations/google-docs/__tests__/get_handlers.test.ts +87 -61
- package/integrations/google-docs/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-docs/__tests__/write_handlers.test.ts +251 -245
- package/integrations/google-docs/credentials.json +50 -29
- package/integrations/google-docs/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-docs/credentials_hint_service_account.md +10 -0
- package/integrations/google-docs/handlers/insert_inline_image_after_first_match.js +1 -1
- package/integrations/google-docs/schemas/insert_inline_image_after_first_match.json +0 -1
- package/integrations/google-drive/__tests__/handlers.test.ts +102 -0
- package/integrations/google-drive/credentials.json +57 -0
- package/integrations/google-drive/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-drive/credentials_hint_service_account.md +10 -0
- package/integrations/google-drive/handlers/create_file.js +15 -0
- package/integrations/google-drive/handlers/create_folder.js +15 -0
- package/integrations/google-drive/handlers/delete_file.js +14 -0
- package/integrations/google-drive/handlers/get_file.js +7 -0
- package/integrations/google-drive/handlers/move_file.js +12 -0
- package/integrations/google-drive/manifest.json +42 -0
- package/integrations/google-drive/schemas/create_file.json +12 -0
- package/integrations/google-drive/schemas/create_folder.json +11 -0
- package/integrations/google-drive/schemas/delete_file.json +10 -0
- package/integrations/google-drive/schemas/get_file.json +10 -0
- package/integrations/google-drive/schemas/move_file.json +12 -0
- package/integrations/google-sheet/__tests__/get_handlers.test.ts +48 -55
- package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
- package/integrations/google-sheet/__tests__/write_handlers.test.ts +65 -63
- package/integrations/google-sheet/credentials.json +50 -29
- package/integrations/google-sheet/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-sheet/credentials_hint_service_account.md +10 -0
- package/integrations/google-slides/__tests__/get_handlers.test.ts +38 -36
- package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -59
- package/integrations/google-slides/credentials.json +50 -29
- package/integrations/google-slides/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-slides/credentials_hint_service_account.md +10 -0
- package/integrations/notion/__tests__/get_handlers.test.ts +18 -15
- package/integrations/notion/__tests__/usage_parity.test.ts +3 -28
- package/integrations/notion/__tests__/write_and_admin_handlers.test.ts +56 -60
- package/integrations/notion/credentials.json +22 -17
- package/integrations/trello/__tests__/get_handlers.test.ts +58 -73
- package/integrations/trello/__tests__/usage_parity.test.ts +3 -28
- package/integrations/trello/__tests__/write_and_admin_handlers.test.ts +49 -67
- package/integrations/trello/credentials.json +26 -21
- package/integrations/trello/handlers/close_board.js +6 -0
- package/integrations/trello/handlers/create_board.js +11 -0
- package/integrations/trello/handlers/delete_board.js +13 -0
- package/integrations/trello/manifest.json +21 -0
- package/integrations/trello/schemas/close_board.json +10 -0
- package/integrations/trello/schemas/create_board.json +12 -0
- package/integrations/trello/schemas/delete_board.json +10 -0
- package/package.json +1 -1
|
@@ -1,76 +1,68 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
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
|
|
4
|
+
// LIVE Google Slides write tests using credentials
|
|
6
5
|
// Required env vars:
|
|
7
|
-
// -
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
16
|
+
let slides: ReturnType<typeof createToolbox>
|
|
17
|
+
let drive: ReturnType<typeof createToolbox>
|
|
28
18
|
|
|
29
19
|
beforeAll(async () => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
const tools = loadIntegrationTools('google-slides')
|
|
39
|
-
expect(tools).toBeTruthy()
|
|
39
|
+
ctx.presentationId = created?.id
|
|
40
|
+
expect(ctx.presentationId).toBeTruthy()
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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 || !
|
|
99
|
+
if (!ctx.presentationId || !ctx.anchorText)
|
|
104
100
|
return expect(true).toBe(true)
|
|
105
|
-
const
|
|
106
|
-
const
|
|
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
|
-
|
|
114
|
-
|
|
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 =
|
|
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
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
|
|
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
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
"injection": {
|
|
29
|
+
"headers": {
|
|
30
|
+
"Authorization": "Bearer {{token}}"
|
|
31
|
+
}
|
|
14
32
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
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 '
|
|
3
|
-
import { loadIntegrationTools } from '
|
|
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
|
|
5
|
+
// LIVE Notion integration tests using credentials
|
|
6
6
|
// Required env vars:
|
|
7
|
-
// -
|
|
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
|
-
'
|
|
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
|
|
28
|
+
const credentialStore = {
|
|
29
|
+
getCredentials: async () => ({ token: env.NOTION_TOKEN || '' }),
|
|
30
|
+
}
|
|
33
31
|
|
|
34
|
-
const proxy = new IntegrationProxy({
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 {
|
|
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
|
|
13
|
-
const
|
|
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 {
|
|
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
|
-
|
|
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
|
-
'
|
|
15
|
-
'
|
|
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
|
|
24
|
-
let buildRead: (name: string) => ((input: any) => Promise<any>)
|
|
19
|
+
let notion: ReturnType<typeof createToolbox>
|
|
25
20
|
|
|
26
21
|
beforeAll(async () => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const proxy =
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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.
|
|
60
|
+
if (!ctx.testDatabaseId)
|
|
65
61
|
return expect(true).toBe(true)
|
|
66
62
|
|
|
67
|
-
const 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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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)
|