@commandable/integration-data 0.0.4 → 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/integrations/airtable/__tests__/get_handlers.test.ts +25 -16
- package/integrations/github/__tests__/write_handlers.test.ts +9 -5
- package/integrations/github/handlers/create_commit.js +2 -17
- package/integrations/google-docs/__tests__/get_handlers.test.ts +1 -1
- package/integrations/google-docs/__tests__/write_handlers.test.ts +7 -4
- 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-sheet/__tests__/get_handlers.test.ts +1 -0
- package/integrations/google-sheet/__tests__/write_handlers.test.ts +1 -0
- package/integrations/google-slides/__tests__/get_handlers.test.ts +1 -0
- package/integrations/google-slides/__tests__/write_handlers.test.ts +4 -5
- package/package.json +1 -1
|
@@ -5,6 +5,9 @@ import { loadIntegrationTools } from '../../../../server/src/integrations/dataLo
|
|
|
5
5
|
// LIVE Airtable integration tests using credentials
|
|
6
6
|
// Required env vars:
|
|
7
7
|
// - AIRTABLE_TOKEN
|
|
8
|
+
// Optional (pins read tests to a specific base/table instead of auto-discovering):
|
|
9
|
+
// - AIRTABLE_TEST_WRITE_BASE_ID
|
|
10
|
+
// - AIRTABLE_TEST_WRITE_TABLE_ID
|
|
8
11
|
|
|
9
12
|
interface Ctx {
|
|
10
13
|
baseId?: string
|
|
@@ -53,24 +56,30 @@ suite('airtable read handlers (live)', () => {
|
|
|
53
56
|
return build(integration) as (input: any) => Promise<any>
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
ctx.recordId = records?.[0]?.id
|
|
59
|
+
// Use explicit test base/table if provided, otherwise auto-discover from first base
|
|
60
|
+
if (env.AIRTABLE_TEST_WRITE_BASE_ID && env.AIRTABLE_TEST_WRITE_TABLE_ID) {
|
|
61
|
+
ctx.baseId = env.AIRTABLE_TEST_WRITE_BASE_ID
|
|
62
|
+
ctx.tableId = env.AIRTABLE_TEST_WRITE_TABLE_ID
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const list_bases = buildHandler('list_bases')
|
|
66
|
+
const bases = await list_bases({})
|
|
67
|
+
ctx.baseId = bases?.bases?.[0]?.id || bases?.[0]?.id
|
|
68
|
+
|
|
69
|
+
if (ctx.baseId) {
|
|
70
|
+
const list_tables = buildHandler('list_tables')
|
|
71
|
+
const tablesResp = await list_tables({ baseId: ctx.baseId })
|
|
72
|
+
const tables = tablesResp?.tables || tablesResp
|
|
73
|
+
ctx.tableId = tables?.[0]?.id
|
|
72
74
|
}
|
|
73
75
|
}
|
|
76
|
+
|
|
77
|
+
if (ctx.baseId && ctx.tableId) {
|
|
78
|
+
const list_records = buildHandler('list_records')
|
|
79
|
+
const recs = await list_records({ baseId: ctx.baseId, tableId: ctx.tableId, pageSize: 1 })
|
|
80
|
+
const records = recs?.records || recs
|
|
81
|
+
ctx.recordId = records?.[0]?.id
|
|
82
|
+
}
|
|
74
83
|
}, 60000)
|
|
75
84
|
|
|
76
85
|
it('list_bases returns bases', async () => {
|
|
@@ -66,11 +66,8 @@ suiteOrSkip('github write handlers (live)', () => {
|
|
|
66
66
|
|
|
67
67
|
it('create_repo -> delete_repo lifecycle (classic_pat only)', async () => {
|
|
68
68
|
if (!toolbox.hasTool('write', 'create_repo')) {
|
|
69
|
-
// This tool is not available for fine_grained_pat -- expected.
|
|
70
69
|
return expect(true).toBe(true)
|
|
71
70
|
}
|
|
72
|
-
if (!ctx.owner)
|
|
73
|
-
return expect(true).toBe(true)
|
|
74
71
|
|
|
75
72
|
const repoName = `cmdtest-repo-${Date.now()}`
|
|
76
73
|
|
|
@@ -82,10 +79,17 @@ suiteOrSkip('github write handlers (live)', () => {
|
|
|
82
79
|
auto_init: true,
|
|
83
80
|
})
|
|
84
81
|
expect(created?.name).toBe(repoName)
|
|
85
|
-
|
|
82
|
+
|
|
83
|
+
// /user/repos creates under the authenticated user, not necessarily ctx.owner
|
|
84
|
+
const createdOwner = created?.owner?.login
|
|
85
|
+
expect(createdOwner).toBeTruthy()
|
|
86
|
+
expect(created?.full_name).toBe(`${createdOwner}/${repoName}`)
|
|
87
|
+
|
|
88
|
+
// GitHub needs a moment to finish provisioning the repo before it can be deleted
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
86
90
|
|
|
87
91
|
const delete_repo = toolbox.write('delete_repo')
|
|
88
|
-
const deleted = await delete_repo({ owner:
|
|
92
|
+
const deleted = await delete_repo({ owner: createdOwner, repo: repoName })
|
|
89
93
|
expect(deleted?.success).toBe(true)
|
|
90
94
|
expect(deleted?.status).toBe(204)
|
|
91
95
|
}, 90000)
|
|
@@ -11,32 +11,17 @@ async (input) => {
|
|
|
11
11
|
const commitData = await commitRes.json()
|
|
12
12
|
const currentTreeSha = commitData.tree.sha
|
|
13
13
|
|
|
14
|
-
// 3.
|
|
14
|
+
// 3. Build tree entries — use inline content for creates/updates, sha null for deletions
|
|
15
15
|
const tree = []
|
|
16
16
|
for (const file of files) {
|
|
17
17
|
if (file.content !== undefined && file.content !== null) {
|
|
18
|
-
// Create a blob for this file
|
|
19
|
-
const contentBase64 = typeof Buffer !== 'undefined'
|
|
20
|
-
? Buffer.from(file.content).toString('base64')
|
|
21
|
-
: btoa(unescape(encodeURIComponent(file.content)))
|
|
22
|
-
|
|
23
|
-
const blobRes = await integration.fetch(`/repos/${owner}/${repo}/git/blobs`, {
|
|
24
|
-
method: 'POST',
|
|
25
|
-
body: {
|
|
26
|
-
content: contentBase64,
|
|
27
|
-
encoding: 'base64',
|
|
28
|
-
},
|
|
29
|
-
})
|
|
30
|
-
const blobData = await blobRes.json()
|
|
31
|
-
|
|
32
18
|
tree.push({
|
|
33
19
|
path: file.path,
|
|
34
20
|
mode: file.mode || '100644',
|
|
35
21
|
type: 'blob',
|
|
36
|
-
|
|
22
|
+
content: file.content,
|
|
37
23
|
})
|
|
38
24
|
} else {
|
|
39
|
-
// File deletion (null sha means delete)
|
|
40
25
|
tree.push({
|
|
41
26
|
path: file.path,
|
|
42
27
|
mode: '100644',
|
|
@@ -16,7 +16,7 @@ interface VariantConfig {
|
|
|
16
16
|
const variants: VariantConfig[] = [
|
|
17
17
|
{
|
|
18
18
|
key: 'service_account',
|
|
19
|
-
credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '' }),
|
|
19
|
+
credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '', subject: env.GOOGLE_IMPERSONATE_SUBJECT || '' }),
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
key: 'oauth_token',
|
|
@@ -16,7 +16,7 @@ interface VariantConfig {
|
|
|
16
16
|
const variants: VariantConfig[] = [
|
|
17
17
|
{
|
|
18
18
|
key: 'service_account',
|
|
19
|
-
credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '' }),
|
|
19
|
+
credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '', subject: env.GOOGLE_IMPERSONATE_SUBJECT || '' }),
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
key: 'oauth_token',
|
|
@@ -49,7 +49,9 @@ suiteOrSkip('google-docs write handlers (live)', () => {
|
|
|
49
49
|
variant.key,
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
-
const folder = await drive.write('create_folder')({
|
|
52
|
+
const folder = await drive.write('create_folder')({
|
|
53
|
+
name: `CmdTest Docs Write ${Date.now()}`,
|
|
54
|
+
})
|
|
53
55
|
ctx.folderId = folder?.id
|
|
54
56
|
expect(ctx.folderId).toBeTruthy()
|
|
55
57
|
|
|
@@ -172,9 +174,10 @@ suiteOrSkip('google-docs write handlers (live)', () => {
|
|
|
172
174
|
}, 60000)
|
|
173
175
|
|
|
174
176
|
it('insert_inline_image_after_first_match inserts an image when allowed', async () => {
|
|
175
|
-
if (!ctx.documentId
|
|
177
|
+
if (!ctx.documentId)
|
|
176
178
|
return expect(true).toBe(true)
|
|
177
179
|
const documentId = ctx.documentId
|
|
180
|
+
const imageUri = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
|
178
181
|
const insert_inline_image_after_first_match = docs.write('insert_inline_image_after_first_match')
|
|
179
182
|
const anchor = `ANCHOR_${Date.now()}`
|
|
180
183
|
const appended = docs.write('append_text')
|
|
@@ -182,7 +185,7 @@ suiteOrSkip('google-docs write handlers (live)', () => {
|
|
|
182
185
|
const before = await get_text({ documentId })
|
|
183
186
|
if (!String(before?.text || '').includes(anchor))
|
|
184
187
|
await appended({ documentId, text: `\n${anchor}\n` })
|
|
185
|
-
const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri:
|
|
188
|
+
const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri: imageUri })
|
|
186
189
|
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
187
190
|
}, 60000)
|
|
188
191
|
|
|
@@ -34,7 +34,7 @@ async (input) => {
|
|
|
34
34
|
return { ok: true }
|
|
35
35
|
|
|
36
36
|
const requests = []
|
|
37
|
-
requests.push({ insertInlineImage: { location: { index: baseIndex }, uri
|
|
37
|
+
requests.push({ insertInlineImage: { location: { index: baseIndex }, uri } })
|
|
38
38
|
requests.push({ replaceAllText: { containsText: { text: marker, matchCase: true }, replaceText: findText } })
|
|
39
39
|
const res = await integration.fetch(`/documents/${encodeURIComponent(documentId)}:batchUpdate`, { method: 'POST', body: { requests } })
|
|
40
40
|
return await res.json()
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
"documentId": { "type": "string" },
|
|
6
6
|
"findText": { "type": "string" },
|
|
7
7
|
"uri": { "type": "string" },
|
|
8
|
-
"altText": { "type": "string" },
|
|
9
8
|
"position": { "type": "string", "enum": ["after", "before"], "default": "after" }
|
|
10
9
|
},
|
|
11
10
|
"required": ["documentId", "findText", "uri"],
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
+
import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, safeCleanup } from '../../__tests__/liveHarness.js'
|
|
3
|
+
|
|
4
|
+
// LIVE Google Drive tests -- runs once per available credential variant.
|
|
5
|
+
// Required env vars (at least one):
|
|
6
|
+
// - GOOGLE_SERVICE_ACCOUNT_JSON (service_account variant)
|
|
7
|
+
// - GOOGLE_TOKEN (oauth_token variant)
|
|
8
|
+
|
|
9
|
+
const env = process.env as Record<string, string | undefined>
|
|
10
|
+
|
|
11
|
+
interface VariantConfig {
|
|
12
|
+
key: string
|
|
13
|
+
credentials: () => Record<string, string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const variants: VariantConfig[] = [
|
|
17
|
+
{
|
|
18
|
+
key: 'service_account',
|
|
19
|
+
credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '', subject: env.GOOGLE_IMPERSONATE_SUBJECT || '' }),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'oauth_token',
|
|
23
|
+
credentials: () => ({ token: env.GOOGLE_TOKEN || '' }),
|
|
24
|
+
},
|
|
25
|
+
].filter(v => Object.values(v.credentials()).some(val => val.trim().length > 0))
|
|
26
|
+
|
|
27
|
+
const suiteOrSkip = variants.length > 0 ? describe : describe.skip
|
|
28
|
+
|
|
29
|
+
suiteOrSkip('google-drive handlers (live)', () => {
|
|
30
|
+
for (const variant of variants) {
|
|
31
|
+
describe(`variant: ${variant.key}`, () => {
|
|
32
|
+
const ctx: { folderId?: string, fileId?: string, destFolderId?: string } = {}
|
|
33
|
+
let drive: ReturnType<typeof createToolbox>
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
const credentialStore = createCredentialStore(async () => variant.credentials())
|
|
37
|
+
const proxy = createProxy(credentialStore)
|
|
38
|
+
drive = createToolbox(
|
|
39
|
+
'google-drive',
|
|
40
|
+
proxy,
|
|
41
|
+
createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds', credentialVariant: variant.key }),
|
|
42
|
+
variant.key,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const folder = await drive.write('create_folder')({ name: `CmdTest Drive ${Date.now()}` })
|
|
46
|
+
ctx.folderId = folder?.id
|
|
47
|
+
expect(ctx.folderId).toBeTruthy()
|
|
48
|
+
|
|
49
|
+
const destFolder = await drive.write('create_folder')({ name: `CmdTest Drive Dest ${Date.now()}` })
|
|
50
|
+
ctx.destFolderId = destFolder?.id
|
|
51
|
+
expect(ctx.destFolderId).toBeTruthy()
|
|
52
|
+
|
|
53
|
+
const file = await drive.write('create_file')({
|
|
54
|
+
name: `CmdTest File ${Date.now()}`,
|
|
55
|
+
mimeType: 'application/vnd.google-apps.document',
|
|
56
|
+
parentId: ctx.folderId,
|
|
57
|
+
})
|
|
58
|
+
ctx.fileId = file?.id
|
|
59
|
+
expect(ctx.fileId).toBeTruthy()
|
|
60
|
+
}, 60000)
|
|
61
|
+
|
|
62
|
+
afterAll(async () => {
|
|
63
|
+
await safeCleanup(async () => ctx.fileId ? drive.write('delete_file')({ fileId: ctx.fileId }) : Promise.resolve())
|
|
64
|
+
await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
|
|
65
|
+
await safeCleanup(async () => ctx.destFolderId ? drive.write('delete_file')({ fileId: ctx.destFolderId }) : Promise.resolve())
|
|
66
|
+
}, 60000)
|
|
67
|
+
|
|
68
|
+
it('get_file returns file metadata', async () => {
|
|
69
|
+
if (!ctx.fileId)
|
|
70
|
+
return expect(true).toBe(true)
|
|
71
|
+
const result = await drive.read('get_file')({ fileId: ctx.fileId })
|
|
72
|
+
expect(result?.id).toBe(ctx.fileId)
|
|
73
|
+
expect(typeof result?.name).toBe('string')
|
|
74
|
+
expect(typeof result?.mimeType).toBe('string')
|
|
75
|
+
}, 30000)
|
|
76
|
+
|
|
77
|
+
it('move_file moves the file to a different folder', async () => {
|
|
78
|
+
if (!ctx.fileId || !ctx.destFolderId || !ctx.folderId)
|
|
79
|
+
return expect(true).toBe(true)
|
|
80
|
+
const result = await drive.write('move_file')({
|
|
81
|
+
fileId: ctx.fileId,
|
|
82
|
+
addParents: ctx.destFolderId,
|
|
83
|
+
removeParents: ctx.folderId,
|
|
84
|
+
})
|
|
85
|
+
expect(result?.id).toBe(ctx.fileId)
|
|
86
|
+
const meta = await drive.read('get_file')({ fileId: ctx.fileId })
|
|
87
|
+
expect(meta?.parents).toContain(ctx.destFolderId)
|
|
88
|
+
}, 30000)
|
|
89
|
+
|
|
90
|
+
it('delete_file deletes a file permanently', async () => {
|
|
91
|
+
const tempFile = await drive.write('create_file')({
|
|
92
|
+
name: `CmdTest Temp ${Date.now()}`,
|
|
93
|
+
mimeType: 'application/vnd.google-apps.document',
|
|
94
|
+
})
|
|
95
|
+
const tempId = tempFile?.id
|
|
96
|
+
expect(tempId).toBeTruthy()
|
|
97
|
+
await drive.write('delete_file')({ fileId: tempId })
|
|
98
|
+
await expect(drive.read('get_file')({ fileId: tempId })).rejects.toThrow()
|
|
99
|
+
}, 30000)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
})
|
|
@@ -21,6 +21,7 @@ suite('google-sheet read handlers (live)', () => {
|
|
|
21
21
|
const credentialStore = createCredentialStore(async () => ({
|
|
22
22
|
token: env.GOOGLE_TOKEN || '',
|
|
23
23
|
serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
|
|
24
|
+
subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
|
|
24
25
|
}))
|
|
25
26
|
const proxy = createProxy(credentialStore)
|
|
26
27
|
sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
|
|
@@ -27,6 +27,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
27
27
|
const credentialStore = createCredentialStore(async () => ({
|
|
28
28
|
token: env.GOOGLE_TOKEN || '',
|
|
29
29
|
serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
|
|
30
|
+
subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
|
|
30
31
|
}))
|
|
31
32
|
const proxy = createProxy(credentialStore)
|
|
32
33
|
sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
|
|
@@ -20,6 +20,7 @@ suite('google-slides read handlers (live)', () => {
|
|
|
20
20
|
const credentialStore = createCredentialStore(async () => ({
|
|
21
21
|
token: env.GOOGLE_TOKEN || '',
|
|
22
22
|
serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
|
|
23
|
+
subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
|
|
23
24
|
}))
|
|
24
25
|
const proxy = createProxy(credentialStore)
|
|
25
26
|
slides = createToolbox('google-slides', proxy, createIntegrationNode('google-slides', { label: 'Google Slides', credentialId: 'google-slides-creds' }))
|
|
@@ -21,6 +21,7 @@ suite('google-slides write handlers (live)', () => {
|
|
|
21
21
|
const credentialStore = createCredentialStore(async () => ({
|
|
22
22
|
token: env.GOOGLE_TOKEN || '',
|
|
23
23
|
serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
|
|
24
|
+
subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
|
|
24
25
|
}))
|
|
25
26
|
const proxy = createProxy(credentialStore)
|
|
26
27
|
slides = createToolbox('google-slides', proxy, createIntegrationNode('google-slides', { label: 'Google Slides', credentialId: 'google-slides-creds' }))
|
|
@@ -95,13 +96,11 @@ suite('google-slides write handlers (live)', () => {
|
|
|
95
96
|
}, 60000)
|
|
96
97
|
|
|
97
98
|
it('insert_image_after_first_match inserts an image when allowed', async () => {
|
|
98
|
-
|
|
99
|
-
if (!ctx.presentationId || !env.GSLIDES_TEST_IMAGE_URI)
|
|
100
|
-
return expect(true).toBe(true)
|
|
101
|
-
if (!ctx.anchorText)
|
|
99
|
+
if (!ctx.presentationId || !ctx.anchorText)
|
|
102
100
|
return expect(true).toBe(true)
|
|
101
|
+
const imageUri = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
|
103
102
|
const handler = slides.write('insert_image_after_first_match')
|
|
104
|
-
const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, uri:
|
|
103
|
+
const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, uri: imageUri })
|
|
105
104
|
expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
|
|
106
105
|
}, 60000)
|
|
107
106
|
|
package/package.json
CHANGED