@commandable/integration-data 0.0.4 → 0.0.6

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 (187) hide show
  1. package/dist/credentials-index.d.ts.map +1 -1
  2. package/dist/credentials-index.js +64 -0
  3. package/dist/credentials-index.js.map +1 -1
  4. package/integrations/airtable/__tests__/get_handlers.test.ts +25 -16
  5. package/integrations/github/__tests__/get_handlers.test.ts +202 -7
  6. package/integrations/github/__tests__/write_handlers.test.ts +178 -24
  7. package/integrations/github/handlers/create_commit.js +5 -12
  8. package/integrations/github/handlers/create_pull_request_review.js +10 -0
  9. package/integrations/github/handlers/create_release.js +14 -0
  10. package/integrations/github/handlers/delete_branch.js +8 -0
  11. package/integrations/github/handlers/delete_file.js +9 -0
  12. package/integrations/github/handlers/fork_repo.js +10 -0
  13. package/integrations/github/handlers/get_commit.js +8 -0
  14. package/integrations/github/handlers/get_file_contents.js +21 -0
  15. package/integrations/github/handlers/get_job_logs.js +6 -0
  16. package/integrations/github/handlers/get_latest_release.js +4 -0
  17. package/integrations/github/handlers/get_me.js +4 -0
  18. package/integrations/github/handlers/get_pull_request.js +4 -0
  19. package/integrations/github/handlers/get_pull_request_diff.js +8 -0
  20. package/integrations/github/handlers/get_repo_tree.js +12 -0
  21. package/integrations/github/handlers/get_workflow_run.js +4 -0
  22. package/integrations/github/handlers/list_branches.js +6 -1
  23. package/integrations/github/handlers/list_commits.js +5 -6
  24. package/integrations/github/handlers/list_issue_comments.js +8 -0
  25. package/integrations/github/handlers/list_issues.js +5 -6
  26. package/integrations/github/handlers/list_labels.js +8 -0
  27. package/integrations/github/handlers/list_pull_request_comments.js +8 -0
  28. package/integrations/github/handlers/list_pull_request_files.js +8 -0
  29. package/integrations/github/handlers/list_pull_requests.js +7 -2
  30. package/integrations/github/handlers/list_releases.js +8 -0
  31. package/integrations/github/handlers/list_tags.js +8 -0
  32. package/integrations/github/handlers/list_workflow_runs.js +11 -0
  33. package/integrations/github/handlers/request_pull_request_reviewers.js +10 -0
  34. package/integrations/github/handlers/search_code.js +8 -0
  35. package/integrations/github/handlers/search_issues.js +8 -0
  36. package/integrations/github/handlers/search_pull_requests.js +8 -0
  37. package/integrations/github/handlers/search_repos.js +10 -0
  38. package/integrations/github/handlers/update_pull_request.js +13 -0
  39. package/integrations/github/manifest.json +58 -20
  40. package/integrations/github/schemas/create_pull_request_review.json +17 -0
  41. package/integrations/github/schemas/create_release.json +16 -0
  42. package/integrations/github/schemas/delete_branch.json +10 -0
  43. package/integrations/github/schemas/delete_file.json +13 -0
  44. package/integrations/github/schemas/fork_repo.json +11 -0
  45. package/integrations/github/schemas/get_commit.json +12 -0
  46. package/integrations/github/schemas/get_file_contents.json +11 -0
  47. package/integrations/github/schemas/get_job_logs.json +10 -0
  48. package/integrations/github/schemas/get_pull_request.json +10 -0
  49. package/integrations/github/schemas/get_pull_request_diff.json +10 -0
  50. package/integrations/github/schemas/get_repo_tree.json +12 -0
  51. package/integrations/github/schemas/get_workflow_run.json +10 -0
  52. package/integrations/github/schemas/list_branches.json +12 -0
  53. package/integrations/github/schemas/list_commits.json +5 -3
  54. package/integrations/github/schemas/list_issue_comments.json +12 -0
  55. package/integrations/github/schemas/list_issues.json +4 -2
  56. package/integrations/github/schemas/list_labels.json +11 -0
  57. package/integrations/github/schemas/list_pull_request_comments.json +12 -0
  58. package/integrations/github/schemas/list_pull_request_files.json +12 -0
  59. package/integrations/github/schemas/list_pull_requests.json +7 -1
  60. package/integrations/github/schemas/list_releases.json +11 -0
  61. package/integrations/github/schemas/list_tags.json +11 -0
  62. package/integrations/github/schemas/list_workflow_runs.json +18 -0
  63. package/integrations/github/schemas/request_pull_request_reviewers.json +20 -0
  64. package/integrations/github/schemas/search_code.json +10 -0
  65. package/integrations/github/schemas/search_issues.json +10 -0
  66. package/integrations/github/schemas/search_pull_requests.json +10 -0
  67. package/integrations/github/schemas/search_repos.json +12 -0
  68. package/integrations/github/schemas/update_pull_request.json +15 -0
  69. package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +0 -13
  70. package/integrations/google-calendar/handlers/get_event.js +5 -1
  71. package/integrations/google-calendar/handlers/list_events.js +2 -0
  72. package/integrations/google-calendar/manifest.json +17 -18
  73. package/integrations/google-calendar/prompt.md +68 -0
  74. package/integrations/google-calendar/schemas/id_calendar_event.json +4 -2
  75. package/integrations/google-calendar/schemas/list_events.json +10 -8
  76. package/integrations/google-docs/__tests__/get_handlers.test.ts +5 -20
  77. package/integrations/google-docs/__tests__/write_handlers.test.ts +38 -52
  78. package/integrations/google-docs/handlers/insert_inline_image_after_first_match.js +1 -1
  79. package/integrations/google-docs/handlers/read_document.js +189 -0
  80. package/integrations/google-docs/manifest.json +16 -31
  81. package/integrations/google-docs/prompt.md +49 -0
  82. package/integrations/google-docs/schemas/insert_inline_image_after_first_match.json +0 -1
  83. package/integrations/google-docs/schemas/{get_document_text.json → read_document.json} +5 -2
  84. package/integrations/google-docs/todo.md +18 -0
  85. package/integrations/google-drive/__tests__/handlers.test.ts +145 -0
  86. package/integrations/google-drive/__tests__/usage_parity.test.ts +9 -0
  87. package/integrations/google-drive/handlers/get_file.js +2 -4
  88. package/integrations/google-drive/handlers/get_file_content.js +41 -0
  89. package/integrations/google-drive/handlers/list_files.js +15 -0
  90. package/integrations/google-drive/handlers/search_files.js +20 -0
  91. package/integrations/google-drive/handlers/share_file.js +20 -0
  92. package/integrations/google-drive/manifest.json +37 -10
  93. package/integrations/google-drive/prompt.md +59 -0
  94. package/integrations/google-drive/schemas/get_file.json +2 -2
  95. package/integrations/google-drive/schemas/get_file_content.json +11 -0
  96. package/integrations/google-drive/schemas/list_files.json +12 -0
  97. package/integrations/google-drive/schemas/search_files.json +14 -0
  98. package/integrations/google-drive/schemas/share_file.json +23 -0
  99. package/integrations/google-gmail/__tests__/get_handlers.test.ts +134 -0
  100. package/integrations/google-gmail/__tests__/usage_parity.test.ts +9 -0
  101. package/integrations/google-gmail/__tests__/write_and_admin_handlers.test.ts +211 -0
  102. package/integrations/google-gmail/credentials.json +57 -0
  103. package/integrations/google-gmail/credentials_hint_oauth_token.md +8 -0
  104. package/integrations/google-gmail/credentials_hint_service_account.md +10 -0
  105. package/integrations/google-gmail/handlers/create_draft_email.js +27 -0
  106. package/integrations/google-gmail/handlers/create_label.js +12 -0
  107. package/integrations/google-gmail/handlers/delete_draft.js +13 -0
  108. package/integrations/google-gmail/handlers/delete_label.js +13 -0
  109. package/integrations/google-gmail/handlers/delete_message.js +13 -0
  110. package/integrations/google-gmail/handlers/delete_thread.js +13 -0
  111. package/integrations/google-gmail/handlers/get_draft.js +6 -0
  112. package/integrations/google-gmail/handlers/get_label.js +6 -0
  113. package/integrations/google-gmail/handlers/get_message.js +14 -0
  114. package/integrations/google-gmail/handlers/get_profile.js +5 -0
  115. package/integrations/google-gmail/handlers/get_thread.js +14 -0
  116. package/integrations/google-gmail/handlers/list_drafts.js +15 -0
  117. package/integrations/google-gmail/handlers/list_labels.js +5 -0
  118. package/integrations/google-gmail/handlers/list_messages.js +19 -0
  119. package/integrations/google-gmail/handlers/list_threads.js +19 -0
  120. package/integrations/google-gmail/handlers/modify_message.js +11 -0
  121. package/integrations/google-gmail/handlers/modify_thread.js +11 -0
  122. package/integrations/google-gmail/handlers/read_email.js +56 -0
  123. package/integrations/google-gmail/handlers/send_draft.js +15 -0
  124. package/integrations/google-gmail/handlers/send_email.js +22 -0
  125. package/integrations/google-gmail/handlers/trash_message.js +6 -0
  126. package/integrations/google-gmail/handlers/trash_thread.js +6 -0
  127. package/integrations/google-gmail/handlers/untrash_message.js +6 -0
  128. package/integrations/google-gmail/handlers/untrash_thread.js +6 -0
  129. package/integrations/google-gmail/handlers/update_label.js +15 -0
  130. package/integrations/google-gmail/manifest.json +33 -0
  131. package/integrations/google-gmail/prompt.md +52 -0
  132. package/integrations/google-gmail/schemas/create_draft_email.json +16 -0
  133. package/integrations/google-gmail/schemas/create_label.json +26 -0
  134. package/integrations/google-gmail/schemas/get_message.json +20 -0
  135. package/integrations/{google-docs/schemas/get_document_structured.json → google-gmail/schemas/get_profile.json} +4 -2
  136. package/integrations/google-gmail/schemas/get_thread.json +20 -0
  137. package/integrations/google-gmail/schemas/id_draft.json +16 -0
  138. package/integrations/google-gmail/schemas/id_label.json +16 -0
  139. package/integrations/google-gmail/schemas/id_message.json +16 -0
  140. package/integrations/google-gmail/schemas/id_thread.json +16 -0
  141. package/integrations/google-gmail/schemas/list_drafts.json +30 -0
  142. package/integrations/{google-sheet/schemas/get_developer_metadata.json → google-gmail/schemas/list_labels.json} +4 -3
  143. package/integrations/google-gmail/schemas/list_messages.json +35 -0
  144. package/integrations/google-gmail/schemas/list_threads.json +35 -0
  145. package/integrations/google-gmail/schemas/modify_message.json +24 -0
  146. package/integrations/google-gmail/schemas/modify_thread.json +24 -0
  147. package/integrations/google-gmail/schemas/read_email.json +10 -0
  148. package/integrations/google-gmail/schemas/send_draft.json +29 -0
  149. package/integrations/google-gmail/schemas/send_email.json +17 -0
  150. package/integrations/google-gmail/schemas/update_label.json +33 -0
  151. package/integrations/google-sheet/__tests__/get_handlers.test.ts +7 -52
  152. package/integrations/google-sheet/__tests__/write_handlers.test.ts +1 -20
  153. package/integrations/google-sheet/handlers/get_spreadsheet.js +2 -0
  154. package/integrations/google-sheet/handlers/read_sheet.js +75 -0
  155. package/integrations/google-sheet/manifest.json +13 -62
  156. package/integrations/google-sheet/prompt.md +49 -0
  157. package/integrations/google-sheet/schemas/get_spreadsheet.json +5 -4
  158. package/integrations/google-sheet/schemas/read_sheet.json +21 -0
  159. package/integrations/google-slides/__tests__/get_handlers.test.ts +13 -9
  160. package/integrations/google-slides/__tests__/write_handlers.test.ts +4 -5
  161. package/integrations/google-slides/handlers/read_presentation.js +51 -0
  162. package/integrations/google-slides/manifest.json +13 -13
  163. package/integrations/google-slides/prompt.md +56 -0
  164. package/integrations/new_integration_prompt.md +5 -1
  165. package/package.json +1 -1
  166. package/integrations/google-calendar/handlers/update_event.js +0 -5
  167. package/integrations/google-calendar/schemas/update_event.json +0 -10
  168. package/integrations/google-docs/handlers/get_document.js +0 -12
  169. package/integrations/google-docs/handlers/get_document_structured.js +0 -6
  170. package/integrations/google-docs/handlers/get_document_text.js +0 -17
  171. package/integrations/google-docs/schemas/get_document.json +0 -11
  172. package/integrations/google-sheet/handlers/batch_clear_values_by_data_filter.js +0 -6
  173. package/integrations/google-sheet/handlers/batch_get_values.js +0 -16
  174. package/integrations/google-sheet/handlers/batch_update_values_by_data_filter.js +0 -16
  175. package/integrations/google-sheet/handlers/get_developer_metadata.js +0 -6
  176. package/integrations/google-sheet/handlers/get_spreadsheet_by_data_filter.js +0 -10
  177. package/integrations/google-sheet/handlers/get_values.js +0 -14
  178. package/integrations/google-sheet/handlers/get_values_by_data_filter.js +0 -14
  179. package/integrations/google-sheet/handlers/search_developer_metadata.js +0 -7
  180. package/integrations/google-sheet/schemas/batch_clear_values_by_data_filter.json +0 -10
  181. package/integrations/google-sheet/schemas/batch_get_values.json +0 -13
  182. package/integrations/google-sheet/schemas/batch_update_values_by_data_filter.json +0 -25
  183. package/integrations/google-sheet/schemas/get_spreadsheet_by_data_filter.json +0 -11
  184. package/integrations/google-sheet/schemas/get_values.json +0 -13
  185. package/integrations/google-sheet/schemas/get_values_by_data_filter.json +0 -17
  186. package/integrations/google-sheet/schemas/search_developer_metadata.json +0 -14
  187. package/integrations/google-slides/handlers/get_presentation.js +0 -6
@@ -24,6 +24,27 @@ const variants: VariantConfig[] = [
24
24
  const hasWriteEnv = hasEnv('GITHUB_TEST_OWNER', 'GITHUB_TEST_REPO')
25
25
  const suiteOrSkip = (variants.length > 0 && hasWriteEnv) ? describe : describe.skip
26
26
 
27
+ async function withRetry<T>(
28
+ fn: () => Promise<T>,
29
+ { maxAttempts = 3, delayMs = 2000, retryIf = (_e: unknown): boolean => true } = {},
30
+ ): Promise<T> {
31
+ let lastError: unknown
32
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
33
+ try {
34
+ return await fn()
35
+ }
36
+ catch (error) {
37
+ lastError = error
38
+ if (attempt < maxAttempts && retryIf(error)) {
39
+ await new Promise(resolve => setTimeout(resolve, delayMs * attempt))
40
+ continue
41
+ }
42
+ break
43
+ }
44
+ }
45
+ throw lastError
46
+ }
47
+
27
48
  suiteOrSkip('github write handlers (live)', () => {
28
49
  for (const variant of variants) {
29
50
  describe(`variant: ${variant.key}`, () => {
@@ -40,7 +61,7 @@ suiteOrSkip('github write handlers (live)', () => {
40
61
  toolbox = createToolbox('github', proxy, node, variant.key)
41
62
  }, 30000)
42
63
 
43
- it('create_issue -> update_issue -> comment_on_issue -> close_issue roundtrip', async () => {
64
+ it('create_issue -> update_issue -> comment_on_issue -> list_issue_comments -> close_issue roundtrip', async () => {
44
65
  if (!ctx.owner || !ctx.repo)
45
66
  return expect(true).toBe(true)
46
67
 
@@ -59,18 +80,60 @@ suiteOrSkip('github write handlers (live)', () => {
59
80
  const comment = await comment_on_issue({ owner: ctx.owner, repo: ctx.repo, issue_number, body: 'A comment from test.' })
60
81
  expect(comment?.id).toBeTruthy()
61
82
 
83
+ const list_issue_comments = toolbox.read('list_issue_comments')
84
+ const comments = await list_issue_comments({ owner: ctx.owner, repo: ctx.repo, issue_number })
85
+ expect(Array.isArray(comments)).toBe(true)
86
+ expect(comments.length).toBeGreaterThan(0)
87
+
62
88
  const close_issue = toolbox.write('close_issue')
63
89
  const closed = await close_issue({ owner: ctx.owner, repo: ctx.repo, issue_number })
64
90
  expect(closed?.state).toBe('closed')
65
91
  }, 90000)
66
92
 
93
+ it('fork_repo forks a public repo (best effort)', async () => {
94
+ if (!ctx.owner || !ctx.repo)
95
+ return expect(true).toBe(true)
96
+ const fork_repo = toolbox.write('fork_repo')
97
+ try {
98
+ const result = await fork_repo({ owner: ctx.owner, repo: ctx.repo })
99
+ // Fork returns the forked repo details
100
+ expect(result).toBeTruthy()
101
+ }
102
+ catch {
103
+ // May fail if repo is private or fork already exists -- that's ok
104
+ expect(true).toBe(true)
105
+ }
106
+ }, 30000)
107
+
108
+ it('create_release creates a draft release (classic_pat only)', async () => {
109
+ if (!toolbox.hasTool('write', 'create_repo'))
110
+ return expect(true).toBe(true)
111
+ if (!ctx.owner || !ctx.repo)
112
+ return expect(true).toBe(true)
113
+ const create_release = toolbox.write('create_release')
114
+ const tagName = `v0.0.0-test-${Date.now()}`
115
+ try {
116
+ const result = await create_release({
117
+ owner: ctx.owner,
118
+ repo: ctx.repo,
119
+ tag_name: tagName,
120
+ name: `Test Release ${tagName}`,
121
+ body: 'Draft release created by integration tests.',
122
+ draft: true,
123
+ })
124
+ expect(result?.tag_name).toBe(tagName)
125
+ expect(result?.draft).toBe(true)
126
+ }
127
+ catch {
128
+ // May fail if insufficient permissions -- that's ok
129
+ expect(true).toBe(true)
130
+ }
131
+ }, 30000)
132
+
67
133
  it('create_repo -> delete_repo lifecycle (classic_pat only)', async () => {
68
134
  if (!toolbox.hasTool('write', 'create_repo')) {
69
- // This tool is not available for fine_grained_pat -- expected.
70
135
  return expect(true).toBe(true)
71
136
  }
72
- if (!ctx.owner)
73
- return expect(true).toBe(true)
74
137
 
75
138
  const repoName = `cmdtest-repo-${Date.now()}`
76
139
 
@@ -82,10 +145,15 @@ suiteOrSkip('github write handlers (live)', () => {
82
145
  auto_init: true,
83
146
  })
84
147
  expect(created?.name).toBe(repoName)
85
- expect(created?.full_name).toBe(`${ctx.owner}/${repoName}`)
148
+
149
+ const createdOwner = created?.owner?.login
150
+ expect(createdOwner).toBeTruthy()
151
+ expect(created?.full_name).toBe(`${createdOwner}/${repoName}`)
152
+
153
+ await new Promise(resolve => setTimeout(resolve, 3000))
86
154
 
87
155
  const delete_repo = toolbox.write('delete_repo')
88
- const deleted = await delete_repo({ owner: ctx.owner, repo: repoName })
156
+ const deleted = await delete_repo({ owner: createdOwner, repo: repoName })
89
157
  expect(deleted?.success).toBe(true)
90
158
  expect(deleted?.status).toBe(204)
91
159
  }, 90000)
@@ -113,17 +181,34 @@ suiteOrSkip('github write handlers (live)', () => {
113
181
  expect(file?.commit?.message).toBe(`Add single test file ${timestamp}`)
114
182
  expect(file?.content?.path).toBe(`test-single-${timestamp}.txt`)
115
183
 
116
- const updated = await create_or_update_file({
184
+ // get_file_contents and delete_file roundtrip
185
+ const get_file_contents = toolbox.read('get_file_contents')
186
+ const contents = await get_file_contents({
117
187
  owner: ctx.owner,
118
188
  repo: ctx.repo,
119
189
  path: `test-single-${timestamp}.txt`,
120
- message: `Update test file ${timestamp}`,
121
- content: `Updated content at ${timestamp}`,
190
+ ref: branchName,
191
+ })
192
+ expect(contents?.encoding).toBe('utf-8')
193
+ expect(contents?.content).toContain('Hello')
194
+ const fileSha = contents?.sha
195
+
196
+ const delete_file = toolbox.write('delete_file')
197
+ const deleted = await delete_file({
198
+ owner: ctx.owner,
199
+ repo: ctx.repo,
200
+ path: `test-single-${timestamp}.txt`,
201
+ message: `Delete test file ${timestamp}`,
202
+ sha: fileSha,
122
203
  branch: branchName,
123
- sha: file.content.sha,
124
204
  })
125
- expect(updated?.commit?.message).toBe(`Update test file ${timestamp}`)
126
- }, 90000)
205
+ expect(deleted?.commit?.message).toBe(`Delete test file ${timestamp}`)
206
+
207
+ // delete_branch cleanup
208
+ const delete_branch = toolbox.write('delete_branch')
209
+ const deletedBranch = await delete_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
210
+ expect(deletedBranch?.success).toBe(true)
211
+ }, 120000)
127
212
 
128
213
  it('create_commit: multiple files in one commit', async () => {
129
214
  if (!ctx.owner || !ctx.repo)
@@ -152,22 +237,36 @@ suiteOrSkip('github write handlers (live)', () => {
152
237
  expect(commit?.commit?.message).toBe(`Add multiple files ${timestamp}`)
153
238
  expect(commit?.files?.length).toBe(3)
154
239
 
155
- const commit2 = await create_commit({
156
- owner: ctx.owner,
157
- repo: ctx.repo,
158
- branch: branchName,
159
- message: `Update and delete files ${timestamp}`,
160
- files: [
161
- { path: `multi-test/file1-${timestamp}.txt`, content: 'Updated content of file 1' },
162
- { path: `multi-test/file2-${timestamp}.txt` },
163
- { path: `multi-test/file4-${timestamp}.txt`, content: 'New file 4' },
164
- ],
165
- })
240
+ // Retry second commit: GitHub's backend sometimes needs a moment to settle
241
+ // after the first commit before accepting a follow-up on the same branch.
242
+ const commit2 = await withRetry(
243
+ () => create_commit({
244
+ owner: ctx.owner,
245
+ repo: ctx.repo,
246
+ branch: branchName,
247
+ message: `Update and delete files ${timestamp}`,
248
+ files: [
249
+ { path: `multi-test/file1-${timestamp}.txt`, content: 'Updated content of file 1' },
250
+ { path: `multi-test/file2-${timestamp}.txt` },
251
+ { path: `multi-test/file4-${timestamp}.txt`, content: 'New file 4' },
252
+ ],
253
+ }),
254
+ {
255
+ maxAttempts: 3,
256
+ delayMs: 2000,
257
+ retryIf: (e: unknown) => String((e as { message?: string })?.message || '').includes('GitRPC::BadObjectState'),
258
+ },
259
+ )
166
260
  expect(commit2?.commit?.sha).toBeTruthy()
167
261
  expect(commit2?.commit?.message).toBe(`Update and delete files ${timestamp}`)
262
+
263
+ // get_commit verifies the commit details
264
+ const get_commit = toolbox.read('get_commit')
265
+ const commitDetails = await get_commit({ owner: ctx.owner, repo: ctx.repo, sha: commit2.commit.sha })
266
+ expect(commitDetails?.sha).toBe(commit2.commit.sha)
168
267
  }, 120000)
169
268
 
170
- it('full PR workflow: create_branch -> create_commit -> create_pull_request -> merge_pull_request', async () => {
269
+ it('full PR workflow: create_branch -> create_commit -> create_pull_request -> update_pull_request -> create_pull_request_review -> merge_pull_request -> delete_branch', async () => {
171
270
  if (!ctx.owner || !ctx.repo)
172
271
  return expect(true).toBe(true)
173
272
 
@@ -207,6 +306,27 @@ suiteOrSkip('github write handlers (live)', () => {
207
306
  expect(pr?.number).toBeTruthy()
208
307
  const prNumber = pr.number
209
308
 
309
+ // update_pull_request
310
+ const update_pull_request = toolbox.write('update_pull_request')
311
+ const updated = await update_pull_request({
312
+ owner: ctx.owner,
313
+ repo: ctx.repo,
314
+ pull_number: prNumber,
315
+ body: 'Updated description by integration test.',
316
+ })
317
+ expect(updated?.number).toBe(prNumber)
318
+
319
+ // get_pull_request verifies state
320
+ const get_pull_request = toolbox.read('get_pull_request')
321
+ const prDetails = await get_pull_request({ owner: ctx.owner, repo: ctx.repo, pull_number: prNumber })
322
+ expect(prDetails?.number).toBe(prNumber)
323
+
324
+ // list_pull_request_files
325
+ const list_pull_request_files = toolbox.read('list_pull_request_files')
326
+ const files = await list_pull_request_files({ owner: ctx.owner, repo: ctx.repo, pull_number: prNumber })
327
+ expect(Array.isArray(files)).toBe(true)
328
+
329
+ // add_labels_to_issue (labels on PR)
210
330
  const add_labels_to_issue = toolbox.write('add_labels_to_issue')
211
331
  try {
212
332
  await add_labels_to_issue({ owner: ctx.owner, repo: ctx.repo, issue_number: prNumber, labels: ['test'] })
@@ -215,9 +335,43 @@ suiteOrSkip('github write handlers (live)', () => {
215
335
  // Label might not exist -- that's ok for this test
216
336
  }
217
337
 
338
+ // request_pull_request_reviewers (may fail if requesting from self)
339
+ const request_pull_request_reviewers = toolbox.write('request_pull_request_reviewers')
340
+ try {
341
+ await request_pull_request_reviewers({
342
+ owner: ctx.owner,
343
+ repo: ctx.repo,
344
+ pull_number: prNumber,
345
+ reviewers: [],
346
+ })
347
+ }
348
+ catch {
349
+ // May fail if requesting from self or insufficient permissions -- that's ok
350
+ }
351
+
352
+ // create_pull_request_review (comment only -- can't APPROVE own PRs typically)
353
+ const create_pull_request_review = toolbox.write('create_pull_request_review')
354
+ try {
355
+ await create_pull_request_review({
356
+ owner: ctx.owner,
357
+ repo: ctx.repo,
358
+ pull_number: prNumber,
359
+ event: 'COMMENT',
360
+ body: 'LGTM from integration test',
361
+ })
362
+ }
363
+ catch {
364
+ // May fail if author is same as reviewer in some repo configs -- that's ok
365
+ }
366
+
218
367
  const merge_pull_request = toolbox.write('merge_pull_request')
219
368
  const merged = await merge_pull_request({ owner: ctx.owner, repo: ctx.repo, pull_number: prNumber, merge_method: 'squash' })
220
369
  expect(merged?.merged).toBe(true)
370
+
371
+ // delete_branch after merge
372
+ const delete_branch = toolbox.write('delete_branch')
373
+ const deletedBranch = await delete_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
374
+ expect(deletedBranch?.success).toBe(true)
221
375
  }, 150000)
222
376
  })
223
377
  }
@@ -11,24 +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.
15
+ // Always create blobs explicitly rather than using inline tree content, because mixing inline
16
+ // content with sha:null deletion entries in the same tree request causes GitRPC::BadObjectState.
15
17
  const tree = []
16
18
  for (const file of files) {
17
19
  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
20
  const blobRes = await integration.fetch(`/repos/${owner}/${repo}/git/blobs`, {
24
21
  method: 'POST',
25
- body: {
26
- content: contentBase64,
27
- encoding: 'base64',
28
- },
22
+ body: { content: file.content, encoding: 'utf-8' },
29
23
  })
30
24
  const blobData = await blobRes.json()
31
-
32
25
  tree.push({
33
26
  path: file.path,
34
27
  mode: file.mode || '100644',
@@ -36,7 +29,7 @@ async (input) => {
36
29
  sha: blobData.sha,
37
30
  })
38
31
  } else {
39
- // File deletion (null sha means delete)
32
+ // sha: null removes the file from the tree
40
33
  tree.push({
41
34
  path: file.path,
42
35
  mode: '100644',
@@ -0,0 +1,10 @@
1
+ async (input) => {
2
+ const body = { event: input.event }
3
+ if (input.body !== undefined) body.body = input.body
4
+ if (input.commit_id !== undefined) body.commit_id = input.commit_id
5
+ const res = await integration.fetch(
6
+ `/repos/${input.owner}/${input.repo}/pulls/${input.pull_number}/reviews`,
7
+ { method: 'POST', body }
8
+ )
9
+ return await res.json()
10
+ }
@@ -0,0 +1,14 @@
1
+ async (input) => {
2
+ const body = { tag_name: input.tag_name }
3
+ if (input.name !== undefined) body.name = input.name
4
+ if (input.body !== undefined) body.body = input.body
5
+ if (input.draft !== undefined) body.draft = input.draft
6
+ if (input.prerelease !== undefined) body.prerelease = input.prerelease
7
+ if (input.target_commitish !== undefined) body.target_commitish = input.target_commitish
8
+ if (input.generate_release_notes !== undefined) body.generate_release_notes = input.generate_release_notes
9
+ const res = await integration.fetch(
10
+ `/repos/${input.owner}/${input.repo}/releases`,
11
+ { method: 'POST', body }
12
+ )
13
+ return await res.json()
14
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const res = await integration.fetch(
3
+ `/repos/${input.owner}/${input.repo}/git/refs/heads/${input.branch}`,
4
+ { method: 'DELETE' }
5
+ )
6
+ if (res.status === 204) return { success: true, branch: input.branch }
7
+ return await res.json()
8
+ }
@@ -0,0 +1,9 @@
1
+ async (input) => {
2
+ const body = { message: input.message, sha: input.sha }
3
+ if (input.branch) body.branch = input.branch
4
+ const res = await integration.fetch(
5
+ `/repos/${input.owner}/${input.repo}/contents/${input.path}`,
6
+ { method: 'DELETE', body }
7
+ )
8
+ return await res.json()
9
+ }
@@ -0,0 +1,10 @@
1
+ async (input) => {
2
+ const body = {}
3
+ if (input.organization) body.organization = input.organization
4
+ if (input.name) body.name = input.name
5
+ const res = await integration.fetch(
6
+ `/repos/${input.owner}/${input.repo}/forks`,
7
+ { method: 'POST', body }
8
+ )
9
+ return await res.json()
10
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.page) params.set('page', String(input.page))
4
+ if (input.per_page) params.set('per_page', String(input.per_page))
5
+ const query = params.toString() ? `?${params.toString()}` : ''
6
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/commits/${input.sha}${query}`)
7
+ return await res.json()
8
+ }
@@ -0,0 +1,21 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.ref) params.set('ref', input.ref)
4
+ const query = params.toString() ? `?${params.toString()}` : ''
5
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/contents/${input.path}${query}`)
6
+ const data = await res.json()
7
+ if (data && data.content && data.encoding === 'base64') {
8
+ try {
9
+ const b64 = data.content.replace(/\n/g, '')
10
+ // Decode base64 → binary string → percent-encode each byte → UTF-8 decode
11
+ data.content = decodeURIComponent(
12
+ atob(b64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
13
+ )
14
+ data.encoding = 'utf-8'
15
+ }
16
+ catch (e) {
17
+ // Binary file — leave content as base64
18
+ }
19
+ }
20
+ return data
21
+ }
@@ -0,0 +1,6 @@
1
+ async (input) => {
2
+ // GitHub returns a redirect to the actual log blob URL; fetch follows it automatically
3
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/actions/jobs/${input.job_id}/logs`)
4
+ const logs = await res.text()
5
+ return { logs }
6
+ }
@@ -0,0 +1,4 @@
1
+ async (input) => {
2
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/releases/latest`)
3
+ return await res.json()
4
+ }
@@ -0,0 +1,4 @@
1
+ async (input) => {
2
+ const res = await integration.fetch('/user')
3
+ return await res.json()
4
+ }
@@ -0,0 +1,4 @@
1
+ async (input) => {
2
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/pulls/${input.pull_number}`)
3
+ return await res.json()
4
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const res = await integration.fetch(
3
+ `/repos/${input.owner}/${input.repo}/pulls/${input.pull_number}`,
4
+ { headers: { 'Accept': 'application/vnd.github.diff' } }
5
+ )
6
+ const diff = await res.text()
7
+ return { diff }
8
+ }
@@ -0,0 +1,12 @@
1
+ async (input) => {
2
+ const ref = input.ref || 'HEAD'
3
+ const params = new URLSearchParams()
4
+ if (input.recursive !== false) params.set('recursive', '1')
5
+ const query = params.toString() ? `?${params.toString()}` : ''
6
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/git/trees/${ref}${query}`)
7
+ const data = await res.json()
8
+ if (input.path_filter && Array.isArray(data.tree)) {
9
+ data.tree = data.tree.filter((item) => item.path && item.path.startsWith(input.path_filter))
10
+ }
11
+ return data
12
+ }
@@ -0,0 +1,4 @@
1
+ async (input) => {
2
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/actions/runs/${input.run_id}`)
3
+ return await res.json()
4
+ }
@@ -1,4 +1,9 @@
1
1
  async (input) => {
2
- const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/branches`)
2
+ const params = new URLSearchParams()
3
+ if (input.protected !== undefined) params.set('protected', String(input.protected))
4
+ if (input.page) params.set('page', String(input.page))
5
+ if (input.per_page) params.set('per_page', String(input.per_page))
6
+ const query = params.toString() ? `?${params.toString()}` : ''
7
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/branches${query}`)
3
8
  return await res.json()
4
9
  }
@@ -1,11 +1,10 @@
1
1
  async (input) => {
2
2
  const params = new URLSearchParams()
3
- if (input.sha)
4
- params.set('sha', input.sha)
5
- if (typeof input.path === 'string' && input.path.length > 0)
6
- params.set('path', input.path)
7
- if (input.author)
8
- params.set('author', input.author)
3
+ if (input.sha) params.set('sha', input.sha)
4
+ if (typeof input.path === 'string' && input.path.length > 0) params.set('path', input.path)
5
+ if (input.author) params.set('author', input.author)
6
+ if (input.page) params.set('page', String(input.page))
7
+ if (input.per_page) params.set('per_page', String(input.per_page))
9
8
  const query = params.toString() ? `?${params.toString()}` : ''
10
9
  const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/commits${query}`)
11
10
  return await res.json()
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.page) params.set('page', String(input.page))
4
+ if (input.per_page) params.set('per_page', String(input.per_page))
5
+ const query = params.toString() ? `?${params.toString()}` : ''
6
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/issues/${input.issue_number}/comments${query}`)
7
+ return await res.json()
8
+ }
@@ -1,11 +1,10 @@
1
1
  async (input) => {
2
2
  const params = new URLSearchParams()
3
- if (input.state)
4
- params.set('state', input.state)
5
- if (input.labels)
6
- params.set('labels', input.labels)
7
- if (input.assignee)
8
- params.set('assignee', input.assignee)
3
+ if (input.state) params.set('state', input.state)
4
+ if (input.labels) params.set('labels', input.labels)
5
+ if (input.assignee) params.set('assignee', input.assignee)
6
+ if (input.page) params.set('page', String(input.page))
7
+ if (input.per_page) params.set('per_page', String(input.per_page))
9
8
  const query = params.toString() ? `?${params.toString()}` : ''
10
9
  const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/issues${query}`)
11
10
  return await res.json()
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.page) params.set('page', String(input.page))
4
+ if (input.per_page) params.set('per_page', String(input.per_page))
5
+ const query = params.toString() ? `?${params.toString()}` : ''
6
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/labels${query}`)
7
+ return await res.json()
8
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.page) params.set('page', String(input.page))
4
+ if (input.per_page) params.set('per_page', String(input.per_page))
5
+ const query = params.toString() ? `?${params.toString()}` : ''
6
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/pulls/${input.pull_number}/comments${query}`)
7
+ return await res.json()
8
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.page) params.set('page', String(input.page))
4
+ if (input.per_page) params.set('per_page', String(input.per_page))
5
+ const query = params.toString() ? `?${params.toString()}` : ''
6
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/pulls/${input.pull_number}/files${query}`)
7
+ return await res.json()
8
+ }
@@ -1,7 +1,12 @@
1
1
  async (input) => {
2
2
  const params = new URLSearchParams()
3
- if (input.state)
4
- params.set('state', input.state)
3
+ if (input.state) params.set('state', input.state)
4
+ if (input.head) params.set('head', input.head)
5
+ if (input.base) params.set('base', input.base)
6
+ if (input.sort) params.set('sort', input.sort)
7
+ if (input.direction) params.set('direction', input.direction)
8
+ if (input.page) params.set('page', String(input.page))
9
+ if (input.per_page) params.set('per_page', String(input.per_page))
5
10
  const query = params.toString() ? `?${params.toString()}` : ''
6
11
  const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/pulls${query}`)
7
12
  return await res.json()
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.page) params.set('page', String(input.page))
4
+ if (input.per_page) params.set('per_page', String(input.per_page))
5
+ const query = params.toString() ? `?${params.toString()}` : ''
6
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/releases${query}`)
7
+ return await res.json()
8
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.page) params.set('page', String(input.page))
4
+ if (input.per_page) params.set('per_page', String(input.per_page))
5
+ const query = params.toString() ? `?${params.toString()}` : ''
6
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/tags${query}`)
7
+ return await res.json()
8
+ }
@@ -0,0 +1,11 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ if (input.branch) params.set('branch', input.branch)
4
+ if (input.status) params.set('status', input.status)
5
+ if (input.event) params.set('event', input.event)
6
+ if (input.page) params.set('page', String(input.page))
7
+ if (input.per_page) params.set('per_page', String(input.per_page))
8
+ const query = params.toString() ? `?${params.toString()}` : ''
9
+ const res = await integration.fetch(`/repos/${input.owner}/${input.repo}/actions/runs${query}`)
10
+ return await res.json()
11
+ }
@@ -0,0 +1,10 @@
1
+ async (input) => {
2
+ const body = {}
3
+ if (input.reviewers) body.reviewers = input.reviewers
4
+ if (input.team_reviewers) body.team_reviewers = input.team_reviewers
5
+ const res = await integration.fetch(
6
+ `/repos/${input.owner}/${input.repo}/pulls/${input.pull_number}/requested_reviewers`,
7
+ { method: 'POST', body }
8
+ )
9
+ return await res.json()
10
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ params.set('q', input.query)
4
+ if (input.page) params.set('page', String(input.page))
5
+ if (input.per_page) params.set('per_page', String(input.per_page))
6
+ const res = await integration.fetch(`/search/code?${params.toString()}`)
7
+ return await res.json()
8
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ params.set('q', input.query)
4
+ if (input.page) params.set('page', String(input.page))
5
+ if (input.per_page) params.set('per_page', String(input.per_page))
6
+ const res = await integration.fetch(`/search/issues?${params.toString()}`)
7
+ return await res.json()
8
+ }
@@ -0,0 +1,8 @@
1
+ async (input) => {
2
+ const params = new URLSearchParams()
3
+ params.set('q', input.query)
4
+ if (input.page) params.set('page', String(input.page))
5
+ if (input.per_page) params.set('per_page', String(input.per_page))
6
+ const res = await integration.fetch(`/search/issues?${params.toString()}`)
7
+ return await res.json()
8
+ }