@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.
- package/dist/credentials-index.d.ts +4 -21
- package/dist/credentials-index.d.ts.map +1 -1
- package/dist/credentials-index.js +407 -215
- package/dist/credentials-index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +38 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +70 -16
- package/dist/loader.js.map +1 -1
- package/integrations/__tests__/liveHarness.ts +84 -0
- package/integrations/__tests__/usageParity.ts +54 -0
- package/integrations/airtable/__tests__/get_handlers.test.ts +18 -15
- package/integrations/airtable/__tests__/usage_parity.test.ts +3 -29
- package/integrations/airtable/__tests__/write_and_admin_handlers.test.ts +20 -17
- package/integrations/airtable/credentials.json +21 -16
- package/integrations/github/__tests__/get_handlers.test.ts +101 -108
- package/integrations/github/__tests__/usage_parity.test.ts +15 -27
- package/integrations/github/__tests__/write_handlers.test.ts +219 -306
- package/integrations/github/credentials.json +40 -15
- package/integrations/github/credentials_hint_classic_pat.md +8 -0
- package/integrations/github/credentials_hint_fine_grained_pat.md +9 -0
- package/integrations/github/manifest.json +2 -2
- package/integrations/google-calendar/__tests__/get_handlers.test.ts +21 -13
- package/integrations/google-calendar/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +24 -17
- package/integrations/google-calendar/credentials.json +50 -29
- package/integrations/google-calendar/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-calendar/credentials_hint_service_account.md +10 -0
- package/integrations/google-docs/__tests__/get_handlers.test.ts +87 -61
- package/integrations/google-docs/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-docs/__tests__/write_handlers.test.ts +248 -245
- package/integrations/google-docs/credentials.json +50 -29
- package/integrations/google-docs/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-docs/credentials_hint_service_account.md +10 -0
- package/integrations/google-drive/credentials.json +57 -0
- package/integrations/google-drive/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-drive/credentials_hint_service_account.md +10 -0
- package/integrations/google-drive/handlers/create_file.js +15 -0
- package/integrations/google-drive/handlers/create_folder.js +15 -0
- package/integrations/google-drive/handlers/delete_file.js +14 -0
- package/integrations/google-drive/handlers/get_file.js +7 -0
- package/integrations/google-drive/handlers/move_file.js +12 -0
- package/integrations/google-drive/manifest.json +42 -0
- package/integrations/google-drive/schemas/create_file.json +12 -0
- package/integrations/google-drive/schemas/create_folder.json +11 -0
- package/integrations/google-drive/schemas/delete_file.json +10 -0
- package/integrations/google-drive/schemas/get_file.json +10 -0
- package/integrations/google-drive/schemas/move_file.json +12 -0
- package/integrations/google-sheet/__tests__/get_handlers.test.ts +47 -55
- package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
- package/integrations/google-sheet/__tests__/write_handlers.test.ts +64 -63
- package/integrations/google-sheet/credentials.json +50 -29
- package/integrations/google-sheet/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-sheet/credentials_hint_service_account.md +10 -0
- package/integrations/google-slides/__tests__/get_handlers.test.ts +37 -36
- package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -58
- package/integrations/google-slides/credentials.json +50 -29
- package/integrations/google-slides/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-slides/credentials_hint_service_account.md +10 -0
- package/integrations/notion/__tests__/get_handlers.test.ts +18 -15
- package/integrations/notion/__tests__/usage_parity.test.ts +3 -28
- package/integrations/notion/__tests__/write_and_admin_handlers.test.ts +56 -60
- package/integrations/notion/credentials.json +22 -17
- package/integrations/trello/__tests__/get_handlers.test.ts +58 -73
- package/integrations/trello/__tests__/usage_parity.test.ts +3 -28
- package/integrations/trello/__tests__/write_and_admin_handlers.test.ts +49 -67
- package/integrations/trello/credentials.json +26 -21
- package/integrations/trello/handlers/close_board.js +6 -0
- package/integrations/trello/handlers/create_board.js +11 -0
- package/integrations/trello/handlers/delete_board.js +13 -0
- package/integrations/trello/manifest.json +21 -0
- package/integrations/trello/schemas/close_board.json +10 -0
- package/integrations/trello/schemas/create_board.json +12 -0
- package/integrations/trello/schemas/delete_board.json +10 -0
- package/package.json +1 -1
|
@@ -1,249 +1,252 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// -
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
+
import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, safeCleanup } from '../../__tests__/liveHarness.js'
|
|
3
|
+
|
|
4
|
+
// LIVE Google Docs write 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
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
const variants: VariantConfig[] = [
|
|
17
|
+
{
|
|
18
|
+
key: 'service_account',
|
|
19
|
+
credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '' }),
|
|
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-docs write handlers (live)', () => {
|
|
30
|
+
for (const variant of variants) {
|
|
31
|
+
describe(`variant: ${variant.key}`, () => {
|
|
32
|
+
const ctx: { documentId?: string, folderId?: string } = {}
|
|
33
|
+
let docs: ReturnType<typeof createToolbox>
|
|
34
|
+
let drive: ReturnType<typeof createToolbox>
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
const credentialStore = createCredentialStore(async () => variant.credentials())
|
|
38
|
+
const proxy = createProxy(credentialStore)
|
|
39
|
+
docs = createToolbox(
|
|
40
|
+
'google-docs',
|
|
41
|
+
proxy,
|
|
42
|
+
createIntegrationNode('google-docs', { label: 'Google Docs', credentialId: 'google-docs-creds', credentialVariant: variant.key }),
|
|
43
|
+
variant.key,
|
|
44
|
+
)
|
|
45
|
+
drive = createToolbox(
|
|
46
|
+
'google-drive',
|
|
47
|
+
proxy,
|
|
48
|
+
createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds', credentialVariant: variant.key }),
|
|
49
|
+
variant.key,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const folder = await drive.write('create_folder')({ name: `CmdTest Docs Write ${Date.now()}` })
|
|
53
|
+
ctx.folderId = folder?.id
|
|
54
|
+
expect(ctx.folderId).toBeTruthy()
|
|
55
|
+
|
|
56
|
+
const doc = await drive.write('create_file')({
|
|
57
|
+
name: `CmdTest Doc ${Date.now()}`,
|
|
58
|
+
mimeType: 'application/vnd.google-apps.document',
|
|
59
|
+
parentId: ctx.folderId,
|
|
60
|
+
})
|
|
61
|
+
ctx.documentId = doc?.id
|
|
62
|
+
expect(ctx.documentId).toBeTruthy()
|
|
63
|
+
}, 60000)
|
|
64
|
+
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
await safeCleanup(async () => ctx.documentId ? drive.write('delete_file')({ fileId: ctx.documentId }) : Promise.resolve())
|
|
67
|
+
await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
|
|
68
|
+
}, 60000)
|
|
69
|
+
|
|
70
|
+
it('batch_update can perform a trivial replaceAllText no-op', async () => {
|
|
71
|
+
const documentId = ctx.documentId
|
|
72
|
+
if (!documentId)
|
|
73
|
+
return expect(true).toBe(true)
|
|
74
|
+
const batch_update = docs.write('batch_update')
|
|
75
|
+
const res = await batch_update({ documentId, requests: [
|
|
76
|
+
{ replaceAllText: { containsText: { text: '___unlikely___', matchCase: true }, replaceText: '___unlikely___' } },
|
|
77
|
+
] })
|
|
78
|
+
expect(Array.isArray(res?.replies) || res?.documentId).toBeTruthy()
|
|
79
|
+
}, 60000)
|
|
80
|
+
|
|
81
|
+
it('append_text appends content', async () => {
|
|
82
|
+
const documentId = ctx.documentId
|
|
83
|
+
if (!documentId)
|
|
84
|
+
return expect(true).toBe(true)
|
|
85
|
+
const append_text = docs.write('append_text')
|
|
86
|
+
const marker = `CmdTest ${Date.now()}`
|
|
87
|
+
const res = await append_text({ documentId, text: marker })
|
|
88
|
+
expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
|
|
89
|
+
const get_text = docs.read('get_document_text')
|
|
90
|
+
const after = await get_text({ documentId })
|
|
91
|
+
expect(String(after?.text || '')).toContain(marker)
|
|
92
|
+
}, 60000)
|
|
93
|
+
|
|
94
|
+
it('insert_text_after_first_match inserts text near target', async () => {
|
|
95
|
+
const documentId = ctx.documentId
|
|
96
|
+
if (!documentId)
|
|
97
|
+
return expect(true).toBe(true)
|
|
98
|
+
const insert_text_after_first_match = docs.write('insert_text_after_first_match')
|
|
99
|
+
const get_text = docs.read('get_document_text')
|
|
100
|
+
const anchor = `ANCHOR_${Date.now()}`
|
|
101
|
+
const appended = docs.write('append_text')
|
|
102
|
+
const before = await get_text({ documentId })
|
|
103
|
+
if (!String(before?.text || '').includes(anchor))
|
|
104
|
+
await appended({ documentId, text: `\n${anchor}\n` })
|
|
105
|
+
const insertSnippet = ` CmdTest ${Date.now()} `
|
|
106
|
+
const res = await insert_text_after_first_match({ documentId, findText: anchor, insertText: insertSnippet, position: 'after' })
|
|
107
|
+
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
108
|
+
const after = await get_text({ documentId })
|
|
109
|
+
const text = String(after?.text || '')
|
|
110
|
+
expect(text).toContain(anchor)
|
|
111
|
+
expect(text).toContain(insertSnippet)
|
|
112
|
+
}, 60000)
|
|
113
|
+
|
|
114
|
+
it('replace_all_text replaces occurrences', async () => {
|
|
115
|
+
const documentId = ctx.documentId
|
|
116
|
+
if (!documentId)
|
|
117
|
+
return expect(true).toBe(true)
|
|
118
|
+
const replace_all_text = docs.write('replace_all_text')
|
|
119
|
+
const res = await replace_all_text({ documentId, findText: '___unlikely___', replaceText: '___unlikely___', matchCase: true })
|
|
120
|
+
expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
|
|
121
|
+
}, 60000)
|
|
122
|
+
|
|
123
|
+
it('style_first_match applies style to first match', async () => {
|
|
124
|
+
const documentId = ctx.documentId
|
|
125
|
+
if (!documentId)
|
|
126
|
+
return expect(true).toBe(true)
|
|
127
|
+
const style_first_match = docs.write('style_first_match')
|
|
128
|
+
const get_struct = docs.read('get_document_structured')
|
|
129
|
+
const anchor = `ANCHOR_${Date.now()}`
|
|
130
|
+
const appended = docs.write('append_text')
|
|
131
|
+
const before = await get_struct({ documentId })
|
|
132
|
+
if (!JSON.stringify(before?.body || {}).includes(anchor))
|
|
133
|
+
await appended({ documentId, text: `\n${anchor}\n` })
|
|
134
|
+
const res = await style_first_match({ documentId, findText: anchor, textStyle: { bold: true } })
|
|
135
|
+
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
136
|
+
}, 60000)
|
|
137
|
+
|
|
138
|
+
it('insert_table_after_first_match inserts a table near target', async () => {
|
|
139
|
+
const documentId = ctx.documentId
|
|
140
|
+
if (!documentId)
|
|
141
|
+
return expect(true).toBe(true)
|
|
142
|
+
const insert_table_after_first_match = docs.write('insert_table_after_first_match')
|
|
143
|
+
const get_struct = docs.read('get_document_structured')
|
|
144
|
+
const anchor = `ANCHOR_${Date.now()}`
|
|
145
|
+
const appended = docs.write('append_text')
|
|
146
|
+
const before = await get_struct({ documentId })
|
|
147
|
+
if (!JSON.stringify(before?.body || {}).includes(anchor))
|
|
148
|
+
await appended({ documentId, text: `\n${anchor}\n` })
|
|
149
|
+
const res = await insert_table_after_first_match({ documentId, findText: anchor, rows: 1, columns: 1 })
|
|
150
|
+
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
151
|
+
const after = await get_struct({ documentId })
|
|
152
|
+
const hasTable = (after?.body?.content || []).some((el: any) => Boolean(el.table))
|
|
153
|
+
expect(hasTable).toBe(true)
|
|
154
|
+
}, 60000)
|
|
155
|
+
|
|
156
|
+
it('insert_page_break_after_first_match inserts a break near target', async () => {
|
|
157
|
+
const documentId = ctx.documentId
|
|
158
|
+
if (!documentId)
|
|
159
|
+
return expect(true).toBe(true)
|
|
160
|
+
const insert_page_break_after_first_match = docs.write('insert_page_break_after_first_match')
|
|
161
|
+
const get_struct = docs.read('get_document_structured')
|
|
162
|
+
const anchor = `ANCHOR_${Date.now()}`
|
|
163
|
+
const appended = docs.write('append_text')
|
|
164
|
+
const before = await get_struct({ documentId })
|
|
165
|
+
if (!JSON.stringify(before?.body || {}).includes(anchor))
|
|
166
|
+
await appended({ documentId, text: `\n${anchor}\n` })
|
|
167
|
+
const res = await insert_page_break_after_first_match({ documentId, findText: anchor })
|
|
168
|
+
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
169
|
+
const after = await get_struct({ documentId })
|
|
170
|
+
const hasBreak = (after?.body?.content || []).some((el: any) => Boolean(el.sectionBreak))
|
|
171
|
+
expect(hasBreak).toBe(true)
|
|
172
|
+
}, 60000)
|
|
173
|
+
|
|
174
|
+
it('insert_inline_image_after_first_match inserts an image when allowed', async () => {
|
|
175
|
+
if (!ctx.documentId || !process.env.GDOCS_TEST_IMAGE_URI)
|
|
176
|
+
return expect(true).toBe(true)
|
|
177
|
+
const documentId = ctx.documentId
|
|
178
|
+
const insert_inline_image_after_first_match = docs.write('insert_inline_image_after_first_match')
|
|
179
|
+
const anchor = `ANCHOR_${Date.now()}`
|
|
180
|
+
const appended = docs.write('append_text')
|
|
181
|
+
const get_text = docs.read('get_document_text')
|
|
182
|
+
const before = await get_text({ documentId })
|
|
183
|
+
if (!String(before?.text || '').includes(anchor))
|
|
184
|
+
await appended({ documentId, text: `\n${anchor}\n` })
|
|
185
|
+
const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri: process.env.GDOCS_TEST_IMAGE_URI!, altText: 'CmdTest' })
|
|
186
|
+
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
187
|
+
}, 60000)
|
|
188
|
+
|
|
189
|
+
it('delete_first_match deletes a small span (no-op ok)', async () => {
|
|
190
|
+
const documentId = ctx.documentId
|
|
191
|
+
if (!documentId)
|
|
192
|
+
return expect(true).toBe(true)
|
|
193
|
+
const delete_first_match = docs.write('delete_first_match')
|
|
194
|
+
const get_text = docs.read('get_document_text')
|
|
195
|
+
const anchor = `ANCHOR_${Date.now()}`
|
|
196
|
+
const appended = docs.write('append_text')
|
|
197
|
+
const before = await get_text({ documentId })
|
|
198
|
+
if (!String(before?.text || '').includes(anchor))
|
|
199
|
+
await appended({ documentId, text: `\n${anchor}\n` })
|
|
200
|
+
const res = await delete_first_match({ documentId, findText: anchor })
|
|
201
|
+
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
202
|
+
const after = await get_text({ documentId })
|
|
203
|
+
expect(String(after?.text || '')).not.toContain(anchor)
|
|
204
|
+
}, 60000)
|
|
205
|
+
|
|
206
|
+
it('update_paragraph_style_for_first_match updates paragraph style near target', async () => {
|
|
207
|
+
const documentId = ctx.documentId
|
|
208
|
+
if (!documentId)
|
|
209
|
+
return expect(true).toBe(true)
|
|
210
|
+
const update_paragraph_style_for_first_match = docs.write('update_paragraph_style_for_first_match')
|
|
211
|
+
const get_struct = docs.read('get_document_structured')
|
|
212
|
+
const anchor = `ANCHOR_${Date.now()}`
|
|
213
|
+
const appended = docs.write('append_text')
|
|
214
|
+
const before = await get_struct({ documentId })
|
|
215
|
+
if (!JSON.stringify(before?.body || {}).includes(anchor))
|
|
216
|
+
await appended({ documentId, text: `\n${anchor}\n` })
|
|
217
|
+
const res = await update_paragraph_style_for_first_match({ documentId, findText: anchor, paragraphStyle: { alignment: 'CENTER' } })
|
|
218
|
+
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
219
|
+
const after = await get_struct({ documentId })
|
|
220
|
+
let foundAligned = false
|
|
221
|
+
for (const el of (after?.body?.content || [])) {
|
|
222
|
+
if (!el.paragraph)
|
|
223
|
+
continue
|
|
224
|
+
const p = el.paragraph
|
|
225
|
+
const text = (p.elements || []).map((e: any) => e?.textRun?.content || '').join('')
|
|
226
|
+
if (text.includes(anchor)) {
|
|
227
|
+
if (p.paragraphStyle?.alignment === 'CENTER')
|
|
228
|
+
foundAligned = true
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
expect(foundAligned).toBe(true)
|
|
233
|
+
}, 60000)
|
|
234
|
+
|
|
235
|
+
it('update_document_style updates doc style with no-op', async () => {
|
|
236
|
+
const documentId = ctx.documentId
|
|
237
|
+
if (!documentId)
|
|
238
|
+
return expect(true).toBe(true)
|
|
239
|
+
const update_document_style = docs.write('update_document_style')
|
|
240
|
+
const res = await update_document_style({ documentId, documentStyle: { useFirstPageHeaderFooter: false } })
|
|
241
|
+
expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
|
|
242
|
+
}, 60000)
|
|
243
|
+
|
|
244
|
+
it('create_document creates a document (self-cleaning)', async () => {
|
|
245
|
+
const created = await docs.write('create_document')({ title: `CmdTest Doc Tool ${Date.now()}` })
|
|
246
|
+
const id = created?.documentId
|
|
247
|
+
expect(typeof id).toBe('string')
|
|
248
|
+
await safeCleanup(async () => id ? drive.write('delete_file')({ fileId: id }) : Promise.resolve())
|
|
249
|
+
}, 60000)
|
|
37
250
|
})
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const tools = loadIntegrationTools('google-docs')
|
|
41
|
-
expect(tools).toBeTruthy()
|
|
42
|
-
|
|
43
|
-
buildWriteHandler = (name: string) => {
|
|
44
|
-
const tool = tools!.write.find(t => t.name === name)
|
|
45
|
-
expect(tool, `write tool ${name} exists`).toBeTruthy()
|
|
46
|
-
const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
|
|
47
|
-
const build = new Function('integration', `return (${tool!.handlerCode});`)
|
|
48
|
-
return build(integration) as (input: any) => Promise<any>
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
buildReadHandler = (name: string) => {
|
|
52
|
-
const tool = tools!.read.find(t => t.name === name)
|
|
53
|
-
expect(tool, `read tool ${name} exists`).toBeTruthy()
|
|
54
|
-
const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
|
|
55
|
-
const build = new Function('integration', `return (${tool!.handlerCode});`)
|
|
56
|
-
return build(integration) as (input: any) => Promise<any>
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
ctx.documentId = GDOCS_TEST_DOCUMENT_ID
|
|
60
|
-
}, 60000)
|
|
61
|
-
|
|
62
|
-
it('create_document creates a document when allowed', async () => {
|
|
63
|
-
if (!process.env.GDOCS_ALLOW_CREATE)
|
|
64
|
-
return expect(true).toBe(true)
|
|
65
|
-
const create_document = buildWriteHandler('create_document')
|
|
66
|
-
const res = await create_document({ title: `CmdTest Doc ${Date.now()}` })
|
|
67
|
-
expect(res?.documentId).toBeTruthy()
|
|
68
|
-
}, 60000)
|
|
69
|
-
|
|
70
|
-
it('batch_update can perform a trivial replaceAllText no-op', async () => {
|
|
71
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
72
|
-
if (!documentId)
|
|
73
|
-
return expect(true).toBe(true)
|
|
74
|
-
const batch_update = buildWriteHandler('batch_update')
|
|
75
|
-
const res = await batch_update({ documentId, requests: [
|
|
76
|
-
{ replaceAllText: { containsText: { text: '___unlikely___', matchCase: true }, replaceText: '___unlikely___' } },
|
|
77
|
-
] })
|
|
78
|
-
expect(Array.isArray(res?.replies) || res?.documentId).toBeTruthy()
|
|
79
|
-
}, 60000)
|
|
80
|
-
|
|
81
|
-
it('append_text appends content', async () => {
|
|
82
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
83
|
-
if (!documentId)
|
|
84
|
-
return expect(true).toBe(true)
|
|
85
|
-
const append_text = buildWriteHandler('append_text')
|
|
86
|
-
const marker = `CmdTest ${Date.now()}`
|
|
87
|
-
const res = await append_text({ documentId, text: marker })
|
|
88
|
-
expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
|
|
89
|
-
const get_text = buildReadHandler('get_document_text')
|
|
90
|
-
const after = await get_text({ documentId })
|
|
91
|
-
expect(String(after?.text || '')).toContain(marker)
|
|
92
|
-
}, 60000)
|
|
93
|
-
|
|
94
|
-
it('insert_text_after_first_match inserts text near target', async () => {
|
|
95
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
96
|
-
if (!documentId)
|
|
97
|
-
return expect(true).toBe(true)
|
|
98
|
-
const insert_text_after_first_match = buildWriteHandler('insert_text_after_first_match')
|
|
99
|
-
const get_text = buildReadHandler('get_document_text')
|
|
100
|
-
const anchor = `ANCHOR_${Date.now()}`
|
|
101
|
-
const appended = buildWriteHandler('append_text')
|
|
102
|
-
const before = await get_text({ documentId })
|
|
103
|
-
if (!String(before?.text || '').includes(anchor)) {
|
|
104
|
-
await appended({ documentId, text: `\n${anchor}\n` })
|
|
105
|
-
}
|
|
106
|
-
const insertSnippet = ` CmdTest ${Date.now()} `
|
|
107
|
-
const res = await insert_text_after_first_match({ documentId, findText: anchor, insertText: insertSnippet, position: 'after' })
|
|
108
|
-
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
109
|
-
const after = await get_text({ documentId })
|
|
110
|
-
const text = String(after?.text || '')
|
|
111
|
-
expect(text).toContain(anchor)
|
|
112
|
-
expect(text).toContain(insertSnippet)
|
|
113
|
-
}, 60000)
|
|
114
|
-
|
|
115
|
-
it('replace_all_text replaces occurrences', async () => {
|
|
116
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
117
|
-
if (!documentId)
|
|
118
|
-
return expect(true).toBe(true)
|
|
119
|
-
const replace_all_text = buildWriteHandler('replace_all_text')
|
|
120
|
-
const res = await replace_all_text({ documentId, findText: '___unlikely___', replaceText: '___unlikely___', matchCase: true })
|
|
121
|
-
expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
|
|
122
|
-
}, 60000)
|
|
123
|
-
|
|
124
|
-
it('style_first_match applies style to first match', async () => {
|
|
125
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
126
|
-
if (!documentId)
|
|
127
|
-
return expect(true).toBe(true)
|
|
128
|
-
const style_first_match = buildWriteHandler('style_first_match')
|
|
129
|
-
const get_struct = buildReadHandler('get_document_structured')
|
|
130
|
-
const anchor = `ANCHOR_${Date.now()}`
|
|
131
|
-
const appended = buildWriteHandler('append_text')
|
|
132
|
-
const before = await get_struct({ documentId })
|
|
133
|
-
const hasAnchorBefore = JSON.stringify(before?.body || {}).includes(anchor)
|
|
134
|
-
if (!hasAnchorBefore)
|
|
135
|
-
await appended({ documentId, text: `\n${anchor}\n` })
|
|
136
|
-
const res = await style_first_match({ documentId, findText: anchor, textStyle: { bold: true } })
|
|
137
|
-
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
138
|
-
}, 60000)
|
|
139
|
-
|
|
140
|
-
it('insert_table_after_first_match inserts a table near target', async () => {
|
|
141
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
142
|
-
if (!documentId)
|
|
143
|
-
return expect(true).toBe(true)
|
|
144
|
-
const insert_table_after_first_match = buildWriteHandler('insert_table_after_first_match')
|
|
145
|
-
const get_struct = buildReadHandler('get_document_structured')
|
|
146
|
-
const anchor = `ANCHOR_${Date.now()}`
|
|
147
|
-
const appended = buildWriteHandler('append_text')
|
|
148
|
-
const before = await get_struct({ documentId })
|
|
149
|
-
const hasAnchorBefore = JSON.stringify(before?.body || {}).includes(anchor)
|
|
150
|
-
if (!hasAnchorBefore)
|
|
151
|
-
await appended({ documentId, text: `\n${anchor}\n` })
|
|
152
|
-
const res = await insert_table_after_first_match({ documentId, findText: anchor, rows: 1, columns: 1 })
|
|
153
|
-
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
154
|
-
const after = await get_struct({ documentId })
|
|
155
|
-
const hasTable = (after?.body?.content || []).some((el: any) => Boolean(el.table))
|
|
156
|
-
expect(hasTable).toBe(true)
|
|
157
|
-
}, 60000)
|
|
158
|
-
|
|
159
|
-
it('insert_page_break_after_first_match inserts a break near target', async () => {
|
|
160
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
161
|
-
if (!documentId)
|
|
162
|
-
return expect(true).toBe(true)
|
|
163
|
-
const insert_page_break_after_first_match = buildWriteHandler('insert_page_break_after_first_match')
|
|
164
|
-
const get_struct = buildReadHandler('get_document_structured')
|
|
165
|
-
const anchor = `ANCHOR_${Date.now()}`
|
|
166
|
-
const appended = buildWriteHandler('append_text')
|
|
167
|
-
const before = await get_struct({ documentId })
|
|
168
|
-
const hasAnchorBefore = JSON.stringify(before?.body || {}).includes(anchor)
|
|
169
|
-
if (!hasAnchorBefore)
|
|
170
|
-
await appended({ documentId, text: `\n${anchor}\n` })
|
|
171
|
-
const res = await insert_page_break_after_first_match({ documentId, findText: anchor })
|
|
172
|
-
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
173
|
-
const after = await get_struct({ documentId })
|
|
174
|
-
const hasBreak = (after?.body?.content || []).some((el: any) => Boolean(el.sectionBreak))
|
|
175
|
-
expect(hasBreak).toBe(true)
|
|
176
|
-
}, 60000)
|
|
177
|
-
|
|
178
|
-
it('insert_inline_image_after_first_match inserts an image when allowed', async () => {
|
|
179
|
-
if (!process.env.GDOCS_TEST_DOCUMENT_ID || !process.env.GDOCS_TEST_IMAGE_URI)
|
|
180
|
-
return expect(true).toBe(true)
|
|
181
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
182
|
-
const insert_inline_image_after_first_match = buildWriteHandler('insert_inline_image_after_first_match')
|
|
183
|
-
const anchor = `ANCHOR_${Date.now()}`
|
|
184
|
-
const appended = buildWriteHandler('append_text')
|
|
185
|
-
const get_text = buildReadHandler('get_document_text')
|
|
186
|
-
const before = await get_text({ documentId })
|
|
187
|
-
if (!String(before?.text || '').includes(anchor))
|
|
188
|
-
await appended({ documentId, text: `\n${anchor}\n` })
|
|
189
|
-
const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri: process.env.GDOCS_TEST_IMAGE_URI!, altText: 'CmdTest' })
|
|
190
|
-
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
191
|
-
}, 60000)
|
|
192
|
-
|
|
193
|
-
it('delete_first_match deletes a small span (no-op ok)', async () => {
|
|
194
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
195
|
-
if (!documentId)
|
|
196
|
-
return expect(true).toBe(true)
|
|
197
|
-
const delete_first_match = buildWriteHandler('delete_first_match')
|
|
198
|
-
const get_text = buildReadHandler('get_document_text')
|
|
199
|
-
const anchor = `ANCHOR_${Date.now()}`
|
|
200
|
-
const appended = buildWriteHandler('append_text')
|
|
201
|
-
const before = await get_text({ documentId })
|
|
202
|
-
if (!String(before?.text || '').includes(anchor))
|
|
203
|
-
await appended({ documentId, text: `\n${anchor}\n` })
|
|
204
|
-
const res = await delete_first_match({ documentId, findText: anchor })
|
|
205
|
-
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
206
|
-
const after = await get_text({ documentId })
|
|
207
|
-
expect(String(after?.text || '')).not.toContain(anchor)
|
|
208
|
-
}, 60000)
|
|
209
|
-
|
|
210
|
-
it('update_paragraph_style_for_first_match updates paragraph style near target', async () => {
|
|
211
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
212
|
-
if (!documentId)
|
|
213
|
-
return expect(true).toBe(true)
|
|
214
|
-
const update_paragraph_style_for_first_match = buildWriteHandler('update_paragraph_style_for_first_match')
|
|
215
|
-
const get_struct = buildReadHandler('get_document_structured')
|
|
216
|
-
const anchor = `ANCHOR_${Date.now()}`
|
|
217
|
-
const appended = buildWriteHandler('append_text')
|
|
218
|
-
const before = await get_struct({ documentId })
|
|
219
|
-
const hasAnchorBefore = JSON.stringify(before?.body || {}).includes(anchor)
|
|
220
|
-
if (!hasAnchorBefore)
|
|
221
|
-
await appended({ documentId, text: `\n${anchor}\n` })
|
|
222
|
-
const res = await update_paragraph_style_for_first_match({ documentId, findText: anchor, paragraphStyle: { alignment: 'CENTER' } })
|
|
223
|
-
expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
|
|
224
|
-
const after = await get_struct({ documentId })
|
|
225
|
-
// find the paragraph containing anchor and verify alignment
|
|
226
|
-
let foundAligned = false
|
|
227
|
-
for (const el of (after?.body?.content || [])) {
|
|
228
|
-
if (!el.paragraph)
|
|
229
|
-
continue
|
|
230
|
-
const p = el.paragraph
|
|
231
|
-
const text = (p.elements || []).map((e: any) => e?.textRun?.content || '').join('')
|
|
232
|
-
if (text.includes(anchor)) {
|
|
233
|
-
if (p.paragraphStyle?.alignment === 'CENTER')
|
|
234
|
-
foundAligned = true
|
|
235
|
-
break
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
expect(foundAligned).toBe(true)
|
|
239
|
-
}, 60000)
|
|
240
|
-
|
|
241
|
-
it('update_document_style updates doc style with no-op', async () => {
|
|
242
|
-
const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
|
|
243
|
-
if (!documentId)
|
|
244
|
-
return expect(true).toBe(true)
|
|
245
|
-
const update_document_style = buildWriteHandler('update_document_style')
|
|
246
|
-
const res = await update_document_style({ documentId, documentStyle: { useFirstPageHeaderFooter: false } })
|
|
247
|
-
expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
|
|
248
|
-
}, 60000)
|
|
251
|
+
}
|
|
249
252
|
})
|
|
@@ -1,36 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
|
|
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 documents + drive.",
|
|
22
|
+
"items": { "type": "string" }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": ["serviceAccountJson"],
|
|
26
|
+
"additionalProperties": false
|
|
9
27
|
},
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
"injection": {
|
|
29
|
+
"headers": {
|
|
30
|
+
"Authorization": "Bearer {{token}}"
|
|
31
|
+
}
|
|
14
32
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 documents and drive scopes."
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"required": ["token"],
|
|
47
|
+
"additionalProperties": false
|
|
19
48
|
},
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
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 scopes: `https://www.googleapis.com/auth/documents` and `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 Docs API** and **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 your target Google Docs documents with the service account's `client_email`
|
|
9
|
+
|
|
10
|
+
For Google Workspace users: if you need to access user documents, configure domain-wide delegation and set the `subject` field to the user's email.
|
|
@@ -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
|
+
}
|