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,404 @@
|
|
|
1
|
+
import { isAbsolute, normalize, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export type DiffLineRange = {
|
|
4
|
+
startLine: number;
|
|
5
|
+
endLine: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DiffTocEntry = {
|
|
9
|
+
filename: string;
|
|
10
|
+
startLine: number;
|
|
11
|
+
endLine: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DiffCoverageFileBreakdown = {
|
|
15
|
+
filename: string;
|
|
16
|
+
startLine: number;
|
|
17
|
+
endLine: number;
|
|
18
|
+
totalLines: number;
|
|
19
|
+
coveredLines: number;
|
|
20
|
+
coveredRanges: DiffLineRange[];
|
|
21
|
+
unreadRanges: DiffLineRange[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type DiffCoverageBreakdown = {
|
|
25
|
+
totalLines: number;
|
|
26
|
+
coveredLines: number;
|
|
27
|
+
unreadLines: number;
|
|
28
|
+
coveragePercent: number;
|
|
29
|
+
coveredRanges: DiffLineRange[];
|
|
30
|
+
unreadRanges: DiffLineRange[];
|
|
31
|
+
files: DiffCoverageFileBreakdown[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type DiffCoverageState = {
|
|
35
|
+
diffPath: string;
|
|
36
|
+
totalLines: number;
|
|
37
|
+
tocEntries: DiffTocEntry[];
|
|
38
|
+
coveredRanges: DiffLineRange[];
|
|
39
|
+
coveragePreflightRan: boolean;
|
|
40
|
+
lastBreakdown?: string | undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type ReadTarget = {
|
|
44
|
+
path: string;
|
|
45
|
+
offset?: number | undefined;
|
|
46
|
+
limit?: number | undefined;
|
|
47
|
+
startLine?: number | undefined;
|
|
48
|
+
endLine?: number | undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type OffsetBase = "zero" | "one";
|
|
52
|
+
|
|
53
|
+
export function countLines(params: { content: string }): number {
|
|
54
|
+
const content = params.content;
|
|
55
|
+
if (content.length === 0) return 0;
|
|
56
|
+
return content.split("\n").length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parseDiffTocEntries(params: { toc: string }): DiffTocEntry[] {
|
|
60
|
+
const lines = params.toc.split("\n");
|
|
61
|
+
const entries: DiffTocEntry[] = [];
|
|
62
|
+
// production TOC lines (see formatFilesWithLineNumbers in checkout.ts) append
|
|
63
|
+
// ` · diff-<sha256>` so the agent has the GitHub "Files Changed" anchor
|
|
64
|
+
// precomputed. accept that suffix optionally so we also parse the shorter
|
|
65
|
+
// shape used in tests and in reviewComments.
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const match = line.match(/^- (.+) (?:→|->) lines (\d+)-(\d+)(?: · diff-[0-9a-f]+)?$/);
|
|
68
|
+
if (!match) continue;
|
|
69
|
+
const startLine = Number.parseInt(match[2]!, 10);
|
|
70
|
+
const endLine = Number.parseInt(match[3]!, 10);
|
|
71
|
+
if (!Number.isFinite(startLine) || !Number.isFinite(endLine)) continue;
|
|
72
|
+
entries.push({ filename: match[1]!, startLine, endLine });
|
|
73
|
+
}
|
|
74
|
+
return entries;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createDiffCoverageState(params: {
|
|
78
|
+
diffPath: string;
|
|
79
|
+
totalLines: number;
|
|
80
|
+
toc: string;
|
|
81
|
+
previous?: DiffCoverageState | undefined;
|
|
82
|
+
}): DiffCoverageState {
|
|
83
|
+
return {
|
|
84
|
+
diffPath: params.diffPath,
|
|
85
|
+
totalLines: params.totalLines,
|
|
86
|
+
tocEntries: parseDiffTocEntries({ toc: params.toc }),
|
|
87
|
+
coveredRanges: [],
|
|
88
|
+
// carry forward across checkout_pr refreshes so the nudge stays "once per
|
|
89
|
+
// review session". coveredRanges are intentionally not carried because
|
|
90
|
+
// line numbers are tied to the previous diff's content.
|
|
91
|
+
coveragePreflightRan: params.previous?.coveragePreflightRan ?? false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function recordDiffReadFromToolUse(params: {
|
|
96
|
+
state: DiffCoverageState | undefined;
|
|
97
|
+
toolName: string;
|
|
98
|
+
input: unknown;
|
|
99
|
+
cwd: string;
|
|
100
|
+
}): boolean {
|
|
101
|
+
const state = params.state;
|
|
102
|
+
if (!state) return false;
|
|
103
|
+
if (!isReadTool(params.toolName)) return false;
|
|
104
|
+
const readTarget = extractReadTarget({ input: params.input });
|
|
105
|
+
if (!readTarget) return false;
|
|
106
|
+
|
|
107
|
+
const normalizedReadPath = normalizePath({ path: readTarget.path, cwd: params.cwd });
|
|
108
|
+
const normalizedDiffPath = normalize(state.diffPath);
|
|
109
|
+
if (normalizedReadPath !== normalizedDiffPath) return false;
|
|
110
|
+
|
|
111
|
+
const range = resolveReadRange({
|
|
112
|
+
totalLines: state.totalLines,
|
|
113
|
+
offset: readTarget.offset,
|
|
114
|
+
limit: readTarget.limit,
|
|
115
|
+
startLine: readTarget.startLine,
|
|
116
|
+
endLine: readTarget.endLine,
|
|
117
|
+
offsetBase: resolveOffsetBase({ toolName: params.toolName }),
|
|
118
|
+
});
|
|
119
|
+
if (!range) return false;
|
|
120
|
+
|
|
121
|
+
state.coveredRanges = mergeRanges({ ranges: state.coveredRanges, nextRange: range });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getDiffCoverageBreakdown(params: {
|
|
126
|
+
state: DiffCoverageState;
|
|
127
|
+
}): DiffCoverageBreakdown {
|
|
128
|
+
const state = params.state;
|
|
129
|
+
const coveredRanges = mergeRangesList({ ranges: state.coveredRanges });
|
|
130
|
+
const unreadRanges = invertRanges({ totalLines: state.totalLines, coveredRanges });
|
|
131
|
+
const coveredLines = countLinesInRanges({ ranges: coveredRanges });
|
|
132
|
+
const unreadLines = Math.max(0, state.totalLines - coveredLines);
|
|
133
|
+
const coveragePercent = state.totalLines
|
|
134
|
+
? Number(((coveredLines / state.totalLines) * 100).toFixed(2))
|
|
135
|
+
: 100;
|
|
136
|
+
|
|
137
|
+
const files: DiffCoverageFileBreakdown[] = [];
|
|
138
|
+
for (const entry of state.tocEntries) {
|
|
139
|
+
const fileRange: DiffLineRange = { startLine: entry.startLine, endLine: entry.endLine };
|
|
140
|
+
const coveredInFile = intersectRangesWithRange({ ranges: coveredRanges, target: fileRange });
|
|
141
|
+
const unreadInFile = intersectRangesWithRange({ ranges: unreadRanges, target: fileRange });
|
|
142
|
+
const totalFileLines = Math.max(0, entry.endLine - entry.startLine + 1);
|
|
143
|
+
const fileCoveredLines = countLinesInRanges({ ranges: coveredInFile });
|
|
144
|
+
files.push({
|
|
145
|
+
filename: entry.filename,
|
|
146
|
+
startLine: entry.startLine,
|
|
147
|
+
endLine: entry.endLine,
|
|
148
|
+
totalLines: totalFileLines,
|
|
149
|
+
coveredLines: fileCoveredLines,
|
|
150
|
+
coveredRanges: coveredInFile,
|
|
151
|
+
unreadRanges: unreadInFile,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
totalLines: state.totalLines,
|
|
157
|
+
coveredLines,
|
|
158
|
+
unreadLines,
|
|
159
|
+
coveragePercent,
|
|
160
|
+
coveredRanges,
|
|
161
|
+
unreadRanges,
|
|
162
|
+
files,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function renderDiffCoverageBreakdown(params: {
|
|
167
|
+
diffPath: string;
|
|
168
|
+
breakdown: DiffCoverageBreakdown;
|
|
169
|
+
}): string {
|
|
170
|
+
const breakdown = params.breakdown;
|
|
171
|
+
const lines: string[] = [];
|
|
172
|
+
lines.push(`diff coverage report for \`${params.diffPath}\``);
|
|
173
|
+
lines.push(
|
|
174
|
+
`overall: ${breakdown.coveredLines}/${breakdown.totalLines} lines read (${breakdown.coveragePercent}%), unread: ${breakdown.unreadLines}`,
|
|
175
|
+
);
|
|
176
|
+
lines.push(`covered ranges: ${formatRanges({ ranges: breakdown.coveredRanges })}`);
|
|
177
|
+
lines.push(`unread ranges: ${formatRanges({ ranges: breakdown.unreadRanges })}`);
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push("per-file TOC coverage:");
|
|
180
|
+
for (const file of breakdown.files) {
|
|
181
|
+
const filePercent = file.totalLines
|
|
182
|
+
? Number(((file.coveredLines / file.totalLines) * 100).toFixed(2))
|
|
183
|
+
: 100;
|
|
184
|
+
lines.push(
|
|
185
|
+
`- ${file.filename} (toc lines ${file.startLine}-${file.endLine}): ${file.coveredLines}/${file.totalLines} lines read (${filePercent}%)`,
|
|
186
|
+
);
|
|
187
|
+
lines.push(` read: ${formatRanges({ ranges: file.coveredRanges })}`);
|
|
188
|
+
lines.push(` unread: ${formatRanges({ ranges: file.unreadRanges })}`);
|
|
189
|
+
}
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveOffsetBase(params: { toolName: string }): OffsetBase {
|
|
194
|
+
const lower = params.toolName.toLowerCase();
|
|
195
|
+
if (lower === "readfile" || lower.endsWith(".readfile")) {
|
|
196
|
+
return "one";
|
|
197
|
+
}
|
|
198
|
+
return "zero";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isReadTool(toolName: string): boolean {
|
|
202
|
+
const lower = toolName.toLowerCase();
|
|
203
|
+
if (lower === "read" || lower === "readfile") return true;
|
|
204
|
+
if (lower.endsWith(".read") || lower.endsWith(".readfile")) return true;
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function extractReadTarget(params: { input: unknown }): ReadTarget | null {
|
|
209
|
+
const inputRecord = asRecord(params.input);
|
|
210
|
+
if (!inputRecord) return null;
|
|
211
|
+
|
|
212
|
+
const direct = extractReadTargetFromRecord({ record: inputRecord });
|
|
213
|
+
if (direct) return direct;
|
|
214
|
+
|
|
215
|
+
const nestedCandidates = [inputRecord.args, inputRecord.params, inputRecord.input];
|
|
216
|
+
for (const candidate of nestedCandidates) {
|
|
217
|
+
const nestedRecord = asRecord(candidate);
|
|
218
|
+
if (!nestedRecord) continue;
|
|
219
|
+
const nested = extractReadTargetFromRecord({ record: nestedRecord });
|
|
220
|
+
if (nested) return nested;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractReadTargetFromRecord(params: {
|
|
227
|
+
record: Record<string, unknown>;
|
|
228
|
+
}): ReadTarget | null {
|
|
229
|
+
const record = params.record;
|
|
230
|
+
const pathValue =
|
|
231
|
+
readString({ value: record.path }) ??
|
|
232
|
+
readString({ value: record.file_path }) ??
|
|
233
|
+
readString({ value: record.filePath }) ??
|
|
234
|
+
readString({ value: record.filepath }) ??
|
|
235
|
+
readString({ value: record.file }) ??
|
|
236
|
+
readString({ value: record.target_file });
|
|
237
|
+
|
|
238
|
+
if (!pathValue) return null;
|
|
239
|
+
|
|
240
|
+
const offset = readNumber({ value: record.offset });
|
|
241
|
+
const limit = readNumber({ value: record.limit });
|
|
242
|
+
const startLine =
|
|
243
|
+
readNumber({ value: record.start_line }) ??
|
|
244
|
+
readNumber({ value: record.startLine }) ??
|
|
245
|
+
readNumber({ value: record.line_start });
|
|
246
|
+
const endLine =
|
|
247
|
+
readNumber({ value: record.end_line }) ??
|
|
248
|
+
readNumber({ value: record.endLine }) ??
|
|
249
|
+
readNumber({ value: record.line_end });
|
|
250
|
+
|
|
251
|
+
return { path: pathValue, offset, limit, startLine, endLine };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveReadRange(params: {
|
|
255
|
+
totalLines: number;
|
|
256
|
+
offset?: number | undefined;
|
|
257
|
+
limit?: number | undefined;
|
|
258
|
+
startLine?: number | undefined;
|
|
259
|
+
endLine?: number | undefined;
|
|
260
|
+
offsetBase: OffsetBase;
|
|
261
|
+
}): DiffLineRange | null {
|
|
262
|
+
const totalLines = params.totalLines;
|
|
263
|
+
if (totalLines <= 0) return null;
|
|
264
|
+
|
|
265
|
+
if (params.startLine !== undefined || params.endLine !== undefined) {
|
|
266
|
+
const rawStart = params.startLine ?? 1;
|
|
267
|
+
const rawEnd = params.endLine ?? totalLines;
|
|
268
|
+
const startLine = clampLine({ value: rawStart, totalLines });
|
|
269
|
+
const endLine = clampLine({ value: rawEnd, totalLines });
|
|
270
|
+
if (endLine < startLine) return null;
|
|
271
|
+
return { startLine, endLine };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let startLine = 1;
|
|
275
|
+
if (params.offset !== undefined) {
|
|
276
|
+
if (params.offset >= 0) {
|
|
277
|
+
const normalizedOffset =
|
|
278
|
+
params.offsetBase === "zero" ? params.offset + 1 : params.offset === 0 ? 1 : params.offset;
|
|
279
|
+
startLine = clampLine({ value: normalizedOffset, totalLines });
|
|
280
|
+
} else {
|
|
281
|
+
startLine = clampLine({ value: totalLines + params.offset + 1, totalLines });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let endLine = totalLines;
|
|
286
|
+
if (params.limit !== undefined) {
|
|
287
|
+
if (params.limit <= 0) return null;
|
|
288
|
+
endLine = clampLine({ value: startLine + params.limit - 1, totalLines });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (endLine < startLine) return null;
|
|
292
|
+
return { startLine, endLine };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function normalizePath(params: { path: string; cwd: string }): string {
|
|
296
|
+
if (isAbsolute(params.path)) return normalize(params.path);
|
|
297
|
+
return normalize(resolve(params.cwd, params.path));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function mergeRanges(params: {
|
|
301
|
+
ranges: DiffLineRange[];
|
|
302
|
+
nextRange: DiffLineRange;
|
|
303
|
+
}): DiffLineRange[] {
|
|
304
|
+
return mergeRangesList({ ranges: [...params.ranges, params.nextRange] });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function mergeRangesList(params: { ranges: DiffLineRange[] }): DiffLineRange[] {
|
|
308
|
+
if (params.ranges.length === 0) return [];
|
|
309
|
+
const sorted = [...params.ranges].sort((a, b) => a.startLine - b.startLine);
|
|
310
|
+
const merged: DiffLineRange[] = [];
|
|
311
|
+
for (const range of sorted) {
|
|
312
|
+
const last = merged[merged.length - 1];
|
|
313
|
+
if (!last) {
|
|
314
|
+
merged.push({ startLine: range.startLine, endLine: range.endLine });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (range.startLine <= last.endLine + 1) {
|
|
318
|
+
if (range.endLine > last.endLine) {
|
|
319
|
+
last.endLine = range.endLine;
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
merged.push({ startLine: range.startLine, endLine: range.endLine });
|
|
324
|
+
}
|
|
325
|
+
return merged;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function invertRanges(params: {
|
|
329
|
+
totalLines: number;
|
|
330
|
+
coveredRanges: DiffLineRange[];
|
|
331
|
+
}): DiffLineRange[] {
|
|
332
|
+
if (params.totalLines <= 0) return [];
|
|
333
|
+
if (params.coveredRanges.length === 0) {
|
|
334
|
+
return [{ startLine: 1, endLine: params.totalLines }];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const unread: DiffLineRange[] = [];
|
|
338
|
+
let cursor = 1;
|
|
339
|
+
for (const range of params.coveredRanges) {
|
|
340
|
+
if (cursor < range.startLine) {
|
|
341
|
+
unread.push({ startLine: cursor, endLine: range.startLine - 1 });
|
|
342
|
+
}
|
|
343
|
+
cursor = Math.max(cursor, range.endLine + 1);
|
|
344
|
+
}
|
|
345
|
+
if (cursor <= params.totalLines) {
|
|
346
|
+
unread.push({ startLine: cursor, endLine: params.totalLines });
|
|
347
|
+
}
|
|
348
|
+
return unread;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function intersectRangesWithRange(params: {
|
|
352
|
+
ranges: DiffLineRange[];
|
|
353
|
+
target: DiffLineRange;
|
|
354
|
+
}): DiffLineRange[] {
|
|
355
|
+
const intersections: DiffLineRange[] = [];
|
|
356
|
+
for (const range of params.ranges) {
|
|
357
|
+
if (range.endLine < params.target.startLine) continue;
|
|
358
|
+
if (range.startLine > params.target.endLine) continue;
|
|
359
|
+
const startLine = Math.max(range.startLine, params.target.startLine);
|
|
360
|
+
const endLine = Math.min(range.endLine, params.target.endLine);
|
|
361
|
+
if (endLine >= startLine) {
|
|
362
|
+
intersections.push({ startLine, endLine });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return intersections;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function countLinesInRanges(params: { ranges: DiffLineRange[] }): number {
|
|
369
|
+
let total = 0;
|
|
370
|
+
for (const range of params.ranges) {
|
|
371
|
+
total += range.endLine - range.startLine + 1;
|
|
372
|
+
}
|
|
373
|
+
return total;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function formatRanges(params: { ranges: DiffLineRange[] }): string {
|
|
377
|
+
if (params.ranges.length === 0) return "none";
|
|
378
|
+
return params.ranges.map((range) => `${range.startLine}-${range.endLine}`).join(", ");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function clampLine(params: { value: number; totalLines: number }): number {
|
|
382
|
+
if (params.value < 1) return 1;
|
|
383
|
+
if (params.value > params.totalLines) return params.totalLines;
|
|
384
|
+
return params.value;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
388
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
389
|
+
return Object.fromEntries(Object.entries(value));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function readString(params: { value: unknown }): string | undefined {
|
|
393
|
+
if (typeof params.value === "string") return params.value;
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function readNumber(params: { value: unknown }): number | undefined {
|
|
398
|
+
if (typeof params.value === "number" && Number.isFinite(params.value)) return params.value;
|
|
399
|
+
if (typeof params.value === "string") {
|
|
400
|
+
const parsed = Number.parseInt(params.value, 10);
|
|
401
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ToolState } from "#app/toolState";
|
|
3
|
+
import { reportErrorToComment } from "#app/utils/errorReport";
|
|
4
|
+
import { updateProgressComment } from "#app/utils/progressComment";
|
|
5
|
+
|
|
6
|
+
const createComment = vi.hoisted(() => vi.fn());
|
|
7
|
+
|
|
8
|
+
vi.mock("#app/utils/github", () => ({
|
|
9
|
+
parseRepoContext: vi.fn(() => ({ owner: "acme", name: "repo" })),
|
|
10
|
+
createOctokit: vi.fn(() => ({ rest: { issues: { createComment } } })),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("#app/utils/token", () => ({
|
|
14
|
+
getGitHubInstallationToken: vi.fn(() => "installation-token"),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("#app/utils/progressComment", () => ({
|
|
18
|
+
updateProgressComment: vi.fn(async () => ({})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
function makeToolState(overrides: Partial<ToolState> = {}): ToolState {
|
|
22
|
+
return {
|
|
23
|
+
prepushFailureCount: 0,
|
|
24
|
+
backgroundProcesses: new Map(),
|
|
25
|
+
progressComment: undefined,
|
|
26
|
+
hadProgressComment: false,
|
|
27
|
+
usageEntries: [],
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function updatedBody(): string {
|
|
33
|
+
const call = vi.mocked(updateProgressComment).mock.calls[0];
|
|
34
|
+
if (!call) throw new Error("expected updateProgressComment to have been called");
|
|
35
|
+
return call[2];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
createComment.mockResolvedValue({ data: { id: 777 } });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
vi.unstubAllEnvs();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("reportErrorToComment", () => {
|
|
48
|
+
it("updates an existing progress comment with title, error, and rerun link", async () => {
|
|
49
|
+
vi.stubEnv("GITHUB_RUN_ID", "12345");
|
|
50
|
+
const toolState = makeToolState({
|
|
51
|
+
progressComment: { id: 5, type: "issue" },
|
|
52
|
+
model: "anthropic/claude-opus-4-7",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await reportErrorToComment({ toolState, error: "boom", title: "**Run failed**" });
|
|
56
|
+
|
|
57
|
+
expect(updateProgressComment).toHaveBeenCalledTimes(1);
|
|
58
|
+
const call = vi.mocked(updateProgressComment).mock.calls[0];
|
|
59
|
+
if (!call) throw new Error("expected updateProgressComment to have been called");
|
|
60
|
+
expect(call[1]).toEqual({ id: 5, type: "issue" });
|
|
61
|
+
const body = call[2];
|
|
62
|
+
expect(body).toContain("**Run failed**\n\nboom");
|
|
63
|
+
expect(body).toContain("Rerun failed job");
|
|
64
|
+
expect(body).toContain("/trigger/acme/repo/12345?action=rerun");
|
|
65
|
+
expect(toolState.wasUpdated).toBe(true);
|
|
66
|
+
expect(createComment).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("omits the rerun link and title prefix when GITHUB_RUN_ID / title are absent", async () => {
|
|
70
|
+
vi.stubEnv("GITHUB_RUN_ID", undefined);
|
|
71
|
+
const toolState = makeToolState({ progressComment: { id: 9, type: "review" } });
|
|
72
|
+
|
|
73
|
+
await reportErrorToComment({ toolState, error: "plain failure" });
|
|
74
|
+
|
|
75
|
+
const body = updatedBody();
|
|
76
|
+
expect(body.startsWith("plain failure")).toBe(true);
|
|
77
|
+
expect(body).not.toContain("Rerun failed job");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns silently when there is no comment and createIfMissing is unset", async () => {
|
|
81
|
+
const toolState = makeToolState({ issueNumber: 42 });
|
|
82
|
+
|
|
83
|
+
await reportErrorToComment({ toolState, error: "boom" });
|
|
84
|
+
|
|
85
|
+
expect(updateProgressComment).not.toHaveBeenCalled();
|
|
86
|
+
expect(createComment).not.toHaveBeenCalled();
|
|
87
|
+
expect(toolState.wasUpdated).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns silently when createIfMissing is set but there is no issue number", async () => {
|
|
91
|
+
const toolState = makeToolState();
|
|
92
|
+
|
|
93
|
+
await reportErrorToComment({ toolState, error: "boom", createIfMissing: true });
|
|
94
|
+
|
|
95
|
+
expect(createComment).not.toHaveBeenCalled();
|
|
96
|
+
expect(toolState.wasUpdated).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("creates a fresh issue comment for terminal errors on silent triggers", async () => {
|
|
100
|
+
const toolState = makeToolState({ issueNumber: 42 });
|
|
101
|
+
|
|
102
|
+
await reportErrorToComment({ toolState, error: "billing exhausted", createIfMissing: true });
|
|
103
|
+
|
|
104
|
+
expect(createComment).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(createComment).toHaveBeenCalledWith({
|
|
106
|
+
owner: "acme",
|
|
107
|
+
repo: "repo",
|
|
108
|
+
issue_number: 42,
|
|
109
|
+
body: expect.stringContaining("billing exhausted"),
|
|
110
|
+
});
|
|
111
|
+
expect(toolState.progressComment).toEqual({ id: 777, type: "issue" });
|
|
112
|
+
expect(toolState.wasUpdated).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("swallows a failed fallback comment create instead of throwing", async () => {
|
|
116
|
+
createComment.mockRejectedValueOnce(new Error("403 Forbidden"));
|
|
117
|
+
const toolState = makeToolState({ issueNumber: 42 });
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
reportErrorToComment({ toolState, error: "boom", createIfMissing: true }),
|
|
121
|
+
).resolves.toBeUndefined();
|
|
122
|
+
expect(toolState.wasUpdated).toBeUndefined();
|
|
123
|
+
expect(toolState.progressComment).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("stringifies a non-Error rejection from the fallback create", async () => {
|
|
127
|
+
createComment.mockRejectedValueOnce("string rejection");
|
|
128
|
+
const toolState = makeToolState({ issueNumber: 42 });
|
|
129
|
+
|
|
130
|
+
await expect(
|
|
131
|
+
reportErrorToComment({ toolState, error: "boom", createIfMissing: true }),
|
|
132
|
+
).resolves.toBeUndefined();
|
|
133
|
+
expect(toolState.wasUpdated).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ToolState } from "#app/toolState";
|
|
2
|
+
import { getApiUrl } from "#app/utils/apiUrl";
|
|
3
|
+
import { buildTerramendFooter } from "#app/utils/buildTerramendFooter";
|
|
4
|
+
import { log } from "#app/utils/cli";
|
|
5
|
+
import { createOctokit, parseRepoContext } from "#app/utils/github";
|
|
6
|
+
import { updateProgressComment } from "#app/utils/progressComment";
|
|
7
|
+
import { getGitHubInstallationToken } from "#app/utils/token";
|
|
8
|
+
|
|
9
|
+
interface ReportErrorParams {
|
|
10
|
+
toolState: ToolState;
|
|
11
|
+
error: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
/**
|
|
14
|
+
* When the run has no pre-existing progress comment to update (silent
|
|
15
|
+
* IncrementalReview / pull_request_synchronize, mode-less polls), create
|
|
16
|
+
* a fresh issue comment on `toolState.issueNumber` instead of returning
|
|
17
|
+
* silently. Used for terminal errors (BillingError, TransientError) where
|
|
18
|
+
* the GH job summary is the only other surface and most users never open
|
|
19
|
+
* it. see #775.
|
|
20
|
+
*/
|
|
21
|
+
createIfMissing?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function reportErrorToComment(ctx: ReportErrorParams): Promise<void> {
|
|
25
|
+
const formattedError = ctx.title ? `${ctx.title}\n\n${ctx.error}` : ctx.error;
|
|
26
|
+
|
|
27
|
+
const repoContext = parseRepoContext();
|
|
28
|
+
const octokit = createOctokit(getGitHubInstallationToken());
|
|
29
|
+
const runId = process.env.GITHUB_RUN_ID
|
|
30
|
+
? Number.parseInt(process.env.GITHUB_RUN_ID, 10)
|
|
31
|
+
: undefined;
|
|
32
|
+
|
|
33
|
+
const customParts: string[] = [];
|
|
34
|
+
if (runId) {
|
|
35
|
+
const apiUrl = getApiUrl();
|
|
36
|
+
customParts.push(
|
|
37
|
+
`[Rerun failed job ➔](${apiUrl}/trigger/${repoContext.owner}/${repoContext.name}/${runId}?action=rerun)`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const footer = buildTerramendFooter({
|
|
42
|
+
triggeredBy: true,
|
|
43
|
+
customParts,
|
|
44
|
+
model: ctx.toolState.model,
|
|
45
|
+
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const body = `${formattedError}${footer}`;
|
|
49
|
+
|
|
50
|
+
const comment = ctx.toolState.progressComment;
|
|
51
|
+
if (comment) {
|
|
52
|
+
await updateProgressComment(
|
|
53
|
+
{ octokit, owner: repoContext.owner, repo: repoContext.name },
|
|
54
|
+
comment,
|
|
55
|
+
body,
|
|
56
|
+
);
|
|
57
|
+
ctx.toolState.wasUpdated = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// silent triggers (pull_request_synchronize IncrementalReview, etc.)
|
|
62
|
+
// intentionally have no progress comment. for terminal errors that need
|
|
63
|
+
// user action — billing exhaustion, transient billing-service outage —
|
|
64
|
+
// surface a fresh issue comment instead of leaving the GH job summary as
|
|
65
|
+
// the only signal. see #775.
|
|
66
|
+
if (!ctx.createIfMissing) return;
|
|
67
|
+
if (!ctx.toolState.issueNumber) return;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const created = await octokit.rest.issues.createComment({
|
|
71
|
+
owner: repoContext.owner,
|
|
72
|
+
repo: repoContext.name,
|
|
73
|
+
issue_number: ctx.toolState.issueNumber,
|
|
74
|
+
body,
|
|
75
|
+
});
|
|
76
|
+
ctx.toolState.progressComment = { id: created.data.id, type: "issue" };
|
|
77
|
+
ctx.toolState.wasUpdated = true;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
log.warning(
|
|
80
|
+
`[errorReport] fallback comment create failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
type ExitSignalHandler = (signal: "SIGINT" | "SIGTERM") => void | Promise<void>;
|
|
4
|
+
|
|
5
|
+
const handlers = new Set<ExitSignalHandler>();
|
|
6
|
+
let installed = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register a handler to run when the process receives SIGINT or SIGTERM.
|
|
10
|
+
* Returns a dispose function that removes the handler.
|
|
11
|
+
*/
|
|
12
|
+
export function onExitSignal(handler: ExitSignalHandler): () => void {
|
|
13
|
+
installSignalHandlers();
|
|
14
|
+
handlers.add(handler);
|
|
15
|
+
return () => {
|
|
16
|
+
handlers.delete(handler);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function installSignalHandlers(): void {
|
|
21
|
+
if (installed) return;
|
|
22
|
+
installed = true;
|
|
23
|
+
|
|
24
|
+
async function handleSignal(signal: "SIGINT" | "SIGTERM") {
|
|
25
|
+
await Promise.allSettled([...handlers].map((h) => Promise.try(h, signal)));
|
|
26
|
+
exitWithSignal(signal);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.on("SIGINT", handleSignal);
|
|
30
|
+
process.on("SIGTERM", handleSignal);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function exitWithSignal(signal: "SIGINT" | "SIGTERM") {
|
|
34
|
+
process.exit(128 + os.constants.signals[signal]);
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// LLMs sometimes double-escape JSON strings, producing literal \n \t \"
|
|
2
|
+
// instead of actual newline/tab/quote characters.
|
|
3
|
+
// detected when the string contains literal \n but no actual newlines.
|
|
4
|
+
export function fixDoubleEscapedString(str: string): string {
|
|
5
|
+
if (!str.includes("\n") && str.includes("\\n")) {
|
|
6
|
+
return str.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\"/g, '"');
|
|
7
|
+
}
|
|
8
|
+
return str;
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** stdlib-only GitHub Actions helpers for entryPost.ts (no node_modules). */
|
|
2
|
+
|
|
3
|
+
export function getState(name: string): string {
|
|
4
|
+
return process.env[`STATE_${name}`] ?? "";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function info(message: string): void {
|
|
8
|
+
console.log(message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function warning(message: string): void {
|
|
12
|
+
console.log(`::warning::${message}`);
|
|
13
|
+
}
|