@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,69 +1,62 @@
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 Sheets write tests using managed OAuth
4
+ // LIVE Google Sheets write tests using credentials
6
5
  // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - GSHEETS_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-sheet')
10
- // - GSHEETS_TEST_SPREADSHEET_ID (target spreadsheet ID with write access)
6
+ // - Either GOOGLE_TOKEN, OR GOOGLE_SERVICE_ACCOUNT_JSON
7
+ // - GOOGLE_SHEETS_TEST_SPREADSHEET_ID (target spreadsheet ID with write access)
11
8
 
12
9
  interface Ctx {
13
10
  spreadsheetId?: string
11
+ folderId?: string
12
+ destSpreadsheetId?: string
14
13
  }
15
14
 
16
- const env = process.env as Record<string, string>
17
- const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
18
- const suite = hasEnv(
19
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
20
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
21
- 'GSHEETS_TEST_CONNECTION_ID',
22
- 'GSHEETS_TEST_SPREADSHEET_ID',
23
- )
15
+ const suite = (hasEnv('GOOGLE_TOKEN') || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON'))
24
16
  ? describe
25
17
  : describe.skip
26
18
 
27
19
  suite('google-sheet write handlers (live)', () => {
28
20
  const ctx: Ctx = {}
29
- let buildWriteHandler: (name: string) => ((input: any) => Promise<any>)
21
+ let sheets: ReturnType<typeof createToolbox>
22
+ let drive: ReturnType<typeof createToolbox>
30
23
  let sheetTitle: string | undefined
31
24
 
32
25
  beforeAll(async () => {
33
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GSHEETS_TEST_CONNECTION_ID, GSHEETS_TEST_SPREADSHEET_ID } = env
34
-
35
- const proxy = new IntegrationProxy({
36
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
37
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
26
+ const env = process.env as Record<string, string | undefined>
27
+ const credentialStore = createCredentialStore(async () => ({
28
+ token: env.GOOGLE_TOKEN || '',
29
+ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
30
+ subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
31
+ }))
32
+ const proxy = createProxy(credentialStore)
33
+ sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
34
+ drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
35
+
36
+ const folder = await drive.write('create_folder')({ name: `CmdTest Sheets Write ${Date.now()}` })
37
+ ctx.folderId = folder?.id
38
+ expect(ctx.folderId).toBeTruthy()
39
+
40
+ const created = await drive.write('create_file')({
41
+ name: `CmdTest Sheet ${Date.now()}`,
42
+ mimeType: 'application/vnd.google-apps.spreadsheet',
43
+ parentId: ctx.folderId,
38
44
  })
39
- const integrationNode = { id: 'node-gsheets', type: 'google-sheet', label: 'Google Sheets', connectionId: GSHEETS_TEST_CONNECTION_ID } as any
40
-
41
- const tools = loadIntegrationTools('google-sheet')
42
- expect(tools).toBeTruthy()
45
+ ctx.spreadsheetId = created?.id
46
+ expect(ctx.spreadsheetId).toBeTruthy()
43
47
 
44
- buildWriteHandler = (name: string) => {
45
- const tool = tools!.write.find(t => t.name === name)
46
- expect(tool, `write tool ${name} exists`).toBeTruthy()
47
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
48
- const build = new Function('integration', `return (${tool!.handlerCode});`)
49
- return build(integration) as (input: any) => Promise<any>
50
- }
51
-
52
- ctx.spreadsheetId = GSHEETS_TEST_SPREADSHEET_ID
48
+ const createdDest = await drive.write('create_file')({
49
+ name: `CmdTest Sheet Dest ${Date.now()}`,
50
+ mimeType: 'application/vnd.google-apps.spreadsheet',
51
+ parentId: ctx.folderId,
52
+ })
53
+ ctx.destSpreadsheetId = createdDest?.id
54
+ expect(ctx.destSpreadsheetId).toBeTruthy()
53
55
 
54
56
  // Try to detect a default sheet title
55
57
  if (ctx.spreadsheetId) {
56
- const proxy2 = new IntegrationProxy({
57
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
58
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
59
- })
60
- const node2 = { id: 'node-gsheets', type: 'google-sheet', label: 'Google Sheets', connectionId: GSHEETS_TEST_CONNECTION_ID } as any
61
- const tools2 = loadIntegrationTools('google-sheet')!
62
- const tool = tools2.read.find(t => t.name === 'get_spreadsheet')!
63
- const build = new Function('integration', `return (${tool.handlerCode});`)
64
- const integration = { fetch: (path: string, init?: RequestInit) => proxy2.call(node2, path, init) }
65
58
  try {
66
- const get_spreadsheet = build(integration) as (input: any) => Promise<any>
59
+ const get_spreadsheet = sheets.read('get_spreadsheet')
67
60
  const meta = await get_spreadsheet({ spreadsheetId: ctx.spreadsheetId })
68
61
  sheetTitle = meta?.sheets?.[0]?.properties?.title
69
62
  }
@@ -71,16 +64,27 @@ suite('google-sheet write handlers (live)', () => {
71
64
  }
72
65
  }, 60000)
73
66
 
67
+ afterAll(async () => {
68
+ await safeCleanup(async () => {
69
+ const delete_file = drive.write('delete_file')
70
+ if (ctx.spreadsheetId)
71
+ await delete_file({ fileId: ctx.spreadsheetId })
72
+ if (ctx.destSpreadsheetId)
73
+ await delete_file({ fileId: ctx.destSpreadsheetId })
74
+ })
75
+ await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
76
+ }, 60_000)
77
+
74
78
  it('append_values appends then clear_values clears', async () => {
75
79
  if (!ctx.spreadsheetId)
76
80
  return expect(true).toBe(true)
77
81
 
78
- const append_values = buildWriteHandler('append_values')
82
+ const append_values = sheets.write('append_values')
79
83
  const aTitle = sheetTitle || 'Sheet1'
80
84
  const appendRes = await append_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!A1`, values: [[`CmdTest ${Date.now()}`]], valueInputOption: 'USER_ENTERED' })
81
85
  expect(appendRes?.updates || appendRes?.tableRange || appendRes?.spreadsheetId).toBeTruthy()
82
86
 
83
- const clear_values = buildWriteHandler('clear_values')
87
+ const clear_values = sheets.write('clear_values')
84
88
  const clearRes = await clear_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!A1:A10` })
85
89
  expect(clearRes?.clearedRange || clearRes?.spreadsheetId).toBeTruthy()
86
90
  }, 60000)
@@ -89,7 +93,7 @@ suite('google-sheet write handlers (live)', () => {
89
93
  if (!ctx.spreadsheetId)
90
94
  return expect(true).toBe(true)
91
95
  const aTitle = sheetTitle || 'Sheet1'
92
- const update_values = buildWriteHandler('update_values')
96
+ const update_values = sheets.write('update_values')
93
97
  const res = await update_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!B1:B1`, values: [[`CmdTestU ${Date.now()}`]], valueInputOption: 'USER_ENTERED' })
94
98
  expect(res?.updatedRange || res?.spreadsheetId).toBeTruthy()
95
99
  }, 60000)
@@ -98,7 +102,7 @@ suite('google-sheet write handlers (live)', () => {
98
102
  if (!ctx.spreadsheetId)
99
103
  return expect(true).toBe(true)
100
104
  const aTitle = sheetTitle || 'Sheet1'
101
- const batch_update_values = buildWriteHandler('batch_update_values')
105
+ const batch_update_values = sheets.write('batch_update_values')
102
106
  const res = await batch_update_values({ spreadsheetId: ctx.spreadsheetId, data: [
103
107
  { range: `${aTitle}!C1:C1`, values: [[`CmdTestB1 ${Date.now()}`]] },
104
108
  { range: `${aTitle}!C2:C2`, values: [[`CmdTestB2 ${Date.now()}`]] },
@@ -110,7 +114,7 @@ suite('google-sheet write handlers (live)', () => {
110
114
  if (!ctx.spreadsheetId)
111
115
  return expect(true).toBe(true)
112
116
  const aTitle = sheetTitle || 'Sheet1'
113
- const batch_update_values_by_data_filter = buildWriteHandler('batch_update_values_by_data_filter')
117
+ const batch_update_values_by_data_filter = sheets.write('batch_update_values_by_data_filter')
114
118
  const res = await batch_update_values_by_data_filter({ spreadsheetId: ctx.spreadsheetId, data: [
115
119
  { dataFilter: { a1Range: `${aTitle}!D1:D1` }, values: [[`CmdTestDF ${Date.now()}`]] },
116
120
  ], valueInputOption: 'USER_ENTERED' })
@@ -121,7 +125,7 @@ suite('google-sheet write handlers (live)', () => {
121
125
  if (!ctx.spreadsheetId)
122
126
  return expect(true).toBe(true)
123
127
  const aTitle = sheetTitle || 'Sheet1'
124
- const batch_clear_values = buildWriteHandler('batch_clear_values')
128
+ const batch_clear_values = sheets.write('batch_clear_values')
125
129
  const res = await batch_clear_values({ spreadsheetId: ctx.spreadsheetId, ranges: [`${aTitle}!A1:A2`, `${aTitle}!B1:B2`] })
126
130
  expect(Boolean(res?.spreadsheetId) || Array.isArray(res?.clearedRanges)).toBe(true)
127
131
  }, 60000)
@@ -130,7 +134,7 @@ suite('google-sheet write handlers (live)', () => {
130
134
  if (!ctx.spreadsheetId)
131
135
  return expect(true).toBe(true)
132
136
  const aTitle = sheetTitle || 'Sheet1'
133
- const batch_clear_values_by_data_filter = buildWriteHandler('batch_clear_values_by_data_filter')
137
+ const batch_clear_values_by_data_filter = sheets.write('batch_clear_values_by_data_filter')
134
138
  const res = await batch_clear_values_by_data_filter({ spreadsheetId: ctx.spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!E1:E2` }] })
135
139
  expect(Boolean(res?.spreadsheetId) || Array.isArray(res?.clearedRanges)).toBe(true)
136
140
  }, 60000)
@@ -139,7 +143,7 @@ suite('google-sheet write handlers (live)', () => {
139
143
  if (!ctx.spreadsheetId)
140
144
  return expect(true).toBe(true)
141
145
  const aTitle = sheetTitle || 'Sheet1'
142
- const batch_update = buildWriteHandler('batch_update')
146
+ const batch_update = sheets.write('batch_update')
143
147
  const res = await batch_update({ spreadsheetId: ctx.spreadsheetId, requests: [
144
148
  { findReplace: { find: '___unlikely___', replacement: '___unlikely___', matchCase: true, searchByRegex: false, includeFormulas: false, range: { sheetId: 0 } } },
145
149
  ], includeSpreadsheetInResponse: false })
@@ -147,12 +151,12 @@ suite('google-sheet write handlers (live)', () => {
147
151
  }, 60000)
148
152
 
149
153
  it('copy_to_spreadsheet copies a sheet when destination provided', async () => {
150
- if (!ctx.spreadsheetId || !env.GSHEETS_TEST_DEST_SPREADSHEET_ID)
154
+ if (!ctx.spreadsheetId || !ctx.destSpreadsheetId)
151
155
  return expect(true).toBe(true)
152
- const copy_to_spreadsheet = buildWriteHandler('copy_to_spreadsheet')
156
+ const copy_to_spreadsheet = sheets.write('copy_to_spreadsheet')
153
157
  // Attempt to copy sheet with id 0 (typical for first sheet). If it fails, skip.
154
158
  try {
155
- const res = await copy_to_spreadsheet({ spreadsheetId: ctx.spreadsheetId, sheetId: 0, destinationSpreadsheetId: env.GSHEETS_TEST_DEST_SPREADSHEET_ID })
159
+ const res = await copy_to_spreadsheet({ spreadsheetId: ctx.spreadsheetId, sheetId: 0, destinationSpreadsheetId: ctx.destSpreadsheetId })
156
160
  expect(res?.sheetId !== undefined || res?.spreadsheetId).toBeTruthy()
157
161
  }
158
162
  catch {
@@ -160,12 +164,10 @@ suite('google-sheet write handlers (live)', () => {
160
164
  }
161
165
  }, 60000)
162
166
 
163
- it('create_spreadsheet creates a spreadsheet when allowed', async () => {
164
- if (!env.GSHEETS_ALLOW_CREATE)
165
- return expect(true).toBe(true)
166
- const create_spreadsheet = buildWriteHandler('create_spreadsheet')
167
- const res = await create_spreadsheet({ properties: { title: `CmdTest ${Date.now()}` } })
168
- expect(res?.spreadsheetId).toBeTruthy()
169
- // Note: No delete here to avoid Drive scope; test leaves an artifact when enabled.
167
+ it('create_spreadsheet creates a spreadsheet (self-cleaning)', async () => {
168
+ const created = await sheets.write('create_spreadsheet')({ properties: { title: `CmdTest Sheet Tool ${Date.now()}` } })
169
+ const id = created?.spreadsheetId
170
+ expect(typeof id).toBe('string')
171
+ await safeCleanup(async () => id ? drive.write('delete_file')({ fileId: id }) : Promise.resolve())
170
172
  }, 60000)
171
173
  })
@@ -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 spreadsheets.",
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 spreadsheets scope."
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 scope: `https://www.googleapis.com/auth/spreadsheets`
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 Sheets 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 spreadsheets 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,68 +1,70 @@
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 read tests using managed OAuth
4
+ // LIVE Google Slides read 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 (an accessible presentation ID)
6
+ // - Either GOOGLE_TOKEN, OR GOOGLE_SERVICE_ACCOUNT_JSON
11
7
 
12
- const env = process.env as Record<string, string>
13
- const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
14
- const suite = hasEnv(
15
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
16
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
17
- 'GSLIDES_TEST_CONNECTION_ID',
18
- )
8
+ const suite = (hasEnv('GOOGLE_TOKEN') || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON'))
19
9
  ? describe
20
10
  : describe.skip
21
11
 
22
12
  suite('google-slides read handlers (live)', () => {
23
- let buildReadHandler: (name: string) => ((input: any) => Promise<any>)
13
+ let slides: ReturnType<typeof createToolbox>
14
+ let drive: ReturnType<typeof createToolbox>
15
+ let folderId: string | undefined
16
+ let presentationId: string | undefined
24
17
 
25
18
  beforeAll(async () => {
26
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GSLIDES_TEST_CONNECTION_ID } = env
19
+ const env = process.env as Record<string, string | undefined>
20
+ const credentialStore = createCredentialStore(async () => ({
21
+ token: env.GOOGLE_TOKEN || '',
22
+ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
23
+ subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
24
+ }))
25
+ const proxy = createProxy(credentialStore)
26
+ slides = createToolbox('google-slides', proxy, createIntegrationNode('google-slides', { label: 'Google Slides', credentialId: 'google-slides-creds' }))
27
+ drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
27
28
 
28
- const proxy = new IntegrationProxy({
29
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
30
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
31
- })
32
- const integrationNode = { id: 'node-gslides', type: 'google-slides', label: 'Google Slides', connectionId: GSLIDES_TEST_CONNECTION_ID } as any
29
+ // Create dedicated folder + presentation for this run
30
+ const folder = await drive.write('create_folder')({ name: `CmdTest Slides ${Date.now()}` })
31
+ folderId = folder?.id
32
+ expect(folderId).toBeTruthy()
33
33
 
34
- const tools = loadIntegrationTools('google-slides')
35
- expect(tools).toBeTruthy()
34
+ const created = await drive.write('create_file')({
35
+ name: `CmdTest Slides ${Date.now()}`,
36
+ mimeType: 'application/vnd.google-apps.presentation',
37
+ parentId: folderId,
38
+ })
39
+ presentationId = created?.id
40
+ expect(presentationId).toBeTruthy()
41
+ }, 60000)
36
42
 
37
- buildReadHandler = (name: string) => {
38
- const tool = tools!.read.find(t => t.name === name)
39
- expect(tool, `read tool ${name} exists`).toBeTruthy()
40
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
41
- const build = new Function('integration', `return (${tool!.handlerCode});`)
42
- return build(integration) as (input: any) => Promise<any>
43
- }
43
+ afterAll(async () => {
44
+ if (!folderId)
45
+ return
46
+ await safeCleanup(async () => presentationId ? drive.write('delete_file')({ fileId: presentationId }) : Promise.resolve())
47
+ await safeCleanup(async () => drive.write('delete_file')({ fileId: folderId }))
44
48
  }, 60000)
45
49
 
46
50
  it('get_presentation returns metadata', async () => {
47
- const presentationId = env.GSLIDES_TEST_PRESENTATION_ID
48
51
  if (!presentationId)
49
52
  return expect(true).toBe(true)
50
- const handler = buildReadHandler('get_presentation')
53
+ const handler = slides.read('get_presentation')
51
54
  const result = await handler({ presentationId })
52
55
  expect(result?.presentationId || Array.isArray(result?.slides)).toBeTruthy()
53
56
  }, 30000)
54
57
 
55
58
  it('get_page_thumbnail returns URL data', async () => {
56
- const presentationId = env.GSLIDES_TEST_PRESENTATION_ID
57
59
  if (!presentationId)
58
60
  return expect(true).toBe(true)
59
61
  // First query the presentation to discover a page id
60
- const getPresentation = buildReadHandler('get_presentation')
62
+ const getPresentation = slides.read('get_presentation')
61
63
  const meta = await getPresentation({ presentationId })
62
64
  const firstSlide = meta?.slides?.[0]
63
65
  if (!firstSlide?.objectId)
64
66
  return expect(true).toBe(true)
65
- const handler = buildReadHandler('get_page_thumbnail')
67
+ const handler = slides.read('get_page_thumbnail')
66
68
  const result = await handler({ presentationId, 'pageObjectId': firstSlide.objectId, 'thumbnailProperties.thumbnailSize': 'MEDIUM', 'thumbnailProperties.mimeType': 'PNG' })
67
69
  expect(typeof result?.contentUrl === 'string' || typeof result?.thumbnailUrl === 'string').toBe(true)
68
70
  }, 30000)
@@ -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('google-slides static usage parity', () => {
12
- it('every manifest tool is referenced in tests via build*(name)', () => {
13
- const manifest = loadIntegrationManifest('google-slides')!
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: 'google-slides', importMetaUrl: import.meta.url })
32
7
  expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
33
8
  })
34
9
  })