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,549 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
// Unwrap the ToolResult envelope so the *Tool tests can assert on the raw
|
|
7
|
+
// object each tool returns. The pure formatters above are unaffected.
|
|
8
|
+
vi.mock("#app/mcp/shared", async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import("#app/mcp/shared")>();
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
execute: <T, R>(fn: (params: T) => Promise<R>): ((params: T) => Promise<R>) => fn,
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
buildThreadBlocks,
|
|
18
|
+
countLines,
|
|
19
|
+
type FormatReviewDataInput,
|
|
20
|
+
formatReactionCounts,
|
|
21
|
+
formatReviewData,
|
|
22
|
+
formatReviewThreads,
|
|
23
|
+
GetReviewCommentsTool,
|
|
24
|
+
ListPullRequestReviewsTool,
|
|
25
|
+
type ParsedHunk,
|
|
26
|
+
parseFilePatches,
|
|
27
|
+
ResolveReviewThreadTool,
|
|
28
|
+
type ReviewThread,
|
|
29
|
+
type ReviewThreadComment,
|
|
30
|
+
} from "#app/mcp/reviewComments";
|
|
31
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
32
|
+
|
|
33
|
+
// fixtures captured by action/scripts/refresh-test-fixtures.ts; re-run
|
|
34
|
+
// (with creds) when GitHub's review/threads/listFiles response shape
|
|
35
|
+
// changes, then review the snapshot diff.
|
|
36
|
+
type ReviewFixture = FormatReviewDataInput & {
|
|
37
|
+
owner: string;
|
|
38
|
+
name: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function loadFixture(file: string): ReviewFixture {
|
|
42
|
+
return JSON.parse(
|
|
43
|
+
readFileSync(resolve(import.meta.dirname, "__fixtures__", file), "utf-8"),
|
|
44
|
+
) as ReviewFixture;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("formatReviewData", () => {
|
|
48
|
+
it("formats thread blocks with TOC and correct line numbers", () => {
|
|
49
|
+
const fx = loadFixture("terramend-scratch-pr-49-review-3485940013.json");
|
|
50
|
+
const result = formatReviewData(fx);
|
|
51
|
+
expect(result).toBeDefined();
|
|
52
|
+
if (!result) return;
|
|
53
|
+
|
|
54
|
+
expect(result.formatted.toc).toMatchSnapshot("toc");
|
|
55
|
+
expect(result.formatted.content).toMatchSnapshot("content");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("formats body-only review", () => {
|
|
59
|
+
const fx = loadFixture("terramend-scratch-pr-64-review-3531000326.json");
|
|
60
|
+
const result = formatReviewData(fx);
|
|
61
|
+
expect(result).toBeDefined();
|
|
62
|
+
if (!result) return;
|
|
63
|
+
|
|
64
|
+
expect(result.formatted.toc).toMatchSnapshot("toc");
|
|
65
|
+
expect(result.formatted.content).toMatchSnapshot("content");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns undefined when the review has no threads and no body", () => {
|
|
69
|
+
expect(
|
|
70
|
+
formatReviewData({
|
|
71
|
+
review: { body: "", user: { login: "x" } },
|
|
72
|
+
threads: [],
|
|
73
|
+
prFiles: [],
|
|
74
|
+
pullNumber: 1,
|
|
75
|
+
reviewId: 2,
|
|
76
|
+
}),
|
|
77
|
+
).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("strips an existing terramend footer from the review body", () => {
|
|
81
|
+
const result = formatReviewData({
|
|
82
|
+
review: {
|
|
83
|
+
body: "Real feedback.\n\n<!-- TERRAMEND_DIVIDER_DO_NOT_REMOVE_PLZ -->\n<sup>via Terramend</sup>",
|
|
84
|
+
user: null,
|
|
85
|
+
},
|
|
86
|
+
threads: [],
|
|
87
|
+
prFiles: [],
|
|
88
|
+
pullNumber: 1,
|
|
89
|
+
reviewId: 2,
|
|
90
|
+
});
|
|
91
|
+
expect(result).toBeDefined();
|
|
92
|
+
expect(result?.reviewer).toBe("unknown");
|
|
93
|
+
expect(result?.formatted.content).toContain("Real feedback.");
|
|
94
|
+
expect(result?.formatted.content).not.toContain("via Terramend");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("countLines", () => {
|
|
99
|
+
it("counts newline-separated lines (1 for an empty string)", () => {
|
|
100
|
+
expect(countLines("")).toBe(1);
|
|
101
|
+
expect(countLines("a")).toBe(1);
|
|
102
|
+
expect(countLines("a\nb\nc")).toBe(3);
|
|
103
|
+
expect(countLines("a\n")).toBe(2);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("parseFilePatches", () => {
|
|
108
|
+
it("splits a multi-hunk patch, defaulting omitted counts to 1", () => {
|
|
109
|
+
const patch = ["@@ -1,3 +1,3 @@", " a", "-b", "+B", "@@ -10 +10 @@", "-x", "+X"].join("\n");
|
|
110
|
+
const hunks = parseFilePatches(patch);
|
|
111
|
+
expect(hunks).toHaveLength(2);
|
|
112
|
+
expect(hunks[0]).toMatchObject({ oldStart: 1, oldCount: 3, newStart: 1, newCount: 3 });
|
|
113
|
+
expect(hunks[0]?.content).toEqual([" a", "-b", "+B"]);
|
|
114
|
+
expect(hunks[1]).toMatchObject({ oldStart: 10, oldCount: 1, newStart: 10, newCount: 1 });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns nothing for a patch with no hunk headers", () => {
|
|
118
|
+
expect(parseFilePatches("just text\nno hunks")).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// --- buildThreadBlocks / formatReviewThreads (constructed threads) ----------
|
|
123
|
+
|
|
124
|
+
const TARGET_REVIEW_ID = 777;
|
|
125
|
+
|
|
126
|
+
function comment(over: Partial<ReviewThreadComment> = {}): ReviewThreadComment {
|
|
127
|
+
return {
|
|
128
|
+
fullDatabaseId: "12345",
|
|
129
|
+
body: "please fix this",
|
|
130
|
+
bodyHTML: "<p>please fix this</p>",
|
|
131
|
+
createdAt: "2026-06-10T00:00:00Z",
|
|
132
|
+
diffHunk: ["@@ -10,2 +10,3 @@", " ctx", "-old", "+new", "+new2"].join("\n"),
|
|
133
|
+
line: 12,
|
|
134
|
+
startLine: null,
|
|
135
|
+
originalLine: 12,
|
|
136
|
+
originalStartLine: null,
|
|
137
|
+
author: { login: "reviewer" },
|
|
138
|
+
pullRequestReview: { databaseId: TARGET_REVIEW_ID, author: { login: "reviewer" } },
|
|
139
|
+
reactionGroups: null,
|
|
140
|
+
...over,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function thread(over: Partial<ReviewThread> = {}): ReviewThread {
|
|
145
|
+
return {
|
|
146
|
+
id: "T_1",
|
|
147
|
+
path: "src/foo.ts",
|
|
148
|
+
line: 12,
|
|
149
|
+
startLine: null,
|
|
150
|
+
diffSide: "RIGHT",
|
|
151
|
+
isResolved: false,
|
|
152
|
+
isOutdated: false,
|
|
153
|
+
comments: { nodes: [comment()] },
|
|
154
|
+
...over,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
describe("buildThreadBlocks", () => {
|
|
159
|
+
const emptyPatches = new Map<string, ParsedHunk[]>();
|
|
160
|
+
|
|
161
|
+
it("renders a thread header, the conversation, and the diff context", () => {
|
|
162
|
+
const patches = new Map([["src/foo.ts", parseFilePatches(comment().diffHunk)]]);
|
|
163
|
+
const blocks = buildThreadBlocks([thread()], patches, TARGET_REVIEW_ID);
|
|
164
|
+
expect(blocks).toHaveLength(1);
|
|
165
|
+
const content = blocks[0]?.content.join("\n") ?? "";
|
|
166
|
+
expect(blocks[0]).toMatchObject({ path: "src/foo.ts", lineRange: "12" });
|
|
167
|
+
expect(content).toContain("## src/foo.ts:12");
|
|
168
|
+
// the * marker tags comments belonging to the target review.
|
|
169
|
+
expect(content).toMatch(/comment author=reviewer id=12345 review=777 thread=T_1 \*/);
|
|
170
|
+
expect(content).toContain("please fix this");
|
|
171
|
+
expect(content).toContain("```diff file=src/foo.ts lines=12 side=RIGHT");
|
|
172
|
+
expect(content).toContain("+new2");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("marks resolved and outdated threads in the header", () => {
|
|
176
|
+
const resolved = buildThreadBlocks([thread({ isResolved: true })], emptyPatches, 1);
|
|
177
|
+
expect(resolved[0]?.content[0]).toContain("[RESOLVED]");
|
|
178
|
+
const outdated = buildThreadBlocks([thread({ isOutdated: true })], emptyPatches, 1);
|
|
179
|
+
expect(outdated[0]?.content[0]).toContain("[OUTDATED]");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("skips threads with no comments and sorts by path then line", () => {
|
|
183
|
+
const blocks = buildThreadBlocks(
|
|
184
|
+
[
|
|
185
|
+
thread({ id: "T_b", path: "b.ts", line: 3 }),
|
|
186
|
+
thread({ id: "T_empty", comments: { nodes: [] } }),
|
|
187
|
+
thread({ id: "T_a2", path: "a.ts", line: 9 }),
|
|
188
|
+
thread({ id: "T_a1", path: "a.ts", line: 2 }),
|
|
189
|
+
],
|
|
190
|
+
emptyPatches,
|
|
191
|
+
1,
|
|
192
|
+
);
|
|
193
|
+
expect(blocks.map((b) => [b.path, b.lineRange])).toEqual([
|
|
194
|
+
["a.ts", "2"],
|
|
195
|
+
["a.ts", "9"],
|
|
196
|
+
["b.ts", "3"],
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("falls back to the comment's diffHunk when the file is not in the PR patches", () => {
|
|
201
|
+
const blocks = buildThreadBlocks([thread()], emptyPatches, 1);
|
|
202
|
+
const content = blocks[0]?.content.join("\n") ?? "";
|
|
203
|
+
expect(content).toContain("+new");
|
|
204
|
+
expect(content).not.toContain("no diff context available");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("notes missing diff context when neither patches nor a diffHunk exist", () => {
|
|
208
|
+
const t = thread({ comments: { nodes: [comment({ diffHunk: "" })] } });
|
|
209
|
+
const blocks = buildThreadBlocks([t], emptyPatches, 1);
|
|
210
|
+
expect(blocks[0]?.content.join("\n")).toContain(
|
|
211
|
+
"(no diff context available - comment on unchanged lines)",
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("renders a multi-line range and a multi-hunk overlap with a gap indicator", () => {
|
|
216
|
+
const filePatch = [
|
|
217
|
+
"@@ -1,3 +1,3 @@",
|
|
218
|
+
" a",
|
|
219
|
+
"-b",
|
|
220
|
+
"+B",
|
|
221
|
+
" c",
|
|
222
|
+
"@@ -10,3 +10,3 @@",
|
|
223
|
+
" x",
|
|
224
|
+
"-y",
|
|
225
|
+
"+Y",
|
|
226
|
+
" z",
|
|
227
|
+
].join("\n");
|
|
228
|
+
const patches = new Map([["src/foo.ts", parseFilePatches(filePatch)]]);
|
|
229
|
+
const t = thread({ startLine: 2, line: 11 });
|
|
230
|
+
const blocks = buildThreadBlocks([t], patches, 1);
|
|
231
|
+
expect(blocks[0]?.lineRange).toBe("2-11");
|
|
232
|
+
const content = blocks[0]?.content.join("\n") ?? "";
|
|
233
|
+
expect(content).toContain("@@ -1,3 +1,3 @@");
|
|
234
|
+
expect(content).toContain("@@ -10,3 +10,3 @@");
|
|
235
|
+
expect(content).toContain("... (6 unchanged lines) ...");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("falls back to the first comment's anchors when the thread carries none", () => {
|
|
239
|
+
const t = thread({
|
|
240
|
+
line: null,
|
|
241
|
+
startLine: null,
|
|
242
|
+
comments: { nodes: [comment({ line: null, originalLine: 7 })] },
|
|
243
|
+
});
|
|
244
|
+
const blocks = buildThreadBlocks([t], new Map(), 1);
|
|
245
|
+
expect(blocks[0]?.lineRange).toBe("7");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("labels a missing author and comment body", () => {
|
|
249
|
+
const t = thread({
|
|
250
|
+
comments: { nodes: [comment({ author: null, body: "", fullDatabaseId: null })] },
|
|
251
|
+
});
|
|
252
|
+
const content = buildThreadBlocks([t], new Map(), 1)[0]?.content.join("\n") ?? "";
|
|
253
|
+
expect(content).toContain("author=unknown");
|
|
254
|
+
expect(content).toContain("id=unknown");
|
|
255
|
+
expect(content).toContain("(no comment body)");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// --- the MCP tools (fake octokit, no network) --------------------------------
|
|
260
|
+
|
|
261
|
+
type RawResult = Record<string, unknown>;
|
|
262
|
+
function runTool(
|
|
263
|
+
toolDef: { execute: unknown },
|
|
264
|
+
params: Record<string, unknown>,
|
|
265
|
+
): Promise<RawResult> {
|
|
266
|
+
const fn = toolDef.execute as (p: Record<string, unknown>) => Promise<RawResult>;
|
|
267
|
+
return fn(params);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function makeCtx(over: Record<string, unknown> = {}): ToolContext {
|
|
271
|
+
return {
|
|
272
|
+
repo: { owner: "octo", name: "repo" },
|
|
273
|
+
payload: { event: { trigger: "manual" } },
|
|
274
|
+
octokit: {},
|
|
275
|
+
tmpdir: "",
|
|
276
|
+
githubInstallationToken: "tok",
|
|
277
|
+
toolState: {},
|
|
278
|
+
...over,
|
|
279
|
+
} as unknown as ToolContext;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
describe("GetReviewCommentsTool", () => {
|
|
283
|
+
const savedTempDir = process.env.TERRAMEND_TEMP_DIR;
|
|
284
|
+
const scratchDirs: string[] = [];
|
|
285
|
+
|
|
286
|
+
function scratch(): string {
|
|
287
|
+
const dir = mkdtempSync(join(tmpdir(), "terramend-review-"));
|
|
288
|
+
scratchDirs.push(dir);
|
|
289
|
+
return dir;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
afterEach(() => {
|
|
293
|
+
if (savedTempDir === undefined) delete process.env.TERRAMEND_TEMP_DIR;
|
|
294
|
+
else process.env.TERRAMEND_TEMP_DIR = savedTempDir;
|
|
295
|
+
for (const dir of scratchDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const graphqlResponse = (threads: ReviewThread[]) => ({
|
|
299
|
+
repository: { pullRequest: { reviewThreads: { nodes: threads } } },
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const octokitFor = (threads: ReviewThread[], reviewBody = "Overall summary.") => ({
|
|
303
|
+
graphql: vi.fn().mockResolvedValue(graphqlResponse(threads)),
|
|
304
|
+
paginate: vi.fn().mockResolvedValue([{ filename: "src/foo.ts", patch: comment().diffHunk }]),
|
|
305
|
+
rest: {
|
|
306
|
+
pulls: {
|
|
307
|
+
getReview: vi.fn().mockResolvedValue({
|
|
308
|
+
data: { body: reviewBody, user: { login: "reviewer" } },
|
|
309
|
+
}),
|
|
310
|
+
listFiles: vi.fn(),
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("writes the formatted threads to TERRAMEND_TEMP_DIR and returns the TOC", async () => {
|
|
316
|
+
process.env.TERRAMEND_TEMP_DIR = scratch();
|
|
317
|
+
const ctx = makeCtx({ octokit: octokitFor([thread()]) });
|
|
318
|
+
|
|
319
|
+
const result = await runTool(GetReviewCommentsTool(ctx), {
|
|
320
|
+
pull_number: 5,
|
|
321
|
+
review_id: TARGET_REVIEW_ID,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(result).toMatchObject({
|
|
325
|
+
review_id: TARGET_REVIEW_ID,
|
|
326
|
+
pull_number: 5,
|
|
327
|
+
reviewer: "reviewer",
|
|
328
|
+
threadCount: 1,
|
|
329
|
+
});
|
|
330
|
+
const commentsPath = String(result.commentsPath);
|
|
331
|
+
expect(commentsPath).toBe(
|
|
332
|
+
join(process.env.TERRAMEND_TEMP_DIR ?? "", `review-${TARGET_REVIEW_ID}-threads.md`),
|
|
333
|
+
);
|
|
334
|
+
const written = readFileSync(commentsPath, "utf8");
|
|
335
|
+
expect(written).toContain("## src/foo.ts:12");
|
|
336
|
+
expect(written).toContain("## Review Body");
|
|
337
|
+
expect(result.toc).toContain("src/foo.ts:12");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("drops threads that belong to a different review", async () => {
|
|
341
|
+
process.env.TERRAMEND_TEMP_DIR = scratch();
|
|
342
|
+
const foreign = thread({
|
|
343
|
+
comments: {
|
|
344
|
+
nodes: [comment({ pullRequestReview: { databaseId: 1, author: { login: "x" } } })],
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
const ctx = makeCtx({ octokit: octokitFor([foreign], "") });
|
|
348
|
+
|
|
349
|
+
const result = await runTool(GetReviewCommentsTool(ctx), {
|
|
350
|
+
pull_number: 5,
|
|
351
|
+
review_id: TARGET_REVIEW_ID,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(result).toMatchObject({ threadCount: 0, commentsPath: null, toc: null });
|
|
355
|
+
expect(result.instructions).toMatch(/no threads found/);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("keeps only 👍-approved threads on a fix_review run with approved_only", async () => {
|
|
359
|
+
process.env.TERRAMEND_TEMP_DIR = scratch();
|
|
360
|
+
const approved = thread({
|
|
361
|
+
id: "T_approved",
|
|
362
|
+
path: "approved.ts",
|
|
363
|
+
comments: {
|
|
364
|
+
nodes: [
|
|
365
|
+
comment({
|
|
366
|
+
reactionGroups: [
|
|
367
|
+
{ content: "THUMBS_UP", reactors: { nodes: [{ login: "Alice" }] } },
|
|
368
|
+
{ content: "ROCKET", reactors: { nodes: [{ login: "bob" }] } },
|
|
369
|
+
],
|
|
370
|
+
}),
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
const unapproved = thread({
|
|
375
|
+
id: "T_plain",
|
|
376
|
+
path: "plain.ts",
|
|
377
|
+
comments: {
|
|
378
|
+
nodes: [
|
|
379
|
+
comment({
|
|
380
|
+
reactionGroups: [{ content: "THUMBS_UP", reactors: { nodes: [{ login: "bob" }] } }],
|
|
381
|
+
}),
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
const ctx = makeCtx({
|
|
386
|
+
payload: { event: { trigger: "fix_review", approved_only: true }, triggerer: "alice" },
|
|
387
|
+
octokit: octokitFor([approved, unapproved]),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const result = await runTool(GetReviewCommentsTool(ctx), {
|
|
391
|
+
pull_number: 5,
|
|
392
|
+
review_id: TARGET_REVIEW_ID,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
expect(result.threadCount).toBe(1);
|
|
396
|
+
expect(result.toc).toContain("approved.ts");
|
|
397
|
+
expect(result.toc).not.toContain("plain.ts");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("reports the 👍 filter when nothing matches", async () => {
|
|
401
|
+
process.env.TERRAMEND_TEMP_DIR = scratch();
|
|
402
|
+
const ctx = makeCtx({
|
|
403
|
+
payload: { event: { trigger: "fix_review", approved_only: true }, triggerer: "alice" },
|
|
404
|
+
octokit: octokitFor([thread()], ""),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const result = await runTool(GetReviewCommentsTool(ctx), {
|
|
408
|
+
pull_number: 5,
|
|
409
|
+
review_id: TARGET_REVIEW_ID,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(result.threadCount).toBe(0);
|
|
413
|
+
expect(result.instructions).toMatch(/no threads with 👍 from alice/);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe("ListPullRequestReviewsTool", () => {
|
|
418
|
+
it("lists reviews with their resolved bodies and metadata", async () => {
|
|
419
|
+
const ctx = makeCtx({
|
|
420
|
+
octokit: {
|
|
421
|
+
paginate: vi.fn().mockResolvedValue([
|
|
422
|
+
{
|
|
423
|
+
id: 1,
|
|
424
|
+
node_id: "N1",
|
|
425
|
+
body: "Looks good.",
|
|
426
|
+
state: "APPROVED",
|
|
427
|
+
user: { login: "alice" },
|
|
428
|
+
submitted_at: "2026-06-10T00:00:00Z",
|
|
429
|
+
commit_id: "abc",
|
|
430
|
+
html_url: "https://github.test/r/1",
|
|
431
|
+
},
|
|
432
|
+
]),
|
|
433
|
+
rest: { pulls: { listReviews: vi.fn() } },
|
|
434
|
+
},
|
|
435
|
+
tmpdir: "/tmp",
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const result = await runTool(ListPullRequestReviewsTool(ctx), { pull_number: 9 });
|
|
439
|
+
|
|
440
|
+
expect(result).toMatchObject({ pull_number: 9, count: 1 });
|
|
441
|
+
expect(result.reviews).toEqual([
|
|
442
|
+
expect.objectContaining({ id: 1, node_id: "N1", body: "Looks good.", state: "APPROVED" }),
|
|
443
|
+
]);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("ResolveReviewThreadTool", () => {
|
|
448
|
+
it("resolves a thread via the GraphQL mutation", async () => {
|
|
449
|
+
const graphql = vi
|
|
450
|
+
.fn()
|
|
451
|
+
.mockResolvedValue({ resolveReviewThread: { thread: { id: "T_1", isResolved: true } } });
|
|
452
|
+
const ctx = makeCtx({ octokit: { graphql } });
|
|
453
|
+
|
|
454
|
+
const result = await runTool(ResolveReviewThreadTool(ctx), { thread_id: "T_1" });
|
|
455
|
+
|
|
456
|
+
expect(result).toMatchObject({ success: true, thread_id: "T_1", is_resolved: true });
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("treats an already-resolved thread as success", async () => {
|
|
460
|
+
const graphql = vi.fn().mockRejectedValue(new Error("Thread is already resolved"));
|
|
461
|
+
const ctx = makeCtx({ octokit: { graphql } });
|
|
462
|
+
|
|
463
|
+
const result = await runTool(ResolveReviewThreadTool(ctx), { thread_id: "T_1" });
|
|
464
|
+
|
|
465
|
+
expect(result).toMatchObject({ success: true, is_resolved: true });
|
|
466
|
+
expect(result.message).toMatch(/already resolved/);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("reports a failure without throwing for other errors", async () => {
|
|
470
|
+
const graphql = vi.fn().mockRejectedValue(new Error("Resource not accessible"));
|
|
471
|
+
const ctx = makeCtx({ octokit: { graphql } });
|
|
472
|
+
|
|
473
|
+
const result = await runTool(ResolveReviewThreadTool(ctx), { thread_id: "T_9" });
|
|
474
|
+
|
|
475
|
+
expect(result).toMatchObject({ success: false, is_resolved: false });
|
|
476
|
+
expect(result.message).toMatch(/failed to resolve thread T_9/);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe("formatReviewThreads", () => {
|
|
481
|
+
it("omits the TOC for a body-only review and includes the review body", () => {
|
|
482
|
+
const { toc, content } = formatReviewThreads([], {
|
|
483
|
+
pullNumber: 9,
|
|
484
|
+
reviewId: 11,
|
|
485
|
+
reviewer: "alice",
|
|
486
|
+
reviewBody: "Overall LGTM with nits.",
|
|
487
|
+
});
|
|
488
|
+
expect(toc).toBe("");
|
|
489
|
+
expect(content).toContain("# Review Threads (0) for PR #9 - Review 11 by alice");
|
|
490
|
+
expect(content).not.toContain("## TOC");
|
|
491
|
+
expect(content).toContain("## Review Body");
|
|
492
|
+
expect(content).toContain("Overall LGTM with nits.");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("computes TOC line numbers that point at each block's actual position", () => {
|
|
496
|
+
const blocks = [
|
|
497
|
+
{ path: "a.ts", lineRange: "1", content: ["## a.ts:1", "", "body a"] },
|
|
498
|
+
{ path: "b.ts", lineRange: "2", content: ["## b.ts:2", "", "body b", ""] },
|
|
499
|
+
];
|
|
500
|
+
const { toc, content } = formatReviewThreads(blocks, {
|
|
501
|
+
pullNumber: 1,
|
|
502
|
+
reviewId: 2,
|
|
503
|
+
reviewer: "r",
|
|
504
|
+
});
|
|
505
|
+
const lines = content.split("\n");
|
|
506
|
+
const entries = toc.split("\n");
|
|
507
|
+
expect(entries).toEqual(["- a.ts:1 → lines 10-12", "- b.ts:2 → lines 13-16"]);
|
|
508
|
+
// line numbers are 1-based: lines[9] is line 10.
|
|
509
|
+
expect(lines[9]).toBe("## a.ts:1");
|
|
510
|
+
expect(lines[12]).toBe("## b.ts:2");
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe("formatReactionCounts", () => {
|
|
515
|
+
const reacted = (groups: Array<{ content: string; total: number }>): ReviewThreadComment =>
|
|
516
|
+
comment({
|
|
517
|
+
reactionGroups: groups.map((g) => ({
|
|
518
|
+
content: g.content,
|
|
519
|
+
reactors: { totalCount: g.total, nodes: [] },
|
|
520
|
+
})),
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("renders thumbs counts and omits the tag when there are none", () => {
|
|
524
|
+
expect(formatReactionCounts(comment())).toBe("");
|
|
525
|
+
expect(formatReactionCounts(reacted([{ content: "THUMBS_UP", total: 0 }]))).toBe("");
|
|
526
|
+
expect(
|
|
527
|
+
formatReactionCounts(
|
|
528
|
+
reacted([
|
|
529
|
+
{ content: "THUMBS_UP", total: 2 },
|
|
530
|
+
{ content: "THUMBS_DOWN", total: 1 },
|
|
531
|
+
// non-thumbs reactions carry no accept/reject semantics - ignored.
|
|
532
|
+
{ content: "ROCKET", total: 5 },
|
|
533
|
+
]),
|
|
534
|
+
),
|
|
535
|
+
).toBe(" reactions=\u{1F44D}2,\u{1F44E}1");
|
|
536
|
+
expect(formatReactionCounts(reacted([{ content: "THUMBS_DOWN", total: 3 }]))).toBe(
|
|
537
|
+
" reactions=\u{1F44E}3",
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("lands in the comment tag line of a thread block", () => {
|
|
542
|
+
const t = thread({
|
|
543
|
+
comments: { nodes: [reacted([{ content: "THUMBS_DOWN", total: 1 }])] },
|
|
544
|
+
});
|
|
545
|
+
const blocks = buildThreadBlocks([t], new Map(), TARGET_REVIEW_ID);
|
|
546
|
+
const tagLine = blocks[0]?.content.find((l) => l.startsWith("````comment"));
|
|
547
|
+
expect(tagLine).toContain("reactions=\u{1F44E}1");
|
|
548
|
+
});
|
|
549
|
+
});
|