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,188 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createProcessOutputActivityTimeout, isActivityNoise } from "#app/utils/activity";
|
|
3
|
+
|
|
4
|
+
describe("isActivityNoise", () => {
|
|
5
|
+
it("flags empty and whitespace-only chunks as noise", () => {
|
|
6
|
+
expect(isActivityNoise("")).toBe(true);
|
|
7
|
+
expect(isActivityNoise(" \n\t\n")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("flags pure mcp-proxy reconnect chatter as noise", () => {
|
|
11
|
+
expect(
|
|
12
|
+
isActivityNoise("[mcp-proxy] establishing new SSE stream for session ID abc-123\n"),
|
|
13
|
+
).toBe(true);
|
|
14
|
+
expect(
|
|
15
|
+
isActivityNoise(
|
|
16
|
+
"[mcp-proxy] establishing new SSE stream for session ID a\n[mcp-proxy] received delete request\n",
|
|
17
|
+
),
|
|
18
|
+
).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("flags provider-error retry lines as noise", () => {
|
|
22
|
+
expect(isActivityNoise("» provider error detected (rate_limit): ...\n")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("treats real agent output as activity", () => {
|
|
26
|
+
expect(isActivityNoise('{"type":"tool_use","id":"toolu_01"}\n')).toBe(false);
|
|
27
|
+
expect(isActivityNoise("Leaping into action...\n")).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("treats mixed chunks (some noise + some real output) as activity", () => {
|
|
31
|
+
const mixed =
|
|
32
|
+
"[mcp-proxy] establishing new SSE stream for session ID abc\n" +
|
|
33
|
+
'{"type":"assistant_message"}\n';
|
|
34
|
+
expect(isActivityNoise(mixed)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("accepts Buffer input", () => {
|
|
38
|
+
expect(isActivityNoise(Buffer.from("[mcp-proxy] received delete request\n"))).toBe(true);
|
|
39
|
+
expect(isActivityNoise(Buffer.from('{"type":"tool_use"}\n'))).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("flags chunks with only noise + blank lines as noise", () => {
|
|
43
|
+
const noiseWithBlanks =
|
|
44
|
+
"\n[mcp-proxy] establishing new SSE stream for session ID abc\n\n" +
|
|
45
|
+
"[mcp-proxy] received delete request\n\n";
|
|
46
|
+
expect(isActivityNoise(noiseWithBlanks)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not match the noise pattern mid-line", () => {
|
|
50
|
+
// `[mcp-proxy]` must anchor at start; embedded in agent output it's activity
|
|
51
|
+
expect(isActivityNoise("agent said: [mcp-proxy] was there\n")).toBe(false);
|
|
52
|
+
expect(isActivityNoise("context: provider error detected in log\n")).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("flags debug-timestamp-prefixed noise lines", () => {
|
|
56
|
+
expect(
|
|
57
|
+
isActivityNoise("[2026-04-18T17:00:00.000Z] [mcp-proxy] establishing new SSE stream\n"),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
expect(
|
|
60
|
+
isActivityNoise("[2026-04-18T17:00:00.000Z] » provider error detected (rate_limit)\n"),
|
|
61
|
+
).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("flags our own monitor debug output (local-debug format)", () => {
|
|
65
|
+
// subprocess.ts's spawn activity check fires every 5s when debug is on;
|
|
66
|
+
// without this filter the outer timer would be reset each interval and
|
|
67
|
+
// the agent-hang detection (#12) silently fails in debug-enabled runs.
|
|
68
|
+
expect(
|
|
69
|
+
isActivityNoise(
|
|
70
|
+
"[2026-04-18T17:00:00.000Z] [DEBUG] spawn activity check: pid=123 idle=5000ms / 300000ms\n",
|
|
71
|
+
),
|
|
72
|
+
).toBe(true);
|
|
73
|
+
expect(
|
|
74
|
+
isActivityNoise(
|
|
75
|
+
"[2026-04-18T17:00:00.000Z] [DEBUG] spawn activity timer: pid=123 cmd=claude timeout=300000ms\n",
|
|
76
|
+
),
|
|
77
|
+
).toBe(true);
|
|
78
|
+
expect(
|
|
79
|
+
isActivityNoise(
|
|
80
|
+
"[2026-04-18T17:00:00.000Z] [DEBUG] process activity check: idle=120ms / 300000ms\n",
|
|
81
|
+
),
|
|
82
|
+
).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("flags our own monitor debug output (GH-runner-debug ::debug:: format)", () => {
|
|
86
|
+
expect(isActivityNoise("::debug::spawn activity check: pid=123 idle=5000ms / 300000ms\n")).toBe(
|
|
87
|
+
true,
|
|
88
|
+
);
|
|
89
|
+
expect(isActivityNoise("::debug::process activity check: idle=120ms / 300000ms\n")).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("does not blanket-filter other debug-prefixed lines", () => {
|
|
93
|
+
// the filter is scoped to our own monitor diagnostics so genuine agent
|
|
94
|
+
// output that coincidentally starts with [DEBUG] still counts as activity.
|
|
95
|
+
expect(isActivityNoise("[2026-04-18T17:00:00.000Z] [DEBUG] git auth server listening\n")).toBe(
|
|
96
|
+
false,
|
|
97
|
+
);
|
|
98
|
+
expect(isActivityNoise("::debug::agent stream chunk\n")).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("createProcessOutputActivityTimeout (debug-mode feedback loop)", () => {
|
|
103
|
+
// the monitor's own periodic diagnostic log used to travel through the
|
|
104
|
+
// wrapped process.stdout.write — in debug mode that meant the interval
|
|
105
|
+
// callback kept resetting the activity timer, so the timeout could never
|
|
106
|
+
// fire. guard against that regression by running the monitor under a
|
|
107
|
+
// simulated debug env with a tight timeout and confirming it still rejects.
|
|
108
|
+
const previousStepDebug = process.env.ACTIONS_STEP_DEBUG;
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
process.env.ACTIONS_STEP_DEBUG = "true";
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
if (previousStepDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG;
|
|
116
|
+
else process.env.ACTIONS_STEP_DEBUG = previousStepDebug;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("still times out in debug mode even though the monitor emits periodic diagnostics", async () => {
|
|
120
|
+
const timeout = createProcessOutputActivityTimeout({
|
|
121
|
+
timeoutMs: 150,
|
|
122
|
+
checkIntervalMs: 20,
|
|
123
|
+
});
|
|
124
|
+
try {
|
|
125
|
+
await expect(timeout.promise).rejects.toThrow(/activity timeout/);
|
|
126
|
+
} finally {
|
|
127
|
+
timeout.stop();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("createProcessOutputActivityTimeout forceReject / stop disarming", () => {
|
|
133
|
+
// main.ts arms a 5min safety-net timer on inner-activity kill that later
|
|
134
|
+
// calls forceReject. when the agent succeeds first, main.ts calls stop().
|
|
135
|
+
// stop() must disarm forceReject — otherwise a late safety-net fire would
|
|
136
|
+
// reject a promise nothing is awaiting, re-creating the #12 zombie-run
|
|
137
|
+
// shape (unhandledRejection) or worse, failing a successful run.
|
|
138
|
+
it("forceReject rejects the promise with the given reason", async () => {
|
|
139
|
+
const timeout = createProcessOutputActivityTimeout({
|
|
140
|
+
timeoutMs: 60_000,
|
|
141
|
+
checkIntervalMs: 10_000,
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
timeout.forceReject("safety-net fired");
|
|
145
|
+
await expect(timeout.promise).rejects.toThrow(/safety-net fired/);
|
|
146
|
+
} finally {
|
|
147
|
+
timeout.stop();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("stop() disarms forceReject so a late safety-net fire is a no-op", async () => {
|
|
152
|
+
const timeout = createProcessOutputActivityTimeout({
|
|
153
|
+
timeoutMs: 60_000,
|
|
154
|
+
checkIntervalMs: 10_000,
|
|
155
|
+
});
|
|
156
|
+
// prevent unhandled-rejection noise if the assertion below ever regresses
|
|
157
|
+
timeout.promise.catch(() => {});
|
|
158
|
+
|
|
159
|
+
timeout.stop();
|
|
160
|
+
timeout.forceReject("late safety-net fire after run succeeded");
|
|
161
|
+
|
|
162
|
+
// race the promise against a short sleep; if forceReject reopened the
|
|
163
|
+
// rejection it would win the race. the sleep should always win.
|
|
164
|
+
const sentinel = Symbol("still-pending");
|
|
165
|
+
const winner = await Promise.race([
|
|
166
|
+
timeout.promise.then(
|
|
167
|
+
() => "resolved",
|
|
168
|
+
() => "rejected",
|
|
169
|
+
),
|
|
170
|
+
new Promise((resolve) => setTimeout(() => resolve(sentinel), 50)),
|
|
171
|
+
]);
|
|
172
|
+
expect(winner).toBe(sentinel);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("forceReject is a no-op if the promise already rejected via the timer", async () => {
|
|
176
|
+
const timeout = createProcessOutputActivityTimeout({
|
|
177
|
+
timeoutMs: 60,
|
|
178
|
+
checkIntervalMs: 10,
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
await expect(timeout.promise).rejects.toThrow(/activity timeout/);
|
|
182
|
+
// forceReject after timer rejection must not throw or double-reject
|
|
183
|
+
expect(() => timeout.forceReject("should be ignored")).not.toThrow();
|
|
184
|
+
} finally {
|
|
185
|
+
timeout.stop();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
|
|
3
|
+
function isMonitorDebugEnabled(): boolean {
|
|
4
|
+
return (
|
|
5
|
+
process.env.ACTIONS_STEP_DEBUG === "true" ||
|
|
6
|
+
process.env.RUNNER_DEBUG === "1" ||
|
|
7
|
+
process.env.LOG_LEVEL === "debug"
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* generic `spawn()` idle default for ordinary short-lived subprocesses (prep
|
|
13
|
+
* probes, package-manager invocations, dependency installs). these should be
|
|
14
|
+
* producing output steadily, so a comparatively tight budget catches a wedged
|
|
15
|
+
* command promptly. the long-silent-tool tolerance the agent harnesses need
|
|
16
|
+
* lives in AGENT_ACTIVITY_TIMEOUT_MS, applied explicitly at those sites.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_ACTIVITY_TIMEOUT_MS = 300_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* flat idle budget for the agent activity watchdog (the outer process-output
|
|
22
|
+
* monitor, the v1 harness spawns, and the v2 inner event-silence watchdog).
|
|
23
|
+
* sized to exceed the worst-case legitimate silent tool window (issue #760:
|
|
24
|
+
* `checkout_pr` git fetch+deepen on a large monorepo, ~4-5min) with generous
|
|
25
|
+
* headroom, so no single in-flight tool call can be mistaken for a stall. a
|
|
26
|
+
* timeout this generous needs no suspend/resume bracketing — the cost of a
|
|
27
|
+
* genuinely hung run is only GitHub Actions minutes, not tokens.
|
|
28
|
+
*/
|
|
29
|
+
export const AGENT_ACTIVITY_TIMEOUT_MS = 900_000;
|
|
30
|
+
export const DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* chunks whose every non-empty line matches one of these patterns do not
|
|
34
|
+
* count as agent activity. mcp-proxy SSE reconnects and provider-error
|
|
35
|
+
* retries happen on their own schedule and were keeping the outer activity
|
|
36
|
+
* timer alive long after the agent subprocess had been killed for inactivity,
|
|
37
|
+
* producing multi-hour zombie runs.
|
|
38
|
+
*
|
|
39
|
+
* both patterns anchor to the start of the (optionally debug-timestamped)
|
|
40
|
+
* log line so they don't accidentally match agent output that happens to
|
|
41
|
+
* mention "[mcp-proxy]" or "provider error detected" in analysis text.
|
|
42
|
+
*/
|
|
43
|
+
const DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
|
|
44
|
+
// our own internal monitors (this file's bypass + subprocess.ts's spawn
|
|
45
|
+
// activity timer) emit high-frequency diagnostic logs when debug logging is
|
|
46
|
+
// enabled. in the past those lines reached the wrapped process.stdout.write,
|
|
47
|
+
// missed the noise check, and marked activity every interval — which in
|
|
48
|
+
// debug-enabled runs kept the outer timer alive after the agent subprocess
|
|
49
|
+
// was already dead, re-creating the #12 zombie-run bug. the `(?:spawn|process)
|
|
50
|
+
// activity ` patterns below explicitly filter our own diagnostic lines in both
|
|
51
|
+
// local-debug (`[DEBUG] …`) and GH-runner-debug (`::debug::…`) formats.
|
|
52
|
+
export const ACTIVITY_NOISE_PATTERNS: readonly RegExp[] = [
|
|
53
|
+
new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
|
|
54
|
+
new RegExp(`${DEBUG_TS_PREFIX}» provider error detected`),
|
|
55
|
+
new RegExp(`${DEBUG_TS_PREFIX}\\[DEBUG\\]\\s+(?:spawn|process) activity `),
|
|
56
|
+
/^::debug::(?:spawn|process) activity /,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export function isActivityNoise(chunk: string | Uint8Array): boolean {
|
|
60
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
61
|
+
if (!text.trim()) return true;
|
|
62
|
+
return text.split("\n").every((line) => {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (!trimmed) return true;
|
|
65
|
+
return ACTIVITY_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ActivityTimeoutContext = {
|
|
70
|
+
timeoutMs: number;
|
|
71
|
+
checkIntervalMs: number;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type ActivityTimeout = {
|
|
75
|
+
promise: Promise<never>;
|
|
76
|
+
stop: () => void;
|
|
77
|
+
/** force the timeout to reject immediately with a custom reason */
|
|
78
|
+
forceReject: (reason: string) => void;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type OutputMonitorContext = {
|
|
82
|
+
timeoutMs: number;
|
|
83
|
+
checkIntervalMs: number;
|
|
84
|
+
onTimeout: (idleMs: number) => void;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type OutputMonitor = {
|
|
88
|
+
stop: () => void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type WriteCallback = (error?: Error | null) => void;
|
|
92
|
+
type WriteFunction = {
|
|
93
|
+
(chunk: string | Uint8Array, cb?: WriteCallback): boolean;
|
|
94
|
+
(chunk: string | Uint8Array, encoding?: BufferEncoding, cb?: WriteCallback): boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// module-level activity tracking - allows agents to mark activity on any event
|
|
98
|
+
let _lastActivity = performance.now();
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* mark activity to reset the no-output timeout.
|
|
102
|
+
* call this whenever the agent emits any event, even if it isn't logged to stdout.
|
|
103
|
+
*/
|
|
104
|
+
export function markActivity(): void {
|
|
105
|
+
_lastActivity = performance.now();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** get the time since last activity in milliseconds. */
|
|
109
|
+
export function getIdleMs(): number {
|
|
110
|
+
return Math.round(performance.now() - _lastActivity);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function wrapWrite(original: WriteFunction, onActivity: () => void): WriteFunction {
|
|
114
|
+
const wrapped: WriteFunction = (
|
|
115
|
+
chunk: string | Uint8Array,
|
|
116
|
+
encodingOrCb?: BufferEncoding | WriteCallback,
|
|
117
|
+
cb?: WriteCallback,
|
|
118
|
+
): boolean => {
|
|
119
|
+
if (!isActivityNoise(chunk)) {
|
|
120
|
+
onActivity();
|
|
121
|
+
}
|
|
122
|
+
if (typeof encodingOrCb === "function") {
|
|
123
|
+
return original(chunk, encodingOrCb);
|
|
124
|
+
}
|
|
125
|
+
return original(chunk, encodingOrCb, cb);
|
|
126
|
+
};
|
|
127
|
+
return wrapped;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function startProcessOutputMonitor(ctx: OutputMonitorContext): OutputMonitor {
|
|
131
|
+
let timedOut = false;
|
|
132
|
+
|
|
133
|
+
const originalStdoutWrite: WriteFunction = process.stdout.write.bind(process.stdout);
|
|
134
|
+
const originalStderrWrite: WriteFunction = process.stderr.write.bind(process.stderr);
|
|
135
|
+
|
|
136
|
+
// stdout/stderr writes also mark activity
|
|
137
|
+
process.stdout.write = wrapWrite(originalStdoutWrite, markActivity);
|
|
138
|
+
process.stderr.write = wrapWrite(originalStderrWrite, markActivity);
|
|
139
|
+
|
|
140
|
+
// route the monitor's own diagnostics through the captured original write
|
|
141
|
+
// instead of log.debug — otherwise those lines feed back through the
|
|
142
|
+
// wrapped process.stdout.write, miss isActivityNoise, and call
|
|
143
|
+
// markActivity() themselves. in debug mode the periodic check below would
|
|
144
|
+
// then reset the timer every interval and the timeout would never fire,
|
|
145
|
+
// re-creating the exact zombie-run bug #12 was meant to kill.
|
|
146
|
+
const debugBypass = (msg: string): void => {
|
|
147
|
+
if (!isMonitorDebugEnabled()) return;
|
|
148
|
+
originalStdoutWrite(`[${new Date().toISOString()}] [DEBUG] ${msg}\n`);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
debugBypass(`process activity monitor started: timeout=${ctx.timeoutMs}ms`);
|
|
152
|
+
|
|
153
|
+
const intervalId = setInterval(() => {
|
|
154
|
+
const idleMs = getIdleMs();
|
|
155
|
+
debugBypass(`process activity check: idle=${idleMs}ms / ${ctx.timeoutMs}ms`);
|
|
156
|
+
if (timedOut || idleMs <= ctx.timeoutMs) return;
|
|
157
|
+
timedOut = true;
|
|
158
|
+
ctx.onTimeout(idleMs);
|
|
159
|
+
}, ctx.checkIntervalMs);
|
|
160
|
+
|
|
161
|
+
function stop(): void {
|
|
162
|
+
clearInterval(intervalId);
|
|
163
|
+
process.stdout.write = originalStdoutWrite;
|
|
164
|
+
process.stderr.write = originalStderrWrite;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { stop };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function createProcessOutputActivityTimeout(ctx: ActivityTimeoutContext): ActivityTimeout {
|
|
171
|
+
markActivity(); // reset baseline
|
|
172
|
+
|
|
173
|
+
let rejectFn: ((error: Error) => void) | null = null;
|
|
174
|
+
const promise = new Promise<never>((_, reject) => {
|
|
175
|
+
rejectFn = reject;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
let monitor: OutputMonitor | null = null;
|
|
179
|
+
monitor = startProcessOutputMonitor({
|
|
180
|
+
timeoutMs: ctx.timeoutMs,
|
|
181
|
+
checkIntervalMs: ctx.checkIntervalMs,
|
|
182
|
+
onTimeout: (idleMs) => {
|
|
183
|
+
if (!rejectFn) return;
|
|
184
|
+
const idleSec = Math.round(idleMs / 1000);
|
|
185
|
+
if (monitor) {
|
|
186
|
+
monitor.stop();
|
|
187
|
+
}
|
|
188
|
+
const reject = rejectFn;
|
|
189
|
+
rejectFn = null;
|
|
190
|
+
reject(new Error(`activity timeout: no output for ${idleSec}s`));
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
promise,
|
|
196
|
+
// stop() also disarms forceReject so a late safety-net fire can't reject
|
|
197
|
+
// the promise after the run has already succeeded.
|
|
198
|
+
stop: () => {
|
|
199
|
+
monitor?.stop();
|
|
200
|
+
rejectFn = null;
|
|
201
|
+
},
|
|
202
|
+
forceReject: (reason: string) => {
|
|
203
|
+
if (!rejectFn) return;
|
|
204
|
+
monitor?.stop();
|
|
205
|
+
const reject = rejectFn;
|
|
206
|
+
rejectFn = null;
|
|
207
|
+
reject(new Error(reason));
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { resolveAgent, resolveModel } from "#app/utils/agent";
|
|
6
|
+
import { cleanupVertexCredentials, materializeVertexCredentials } from "#app/utils/vertex";
|
|
7
|
+
|
|
8
|
+
const savedEnv = { ...process.env };
|
|
9
|
+
|
|
10
|
+
const STRIPPED = [
|
|
11
|
+
/_API_KEY$/,
|
|
12
|
+
/^CLAUDE_CODE_OAUTH_TOKEN$/,
|
|
13
|
+
/^AWS_BEARER_TOKEN_BEDROCK$/,
|
|
14
|
+
/^AWS_ACCESS_KEY_ID$/,
|
|
15
|
+
/^AWS_SECRET_ACCESS_KEY$/,
|
|
16
|
+
/^AWS_SESSION_TOKEN$/,
|
|
17
|
+
/^AWS_REGION$/,
|
|
18
|
+
/^BEDROCK_MODEL_ID$/,
|
|
19
|
+
/^GOOGLE_APPLICATION_CREDENTIALS$/,
|
|
20
|
+
/^GOOGLE_CLOUD_PROJECT$/,
|
|
21
|
+
/^VERTEX_SERVICE_ACCOUNT_JSON$/,
|
|
22
|
+
/^VERTEX_LOCATION$/,
|
|
23
|
+
/^VERTEX_MODEL_ID$/,
|
|
24
|
+
/^TERRAMEND_SECRET_HOME$/,
|
|
25
|
+
/^TERRAMEND_MODEL$/,
|
|
26
|
+
/^TERRAMEND_AGENT$/,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
for (const key of Object.keys(process.env)) {
|
|
31
|
+
if (STRIPPED.some((re) => re.test(key))) delete process.env[key];
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
process.env = { ...savedEnv };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("resolveAgent", () => {
|
|
40
|
+
it("returns opencode by default", () => {
|
|
41
|
+
expect(resolveAgent({}).name).toBe("opencode");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("routes anthropic/* to claude when ANTHROPIC_API_KEY is set", () => {
|
|
45
|
+
process.env.ANTHROPIC_API_KEY = "sk-test";
|
|
46
|
+
expect(resolveAgent({ model: "anthropic/claude-opus-4-7" }).name).toBe("claude");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("falls back to opencode for anthropic/* without claude-code creds", () => {
|
|
50
|
+
expect(resolveAgent({ model: "anthropic/claude-opus-4-7" }).name).toBe("opencode");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("bedrock routing", () => {
|
|
54
|
+
it("routes Anthropic Bedrock IDs to claude", () => {
|
|
55
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
56
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
57
|
+
expect(resolveAgent({ model: "eu.anthropic.claude-opus-4-7" }).name).toBe("claude");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("routes Anthropic Bedrock IDs (no region prefix) to claude", () => {
|
|
61
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
62
|
+
process.env.BEDROCK_MODEL_ID = "anthropic.claude-haiku-4-5-20251001-v1:0";
|
|
63
|
+
expect(resolveAgent({ model: "anthropic.claude-haiku-4-5-20251001-v1:0" }).name).toBe(
|
|
64
|
+
"claude",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("routes non-Anthropic Bedrock IDs to opencode", () => {
|
|
69
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
70
|
+
process.env.BEDROCK_MODEL_ID = "amazon.nova-pro-v1:0";
|
|
71
|
+
expect(resolveAgent({ model: "amazon.nova-pro-v1:0" }).name).toBe("opencode");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("routes Llama IDs to opencode", () => {
|
|
75
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
76
|
+
process.env.BEDROCK_MODEL_ID = "eu.meta.llama4-scout-17b-instruct-v1:0";
|
|
77
|
+
expect(resolveAgent({ model: "eu.meta.llama4-scout-17b-instruct-v1:0" }).name).toBe(
|
|
78
|
+
"opencode",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("accepts AWS access keys as auth", () => {
|
|
83
|
+
process.env.AWS_ACCESS_KEY_ID = "AKIA-test";
|
|
84
|
+
process.env.AWS_SECRET_ACCESS_KEY = "secret-test";
|
|
85
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
86
|
+
expect(resolveAgent({ model: "eu.anthropic.claude-opus-4-7" }).name).toBe("claude");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("TERRAMEND_AGENT override wins over Anthropic auto-routing", () => {
|
|
90
|
+
process.env.TERRAMEND_AGENT = "opencode";
|
|
91
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
92
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
93
|
+
expect(resolveAgent({ model: "eu.anthropic.claude-opus-4-7" }).name).toBe("opencode");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("vertex routing", () => {
|
|
98
|
+
it("routes Anthropic Vertex IDs to claude", () => {
|
|
99
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = "{}";
|
|
100
|
+
process.env.VERTEX_MODEL_ID = "claude-opus-4-1@20250805";
|
|
101
|
+
expect(resolveAgent({ model: "claude-opus-4-1@20250805" }).name).toBe("claude");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("routes Gemini Vertex IDs to opencode", () => {
|
|
105
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = "{}";
|
|
106
|
+
process.env.VERTEX_MODEL_ID = "gemini-2.5-pro";
|
|
107
|
+
expect(resolveAgent({ model: "gemini-2.5-pro" }).name).toBe("opencode");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("resolveModel", () => {
|
|
113
|
+
it("TERRAMEND_MODEL override wins", () => {
|
|
114
|
+
process.env.TERRAMEND_MODEL = "anthropic/claude-opus";
|
|
115
|
+
expect(resolveModel({ slug: "openai/gpt" })).toBe("anthropic/claude-opus-4-8");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("TERRAMEND_MODEL bypasses bedrock routing entirely", () => {
|
|
119
|
+
process.env.TERRAMEND_MODEL = "openai/gpt";
|
|
120
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
121
|
+
expect(resolveModel({ slug: "bedrock/byok" })).toBe("openai/gpt-5.5");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("resolves bedrock/byok to BEDROCK_MODEL_ID", () => {
|
|
125
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
126
|
+
expect(resolveModel({ slug: "bedrock/byok" })).toBe("eu.anthropic.claude-opus-4-7");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("throws when bedrock/byok is selected without BEDROCK_MODEL_ID", () => {
|
|
130
|
+
expect(() => resolveModel({ slug: "bedrock/byok" })).toThrow("BEDROCK_MODEL_ID");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("returns the alias resolve for normal slugs", () => {
|
|
134
|
+
expect(resolveModel({ slug: "openai/gpt" })).toBe("openai/gpt-5.5");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns undefined for no slug + no TERRAMEND_MODEL", () => {
|
|
138
|
+
expect(resolveModel({})).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// regression: PR #720 review caught that `resolveCliModel("bedrock/byok")`
|
|
142
|
+
// returns the literal sentinel `"bedrock"` from the alias's `resolve`
|
|
143
|
+
// field. Without routing-aware handling, TERRAMEND_MODEL=bedrock/byok would
|
|
144
|
+
// leak that sentinel downstream and break agent dispatch.
|
|
145
|
+
it("TERRAMEND_MODEL=bedrock/byok defers to BEDROCK_MODEL_ID, not the sentinel", () => {
|
|
146
|
+
process.env.TERRAMEND_MODEL = "bedrock/byok";
|
|
147
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
148
|
+
expect(resolveModel({ slug: "openai/gpt" })).toBe("eu.anthropic.claude-opus-4-7");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("TERRAMEND_MODEL=bedrock/byok throws if BEDROCK_MODEL_ID is missing", () => {
|
|
152
|
+
process.env.TERRAMEND_MODEL = "bedrock/byok";
|
|
153
|
+
expect(() => resolveModel({ slug: "openai/gpt" })).toThrow("BEDROCK_MODEL_ID");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("resolves vertex/byok to VERTEX_MODEL_ID", () => {
|
|
157
|
+
process.env.VERTEX_MODEL_ID = "claude-opus-4-1@20250805";
|
|
158
|
+
expect(resolveModel({ slug: "vertex/byok" })).toBe("claude-opus-4-1@20250805");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("throws when vertex/byok is selected without VERTEX_MODEL_ID", () => {
|
|
162
|
+
expect(() => resolveModel({ slug: "vertex/byok" })).toThrow("VERTEX_MODEL_ID");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("TERRAMEND_MODEL=vertex/byok defers to VERTEX_MODEL_ID, not the sentinel", () => {
|
|
166
|
+
process.env.TERRAMEND_MODEL = "vertex/byok";
|
|
167
|
+
process.env.VERTEX_MODEL_ID = "gemini-2.5-pro";
|
|
168
|
+
expect(resolveModel({ slug: "openai/gpt" })).toBe("gemini-2.5-pro");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("TERRAMEND_MODEL=vertex/byok throws if VERTEX_MODEL_ID is missing", () => {
|
|
172
|
+
process.env.TERRAMEND_MODEL = "vertex/byok";
|
|
173
|
+
expect(() => resolveModel({ slug: "openai/gpt" })).toThrow("VERTEX_MODEL_ID");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("passes a raw TERRAMEND_MODEL specifier through when it is not a registry alias", () => {
|
|
177
|
+
process.env.TERRAMEND_MODEL = "anthropic/claude-opus-4-6-20250514";
|
|
178
|
+
expect(resolveModel({ slug: "openai/gpt" })).toBe("anthropic/claude-opus-4-6-20250514");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("returns undefined for an unknown slug (warn-and-auto-select path)", () => {
|
|
182
|
+
expect(resolveModel({ slug: "nope/not-a-model" })).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("resolveAgent — TERRAMEND_AGENT override and fallthroughs", () => {
|
|
187
|
+
it("returns the named agent for a valid TERRAMEND_AGENT", () => {
|
|
188
|
+
process.env.TERRAMEND_AGENT = "claude";
|
|
189
|
+
expect(resolveAgent({}).name).toBe("claude");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("falls through to the default for an unknown TERRAMEND_AGENT", () => {
|
|
193
|
+
process.env.TERRAMEND_AGENT = "codex";
|
|
194
|
+
expect(resolveAgent({}).name).toBe("opencode");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("unknown TERRAMEND_AGENT still honors Anthropic model routing", () => {
|
|
198
|
+
process.env.TERRAMEND_AGENT = "nope";
|
|
199
|
+
process.env.ANTHROPIC_API_KEY = "sk-test";
|
|
200
|
+
expect(resolveAgent({ model: "anthropic/claude-opus-4-7" }).name).toBe("claude");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("routes anthropic/* to claude on CLAUDE_CODE_OAUTH_TOKEN alone", () => {
|
|
204
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-token";
|
|
205
|
+
expect(resolveAgent({ model: "anthropic/claude-opus-4-7" }).name).toBe("claude");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("falls through to opencode for an invalid model format even with claude creds", () => {
|
|
209
|
+
process.env.ANTHROPIC_API_KEY = "sk-test";
|
|
210
|
+
expect(resolveAgent({ model: "no-slash-model" }).name).toBe("opencode");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("ignores the bedrock route when AWS auth is missing", () => {
|
|
214
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
215
|
+
expect(resolveAgent({ model: "eu.anthropic.claude-opus-4-7" }).name).toBe("opencode");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("ignores the vertex route when VERTEX_MODEL_ID does not match the model", () => {
|
|
219
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = "{}";
|
|
220
|
+
process.env.VERTEX_MODEL_ID = "some-other-model";
|
|
221
|
+
expect(resolveAgent({ model: "claude-opus-4-1@20250805" }).name).toBe("opencode");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("materializeVertexCredentials", () => {
|
|
226
|
+
it("writes service-account JSON outside tmpdir and defaults project from project_id", () => {
|
|
227
|
+
const dir = mkdtempSync(join(tmpdir(), "vertex-creds-test-"));
|
|
228
|
+
process.env.VERTEX_MODEL_ID = "claude-opus-4-1@20250805";
|
|
229
|
+
process.env.TERRAMEND_SECRET_HOME = dir;
|
|
230
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = JSON.stringify({
|
|
231
|
+
project_id: "test-project",
|
|
232
|
+
client_email: "terramend@test-project.iam.gserviceaccount.com",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const credentials = materializeVertexCredentials({ model: "claude-opus-4-1@20250805" });
|
|
237
|
+
|
|
238
|
+
if (!credentials) throw new Error("expected vertex credentials");
|
|
239
|
+
expect(credentials.credentialsPath).toContain(join(dir, ".terramend", "secrets"));
|
|
240
|
+
expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).toBe(credentials.credentialsPath);
|
|
241
|
+
expect(process.env.GOOGLE_CLOUD_PROJECT).toBe("test-project");
|
|
242
|
+
expect(readFileSync(credentials.credentialsPath, "utf8")).toBe(
|
|
243
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON,
|
|
244
|
+
);
|
|
245
|
+
expect(statSync(credentials.credentialsPath).mode & 0o777).toBe(0o600);
|
|
246
|
+
cleanupVertexCredentials(credentials);
|
|
247
|
+
} finally {
|
|
248
|
+
rmSync(dir, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|