@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.
@@ -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
- // Discover base -> table -> record for tests
57
- const list_bases = buildHandler('list_bases')
58
- const bases = await list_bases({})
59
- ctx.baseId = bases?.bases?.[0]?.id || bases?.[0]?.id
60
-
61
- if (ctx.baseId) {
62
- const list_tables = buildHandler('list_tables')
63
- const tablesResp = await list_tables({ baseId: ctx.baseId })
64
- const tables = tablesResp?.tables || tablesResp
65
- ctx.tableId = tables?.[0]?.id
66
-
67
- if (ctx.tableId) {
68
- const list_records = buildHandler('list_records')
69
- const recs = await list_records({ baseId: ctx.baseId, tableId: ctx.tableId, pageSize: 1 })
70
- const records = recs?.records || recs
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
- expect(created?.full_name).toBe(`${ctx.owner}/${repoName}`)
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: ctx.owner, repo: repoName })
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. Create blobs for each file with content
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
- sha: blobData.sha,
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')({ name: `CmdTest Docs Write ${Date.now()}` })
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 || !process.env.GDOCS_TEST_IMAGE_URI)
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: process.env.GDOCS_TEST_IMAGE_URI!, altText: 'CmdTest' })
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, altTextTitle: altText } })
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
- const env = process.env as Record<string, string | undefined>
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: env.GSLIDES_TEST_IMAGE_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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commandable/integration-data",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Integration data (manifests, schemas, handlers, credential configs) for Commandable integrations.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {