@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
@@ -11,32 +11,17 @@ async (input) => {
11
11
  const commitData = await commitRes.json()
12
12
  const currentTreeSha = commitData.tree.sha
13
13
 
14
- // 3. Create blobs for each file with content
14
+ // 3. Build tree entries — use inline content for creates/updates, sha null for deletions
15
15
  const tree = []
16
16
  for (const file of files) {
17
17
  if (file.content !== undefined && file.content !== null) {
18
- // Create a blob for this file
19
- const contentBase64 = typeof Buffer !== 'undefined'
20
- ? Buffer.from(file.content).toString('base64')
21
- : btoa(unescape(encodeURIComponent(file.content)))
22
-
23
- const blobRes = await integration.fetch(`/repos/${owner}/${repo}/git/blobs`, {
24
- method: 'POST',
25
- body: {
26
- content: contentBase64,
27
- encoding: 'base64',
28
- },
29
- })
30
- const blobData = await blobRes.json()
31
-
32
18
  tree.push({
33
19
  path: file.path,
34
20
  mode: file.mode || '100644',
35
21
  type: 'blob',
36
- sha: blobData.sha,
22
+ content: file.content,
37
23
  })
38
24
  } else {
39
- // File deletion (null sha means delete)
40
25
  tree.push({
41
26
  path: file.path,
42
27
  mode: '100644',
@@ -10,8 +10,8 @@
10
10
  { "name": "list_branches", "description": "List branches for a repository.", "inputSchema": "schemas/owner_repo.json", "handler": "handlers/list_branches.js", "scope": "read" },
11
11
  { "name": "list_commits", "description": "List commits for a repository.", "inputSchema": "schemas/list_commits.json", "handler": "handlers/list_commits.js", "scope": "read" },
12
12
 
13
- { "name": "create_repo", "description": "Create a new repository.", "inputSchema": "schemas/create_repo.json", "handler": "handlers/create_repo.js", "scope": "write" },
14
- { "name": "delete_repo", "description": "Delete a repository (requires delete_repo scope).", "inputSchema": "schemas/delete_repo.json", "handler": "handlers/delete_repo.js", "scope": "write" },
13
+ { "name": "create_repo", "description": "Create a new repository.", "inputSchema": "schemas/create_repo.json", "handler": "handlers/create_repo.js", "scope": "write", "credentialVariants": ["classic_pat"] },
14
+ { "name": "delete_repo", "description": "Delete a repository (requires delete_repo scope).", "inputSchema": "schemas/delete_repo.json", "handler": "handlers/delete_repo.js", "scope": "write", "credentialVariants": ["classic_pat"] },
15
15
  { "name": "create_branch", "description": "Create a new branch in a repository.", "inputSchema": "schemas/create_branch.json", "handler": "handlers/create_branch.js", "scope": "write" },
16
16
  { "name": "create_or_update_file", "description": "Create or update a single file (content must be plain text; base64 handled internally).", "inputSchema": "schemas/create_or_update_file.json", "handler": "handlers/create_or_update_file.js", "scope": "write" },
17
17
  { "name": "create_commit", "description": "Create a commit with multiple files (content must be plain text; base64 handled internally).", "inputSchema": "schemas/create_commit.json", "handler": "handlers/create_commit.js", "scope": "write" },
@@ -2,11 +2,9 @@ import { beforeAll, describe, expect, it } from 'vitest'
2
2
  import { IntegrationProxy } from '../../../../server/src/integrations/proxy.js'
3
3
  import { loadIntegrationTools } from '../../../../server/src/integrations/dataLoader.js'
4
4
 
5
- // LIVE Google Calendar read tests using managed OAuth
5
+ // LIVE Google Calendar read tests using credentials
6
6
  // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - GCAL_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-calendar')
7
+ // - Either GOOGLE_TOKEN, OR (GOOGLE_SERVICE_ACCOUNT_JSON + GOOGLE_IMPERSONATE_SUBJECT)
10
8
 
11
9
  interface Ctx {
12
10
  calendarId?: string
@@ -16,10 +14,9 @@ interface Ctx {
16
14
  const env = process.env as Record<string, string>
17
15
  const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
18
16
  const suite = hasEnv(
19
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
20
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
21
- 'GCAL_TEST_CONNECTION_ID',
17
+ 'GOOGLE_TOKEN',
22
18
  )
19
+ || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON', 'GOOGLE_IMPERSONATE_SUBJECT')
23
20
  ? describe
24
21
  : describe.skip
25
22
 
@@ -28,13 +25,24 @@ suite('google-calendar read handlers (live)', () => {
28
25
  let buildHandler: (name: string) => ((input: any) => Promise<any>)
29
26
 
30
27
  beforeAll(async () => {
31
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GCAL_TEST_CONNECTION_ID } = env
28
+ const credentialStore = {
29
+ getCredentials: async () => ({
30
+ token: env.GOOGLE_TOKEN || '',
31
+ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
32
+ subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
33
+ }),
34
+ }
32
35
 
33
- const proxy = new IntegrationProxy({
34
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
35
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
36
- })
37
- const integrationNode = { id: 'node-gcal', type: 'google-calendar', label: 'Google Calendar', connectionId: GCAL_TEST_CONNECTION_ID } as any
36
+ const proxy = new IntegrationProxy({ credentialStore })
37
+ const integrationNode = {
38
+ spaceId: 'ci',
39
+ id: 'node-gcal',
40
+ referenceId: 'node-gcal',
41
+ type: 'google-calendar',
42
+ label: 'Google Calendar',
43
+ connectionMethod: 'credentials',
44
+ credentialId: 'google-calendar-creds',
45
+ } as any
38
46
 
39
47
  const tools = loadIntegrationTools('google-calendar')
40
48
  expect(tools).toBeTruthy()
@@ -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-calendar static usage parity', () => {
12
- it('every manifest tool is referenced in tests via build*(name)', () => {
13
- const manifest = loadIntegrationManifest('google-calendar')!
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-calendar', importMetaUrl: import.meta.url })
32
7
  expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
33
8
  })
34
9
  })
@@ -1,12 +1,10 @@
1
1
  import { beforeAll, describe, expect, it } from 'vitest'
2
- import { IntegrationProxy } from '../../../src/integrations/proxy.js'
3
- import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
2
+ import { IntegrationProxy } from '../../../../server/src/integrations/proxy.js'
3
+ import { loadIntegrationTools } from '../../../../server/src/integrations/dataLoader.js'
4
4
 
5
- // LIVE Google Calendar write/admin tests using managed OAuth
5
+ // LIVE Google Calendar write/admin tests using credentials
6
6
  // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - GCAL_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-calendar')
7
+ // - Either GOOGLE_TOKEN, OR (GOOGLE_SERVICE_ACCOUNT_JSON + GOOGLE_IMPERSONATE_SUBJECT)
10
8
  // Optional:
11
9
  // - GCAL_TEST_CALENDAR_ID (defaults to 'primary')
12
10
 
@@ -18,11 +16,7 @@ interface Ctx {
18
16
 
19
17
  const env = process.env as Record<string, string>
20
18
  const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
21
- const suite = hasEnv(
22
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
23
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
24
- 'GCAL_TEST_CONNECTION_ID',
25
- )
19
+ const suite = hasEnv('GOOGLE_TOKEN') || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON', 'GOOGLE_IMPERSONATE_SUBJECT')
26
20
  ? describe
27
21
  : describe.skip
28
22
 
@@ -33,15 +27,28 @@ suite('google-calendar write & admin handlers (live)', () => {
33
27
  let buildAdmin: (name: string) => ((input: any) => Promise<any>)
34
28
 
35
29
  beforeAll(async () => {
36
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GCAL_TEST_CONNECTION_ID, GCAL_TEST_CALENDAR_ID } = env
30
+ const { GCAL_TEST_CALENDAR_ID } = env
37
31
 
38
32
  ctx.calendarId = GCAL_TEST_CALENDAR_ID || 'primary'
39
33
 
40
- const proxy = new IntegrationProxy({
41
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
42
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
43
- })
44
- const integrationNode = { id: 'node-gcal', type: 'google-calendar', label: 'Google Calendar', connectionId: GCAL_TEST_CONNECTION_ID } as any
34
+ const credentialStore = {
35
+ getCredentials: async () => ({
36
+ token: env.GOOGLE_TOKEN || '',
37
+ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
38
+ subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
39
+ }),
40
+ }
41
+
42
+ const proxy = new IntegrationProxy({ credentialStore })
43
+ const integrationNode = {
44
+ spaceId: 'ci',
45
+ id: 'node-gcal',
46
+ referenceId: 'node-gcal',
47
+ type: 'google-calendar',
48
+ label: 'Google Calendar',
49
+ connectionMethod: 'credentials',
50
+ credentialId: 'google-calendar-creds',
51
+ } as any
45
52
 
46
53
  const tools = loadIntegrationTools('google-calendar')
47
54
  expect(tools).toBeTruthy()
@@ -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": "User email to impersonate via Google Workspace domain-wide delegation. Required for most Calendar operations."
17
+ },
18
+ "scopes": {
19
+ "type": "array",
20
+ "title": "OAuth scopes (optional)",
21
+ "description": "Optional override for OAuth scopes. Defaults to calendar.",
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 calendar 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/calendar`
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 Calendar 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
+
9
+ For Google Workspace users: Calendar access typically requires domain-wide delegation.
10
+ Configure it in the Google Admin console, then set `subject` to the calendar owner's email.
@@ -1,72 +1,98 @@
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 Docs read tests using managed OAuth
6
- // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - GDOCS_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-docs')
10
- // - GDOCS_TEST_DOCUMENT_ID (an accessible document ID)
4
+ // LIVE Google Docs read 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)
11
8
 
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
- 'GDOCS_TEST_CONNECTION_ID',
18
- )
19
- ? describe
20
- : describe.skip
9
+ const env = process.env as Record<string, string | undefined>
21
10
 
22
- suite('google-docs read handlers (live)', () => {
23
- let buildReadHandler: (name: string) => ((input: any) => Promise<any>)
11
+ interface VariantConfig {
12
+ key: string
13
+ credentials: () => Record<string, string>
14
+ }
24
15
 
25
- beforeAll(async () => {
26
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GDOCS_TEST_CONNECTION_ID } = env
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))
27
26
 
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-gdocs', type: 'google-docs', label: 'Google Docs', connectionId: GDOCS_TEST_CONNECTION_ID } as any
27
+ const suiteOrSkip = variants.length > 0 ? describe : describe.skip
28
+
29
+ suiteOrSkip('google-docs read handlers (live)', () => {
30
+ for (const variant of variants) {
31
+ describe(`variant: ${variant.key}`, () => {
32
+ let docs: ReturnType<typeof createToolbox>
33
+ let drive: ReturnType<typeof createToolbox>
34
+ let folderId: string | undefined
35
+ let documentId: string | undefined
36
+
37
+ beforeAll(async () => {
38
+ const credentialStore = createCredentialStore(async () => variant.credentials())
39
+ const proxy = createProxy(credentialStore)
40
+ docs = createToolbox(
41
+ 'google-docs',
42
+ proxy,
43
+ createIntegrationNode('google-docs', { label: 'Google Docs', credentialId: 'google-docs-creds', credentialVariant: variant.key }),
44
+ variant.key,
45
+ )
46
+ drive = createToolbox(
47
+ 'google-drive',
48
+ proxy,
49
+ createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds', credentialVariant: variant.key }),
50
+ variant.key,
51
+ )
33
52
 
34
- const tools = loadIntegrationTools('google-docs')
35
- expect(tools).toBeTruthy()
53
+ const folder = await drive.write('create_folder')({ name: `CmdTest Docs ${Date.now()}` })
54
+ folderId = folder?.id
55
+ expect(folderId).toBeTruthy()
36
56
 
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
- }
44
- }, 60000)
57
+ const created = await drive.write('create_file')({
58
+ name: `CmdTest Doc ${Date.now()}`,
59
+ mimeType: 'application/vnd.google-apps.document',
60
+ parentId: folderId,
61
+ })
62
+ documentId = created?.id
63
+ expect(documentId).toBeTruthy()
64
+ }, 60000)
45
65
 
46
- it('get_document returns metadata/content', async () => {
47
- const documentId = env.GDOCS_TEST_DOCUMENT_ID
48
- if (!documentId)
49
- return expect(true).toBe(true)
50
- const handler = buildReadHandler('get_document')
51
- const result = await handler({ documentId })
52
- expect(result?.documentId || result?.body?.content || result?.title).toBeTruthy()
53
- }, 30000)
66
+ afterAll(async () => {
67
+ if (!folderId)
68
+ return
69
+ await safeCleanup(async () => documentId ? drive.write('delete_file')({ fileId: documentId }) : Promise.resolve())
70
+ await safeCleanup(async () => drive.write('delete_file')({ fileId: folderId }))
71
+ }, 60000)
54
72
 
55
- it('get_document_text returns plain text', async () => {
56
- const documentId = env.GDOCS_TEST_DOCUMENT_ID
57
- if (!documentId)
58
- return expect(true).toBe(true)
59
- const handler = buildReadHandler('get_document_text')
60
- const result = await handler({ documentId })
61
- expect(typeof result?.text === 'string').toBe(true)
62
- }, 30000)
73
+ it('get_document returns metadata/content', async () => {
74
+ if (!documentId)
75
+ return expect(true).toBe(true)
76
+ const handler = docs.read('get_document')
77
+ const result = await handler({ documentId })
78
+ expect(result?.documentId || result?.body?.content || result?.title).toBeTruthy()
79
+ }, 30000)
63
80
 
64
- it('get_document_structured returns body JSON', async () => {
65
- const documentId = env.GDOCS_TEST_DOCUMENT_ID
66
- if (!documentId)
67
- return expect(true).toBe(true)
68
- const handler = buildReadHandler('get_document_structured')
69
- const result = await handler({ documentId })
70
- expect(result?.body || result?.documentId).toBeTruthy()
71
- }, 30000)
81
+ it('get_document_text returns plain text', async () => {
82
+ if (!documentId)
83
+ return expect(true).toBe(true)
84
+ const handler = docs.read('get_document_text')
85
+ const result = await handler({ documentId })
86
+ expect(typeof result?.text === 'string').toBe(true)
87
+ }, 30000)
88
+
89
+ it('get_document_structured returns body JSON', async () => {
90
+ if (!documentId)
91
+ return expect(true).toBe(true)
92
+ const handler = docs.read('get_document_structured')
93
+ const result = await handler({ documentId })
94
+ expect(result?.body || result?.documentId).toBeTruthy()
95
+ }, 30000)
96
+ })
97
+ }
72
98
  })
@@ -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-docs static usage parity', () => {
12
- it('every manifest tool is referenced in tests via build*(name)', () => {
13
- const manifest = loadIntegrationManifest('google-docs')!
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-docs', importMetaUrl: import.meta.url })
32
7
  expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
33
8
  })
34
9
  })