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.
- package/LICENSE +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- 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
|
+
});
|