@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
@@ -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
+ })
@@ -0,0 +1,57 @@
1
+ {
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 drive.",
22
+ "items": { "type": "string" }
23
+ }
24
+ },
25
+ "required": ["serviceAccountJson"],
26
+ "additionalProperties": false
27
+ },
28
+ "injection": {
29
+ "headers": {
30
+ "Authorization": "Bearer {{token}}"
31
+ }
32
+ },
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 drive scope."
44
+ }
45
+ },
46
+ "required": ["token"],
47
+ "additionalProperties": false
48
+ },
49
+ "injection": {
50
+ "headers": {
51
+ "Authorization": "Bearer {{token}}"
52
+ }
53
+ }
54
+ }
55
+ },
56
+ "default": "service_account"
57
+ }
@@ -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,121 @@
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
+ subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
25
+ }))
26
+ const proxy = createProxy(credentialStore)
27
+ sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
28
+ drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
28
29
 
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
30
+ // Create dedicated folder + spreadsheet for this run
31
+ const folder = await drive.write('create_folder')({ name: `CmdTest Sheets ${Date.now()}` })
32
+ folderId = folder?.id
33
+ expect(folderId).toBeTruthy()
34
34
 
35
- const tools = loadIntegrationTools('google-sheet')
36
- expect(tools).toBeTruthy()
35
+ const created = await drive.write('create_file')({
36
+ name: `CmdTest Sheet ${Date.now()}`,
37
+ mimeType: 'application/vnd.google-apps.spreadsheet',
38
+ parentId: folderId,
39
+ })
40
+ spreadsheetId = created?.id
41
+ expect(spreadsheetId).toBeTruthy()
37
42
 
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>
43
+ try {
44
+ const get_spreadsheet = sheets.read('get_spreadsheet')
45
+ const meta = await get_spreadsheet({ spreadsheetId })
46
+ sheetTitle = meta?.sheets?.[0]?.properties?.title
44
47
  }
48
+ catch {}
49
+ }, 60000)
45
50
 
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
- }
51
+ afterAll(async () => {
52
+ if (!folderId)
53
+ return
54
+ await safeCleanup(async () => spreadsheetId ? drive.write('delete_file')({ fileId: spreadsheetId }) : Promise.resolve())
55
+ await safeCleanup(async () => drive.write('delete_file')({ fileId: folderId }))
56
56
  }, 60000)
57
57
 
58
58
  it('get_spreadsheet returns metadata', async () => {
59
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
60
59
  if (!spreadsheetId)
61
60
  return expect(true).toBe(true)
62
- const handler = buildReadHandler('get_spreadsheet')
61
+ const handler = sheets.read('get_spreadsheet')
63
62
  const result = await handler({ spreadsheetId })
64
63
  expect(result?.spreadsheetId || result?.sheets).toBeTruthy()
65
64
  }, 30000)
66
65
 
67
66
  it('get_values returns a value range', async () => {
68
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
69
67
  if (!spreadsheetId)
70
68
  return expect(true).toBe(true)
71
- const handler = buildReadHandler('get_values')
69
+ const handler = sheets.read('get_values')
72
70
  const range = sheetTitle ? `${sheetTitle}!A1:B5` : 'A1:B5'
73
71
  const result = await handler({ spreadsheetId, range })
74
72
  expect(result?.range || result?.values).toBeTruthy()
75
73
  }, 30000)
76
74
 
77
75
  it('batch_get_values returns multiple ranges', async () => {
78
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
79
76
  if (!spreadsheetId)
80
77
  return expect(true).toBe(true)
81
- const handler = buildReadHandler('batch_get_values')
78
+ const handler = sheets.read('batch_get_values')
82
79
  const aTitle = sheetTitle || 'Sheet1'
83
80
  const result = await handler({ spreadsheetId, ranges: [`${aTitle}!A1:A3`, `${aTitle}!B1:B3`] })
84
81
  expect(Array.isArray(result?.valueRanges)).toBe(true)
85
82
  }, 30000)
86
83
 
87
84
  it('get_spreadsheet_by_data_filter returns metadata', async () => {
88
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
89
85
  if (!spreadsheetId)
90
86
  return expect(true).toBe(true)
91
- const handler = buildReadHandler('get_spreadsheet_by_data_filter')
87
+ const handler = sheets.read('get_spreadsheet_by_data_filter')
92
88
  const aTitle = sheetTitle || 'Sheet1'
93
89
  const result = await handler({ spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!A1:A1` }] })
94
90
  expect(result?.spreadsheetId || result?.sheets).toBeTruthy()
95
91
  }, 30000)
96
92
 
97
93
  it('get_values_by_data_filter returns values', async () => {
98
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
99
94
  if (!spreadsheetId)
100
95
  return expect(true).toBe(true)
101
- const handler = buildReadHandler('get_values_by_data_filter')
96
+ const handler = sheets.read('get_values_by_data_filter')
102
97
  const aTitle = sheetTitle || 'Sheet1'
103
98
  const result = await handler({ spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!A1:B2` }] })
104
99
  expect(Array.isArray(result?.valueRanges)).toBe(true)
105
100
  }, 30000)
106
101
 
107
102
  it('search_developer_metadata returns results or empty', async () => {
108
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
109
103
  if (!spreadsheetId)
110
104
  return expect(true).toBe(true)
111
- const handler = buildReadHandler('search_developer_metadata')
105
+ const handler = sheets.read('search_developer_metadata')
112
106
  const result = await handler({ spreadsheetId, dataFilters: [{ developerMetadataLookup: { visibility: 'DOCUMENT' } }] })
113
107
  expect(result !== undefined).toBe(true)
114
108
  }, 30000)
115
109
 
116
110
  it('get_developer_metadata retrieves by id when available', async () => {
117
- const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
118
111
  if (!spreadsheetId)
119
112
  return expect(true).toBe(true)
120
- const search = buildReadHandler('search_developer_metadata')
113
+ const search = sheets.read('search_developer_metadata')
121
114
  const list = await search({ spreadsheetId, dataFilters: [{ developerMetadataLookup: { visibility: 'DOCUMENT' } }] })
122
115
  const first = list?.matchedDeveloperMetadata?.[0]?.developerMetadata || list?.developerMetadata?.[0]
123
116
  if (!first?.metadataId)
124
117
  return expect(true).toBe(true)
125
- const getdm = buildReadHandler('get_developer_metadata')
118
+ const getdm = sheets.read('get_developer_metadata')
126
119
  const got = await getdm({ spreadsheetId, metadataId: first.metadataId })
127
120
  expect(got?.metadataId).toBe(first.metadataId)
128
121
  }, 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
  })