@commandable/integration-data 0.0.1 → 0.0.4

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 (79) 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 +18 -15
  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 +219 -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/manifest.json +2 -2
  26. package/integrations/google-calendar/__tests__/get_handlers.test.ts +21 -13
  27. package/integrations/google-calendar/__tests__/usage_parity.test.ts +3 -28
  28. package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +24 -17
  29. package/integrations/google-calendar/credentials.json +50 -29
  30. package/integrations/google-calendar/credentials_hint_oauth_token.md +8 -0
  31. package/integrations/google-calendar/credentials_hint_service_account.md +10 -0
  32. package/integrations/google-docs/__tests__/get_handlers.test.ts +87 -61
  33. package/integrations/google-docs/__tests__/usage_parity.test.ts +3 -28
  34. package/integrations/google-docs/__tests__/write_handlers.test.ts +248 -245
  35. package/integrations/google-docs/credentials.json +50 -29
  36. package/integrations/google-docs/credentials_hint_oauth_token.md +8 -0
  37. package/integrations/google-docs/credentials_hint_service_account.md +10 -0
  38. package/integrations/google-drive/credentials.json +57 -0
  39. package/integrations/google-drive/credentials_hint_oauth_token.md +8 -0
  40. package/integrations/google-drive/credentials_hint_service_account.md +10 -0
  41. package/integrations/google-drive/handlers/create_file.js +15 -0
  42. package/integrations/google-drive/handlers/create_folder.js +15 -0
  43. package/integrations/google-drive/handlers/delete_file.js +14 -0
  44. package/integrations/google-drive/handlers/get_file.js +7 -0
  45. package/integrations/google-drive/handlers/move_file.js +12 -0
  46. package/integrations/google-drive/manifest.json +42 -0
  47. package/integrations/google-drive/schemas/create_file.json +12 -0
  48. package/integrations/google-drive/schemas/create_folder.json +11 -0
  49. package/integrations/google-drive/schemas/delete_file.json +10 -0
  50. package/integrations/google-drive/schemas/get_file.json +10 -0
  51. package/integrations/google-drive/schemas/move_file.json +12 -0
  52. package/integrations/google-sheet/__tests__/get_handlers.test.ts +47 -55
  53. package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
  54. package/integrations/google-sheet/__tests__/write_handlers.test.ts +64 -63
  55. package/integrations/google-sheet/credentials.json +50 -29
  56. package/integrations/google-sheet/credentials_hint_oauth_token.md +8 -0
  57. package/integrations/google-sheet/credentials_hint_service_account.md +10 -0
  58. package/integrations/google-slides/__tests__/get_handlers.test.ts +37 -36
  59. package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
  60. package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -58
  61. package/integrations/google-slides/credentials.json +50 -29
  62. package/integrations/google-slides/credentials_hint_oauth_token.md +8 -0
  63. package/integrations/google-slides/credentials_hint_service_account.md +10 -0
  64. package/integrations/notion/__tests__/get_handlers.test.ts +18 -15
  65. package/integrations/notion/__tests__/usage_parity.test.ts +3 -28
  66. package/integrations/notion/__tests__/write_and_admin_handlers.test.ts +56 -60
  67. package/integrations/notion/credentials.json +22 -17
  68. package/integrations/trello/__tests__/get_handlers.test.ts +58 -73
  69. package/integrations/trello/__tests__/usage_parity.test.ts +3 -28
  70. package/integrations/trello/__tests__/write_and_admin_handlers.test.ts +49 -67
  71. package/integrations/trello/credentials.json +26 -21
  72. package/integrations/trello/handlers/close_board.js +6 -0
  73. package/integrations/trello/handlers/create_board.js +11 -0
  74. package/integrations/trello/handlers/delete_board.js +13 -0
  75. package/integrations/trello/manifest.json +21 -0
  76. package/integrations/trello/schemas/close_board.json +10 -0
  77. package/integrations/trello/schemas/create_board.json +12 -0
  78. package/integrations/trello/schemas/delete_board.json +10 -0
  79. package/package.json +1 -1
@@ -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/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 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 the Drive folders/files you want to access 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.
@@ -0,0 +1,15 @@
1
+ async (input) => {
2
+ const body = {
3
+ name: input.name,
4
+ mimeType: input.mimeType,
5
+ }
6
+ if (input.parentId)
7
+ body.parents = [input.parentId]
8
+
9
+ const res = await integration.fetch('/files', {
10
+ method: 'POST',
11
+ body,
12
+ })
13
+ return await res.json()
14
+ }
15
+
@@ -0,0 +1,15 @@
1
+ async (input) => {
2
+ const body = {
3
+ name: input.name,
4
+ mimeType: 'application/vnd.google-apps.folder',
5
+ }
6
+ if (input.parentId)
7
+ body.parents = [input.parentId]
8
+
9
+ const res = await integration.fetch('/files', {
10
+ method: 'POST',
11
+ body,
12
+ })
13
+ return await res.json()
14
+ }
15
+
@@ -0,0 +1,14 @@
1
+ async (input) => {
2
+ const res = await integration.fetch(`/files/${encodeURIComponent(input.fileId)}`, {
3
+ method: 'DELETE',
4
+ })
5
+ if (res.status === 204)
6
+ return { success: true, status: 204 }
7
+ try {
8
+ return await res.json()
9
+ }
10
+ catch {
11
+ return { success: res.ok, status: res.status }
12
+ }
13
+ }
14
+
@@ -0,0 +1,7 @@
1
+ async (input) => {
2
+ const res = await integration.fetch(`/files/${encodeURIComponent(input.fileId)}?fields=id,name,mimeType,parents,trashed`, {
3
+ method: 'GET',
4
+ })
5
+ return await res.json()
6
+ }
7
+
@@ -0,0 +1,12 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ params.set('addParents', input.addParents)
4
+ if (input.removeParents)
5
+ params.set('removeParents', input.removeParents)
6
+
7
+ const res = await integration.fetch(`/files/${encodeURIComponent(input.fileId)}?${params.toString()}`, {
8
+ method: 'PATCH',
9
+ })
10
+ return await res.json()
11
+ }
12
+
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "google-drive",
3
+ "version": "0.1.0",
4
+ "tools": [
5
+ {
6
+ "name": "create_folder",
7
+ "description": "Create a folder in Google Drive.",
8
+ "inputSchema": "schemas/create_folder.json",
9
+ "handler": "handlers/create_folder.js",
10
+ "scope": "write"
11
+ },
12
+ {
13
+ "name": "create_file",
14
+ "description": "Create a Drive file (including Google Docs/Sheets/Slides) optionally inside a parent folder.",
15
+ "inputSchema": "schemas/create_file.json",
16
+ "handler": "handlers/create_file.js",
17
+ "scope": "write"
18
+ },
19
+ {
20
+ "name": "move_file",
21
+ "description": "Move a file to a different parent folder (add/remove parents).",
22
+ "inputSchema": "schemas/move_file.json",
23
+ "handler": "handlers/move_file.js",
24
+ "scope": "write"
25
+ },
26
+ {
27
+ "name": "get_file",
28
+ "description": "Get a Drive file’s metadata by fileId.",
29
+ "inputSchema": "schemas/get_file.json",
30
+ "handler": "handlers/get_file.js",
31
+ "scope": "read"
32
+ },
33
+ {
34
+ "name": "delete_file",
35
+ "description": "Permanently delete a Drive file or folder by fileId.",
36
+ "inputSchema": "schemas/delete_file.json",
37
+ "handler": "handlers/delete_file.js",
38
+ "scope": "write"
39
+ }
40
+ ]
41
+ }
42
+
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "required": ["name", "mimeType"],
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "name": { "type": "string" },
8
+ "mimeType": { "type": "string", "description": "Drive mimeType (e.g. application/vnd.google-apps.document)" },
9
+ "parentId": { "type": "string", "description": "Optional parent folder fileId" }
10
+ }
11
+ }
12
+
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "required": ["name"],
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "name": { "type": "string" },
8
+ "parentId": { "type": "string", "description": "Optional parent folder fileId" }
9
+ }
10
+ }
11
+
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "required": ["fileId"],
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "fileId": { "type": "string" }
8
+ }
9
+ }
10
+
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "required": ["fileId"],
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "fileId": { "type": "string" }
8
+ }
9
+ }
10
+
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "required": ["fileId", "addParents"],
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "fileId": { "type": "string" },
8
+ "addParents": { "type": "string", "description": "Comma-separated parent IDs to add" },
9
+ "removeParents": { "type": "string", "description": "Comma-separated parent IDs to remove (optional)" }
10
+ }
11
+ }
12
+
@@ -1,128 +1,120 @@
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 read tests using managed OAuth
4
+ // LIVE Google Sheets read 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 (an accessible spreadsheet 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
- 'GSHEETS_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-sheet read handlers (live)', () => {
23
- let buildReadHandler: (name: string) => ((input: any) => Promise<any>)
13
+ let sheets: ReturnType<typeof createToolbox>
14
+ let drive: ReturnType<typeof createToolbox>
24
15
  let sheetTitle: string | undefined
16
+ let folderId: string | undefined
17
+ let spreadsheetId: string | undefined
25
18
 
26
19
  beforeAll(async () => {
27
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GSHEETS_TEST_CONNECTION_ID } = env
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
+ }))
25
+ const proxy = createProxy(credentialStore)
26
+ sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
27
+ drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
28
28
 
29
- const proxy = new IntegrationProxy({
30
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
31
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
32
- })
33
- const integrationNode = { id: 'node-gsheets', type: 'google-sheet', label: 'Google Sheets', connectionId: GSHEETS_TEST_CONNECTION_ID } as any
29
+ // Create dedicated folder + spreadsheet for this run
30
+ const folder = await drive.write('create_folder')({ name: `CmdTest Sheets ${Date.now()}` })
31
+ folderId = folder?.id
32
+ expect(folderId).toBeTruthy()
34
33
 
35
- const tools = loadIntegrationTools('google-sheet')
36
- expect(tools).toBeTruthy()
34
+ const created = await drive.write('create_file')({
35
+ name: `CmdTest Sheet ${Date.now()}`,
36
+ mimeType: 'application/vnd.google-apps.spreadsheet',
37
+ parentId: folderId,
38
+ })
39
+ spreadsheetId = created?.id
40
+ expect(spreadsheetId).toBeTruthy()
37
41
 
38
- buildReadHandler = (name: string) => {
39
- const tool = tools!.read.find(t => t.name === name)
40
- expect(tool, `read 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>
42
+ try {
43
+ const get_spreadsheet = sheets.read('get_spreadsheet')
44
+ const meta = await get_spreadsheet({ spreadsheetId })
45
+ sheetTitle = meta?.sheets?.[0]?.properties?.title
44
46
  }
47
+ catch {}
48
+ }, 60000)
45
49
 
46
- // Try to detect a default sheet title
47
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
48
- if (spreadsheetId) {
49
- try {
50
- const get_spreadsheet = buildReadHandler('get_spreadsheet')
51
- const meta = await get_spreadsheet({ spreadsheetId })
52
- sheetTitle = meta?.sheets?.[0]?.properties?.title
53
- }
54
- catch {}
55
- }
50
+ afterAll(async () => {
51
+ if (!folderId)
52
+ return
53
+ await safeCleanup(async () => spreadsheetId ? drive.write('delete_file')({ fileId: spreadsheetId }) : Promise.resolve())
54
+ await safeCleanup(async () => drive.write('delete_file')({ fileId: folderId }))
56
55
  }, 60000)
57
56
 
58
57
  it('get_spreadsheet returns metadata', async () => {
59
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
60
58
  if (!spreadsheetId)
61
59
  return expect(true).toBe(true)
62
- const handler = buildReadHandler('get_spreadsheet')
60
+ const handler = sheets.read('get_spreadsheet')
63
61
  const result = await handler({ spreadsheetId })
64
62
  expect(result?.spreadsheetId || result?.sheets).toBeTruthy()
65
63
  }, 30000)
66
64
 
67
65
  it('get_values returns a value range', async () => {
68
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
69
66
  if (!spreadsheetId)
70
67
  return expect(true).toBe(true)
71
- const handler = buildReadHandler('get_values')
68
+ const handler = sheets.read('get_values')
72
69
  const range = sheetTitle ? `${sheetTitle}!A1:B5` : 'A1:B5'
73
70
  const result = await handler({ spreadsheetId, range })
74
71
  expect(result?.range || result?.values).toBeTruthy()
75
72
  }, 30000)
76
73
 
77
74
  it('batch_get_values returns multiple ranges', async () => {
78
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
79
75
  if (!spreadsheetId)
80
76
  return expect(true).toBe(true)
81
- const handler = buildReadHandler('batch_get_values')
77
+ const handler = sheets.read('batch_get_values')
82
78
  const aTitle = sheetTitle || 'Sheet1'
83
79
  const result = await handler({ spreadsheetId, ranges: [`${aTitle}!A1:A3`, `${aTitle}!B1:B3`] })
84
80
  expect(Array.isArray(result?.valueRanges)).toBe(true)
85
81
  }, 30000)
86
82
 
87
83
  it('get_spreadsheet_by_data_filter returns metadata', async () => {
88
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
89
84
  if (!spreadsheetId)
90
85
  return expect(true).toBe(true)
91
- const handler = buildReadHandler('get_spreadsheet_by_data_filter')
86
+ const handler = sheets.read('get_spreadsheet_by_data_filter')
92
87
  const aTitle = sheetTitle || 'Sheet1'
93
88
  const result = await handler({ spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!A1:A1` }] })
94
89
  expect(result?.spreadsheetId || result?.sheets).toBeTruthy()
95
90
  }, 30000)
96
91
 
97
92
  it('get_values_by_data_filter returns values', async () => {
98
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
99
93
  if (!spreadsheetId)
100
94
  return expect(true).toBe(true)
101
- const handler = buildReadHandler('get_values_by_data_filter')
95
+ const handler = sheets.read('get_values_by_data_filter')
102
96
  const aTitle = sheetTitle || 'Sheet1'
103
97
  const result = await handler({ spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!A1:B2` }] })
104
98
  expect(Array.isArray(result?.valueRanges)).toBe(true)
105
99
  }, 30000)
106
100
 
107
101
  it('search_developer_metadata returns results or empty', async () => {
108
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
109
102
  if (!spreadsheetId)
110
103
  return expect(true).toBe(true)
111
- const handler = buildReadHandler('search_developer_metadata')
104
+ const handler = sheets.read('search_developer_metadata')
112
105
  const result = await handler({ spreadsheetId, dataFilters: [{ developerMetadataLookup: { visibility: 'DOCUMENT' } }] })
113
106
  expect(result !== undefined).toBe(true)
114
107
  }, 30000)
115
108
 
116
109
  it('get_developer_metadata retrieves by id when available', async () => {
117
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
118
110
  if (!spreadsheetId)
119
111
  return expect(true).toBe(true)
120
- const search = buildReadHandler('search_developer_metadata')
112
+ const search = sheets.read('search_developer_metadata')
121
113
  const list = await search({ spreadsheetId, dataFilters: [{ developerMetadataLookup: { visibility: 'DOCUMENT' } }] })
122
114
  const first = list?.matchedDeveloperMetadata?.[0]?.developerMetadata || list?.developerMetadata?.[0]
123
115
  if (!first?.metadataId)
124
116
  return expect(true).toBe(true)
125
- const getdm = buildReadHandler('get_developer_metadata')
117
+ const getdm = sheets.read('get_developer_metadata')
126
118
  const got = await getdm({ spreadsheetId, metadataId: first.metadataId })
127
119
  expect(got?.metadataId).toBe(first.metadataId)
128
120
  }, 30000)
@@ -1,35 +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-sheet static usage parity', () => {
12
- it('every manifest tool is referenced in tests via build*(name)', () => {
13
- const manifest = loadIntegrationManifest('google-sheet')!
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
- // Matches buildRead('x'), buildWrite('x'), buildAdmin('x'), buildHandler('x'), buildReadHandler('x'), buildWriteHandler('x')
27
- const nameRe = new RegExp(`build(?:Read|Write|Admin)?(?:Handler)?\\(\\s*['\"\`]${escapeRegExp(name)}['\"\`]\\s*\\)`, 'm')
28
- const found = fileContents.some(src => nameRe.test(src))
29
- if (!found)
30
- missing.push(name)
31
- }
32
-
5
+ it('every manifest tool is referenced in tests', () => {
6
+ const missing = getMissingToolUsages({ integrationName: 'google-sheet', importMetaUrl: import.meta.url })
33
7
  expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
34
8
  })
35
9
  })
@@ -1,69 +1,61 @@
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
+ }))
31
+ const proxy = createProxy(credentialStore)
32
+ sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
33
+ drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
34
+
35
+ const folder = await drive.write('create_folder')({ name: `CmdTest Sheets Write ${Date.now()}` })
36
+ ctx.folderId = folder?.id
37
+ expect(ctx.folderId).toBeTruthy()
38
+
39
+ const created = await drive.write('create_file')({
40
+ name: `CmdTest Sheet ${Date.now()}`,
41
+ mimeType: 'application/vnd.google-apps.spreadsheet',
42
+ parentId: ctx.folderId,
38
43
  })
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()
44
+ ctx.spreadsheetId = created?.id
45
+ expect(ctx.spreadsheetId).toBeTruthy()
43
46
 
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
47
+ const createdDest = await drive.write('create_file')({
48
+ name: `CmdTest Sheet Dest ${Date.now()}`,
49
+ mimeType: 'application/vnd.google-apps.spreadsheet',
50
+ parentId: ctx.folderId,
51
+ })
52
+ ctx.destSpreadsheetId = createdDest?.id
53
+ expect(ctx.destSpreadsheetId).toBeTruthy()
53
54
 
54
55
  // Try to detect a default sheet title
55
56
  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
57
  try {
66
- const get_spreadsheet = build(integration) as (input: any) => Promise<any>
58
+ const get_spreadsheet = sheets.read('get_spreadsheet')
67
59
  const meta = await get_spreadsheet({ spreadsheetId: ctx.spreadsheetId })
68
60
  sheetTitle = meta?.sheets?.[0]?.properties?.title
69
61
  }
@@ -71,16 +63,27 @@ suite('google-sheet write handlers (live)', () => {
71
63
  }
72
64
  }, 60000)
73
65
 
66
+ afterAll(async () => {
67
+ await safeCleanup(async () => {
68
+ const delete_file = drive.write('delete_file')
69
+ if (ctx.spreadsheetId)
70
+ await delete_file({ fileId: ctx.spreadsheetId })
71
+ if (ctx.destSpreadsheetId)
72
+ await delete_file({ fileId: ctx.destSpreadsheetId })
73
+ })
74
+ await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
75
+ }, 60_000)
76
+
74
77
  it('append_values appends then clear_values clears', async () => {
75
78
  if (!ctx.spreadsheetId)
76
79
  return expect(true).toBe(true)
77
80
 
78
- const append_values = buildWriteHandler('append_values')
81
+ const append_values = sheets.write('append_values')
79
82
  const aTitle = sheetTitle || 'Sheet1'
80
83
  const appendRes = await append_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!A1`, values: [[`CmdTest ${Date.now()}`]], valueInputOption: 'USER_ENTERED' })
81
84
  expect(appendRes?.updates || appendRes?.tableRange || appendRes?.spreadsheetId).toBeTruthy()
82
85
 
83
- const clear_values = buildWriteHandler('clear_values')
86
+ const clear_values = sheets.write('clear_values')
84
87
  const clearRes = await clear_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!A1:A10` })
85
88
  expect(clearRes?.clearedRange || clearRes?.spreadsheetId).toBeTruthy()
86
89
  }, 60000)
@@ -89,7 +92,7 @@ suite('google-sheet write handlers (live)', () => {
89
92
  if (!ctx.spreadsheetId)
90
93
  return expect(true).toBe(true)
91
94
  const aTitle = sheetTitle || 'Sheet1'
92
- const update_values = buildWriteHandler('update_values')
95
+ const update_values = sheets.write('update_values')
93
96
  const res = await update_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!B1:B1`, values: [[`CmdTestU ${Date.now()}`]], valueInputOption: 'USER_ENTERED' })
94
97
  expect(res?.updatedRange || res?.spreadsheetId).toBeTruthy()
95
98
  }, 60000)
@@ -98,7 +101,7 @@ suite('google-sheet write handlers (live)', () => {
98
101
  if (!ctx.spreadsheetId)
99
102
  return expect(true).toBe(true)
100
103
  const aTitle = sheetTitle || 'Sheet1'
101
- const batch_update_values = buildWriteHandler('batch_update_values')
104
+ const batch_update_values = sheets.write('batch_update_values')
102
105
  const res = await batch_update_values({ spreadsheetId: ctx.spreadsheetId, data: [
103
106
  { range: `${aTitle}!C1:C1`, values: [[`CmdTestB1 ${Date.now()}`]] },
104
107
  { range: `${aTitle}!C2:C2`, values: [[`CmdTestB2 ${Date.now()}`]] },
@@ -110,7 +113,7 @@ suite('google-sheet write handlers (live)', () => {
110
113
  if (!ctx.spreadsheetId)
111
114
  return expect(true).toBe(true)
112
115
  const aTitle = sheetTitle || 'Sheet1'
113
- const batch_update_values_by_data_filter = buildWriteHandler('batch_update_values_by_data_filter')
116
+ const batch_update_values_by_data_filter = sheets.write('batch_update_values_by_data_filter')
114
117
  const res = await batch_update_values_by_data_filter({ spreadsheetId: ctx.spreadsheetId, data: [
115
118
  { dataFilter: { a1Range: `${aTitle}!D1:D1` }, values: [[`CmdTestDF ${Date.now()}`]] },
116
119
  ], valueInputOption: 'USER_ENTERED' })
@@ -121,7 +124,7 @@ suite('google-sheet write handlers (live)', () => {
121
124
  if (!ctx.spreadsheetId)
122
125
  return expect(true).toBe(true)
123
126
  const aTitle = sheetTitle || 'Sheet1'
124
- const batch_clear_values = buildWriteHandler('batch_clear_values')
127
+ const batch_clear_values = sheets.write('batch_clear_values')
125
128
  const res = await batch_clear_values({ spreadsheetId: ctx.spreadsheetId, ranges: [`${aTitle}!A1:A2`, `${aTitle}!B1:B2`] })
126
129
  expect(Boolean(res?.spreadsheetId) || Array.isArray(res?.clearedRanges)).toBe(true)
127
130
  }, 60000)
@@ -130,7 +133,7 @@ suite('google-sheet write handlers (live)', () => {
130
133
  if (!ctx.spreadsheetId)
131
134
  return expect(true).toBe(true)
132
135
  const aTitle = sheetTitle || 'Sheet1'
133
- const batch_clear_values_by_data_filter = buildWriteHandler('batch_clear_values_by_data_filter')
136
+ const batch_clear_values_by_data_filter = sheets.write('batch_clear_values_by_data_filter')
134
137
  const res = await batch_clear_values_by_data_filter({ spreadsheetId: ctx.spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!E1:E2` }] })
135
138
  expect(Boolean(res?.spreadsheetId) || Array.isArray(res?.clearedRanges)).toBe(true)
136
139
  }, 60000)
@@ -139,7 +142,7 @@ suite('google-sheet write handlers (live)', () => {
139
142
  if (!ctx.spreadsheetId)
140
143
  return expect(true).toBe(true)
141
144
  const aTitle = sheetTitle || 'Sheet1'
142
- const batch_update = buildWriteHandler('batch_update')
145
+ const batch_update = sheets.write('batch_update')
143
146
  const res = await batch_update({ spreadsheetId: ctx.spreadsheetId, requests: [
144
147
  { findReplace: { find: '___unlikely___', replacement: '___unlikely___', matchCase: true, searchByRegex: false, includeFormulas: false, range: { sheetId: 0 } } },
145
148
  ], includeSpreadsheetInResponse: false })
@@ -147,12 +150,12 @@ suite('google-sheet write handlers (live)', () => {
147
150
  }, 60000)
148
151
 
149
152
  it('copy_to_spreadsheet copies a sheet when destination provided', async () => {
150
- if (!ctx.spreadsheetId || !env.GSHEETS_TEST_DEST_SPREADSHEET_ID)
153
+ if (!ctx.spreadsheetId || !ctx.destSpreadsheetId)
151
154
  return expect(true).toBe(true)
152
- const copy_to_spreadsheet = buildWriteHandler('copy_to_spreadsheet')
155
+ const copy_to_spreadsheet = sheets.write('copy_to_spreadsheet')
153
156
  // Attempt to copy sheet with id 0 (typical for first sheet). If it fails, skip.
154
157
  try {
155
- const res = await copy_to_spreadsheet({ spreadsheetId: ctx.spreadsheetId, sheetId: 0, destinationSpreadsheetId: env.GSHEETS_TEST_DEST_SPREADSHEET_ID })
158
+ const res = await copy_to_spreadsheet({ spreadsheetId: ctx.spreadsheetId, sheetId: 0, destinationSpreadsheetId: ctx.destSpreadsheetId })
156
159
  expect(res?.sheetId !== undefined || res?.spreadsheetId).toBeTruthy()
157
160
  }
158
161
  catch {
@@ -160,12 +163,10 @@ suite('google-sheet write handlers (live)', () => {
160
163
  }
161
164
  }, 60000)
162
165
 
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.
166
+ it('create_spreadsheet creates a spreadsheet (self-cleaning)', async () => {
167
+ const created = await sheets.write('create_spreadsheet')({ properties: { title: `CmdTest Sheet Tool ${Date.now()}` } })
168
+ const id = created?.spreadsheetId
169
+ expect(typeof id).toBe('string')
170
+ await safeCleanup(async () => id ? drive.write('delete_file')({ fileId: id }) : Promise.resolve())
170
171
  }, 60000)
171
172
  })