terramend 0.2.0

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 (406) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +145 -0
  3. package/dist/agents/claude.d.ts +73 -0
  4. package/dist/agents/claudePretoolGate.d.ts +99 -0
  5. package/dist/agents/gateServer.d.ts +7 -0
  6. package/dist/agents/index.d.ts +6 -0
  7. package/dist/agents/nativeFsDenies.d.ts +28 -0
  8. package/dist/agents/opencode.d.ts +231 -0
  9. package/dist/agents/opencodePlugin.d.ts +85 -0
  10. package/dist/agents/opencodeShared.d.ts +40 -0
  11. package/dist/agents/postRun.d.ts +132 -0
  12. package/dist/agents/reviewer.d.ts +38 -0
  13. package/dist/agents/sessionLabeler.d.ts +97 -0
  14. package/dist/agents/shared.d.ts +189 -0
  15. package/dist/agents/subagentModels.d.ts +19 -0
  16. package/dist/agents/subagentToolGates.d.ts +55 -0
  17. package/dist/cli.mjs +197426 -0
  18. package/dist/external.d.ts +227 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +196783 -0
  21. package/dist/internal/index.d.ts +18 -0
  22. package/dist/internal.js +1714 -0
  23. package/dist/lifecycle.d.ts +2 -0
  24. package/dist/main.d.ts +8 -0
  25. package/dist/mcp/arkConfig.d.ts +1 -0
  26. package/dist/mcp/checkSuite.d.ts +25 -0
  27. package/dist/mcp/checkout.d.ts +77 -0
  28. package/dist/mcp/comment.d.ts +119 -0
  29. package/dist/mcp/commitInfo.d.ts +9 -0
  30. package/dist/mcp/crosswalk.d.ts +105 -0
  31. package/dist/mcp/dependencies.d.ts +8 -0
  32. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  33. package/dist/mcp/git.d.ts +46 -0
  34. package/dist/mcp/guardrails.d.ts +104 -0
  35. package/dist/mcp/issue.d.ts +18 -0
  36. package/dist/mcp/issueComments.d.ts +9 -0
  37. package/dist/mcp/issueEvents.d.ts +9 -0
  38. package/dist/mcp/issueInfo.d.ts +9 -0
  39. package/dist/mcp/labels.d.ts +12 -0
  40. package/dist/mcp/localContext.d.ts +19 -0
  41. package/dist/mcp/moduleExtraction.d.ts +71 -0
  42. package/dist/mcp/moduleTests.d.ts +104 -0
  43. package/dist/mcp/modules.d.ts +179 -0
  44. package/dist/mcp/output.d.ts +12 -0
  45. package/dist/mcp/pathSafety.d.ts +14 -0
  46. package/dist/mcp/policy.d.ts +48 -0
  47. package/dist/mcp/pr.d.ts +49 -0
  48. package/dist/mcp/prInfo.d.ts +9 -0
  49. package/dist/mcp/providerSchema.d.ts +50 -0
  50. package/dist/mcp/review.d.ts +199 -0
  51. package/dist/mcp/reviewComments.d.ts +178 -0
  52. package/dist/mcp/roots.d.ts +58 -0
  53. package/dist/mcp/scope.d.ts +15 -0
  54. package/dist/mcp/selectMode.d.ts +18 -0
  55. package/dist/mcp/server.d.ts +48 -0
  56. package/dist/mcp/shared.d.ts +47 -0
  57. package/dist/mcp/shell.d.ts +37 -0
  58. package/dist/mcp/staleFix.d.ts +51 -0
  59. package/dist/mcp/terraform/cost.d.ts +55 -0
  60. package/dist/mcp/terraform/currency.d.ts +94 -0
  61. package/dist/mcp/terraform/decisions.d.ts +178 -0
  62. package/dist/mcp/terraform/findings.d.ts +75 -0
  63. package/dist/mcp/terraform/plan.d.ts +157 -0
  64. package/dist/mcp/terraform/scanners.d.ts +131 -0
  65. package/dist/mcp/terraform/tools.d.ts +63 -0
  66. package/dist/mcp/terraform/types.d.ts +172 -0
  67. package/dist/mcp/terraform.d.ts +22 -0
  68. package/dist/mcp/terratest.d.ts +83 -0
  69. package/dist/mcp/upload.d.ts +6 -0
  70. package/dist/models.d.ts +171 -0
  71. package/dist/modes.d.ts +26 -0
  72. package/dist/prep/index.d.ts +7 -0
  73. package/dist/prep/installNodeDependencies.d.ts +2 -0
  74. package/dist/prep/installPythonDependencies.d.ts +2 -0
  75. package/dist/prep/types.d.ts +31 -0
  76. package/dist/reviewQuality.d.ts +64 -0
  77. package/dist/skills/terraform-best-practices/SKILL.md +369 -0
  78. package/dist/toolState.d.ts +135 -0
  79. package/dist/utils/activity.d.ts +40 -0
  80. package/dist/utils/agent.d.ts +20 -0
  81. package/dist/utils/agentHangReport.d.ts +38 -0
  82. package/dist/utils/apiFetch.d.ts +19 -0
  83. package/dist/utils/apiKeys.d.ts +41 -0
  84. package/dist/utils/apiUrl.d.ts +20 -0
  85. package/dist/utils/assets.d.ts +8 -0
  86. package/dist/utils/billingErrors.d.ts +85 -0
  87. package/dist/utils/body.d.ts +34 -0
  88. package/dist/utils/buildTerramendFooter.d.ts +25 -0
  89. package/dist/utils/byokFallback.d.ts +85 -0
  90. package/dist/utils/claudeSubscription.d.ts +30 -0
  91. package/dist/utils/cli.d.ts +10 -0
  92. package/dist/utils/codexHome.d.ts +29 -0
  93. package/dist/utils/codexOAuth.d.ts +60 -0
  94. package/dist/utils/diffCoverage.d.ts +63 -0
  95. package/dist/utils/errorReport.d.ts +17 -0
  96. package/dist/utils/exitHandler.d.ts +8 -0
  97. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  98. package/dist/utils/gitAuth.d.ts +84 -0
  99. package/dist/utils/gitAuthServer.d.ts +24 -0
  100. package/dist/utils/github.d.ts +78 -0
  101. package/dist/utils/globals.d.ts +3 -0
  102. package/dist/utils/install.d.ts +60 -0
  103. package/dist/utils/instructions.d.ts +48 -0
  104. package/dist/utils/leapingComment.d.ts +11 -0
  105. package/dist/utils/learnings.d.ts +62 -0
  106. package/dist/utils/learningsTruncate.d.ts +25 -0
  107. package/dist/utils/lifecycle.d.ts +57 -0
  108. package/dist/utils/log.d.ts +111 -0
  109. package/dist/utils/normalizeEnv.d.ts +30 -0
  110. package/dist/utils/openCodeModels.d.ts +11 -0
  111. package/dist/utils/overrides.d.ts +40 -0
  112. package/dist/utils/packageManager.d.ts +49 -0
  113. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  114. package/dist/utils/payload.d.ts +105 -0
  115. package/dist/utils/prSummary.d.ts +61 -0
  116. package/dist/utils/progressComment.d.ts +146 -0
  117. package/dist/utils/providerErrors.d.ts +31 -0
  118. package/dist/utils/rangeDiff.d.ts +51 -0
  119. package/dist/utils/remediationCommand.d.ts +55 -0
  120. package/dist/utils/retry.d.ts +13 -0
  121. package/dist/utils/reviewCleanup.d.ts +14 -0
  122. package/dist/utils/run.d.ts +9 -0
  123. package/dist/utils/runContext.d.ts +60 -0
  124. package/dist/utils/runContextData.d.ts +23 -0
  125. package/dist/utils/runErrorRenderer.d.ts +64 -0
  126. package/dist/utils/runLifecycle.d.ts +86 -0
  127. package/dist/utils/runStartupLog.d.ts +15 -0
  128. package/dist/utils/secrets.d.ts +22 -0
  129. package/dist/utils/setup.d.ts +90 -0
  130. package/dist/utils/shell.d.ts +32 -0
  131. package/dist/utils/skills.d.ts +10 -0
  132. package/dist/utils/subprocess.d.ts +80 -0
  133. package/dist/utils/terraformMcp.d.ts +42 -0
  134. package/dist/utils/time.d.ts +15 -0
  135. package/dist/utils/timer.d.ts +23 -0
  136. package/dist/utils/todoTracking.d.ts +16 -0
  137. package/dist/utils/token.d.ts +39 -0
  138. package/dist/utils/version.d.ts +2 -0
  139. package/dist/utils/versioning.d.ts +7 -0
  140. package/dist/utils/vertex.d.ts +16 -0
  141. package/dist/utils/workflow.d.ts +13 -0
  142. package/package.json +119 -0
  143. package/src/agents/claude.test.ts +1016 -0
  144. package/src/agents/claude.ts +1246 -0
  145. package/src/agents/claudePretoolGate.test.ts +28 -0
  146. package/src/agents/claudePretoolGate.ts +173 -0
  147. package/src/agents/gateServer.test.ts +204 -0
  148. package/src/agents/gateServer.ts +124 -0
  149. package/src/agents/index.ts +10 -0
  150. package/src/agents/nativeFsDenies.ts +82 -0
  151. package/src/agents/opencode.test.ts +1440 -0
  152. package/src/agents/opencode.ts +1312 -0
  153. package/src/agents/opencodePlugin.ts +222 -0
  154. package/src/agents/opencodeShared.test.ts +34 -0
  155. package/src/agents/opencodeShared.ts +121 -0
  156. package/src/agents/postRun.test.ts +549 -0
  157. package/src/agents/postRun.ts +535 -0
  158. package/src/agents/reviewer.ts +104 -0
  159. package/src/agents/sessionLabeler.test.ts +247 -0
  160. package/src/agents/sessionLabeler.ts +178 -0
  161. package/src/agents/shared.test.ts +76 -0
  162. package/src/agents/shared.ts +292 -0
  163. package/src/agents/subagentModels.test.ts +113 -0
  164. package/src/agents/subagentModels.ts +40 -0
  165. package/src/agents/subagentRegistration.test.ts +41 -0
  166. package/src/agents/subagentToolGates.ts +114 -0
  167. package/src/cli.test.ts +129 -0
  168. package/src/cli.ts +105 -0
  169. package/src/commands/gha.test.ts +192 -0
  170. package/src/commands/gha.ts +188 -0
  171. package/src/commands/mcp.ts +122 -0
  172. package/src/config.ts +1 -0
  173. package/src/entry.ts +7 -0
  174. package/src/entryPost.stdlibOnly.test.ts +109 -0
  175. package/src/entryPost.ts +99 -0
  176. package/src/external.test.ts +16 -0
  177. package/src/external.ts +302 -0
  178. package/src/index.ts +11 -0
  179. package/src/internal/index.ts +71 -0
  180. package/src/lifecycle.ts +2 -0
  181. package/src/main.test.ts +873 -0
  182. package/src/main.ts +712 -0
  183. package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
  184. package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
  185. package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
  186. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  187. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  188. package/src/mcp/arkConfig.ts +7 -0
  189. package/src/mcp/checkSuite.test.ts +245 -0
  190. package/src/mcp/checkSuite.ts +255 -0
  191. package/src/mcp/checkout.test.ts +752 -0
  192. package/src/mcp/checkout.ts +886 -0
  193. package/src/mcp/comment.test.ts +772 -0
  194. package/src/mcp/comment.ts +582 -0
  195. package/src/mcp/commitInfo.test.ts +127 -0
  196. package/src/mcp/commitInfo.ts +61 -0
  197. package/src/mcp/crosswalk.test.ts +106 -0
  198. package/src/mcp/crosswalk.ts +339 -0
  199. package/src/mcp/dependencies.test.ts +309 -0
  200. package/src/mcp/dependencies.ts +189 -0
  201. package/src/mcp/geminiSanitizer.test.ts +287 -0
  202. package/src/mcp/geminiSanitizer.ts +207 -0
  203. package/src/mcp/git.test.ts +1083 -0
  204. package/src/mcp/git.ts +890 -0
  205. package/src/mcp/guardrails.test.ts +705 -0
  206. package/src/mcp/guardrails.ts +465 -0
  207. package/src/mcp/issue.test.ts +113 -0
  208. package/src/mcp/issue.ts +73 -0
  209. package/src/mcp/issueComments.test.ts +69 -0
  210. package/src/mcp/issueComments.ts +48 -0
  211. package/src/mcp/issueEvents.test.ts +134 -0
  212. package/src/mcp/issueEvents.ts +100 -0
  213. package/src/mcp/issueInfo.test.ts +104 -0
  214. package/src/mcp/issueInfo.ts +72 -0
  215. package/src/mcp/labels.test.ts +52 -0
  216. package/src/mcp/labels.ts +34 -0
  217. package/src/mcp/localContext.ts +28 -0
  218. package/src/mcp/localServer.test.ts +75 -0
  219. package/src/mcp/localServer.ts +131 -0
  220. package/src/mcp/moduleExtraction.test.ts +261 -0
  221. package/src/mcp/moduleExtraction.ts +313 -0
  222. package/src/mcp/moduleTests.test.ts +269 -0
  223. package/src/mcp/moduleTests.ts +421 -0
  224. package/src/mcp/modules.test.ts +640 -0
  225. package/src/mcp/modules.ts +696 -0
  226. package/src/mcp/output.test.ts +96 -0
  227. package/src/mcp/output.ts +70 -0
  228. package/src/mcp/pathSafety.test.ts +44 -0
  229. package/src/mcp/pathSafety.ts +28 -0
  230. package/src/mcp/policy.test.ts +282 -0
  231. package/src/mcp/policy.ts +199 -0
  232. package/src/mcp/pr.test.ts +387 -0
  233. package/src/mcp/pr.ts +194 -0
  234. package/src/mcp/prInfo.test.ts +96 -0
  235. package/src/mcp/prInfo.ts +91 -0
  236. package/src/mcp/providerSchema.test.ts +85 -0
  237. package/src/mcp/providerSchema.ts +175 -0
  238. package/src/mcp/review.test.ts +936 -0
  239. package/src/mcp/review.ts +923 -0
  240. package/src/mcp/reviewComments.test.ts +549 -0
  241. package/src/mcp/reviewComments.ts +896 -0
  242. package/src/mcp/roots.test.ts +175 -0
  243. package/src/mcp/roots.ts +217 -0
  244. package/src/mcp/scope.test.ts +59 -0
  245. package/src/mcp/scope.ts +65 -0
  246. package/src/mcp/security.test.ts +720 -0
  247. package/src/mcp/selectMode.test.ts +210 -0
  248. package/src/mcp/selectMode.ts +181 -0
  249. package/src/mcp/server.test.ts +292 -0
  250. package/src/mcp/server.ts +403 -0
  251. package/src/mcp/shared.ts +100 -0
  252. package/src/mcp/shell.test.ts +520 -0
  253. package/src/mcp/shell.ts +505 -0
  254. package/src/mcp/staleFix.test.ts +237 -0
  255. package/src/mcp/staleFix.ts +277 -0
  256. package/src/mcp/terraform/cost.ts +163 -0
  257. package/src/mcp/terraform/currency.test.ts +338 -0
  258. package/src/mcp/terraform/currency.ts +336 -0
  259. package/src/mcp/terraform/decisions.ts +527 -0
  260. package/src/mcp/terraform/findings.ts +333 -0
  261. package/src/mcp/terraform/plan.ts +348 -0
  262. package/src/mcp/terraform/scanners.ts +809 -0
  263. package/src/mcp/terraform/tools.test.ts +1071 -0
  264. package/src/mcp/terraform/tools.ts +908 -0
  265. package/src/mcp/terraform/types.ts +305 -0
  266. package/src/mcp/terraform.test.ts +1957 -0
  267. package/src/mcp/terraform.ts +23 -0
  268. package/src/mcp/terratest.test.ts +105 -0
  269. package/src/mcp/terratest.ts +196 -0
  270. package/src/mcp/toolFiltering.test.ts +85 -0
  271. package/src/mcp/upload.test.ts +180 -0
  272. package/src/mcp/upload.ts +112 -0
  273. package/src/models.test.ts +300 -0
  274. package/src/models.ts +708 -0
  275. package/src/modes.test.ts +107 -0
  276. package/src/modes.ts +880 -0
  277. package/src/prep/index.ts +43 -0
  278. package/src/prep/installNodeDependencies.test.ts +298 -0
  279. package/src/prep/installNodeDependencies.ts +196 -0
  280. package/src/prep/installPythonDependencies.test.ts +268 -0
  281. package/src/prep/installPythonDependencies.ts +199 -0
  282. package/src/prep/types.ts +38 -0
  283. package/src/reviewQuality.test.ts +63 -0
  284. package/src/reviewQuality.ts +134 -0
  285. package/src/runCli.test.ts +214 -0
  286. package/src/runCli.ts +282 -0
  287. package/src/skills/terraform-best-practices/SKILL.md +369 -0
  288. package/src/toolState.test.ts +45 -0
  289. package/src/toolState.ts +252 -0
  290. package/src/utils/activity.test.ts +188 -0
  291. package/src/utils/activity.ts +210 -0
  292. package/src/utils/agent.test.ts +251 -0
  293. package/src/utils/agent.ts +139 -0
  294. package/src/utils/agentHangReport.test.ts +203 -0
  295. package/src/utils/agentHangReport.ts +170 -0
  296. package/src/utils/apiFetch.test.ts +115 -0
  297. package/src/utils/apiFetch.ts +62 -0
  298. package/src/utils/apiKeys.test.ts +344 -0
  299. package/src/utils/apiKeys.ts +206 -0
  300. package/src/utils/apiUrl.test.ts +30 -0
  301. package/src/utils/apiUrl.ts +59 -0
  302. package/src/utils/assets.test.ts +153 -0
  303. package/src/utils/assets.ts +107 -0
  304. package/src/utils/billingErrors.test.ts +121 -0
  305. package/src/utils/billingErrors.ts +189 -0
  306. package/src/utils/body.test.ts +217 -0
  307. package/src/utils/body.ts +168 -0
  308. package/src/utils/buildTerramendFooter.test.ts +38 -0
  309. package/src/utils/buildTerramendFooter.ts +82 -0
  310. package/src/utils/byokFallback.test.ts +205 -0
  311. package/src/utils/byokFallback.ts +128 -0
  312. package/src/utils/claudeSubscription.test.ts +179 -0
  313. package/src/utils/claudeSubscription.ts +93 -0
  314. package/src/utils/cli.ts +31 -0
  315. package/src/utils/codexHome.test.ts +190 -0
  316. package/src/utils/codexHome.ts +191 -0
  317. package/src/utils/codexOAuth.ts +147 -0
  318. package/src/utils/codexRefreshDetect.test.ts +85 -0
  319. package/src/utils/codexRefreshDetect.ts +35 -0
  320. package/src/utils/diffCoverage.test.ts +468 -0
  321. package/src/utils/diffCoverage.ts +404 -0
  322. package/src/utils/errorReport.test.ts +135 -0
  323. package/src/utils/errorReport.ts +83 -0
  324. package/src/utils/exitHandler.ts +35 -0
  325. package/src/utils/fixDoubleEscapedString.ts +9 -0
  326. package/src/utils/ghaCore.ts +13 -0
  327. package/src/utils/gitAuth.test.ts +322 -0
  328. package/src/utils/gitAuth.ts +263 -0
  329. package/src/utils/gitAuthServer.test.ts +260 -0
  330. package/src/utils/gitAuthServer.ts +182 -0
  331. package/src/utils/github.test.ts +615 -0
  332. package/src/utils/github.ts +538 -0
  333. package/src/utils/globals.ts +9 -0
  334. package/src/utils/humanEditCapture.test.ts +100 -0
  335. package/src/utils/humanEditCapture.ts +193 -0
  336. package/src/utils/install.test.ts +768 -0
  337. package/src/utils/install.ts +492 -0
  338. package/src/utils/instructions.test.ts +240 -0
  339. package/src/utils/instructions.ts +543 -0
  340. package/src/utils/leapingComment.test.ts +51 -0
  341. package/src/utils/leapingComment.ts +18 -0
  342. package/src/utils/learnings.test.ts +87 -0
  343. package/src/utils/learnings.ts +138 -0
  344. package/src/utils/learningsTocRender.test.ts +116 -0
  345. package/src/utils/learningsTruncate.test.ts +39 -0
  346. package/src/utils/learningsTruncate.ts +42 -0
  347. package/src/utils/lifecycle.test.ts +195 -0
  348. package/src/utils/lifecycle.ts +198 -0
  349. package/src/utils/log.test.ts +402 -0
  350. package/src/utils/log.ts +432 -0
  351. package/src/utils/normalizeEnv.test.ts +91 -0
  352. package/src/utils/normalizeEnv.ts +106 -0
  353. package/src/utils/openCodeModels.ts +82 -0
  354. package/src/utils/overrides.test.ts +89 -0
  355. package/src/utils/overrides.ts +98 -0
  356. package/src/utils/packageManager.test.ts +321 -0
  357. package/src/utils/packageManager.ts +257 -0
  358. package/src/utils/patchWorkflowRunFields.test.ts +92 -0
  359. package/src/utils/patchWorkflowRunFields.ts +150 -0
  360. package/src/utils/payload.test.ts +497 -0
  361. package/src/utils/payload.ts +371 -0
  362. package/src/utils/postApiFetch.ts +51 -0
  363. package/src/utils/prSummary.test.ts +224 -0
  364. package/src/utils/prSummary.ts +147 -0
  365. package/src/utils/progressComment.ts +261 -0
  366. package/src/utils/providerErrors.test.ts +315 -0
  367. package/src/utils/providerErrors.ts +172 -0
  368. package/src/utils/rangeDiff.test.ts +236 -0
  369. package/src/utils/rangeDiff.ts +182 -0
  370. package/src/utils/remediationCommand.test.ts +163 -0
  371. package/src/utils/remediationCommand.ts +119 -0
  372. package/src/utils/retry.test.ts +153 -0
  373. package/src/utils/retry.ts +58 -0
  374. package/src/utils/reviewCleanup.ts +106 -0
  375. package/src/utils/run.ts +99 -0
  376. package/src/utils/runContext.ts +145 -0
  377. package/src/utils/runContextData.ts +58 -0
  378. package/src/utils/runErrorRenderer.test.ts +95 -0
  379. package/src/utils/runErrorRenderer.ts +259 -0
  380. package/src/utils/runFixture.ts +76 -0
  381. package/src/utils/runLifecycle.ts +237 -0
  382. package/src/utils/runStartupLog.ts +60 -0
  383. package/src/utils/secrets.test.ts +103 -0
  384. package/src/utils/secrets.ts +177 -0
  385. package/src/utils/setup.test.ts +509 -0
  386. package/src/utils/setup.ts +352 -0
  387. package/src/utils/shell.ts +103 -0
  388. package/src/utils/skills.test.ts +46 -0
  389. package/src/utils/skills.ts +67 -0
  390. package/src/utils/subprocess.test.ts +170 -0
  391. package/src/utils/subprocess.ts +438 -0
  392. package/src/utils/terraformMcp.test.ts +63 -0
  393. package/src/utils/terraformMcp.ts +83 -0
  394. package/src/utils/time.test.ts +105 -0
  395. package/src/utils/time.ts +59 -0
  396. package/src/utils/timer.test.ts +91 -0
  397. package/src/utils/timer.ts +72 -0
  398. package/src/utils/todoTracking.test.ts +223 -0
  399. package/src/utils/todoTracking.ts +167 -0
  400. package/src/utils/token.test.ts +239 -0
  401. package/src/utils/token.ts +186 -0
  402. package/src/utils/version.ts +10 -0
  403. package/src/utils/versioning.test.ts +34 -0
  404. package/src/utils/versioning.ts +44 -0
  405. package/src/utils/vertex.ts +85 -0
  406. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Facade for the Terraform MCP toolchain. The implementation lives in the
3
+ * per-concern modules under `./terraform/`; this file re-exports every symbol so
4
+ * existing importers (`#app/mcp/terraform`) keep resolving unchanged.
5
+ *
6
+ * types — Concern + shared types/helpers (ids, paths, roots, severity)
7
+ * scanners — fmt / validate / tflint / trivy / checkov + provider/arg schema
8
+ * decisions — grouping, autonomy, confidence, refusal, prevention, co-location
9
+ * cost — infracost breakdown / delta / escalation
10
+ * currency — registry version currency (provider + module upgrade intel)
11
+ * findings — reviewer findings + SARIF ingest/emit
12
+ * plan — plan parsing + destroy/blast/stability/aggregation
13
+ * tools — the MCP Tool factories + their *Params schemas
14
+ */
15
+
16
+ export * from "#app/mcp/terraform/cost";
17
+ export * from "#app/mcp/terraform/currency";
18
+ export * from "#app/mcp/terraform/decisions";
19
+ export * from "#app/mcp/terraform/findings";
20
+ export * from "#app/mcp/terraform/plan";
21
+ export * from "#app/mcp/terraform/scanners";
22
+ export * from "#app/mcp/terraform/tools";
23
+ export * from "#app/mcp/terraform/types";
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ToolContext } from "#app/mcp/server";
3
+ import {
4
+ ScaffoldTerratestTool,
5
+ scaffoldTerraformTest,
6
+ scaffoldTerratest,
7
+ } from "#app/mcp/terratest";
8
+
9
+ describe("scaffoldTerratest (§28)", () => {
10
+ it("emits a plan-only Go test + a native test, and no examples/ fixture", () => {
11
+ const s = scaffoldTerratest({ moduleName: "vpc", modulePath: "modules/vpc" });
12
+ const paths = s.files.map((f) => f.path);
13
+ expect(paths).toEqual(["test/vpc_test.go", "modules/vpc/tests/vpc.tftest.hcl"]);
14
+ expect(paths.some((p) => p.startsWith("examples/"))).toBe(false);
15
+ });
16
+
17
+ it("points the Go test's TerraformDir at the module dir (not an example)", () => {
18
+ const s = scaffoldTerratest({ moduleName: "vpc", modulePath: "modules/vpc" });
19
+ const go = s.files.find((f) => f.path === "test/vpc_test.go")!.content;
20
+ // from test/ → up one level → modules/vpc
21
+ expect(go).toContain('TerraformDir: "../modules/vpc"');
22
+ expect(go).not.toContain("examples/");
23
+ });
24
+
25
+ it("PascalCases the Go test function and is plan-only (no apply)", () => {
26
+ const s = scaffoldTerratest({ moduleName: "my-cool-vpc", modulePath: "modules/x" });
27
+ const go = s.files.find((f) => f.path.endsWith("_test.go"))!.content;
28
+ expect(go).toContain("func TestMyCoolVpc(t *testing.T)");
29
+ expect(go).toContain("InitAndPlan");
30
+ expect(go).not.toMatch(/InitAndApply|\bApply\b/);
31
+ });
32
+
33
+ it("surfaces the module's variables as TODO placeholders in both tests", () => {
34
+ const s = scaffoldTerratest({
35
+ moduleName: "s3",
36
+ modulePath: "modules/s3",
37
+ variables: [{ name: "bucket_name", required: true }, { name: "tags" }],
38
+ });
39
+ const go = s.files.find((f) => f.path === "test/s3_test.go")!.content;
40
+ expect(go).toContain('// "bucket_name": nil, // (required)');
41
+ expect(go).toContain('// "tags": nil, // (optional)');
42
+ const native = s.files.find((f) => f.path === "modules/s3/tests/s3.tftest.hcl")!.content;
43
+ expect(native).toContain("# bucket_name = null # (required)");
44
+ expect(native).toContain("# tags = null # (optional)");
45
+ });
46
+
47
+ it("sanitizes a name with odd characters for the path and function", () => {
48
+ const s = scaffoldTerratest({ moduleName: "aws/s3 bucket", modulePath: "modules/s3" });
49
+ const go = s.files.find((f) => f.path.endsWith("_test.go"))!.content;
50
+ expect(go).toContain("func TestAwsS3Bucket(");
51
+ expect(s.files.some((f) => f.path === "test/aws-s3-bucket_test.go")).toBe(true);
52
+ });
53
+
54
+ it("bundles a Terraform-native test that plans the module in place", () => {
55
+ const s = scaffoldTerratest({ moduleName: "vpc", modulePath: "modules/vpc" });
56
+ const native = s.files.find((f) => f.path === "modules/vpc/tests/vpc.tftest.hcl")!;
57
+ expect(native.content).toContain("command = plan");
58
+ expect(native.content).not.toContain("examples/");
59
+ });
60
+ });
61
+
62
+ describe("scaffoldTerraformTest (§28 native variant)", () => {
63
+ it("emits a plan-only .tftest.hcl run block inside the module's tests/ dir", () => {
64
+ const f = scaffoldTerraformTest({ moduleName: "my-vpc", modulePath: "modules/my-vpc" });
65
+ expect(f.path).toBe("modules/my-vpc/tests/my-vpc.tftest.hcl");
66
+ expect(f.content).toContain('run "plan_my_vpc"');
67
+ expect(f.content).toContain("command = plan");
68
+ expect(f.content).not.toMatch(/command\s*=\s*apply/);
69
+ });
70
+ });
71
+
72
+ describe("ScaffoldTerratestTool", () => {
73
+ async function runScaffoldTool(terratest: boolean, params: unknown): Promise<string> {
74
+ const tool = ScaffoldTerratestTool({ payload: { terratest } } as unknown as ToolContext);
75
+ const exec = tool.execute as (
76
+ p: unknown,
77
+ c: unknown,
78
+ ) => Promise<{ content: [{ type: "text"; text: string }] }>;
79
+ const result = await exec(params, {});
80
+ return result.content[0].text;
81
+ }
82
+
83
+ it("degrades green when the terratest input is disabled", async () => {
84
+ const text = await runScaffoldTool(false, { module_name: "vpc", module_path: "modules/vpc" });
85
+ expect(text).toContain("enabled: false");
86
+ expect(text).toContain("opt-in");
87
+ });
88
+
89
+ it("returns the scaffold files when enabled (variables defaulted)", async () => {
90
+ const text = await runScaffoldTool(true, { module_name: "vpc", module_path: "modules/vpc" });
91
+ expect(text).toContain("enabled: true");
92
+ expect(text).toContain("test/vpc_test.go");
93
+ expect(text).toContain("modules/vpc/tests/vpc.tftest.hcl");
94
+ });
95
+
96
+ it("threads the variables through to the scaffold", async () => {
97
+ const text = await runScaffoldTool(true, {
98
+ module_name: "s3",
99
+ module_path: "modules/s3",
100
+ variables: [{ name: "bucket_name", required: true }],
101
+ });
102
+ expect(text).toContain("bucket_name");
103
+ expect(text).toContain("(required)");
104
+ });
105
+ });
@@ -0,0 +1,196 @@
1
+ import { type } from "arktype";
2
+ import type { ToolContext } from "#app/mcp/server";
3
+ import { execute, tool } from "#app/mcp/shared";
4
+ import { log } from "#app/utils/cli";
5
+
6
+ /**
7
+ * §28 Terratest scaffolding (opt-in via the `terratest` input). When Terramend
8
+ * GENERATES a reusable module, it can also scaffold a minimal Go
9
+ * [Terratest](https://terratest.gruntwork.io/) smoke test + a Terraform-native
10
+ * `*.tftest.hcl` so the generated infrastructure is testable from the first
11
+ * commit. Both tests plan the module **directly** — Terramend does not generate
12
+ * `examples/` fixtures.
13
+ *
14
+ * Design choices:
15
+ * - **Plan-only, never apply.** The scaffolded tests run `terraform init` +
16
+ * `plan` against the module and assert it plans cleanly. Terramend never
17
+ * applies (no cloud credentials — the sovereignty stance), so the generated
18
+ * tests mirror that: they're a deployability smoke test the USER runs in their
19
+ * own pipeline (with creds) for real apply/assert coverage.
20
+ * - **Pure generation.** The file contents are computed deterministically here
21
+ * and unit-tested; the agent writes the returned files with its own tools.
22
+ * - The Go test + native test files fall outside the Terraform-only default
23
+ * allow-list, so the `terratest` input also widens the push guardrail (see
24
+ * guardrails.ts).
25
+ */
26
+
27
+ export interface ScaffoldFile {
28
+ /** repo-relative path to write. */
29
+ path: string;
30
+ content: string;
31
+ }
32
+
33
+ export interface TerratestScaffold {
34
+ files: ScaffoldFile[];
35
+ notes: string[];
36
+ }
37
+
38
+ /** PascalCase a module name for the Go test function (`my-vpc` → `MyVpc`). */
39
+ function pascalCase(name: string): string {
40
+ const parts = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
41
+ const pascal = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
42
+ return /^[A-Za-z]/.test(pascal) ? pascal : `M${pascal}`;
43
+ }
44
+
45
+ /** compute a repo-relative POSIX path from `fromDir` up to `toPath` — both are
46
+ * repo-relative POSIX paths (e.g. from `test` to `modules/vpc` → `../modules/vpc`). */
47
+ function relativeUp(fromDir: string, toPath: string): string {
48
+ const up = fromDir
49
+ .split("/")
50
+ .filter(Boolean)
51
+ .map(() => "..");
52
+ const rel = `${up.join("/")}/${toPath}`.replace(/\/+/g, "/");
53
+ return rel.startsWith(".") ? rel : `./${rel}`;
54
+ }
55
+
56
+ /**
57
+ * Build a Terratest smoke-test + native test for a generated module. Pure:
58
+ * `moduleName` names the module, `modulePath` is its repo-relative dir, and
59
+ * `variables` (optional) are surfaced as TODO placeholders so the test author
60
+ * fills in real values. Both tests plan the module directly (no `examples/`
61
+ * fixture). Returns the files to write + operator notes.
62
+ */
63
+ export function scaffoldTerratest(opts: {
64
+ moduleName: string;
65
+ modulePath: string;
66
+ variables?: { name: string; required?: boolean }[];
67
+ }): TerratestScaffold {
68
+ const name = opts.moduleName.replace(/[^A-Za-z0-9_-]/g, "-");
69
+ const fn = pascalCase(name);
70
+ const terraformDir = relativeUp("test", opts.modulePath);
71
+ const variables = opts.variables ?? [];
72
+
73
+ const goVarLines = variables
74
+ .map((v) => `\t\t\t// "${v.name}": nil, // ${v.required ? "(required)" : "(optional)"}`)
75
+ .join("\n");
76
+ const goTest = `package test
77
+
78
+ import (
79
+ \t"testing"
80
+
81
+ \t"github.com/gruntwork-io/terratest/modules/terraform"
82
+ \t"github.com/stretchr/testify/require"
83
+ )
84
+
85
+ // Test${fn} is a PLAN-ONLY smoke test for the ${name} module: it runs
86
+ // terraform init + plan against the module and asserts it plans cleanly. It
87
+ // deliberately does NOT apply, so it creates no real cloud resources. Add
88
+ // apply/assert coverage in a pipeline where credentials exist.
89
+ func Test${fn}(t *testing.T) {
90
+ \tt.Parallel()
91
+
92
+ \topts := &terraform.Options{
93
+ \t\tTerraformDir: "${terraformDir}",
94
+ \t\tNoColor: true,
95
+ \t\tVars: map[string]interface{}{
96
+ \t\t\t// TODO: set the module's variables and a provider configuration before running.
97
+ ${goVarLines || "\t\t\t// (no variables detected)"}
98
+ \t\t},
99
+ \t}
100
+
101
+ \tout := terraform.InitAndPlan(t, opts)
102
+ \trequire.NotEmpty(t, out)
103
+ }
104
+ `;
105
+
106
+ return {
107
+ files: [
108
+ { path: `test/${name}_test.go`, content: goTest },
109
+ // a Terraform-native test too (no Go needed) — the lighter option.
110
+ scaffoldTerraformTest({ moduleName: name, modulePath: opts.modulePath, variables }),
111
+ ],
112
+ notes: [
113
+ "Set the module's variables and a provider configuration before running the tests (the scaffold leaves them as TODO placeholders).",
114
+ `Both tests are plan-only (no apply). Go/Terratest: \`cd test && go test -run Test${fn} -v\`. Native: \`cd ${opts.modulePath} && terraform test\`.`,
115
+ "The native `*.tftest.hcl` needs no Go; the Go test needs a go.mod with terratest + testify.",
116
+ ],
117
+ };
118
+ }
119
+
120
+ /**
121
+ * §28 (native variant) — scaffold a Terraform-native test (Terraform 1.6+) that
122
+ * lives in the module's own `tests/` dir and plans the module in place. Lighter
123
+ * than Terratest: no Go toolchain, just HCL that Terraform runs with
124
+ * `terraform test`. Plan-only `run` block (no apply, so no cloud needed to
125
+ * construct the test). Pure.
126
+ */
127
+ export function scaffoldTerraformTest(opts: {
128
+ moduleName: string;
129
+ modulePath: string;
130
+ variables?: { name: string; required?: boolean }[];
131
+ }): ScaffoldFile {
132
+ const name = opts.moduleName.replace(/[^A-Za-z0-9_-]/g, "-");
133
+ const varLines = (opts.variables ?? [])
134
+ .map((v) => ` # ${v.name} = null # ${v.required ? "(required)" : "(optional)"}`)
135
+ .join("\n");
136
+ const content = `# Terraform-native test for the ${name} module (Terraform 1.6+).
137
+ # Plan-only — asserts the module plans cleanly without applying. Run with:
138
+ # cd ${opts.modulePath} && terraform test
139
+ run "plan_${name.replace(/-/g, "_")}" {
140
+ command = plan
141
+
142
+ variables {
143
+ # TODO: set the module's variables and a provider configuration before running.
144
+ ${varLines || " # (no variables detected)"}
145
+ }
146
+
147
+ # add assertions against planned values, e.g.:
148
+ # assert {
149
+ # condition = output.id != ""
150
+ # error_message = "module must expose an id output"
151
+ # }
152
+ }
153
+ `;
154
+ return { path: `${opts.modulePath}/tests/${name}.tftest.hcl`, content };
155
+ }
156
+
157
+ export const ScaffoldTerratestParams = type({
158
+ module_name: type.string.describe("the generated module's name (e.g. 'vpc')."),
159
+ module_path: type.string.describe("the module's repo-relative dir (e.g. 'modules/vpc')."),
160
+ "variables?": type({ name: "string", "required?": "boolean" })
161
+ .array()
162
+ .describe(
163
+ "optional list of the module's variables, surfaced as TODO placeholders in the tests.",
164
+ ),
165
+ });
166
+
167
+ export function ScaffoldTerratestTool(ctx: ToolContext) {
168
+ return tool({
169
+ name: "scaffold_terratest",
170
+ description:
171
+ "Scaffold a minimal Go Terratest smoke test + a Terraform-native `*.tftest.hcl` for a module you " +
172
+ "GENERATED, so the new infrastructure is testable from the first commit. Both tests plan the module " +
173
+ "directly (Terramend does not generate `examples/` fixtures). Opt-in: only available when the " +
174
+ "`terratest` input is enabled (and that input also widens the push guardrail to allow the test " +
175
+ "files). Returns the file paths + contents to write with your own tools. The tests are PLAN-ONLY " +
176
+ "(never apply — Terramend holds no cloud credentials); they're for the user to run in their pipeline. " +
177
+ "Use it only when generating a reusable module, not for a one-off resource fix.",
178
+ parameters: ScaffoldTerratestParams,
179
+ execute: execute(async ({ module_name, module_path, variables }) => {
180
+ if (!ctx.payload.terratest) {
181
+ return {
182
+ enabled: false,
183
+ reason:
184
+ "terratest scaffolding is opt-in — set the `terratest: true` action input to enable it (it also widens allowed_paths to permit the test files).",
185
+ };
186
+ }
187
+ const scaffold = scaffoldTerratest({
188
+ moduleName: module_name,
189
+ modulePath: module_path,
190
+ variables: variables ?? [],
191
+ });
192
+ log.info(`» scaffold_terratest: ${scaffold.files.length} file(s) for module ${module_name}`);
193
+ return { enabled: true, ...scaffold };
194
+ }),
195
+ });
196
+ }
@@ -0,0 +1,85 @@
1
+ import { createServer } from "node:net";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { type } from "arktype";
5
+ import { FastMCP } from "fastmcp";
6
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
7
+ import { execute, tool } from "#app/mcp/shared";
8
+
9
+ function getRandomPort(): Promise<number> {
10
+ return new Promise((resolve, reject) => {
11
+ const srv = createServer();
12
+ srv.listen(0, "127.0.0.1", () => {
13
+ const addr = srv.address();
14
+ if (!addr || typeof addr === "string") return reject(new Error("bad address"));
15
+ const port = addr.port;
16
+ srv.close(() => resolve(port));
17
+ });
18
+ });
19
+ }
20
+
21
+ async function connectMcpClient(url: string): Promise<Client> {
22
+ const transport = new StreamableHTTPClientTransport(new URL(url));
23
+ const client = new Client({ name: "test-client", version: "0.0.1" });
24
+ // @ts-expect-error — exactOptionalPropertyTypes mismatch: SDK Transport.sessionId?: string vs StreamableHTTPClientTransport getter returning string | undefined
25
+ await client.connect(transport);
26
+ return client;
27
+ }
28
+
29
+ function mockTool(name: string, description: string) {
30
+ return tool({
31
+ name,
32
+ description,
33
+ parameters: type({ value: "string" }),
34
+ execute: execute(async () => ({ ok: true })),
35
+ });
36
+ }
37
+
38
+ describe("MCP server tool registration - integration", () => {
39
+ let server: FastMCP;
40
+ let serverUrl: string;
41
+ const clients: Client[] = [];
42
+
43
+ beforeAll(async () => {
44
+ const port = await getRandomPort();
45
+ serverUrl = `http://127.0.0.1:${port}/mcp`;
46
+
47
+ server = new FastMCP({ name: "test-server", version: "0.0.1" });
48
+ server.addTool(mockTool("shell", "run shell commands"));
49
+ server.addTool(mockTool("git", "run git commands"));
50
+ server.addTool(mockTool("set_output", "set output"));
51
+ server.addTool(mockTool("select_mode", "select a mode"));
52
+ server.addTool(mockTool("push_branch", "push branch"));
53
+ server.addTool(mockTool("create_pull_request", "create PR"));
54
+
55
+ await server.start({
56
+ transportType: "httpStream",
57
+ httpStream: { port, host: "127.0.0.1", endpoint: "/mcp" },
58
+ });
59
+ });
60
+
61
+ afterAll(async () => {
62
+ for (const client of clients) {
63
+ try {
64
+ await client.close();
65
+ } catch {
66
+ // best-effort cleanup
67
+ }
68
+ }
69
+ await server.stop();
70
+ });
71
+
72
+ it("server exposes all registered tools", async () => {
73
+ const client = await connectMcpClient(serverUrl);
74
+ clients.push(client);
75
+ const result = await client.listTools();
76
+ const names = result.tools.map((t) => t.name);
77
+ expect(names).toContain("select_mode");
78
+ expect(names).toContain("push_branch");
79
+ expect(names).toContain("create_pull_request");
80
+ expect(names).toContain("shell");
81
+ expect(names).toContain("git");
82
+ expect(names).toContain("set_output");
83
+ expect(names.length).toBe(6);
84
+ });
85
+ });
@@ -0,0 +1,180 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { fileTypeFromBuffer } from "file-type";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type { ToolContext } from "#app/mcp/server";
7
+ import { UploadFileTool } from "#app/mcp/upload";
8
+ import { apiFetch } from "#app/utils/apiFetch";
9
+
10
+ vi.mock("node:fs", async (importOriginal) => {
11
+ const actual = await importOriginal<typeof import("node:fs")>();
12
+ return {
13
+ ...actual,
14
+ default: actual,
15
+ realpathSync: vi.fn((p: unknown) => String(p)),
16
+ readFileSync: vi.fn(() => Buffer.from("file-bytes")),
17
+ };
18
+ });
19
+
20
+ vi.mock("file-type", () => ({
21
+ fileTypeFromBuffer: vi.fn(async () => ({ mime: "image/png", ext: "png" })),
22
+ }));
23
+
24
+ vi.mock("#app/utils/apiFetch", () => ({
25
+ apiFetch: vi.fn(),
26
+ }));
27
+
28
+ const apiFetchMock = vi.mocked(apiFetch);
29
+ const fileTypeMock = vi.mocked(fileTypeFromBuffer);
30
+ const realpathMock = vi.mocked(fs.realpathSync);
31
+
32
+ type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
33
+
34
+ async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
35
+ const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
36
+ return exec(params, {});
37
+ }
38
+
39
+ const REPO_ROOT = path.join(path.sep, "ws", "repo");
40
+
41
+ function makeCtx(): ToolContext {
42
+ return { apiToken: "jwt-token" } as unknown as ToolContext;
43
+ }
44
+
45
+ function signedUrlResponse(body: Record<string, unknown>, ok = true) {
46
+ return {
47
+ ok,
48
+ json: async () => body,
49
+ text: async () => JSON.stringify(body),
50
+ } as unknown as Response;
51
+ }
52
+
53
+ const putFetch = vi.fn(async () => ({ ok: true, statusText: "OK" }));
54
+
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ realpathMock.mockImplementation((p: unknown) => String(p));
58
+ fileTypeMock.mockResolvedValue({ mime: "image/png", ext: "png" } as never);
59
+ vi.stubEnv("GITHUB_WORKSPACE", REPO_ROOT);
60
+ vi.stubGlobal("fetch", putFetch);
61
+ putFetch.mockImplementation(async () => ({ ok: true, statusText: "OK" }));
62
+ apiFetchMock.mockResolvedValue(
63
+ signedUrlResponse({
64
+ uploadUrl: "https://bucket/upload?sig=1",
65
+ publicUrl: "https://cdn/file.png",
66
+ }),
67
+ );
68
+ });
69
+
70
+ afterEach(() => {
71
+ vi.unstubAllEnvs();
72
+ vi.unstubAllGlobals();
73
+ });
74
+
75
+ describe("UploadFileTool", () => {
76
+ it("uploads a repo file and returns the public URL", async () => {
77
+ const filePath = path.join(REPO_ROOT, "shot.png");
78
+ const result = await runTool(UploadFileTool(makeCtx()), { path: filePath });
79
+
80
+ expect(result.isError).toBeUndefined();
81
+ expect(result.content[0].text).toContain("https://cdn/file.png");
82
+ expect(result.content[0].text).toContain("filename: shot.png");
83
+ expect(result.content[0].text).toContain("contentType: image/png");
84
+ expect(apiFetchMock).toHaveBeenCalledWith(
85
+ expect.objectContaining({
86
+ path: "/api/upload/signed-url",
87
+ method: "POST",
88
+ headers: expect.objectContaining({ Authorization: "Bearer jwt-token" }),
89
+ }),
90
+ );
91
+ expect(putFetch).toHaveBeenCalledWith(
92
+ "https://bucket/upload?sig=1",
93
+ expect.objectContaining({
94
+ method: "PUT",
95
+ headers: expect.objectContaining({
96
+ "Content-Type": "image/png",
97
+ "Content-Length": String(Buffer.from("file-bytes").length),
98
+ }),
99
+ }),
100
+ );
101
+ });
102
+
103
+ it("allows files inside the OS temp dir", async () => {
104
+ const filePath = path.join(os.tmpdir(), "scratch", "artifact.txt");
105
+ const result = await runTool(UploadFileTool(makeCtx()), { path: filePath });
106
+
107
+ expect(result.isError).toBeUndefined();
108
+ expect(result.content[0].text).toContain("success: true");
109
+ });
110
+
111
+ it("refuses to read a file outside the repo and the temp dir", async () => {
112
+ const filePath = path.join(path.sep, "etc", "secrets", "auth.json");
113
+ const result = await runTool(UploadFileTool(makeCtx()), { path: filePath });
114
+
115
+ expect(result.isError).toBe(true);
116
+ expect(result.content[0].text).toContain("refusing to read");
117
+ expect(apiFetchMock).not.toHaveBeenCalled();
118
+ expect(vi.mocked(fs.readFileSync)).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("refuses to read from the .git directory", async () => {
122
+ const filePath = path.join(REPO_ROOT, ".git", "config");
123
+ const result = await runTool(UploadFileTool(makeCtx()), { path: filePath });
124
+
125
+ expect(result.isError).toBe(true);
126
+ expect(result.content[0].text).toContain(".git directory");
127
+ });
128
+
129
+ it("falls back to process.cwd() when GITHUB_WORKSPACE is unset", async () => {
130
+ vi.stubEnv("GITHUB_WORKSPACE", "");
131
+ const filePath = path.join(process.cwd(), "inside.txt");
132
+ const result = await runTool(UploadFileTool(makeCtx()), { path: filePath });
133
+
134
+ expect(result.isError).toBeUndefined();
135
+ expect(result.content[0].text).toContain("success: true");
136
+ });
137
+
138
+ it("defaults the content type when file-type cannot detect one", async () => {
139
+ fileTypeMock.mockResolvedValue(undefined as never);
140
+ const filePath = path.join(REPO_ROOT, "notes.txt");
141
+ const result = await runTool(UploadFileTool(makeCtx()), { path: filePath });
142
+
143
+ expect(result.content[0].text).toContain("contentType: application/octet-stream");
144
+ });
145
+
146
+ it("sets Content-Disposition only when the API returns one", async () => {
147
+ apiFetchMock.mockResolvedValue(
148
+ signedUrlResponse({
149
+ uploadUrl: "https://bucket/upload",
150
+ publicUrl: "https://cdn/f",
151
+ contentDisposition: "attachment",
152
+ }),
153
+ );
154
+ await runTool(UploadFileTool(makeCtx()), { path: path.join(REPO_ROOT, "f.bin") });
155
+
156
+ const headers = (putFetch.mock.calls[0] as unknown[])[1] as { headers: Record<string, string> };
157
+ expect(headers.headers["Content-Disposition"]).toBe("attachment");
158
+ });
159
+
160
+ it("surfaces a signed-url failure as a tool error", async () => {
161
+ apiFetchMock.mockResolvedValue(signedUrlResponse({ error: "quota exceeded" }, false));
162
+ const result = await runTool(UploadFileTool(makeCtx()), {
163
+ path: path.join(REPO_ROOT, "f.bin"),
164
+ });
165
+
166
+ expect(result.isError).toBe(true);
167
+ expect(result.content[0].text).toContain("failed to get upload URL");
168
+ expect(putFetch).not.toHaveBeenCalled();
169
+ });
170
+
171
+ it("surfaces a failed PUT upload as a tool error", async () => {
172
+ putFetch.mockImplementation(async () => ({ ok: false, statusText: "Forbidden" }));
173
+ const result = await runTool(UploadFileTool(makeCtx()), {
174
+ path: path.join(REPO_ROOT, "f.bin"),
175
+ });
176
+
177
+ expect(result.isError).toBe(true);
178
+ expect(result.content[0].text).toContain("failed to upload file: Forbidden");
179
+ });
180
+ });