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, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
buildLearningsReflectionPrompt,
|
|
7
|
+
buildPostRunPrompt,
|
|
8
|
+
buildStopHookPrompt,
|
|
9
|
+
buildSummaryStalePrompt,
|
|
10
|
+
buildUnsubmittedReviewPrompt,
|
|
11
|
+
collectPostRunIssues,
|
|
12
|
+
finalizeAgentResult,
|
|
13
|
+
getUnsubmittedReview,
|
|
14
|
+
runPostRunRetryLoop,
|
|
15
|
+
shouldRunReflection,
|
|
16
|
+
} from "#app/agents/postRun";
|
|
17
|
+
import {
|
|
18
|
+
type AgentResult,
|
|
19
|
+
type AgentRunContext,
|
|
20
|
+
type AgentUsage,
|
|
21
|
+
getGitStatus,
|
|
22
|
+
MAX_POST_RUN_RETRIES,
|
|
23
|
+
} from "#app/agents/shared";
|
|
24
|
+
import type { ToolState } from "#app/toolState";
|
|
25
|
+
|
|
26
|
+
// getGitStatus shells out to `git status --porcelain` in the test runner's
|
|
27
|
+
// cwd, which is the (frequently dirty) dev checkout — mock it so the
|
|
28
|
+
// dirty-tree gate is deterministic. everything else stays real.
|
|
29
|
+
vi.mock("#app/agents/shared", async (importOriginal) => {
|
|
30
|
+
const actual = await importOriginal<typeof import("#app/agents/shared")>();
|
|
31
|
+
return { ...actual, getGitStatus: vi.fn(() => "") };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const getGitStatusMock = vi.mocked(getGitStatus);
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
getGitStatusMock.mockReset();
|
|
38
|
+
getGitStatusMock.mockReturnValue("");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const tempDir = mkdtempSync(join(tmpdir(), "terramend-postrun-"));
|
|
42
|
+
|
|
43
|
+
afterAll(() => {
|
|
44
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function makeToolState(overrides: Partial<ToolState> = {}): ToolState {
|
|
48
|
+
return {
|
|
49
|
+
progressComment: undefined,
|
|
50
|
+
hadProgressComment: true,
|
|
51
|
+
prepushFailureCount: 0,
|
|
52
|
+
backgroundProcesses: new Map(),
|
|
53
|
+
usageEntries: [],
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeCtx(overrides: Partial<ToolState> = {}): AgentRunContext {
|
|
59
|
+
// runPostRunRetryLoop / collectPostRunIssues / finalizeAgentResult only read
|
|
60
|
+
// `ctx.toolState`, so a minimal context is sufficient for these tests.
|
|
61
|
+
return { toolState: makeToolState(overrides) } as unknown as AgentRunContext;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeUsage(n: number): AgentUsage {
|
|
65
|
+
return { agent: "test", inputTokens: n, outputTokens: n };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("getUnsubmittedReview", () => {
|
|
69
|
+
it("returns null when mode is not a review mode", () => {
|
|
70
|
+
expect(getUnsubmittedReview(makeToolState({ selectedMode: "Build" }))).toBeNull();
|
|
71
|
+
expect(getUnsubmittedReview(makeToolState())).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns null when a review was already submitted", () => {
|
|
75
|
+
expect(
|
|
76
|
+
getUnsubmittedReview(
|
|
77
|
+
makeToolState({
|
|
78
|
+
selectedMode: "Review",
|
|
79
|
+
review: { id: 1, nodeId: "n", reviewedSha: undefined },
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("fires for Review even when report_progress wrote a final summary", () => {
|
|
86
|
+
// Review's only valid exit is `create_pull_request_review`. a summary
|
|
87
|
+
// comment is not a substitute, and accepting it here previously let
|
|
88
|
+
// subagent-flipped `finalSummaryWritten` silence the gate.
|
|
89
|
+
expect(
|
|
90
|
+
getUnsubmittedReview(makeToolState({ selectedMode: "Review", finalSummaryWritten: true })),
|
|
91
|
+
).toBe("Review");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns null for IncrementalReview when report_progress wrote a final summary", () => {
|
|
95
|
+
// IncrementalReview treats `report_progress` as a legitimate
|
|
96
|
+
// "no review warranted" exit, matching the post-failure error message.
|
|
97
|
+
expect(
|
|
98
|
+
getUnsubmittedReview(
|
|
99
|
+
makeToolState({ selectedMode: "IncrementalReview", finalSummaryWritten: true }),
|
|
100
|
+
),
|
|
101
|
+
).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns null when there is no progress comment to anchor the failure to", () => {
|
|
105
|
+
expect(
|
|
106
|
+
getUnsubmittedReview(makeToolState({ selectedMode: "Review", hadProgressComment: false })),
|
|
107
|
+
).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns the selected mode when the gate should fire", () => {
|
|
111
|
+
expect(getUnsubmittedReview(makeToolState({ selectedMode: "Review" }))).toBe("Review");
|
|
112
|
+
expect(getUnsubmittedReview(makeToolState({ selectedMode: "IncrementalReview" }))).toBe(
|
|
113
|
+
"IncrementalReview",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("buildStopHookPrompt", () => {
|
|
119
|
+
it("embeds the exit code and the hook output in a fenced block", () => {
|
|
120
|
+
const prompt = buildStopHookPrompt({ exitCode: 3, output: "lint failed: 2 errors" });
|
|
121
|
+
expect(prompt).toContain("exited with code 3");
|
|
122
|
+
expect(prompt).toContain("```\nlint failed: 2 errors\n```");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("falls back to (no output) when the hook produced nothing", () => {
|
|
126
|
+
expect(buildStopHookPrompt({ exitCode: 1, output: "" })).toContain("(no output)");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("buildSummaryStalePrompt", () => {
|
|
131
|
+
it("names the summary file path", () => {
|
|
132
|
+
const prompt = buildSummaryStalePrompt("/tmp/run/summary.md");
|
|
133
|
+
expect(prompt).toContain("PR SUMMARY UNTOUCHED");
|
|
134
|
+
expect(prompt).toContain("`/tmp/run/summary.md`");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("buildUnsubmittedReviewPrompt", () => {
|
|
139
|
+
it("Review variant demands create_pull_request_review and offers no report_progress exit", () => {
|
|
140
|
+
const prompt = buildUnsubmittedReviewPrompt("Review");
|
|
141
|
+
expect(prompt).toContain("selected Review mode");
|
|
142
|
+
expect(prompt).toContain("`create_pull_request_review`");
|
|
143
|
+
expect(prompt).not.toContain("report_progress");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("IncrementalReview variant offers the report_progress no-findings exit", () => {
|
|
147
|
+
const prompt = buildUnsubmittedReviewPrompt("IncrementalReview");
|
|
148
|
+
expect(prompt).toContain("selected IncrementalReview mode");
|
|
149
|
+
expect(prompt).toContain("`create_pull_request_review`");
|
|
150
|
+
expect(prompt).toContain("`report_progress`");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("buildPostRunPrompt", () => {
|
|
155
|
+
it("returns an empty string when there are no issues", () => {
|
|
156
|
+
expect(buildPostRunPrompt({})).toBe("");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("orders sections stopHook, unsubmittedReview, dirtyTree, summaryStale", () => {
|
|
160
|
+
const prompt = buildPostRunPrompt({
|
|
161
|
+
stopHook: { exitCode: 2, output: "hook says no" },
|
|
162
|
+
unsubmittedReview: "Review",
|
|
163
|
+
dirtyTree: "M src/index.ts",
|
|
164
|
+
summaryStale: { filePath: "/tmp/summary.md" },
|
|
165
|
+
});
|
|
166
|
+
const hook = prompt.indexOf("STOP HOOK FAILED");
|
|
167
|
+
const review = prompt.indexOf("MISSING REVIEW OUTPUT");
|
|
168
|
+
const tree = prompt.indexOf("UNCOMMITTED CHANGES");
|
|
169
|
+
const summary = prompt.indexOf("PR SUMMARY UNTOUCHED");
|
|
170
|
+
expect(hook).toBeGreaterThanOrEqual(0);
|
|
171
|
+
expect(review).toBeGreaterThan(hook);
|
|
172
|
+
expect(tree).toBeGreaterThan(review);
|
|
173
|
+
expect(summary).toBeGreaterThan(tree);
|
|
174
|
+
expect(prompt).toContain("\n\n---\n\n");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("renders a single section without separators", () => {
|
|
178
|
+
const prompt = buildPostRunPrompt({ dirtyTree: "?? new-file.txt" });
|
|
179
|
+
expect(prompt).toContain("?? new-file.txt");
|
|
180
|
+
expect(prompt).not.toContain("---");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("collectPostRunIssues", () => {
|
|
185
|
+
it("flags a dirty tree in a committing mode", async () => {
|
|
186
|
+
getGitStatusMock.mockReturnValue("M src/a.ts");
|
|
187
|
+
const issues = await collectPostRunIssues(makeCtx({ selectedMode: "Build" }));
|
|
188
|
+
expect(issues.dirtyTree).toBe("M src/a.ts");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("flags a dirty tree when no mode was selected", async () => {
|
|
192
|
+
getGitStatusMock.mockReturnValue("M src/a.ts");
|
|
193
|
+
const issues = await collectPostRunIssues(makeCtx());
|
|
194
|
+
expect(issues.dirtyTree).toBe("M src/a.ts");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("suppresses the dirty-tree gate in non-committing modes", async () => {
|
|
198
|
+
getGitStatusMock.mockReturnValue("M src/a.ts");
|
|
199
|
+
const issues = await collectPostRunIssues(
|
|
200
|
+
makeCtx({ selectedMode: "Plan", hadProgressComment: false }),
|
|
201
|
+
);
|
|
202
|
+
expect(issues.dirtyTree).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("reports nothing on a clean tree without summary/review state", async () => {
|
|
206
|
+
const issues = await collectPostRunIssues(makeCtx({ selectedMode: "Build" }));
|
|
207
|
+
expect(issues).toEqual({});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("flags a stale summary when the file is byte-identical to its seed", async () => {
|
|
211
|
+
const filePath = join(tempDir, "summary-stale.md");
|
|
212
|
+
writeFileSync(filePath, "seed content");
|
|
213
|
+
const issues = await collectPostRunIssues(
|
|
214
|
+
makeCtx({ summaryFilePath: filePath, summarySeed: "seed content" }),
|
|
215
|
+
);
|
|
216
|
+
expect(issues.summaryStale).toEqual({ filePath });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("does not flag the summary when the agent edited it", async () => {
|
|
220
|
+
const filePath = join(tempDir, "summary-edited.md");
|
|
221
|
+
writeFileSync(filePath, "agent rewrote this");
|
|
222
|
+
const issues = await collectPostRunIssues(
|
|
223
|
+
makeCtx({ summaryFilePath: filePath, summarySeed: "seed content" }),
|
|
224
|
+
);
|
|
225
|
+
expect(issues.summaryStale).toBeUndefined();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("does not flag the summary when the file is missing", async () => {
|
|
229
|
+
const issues = await collectPostRunIssues(
|
|
230
|
+
makeCtx({ summaryFilePath: join(tempDir, "missing.md"), summarySeed: "seed" }),
|
|
231
|
+
);
|
|
232
|
+
expect(issues.summaryStale).toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("skips the summary-stale check when skipSummaryStale is set", async () => {
|
|
236
|
+
const filePath = join(tempDir, "summary-skipped.md");
|
|
237
|
+
writeFileSync(filePath, "seed content");
|
|
238
|
+
const issues = await collectPostRunIssues(
|
|
239
|
+
makeCtx({ summaryFilePath: filePath, summarySeed: "seed content" }),
|
|
240
|
+
{ skipSummaryStale: true },
|
|
241
|
+
);
|
|
242
|
+
expect(issues.summaryStale).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("flags an unsubmitted review and suppresses the dirty tree for Review mode", async () => {
|
|
246
|
+
getGitStatusMock.mockReturnValue("M src/a.ts");
|
|
247
|
+
const issues = await collectPostRunIssues(makeCtx({ selectedMode: "Review" }));
|
|
248
|
+
expect(issues.unsubmittedReview).toBe("Review");
|
|
249
|
+
expect(issues.dirtyTree).toBeUndefined();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("finalizeAgentResult", () => {
|
|
254
|
+
it("returns a failed result untouched", async () => {
|
|
255
|
+
const result: AgentResult = { success: false, error: "agent crashed" };
|
|
256
|
+
expect(await finalizeAgentResult({ ctx: makeCtx({ selectedMode: "Review" }), result })).toBe(
|
|
257
|
+
result,
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("returns a successful result untouched when the hard gates are clean", async () => {
|
|
262
|
+
const result: AgentResult = { success: true, output: "done" };
|
|
263
|
+
expect(await finalizeAgentResult({ ctx: makeCtx({ selectedMode: "Build" }), result })).toBe(
|
|
264
|
+
result,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("flips success to failed for an unsubmitted Review", async () => {
|
|
269
|
+
const input: AgentResult = { success: true, output: "done" };
|
|
270
|
+
const result = await finalizeAgentResult({
|
|
271
|
+
ctx: makeCtx({ selectedMode: "Review" }),
|
|
272
|
+
result: input,
|
|
273
|
+
});
|
|
274
|
+
expect(result.success).toBe(false);
|
|
275
|
+
expect(result.error).toBe("Review mode finished without calling create_pull_request_review");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("mentions the report_progress exit for IncrementalReview", async () => {
|
|
279
|
+
const input: AgentResult = { success: true, output: "done" };
|
|
280
|
+
const result = await finalizeAgentResult({
|
|
281
|
+
ctx: makeCtx({ selectedMode: "IncrementalReview" }),
|
|
282
|
+
result: input,
|
|
283
|
+
});
|
|
284
|
+
expect(result.success).toBe(false);
|
|
285
|
+
expect(result.error).toBe(
|
|
286
|
+
"IncrementalReview mode finished without calling create_pull_request_review or report_progress",
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("never flips success on soft gates (dirty tree, stale summary)", async () => {
|
|
291
|
+
getGitStatusMock.mockReturnValue("M src/a.ts");
|
|
292
|
+
const filePath = join(tempDir, "summary-finalize.md");
|
|
293
|
+
writeFileSync(filePath, "seed content");
|
|
294
|
+
const result = await finalizeAgentResult({
|
|
295
|
+
ctx: makeCtx({
|
|
296
|
+
selectedMode: "Build",
|
|
297
|
+
summaryFilePath: filePath,
|
|
298
|
+
summarySeed: "seed content",
|
|
299
|
+
}),
|
|
300
|
+
result: { success: true, output: "done" },
|
|
301
|
+
});
|
|
302
|
+
expect(result.success).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("shouldRunReflection", () => {
|
|
307
|
+
it("runs reflection when no mode was selected", () => {
|
|
308
|
+
expect(shouldRunReflection(undefined)).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("runs reflection for high-novelty modes", () => {
|
|
312
|
+
expect(shouldRunReflection("Build")).toBe(true);
|
|
313
|
+
expect(shouldRunReflection("Review")).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("skips reflection for IncrementalReview", () => {
|
|
317
|
+
expect(shouldRunReflection("IncrementalReview")).toBe(false);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("buildLearningsReflectionPrompt", () => {
|
|
322
|
+
it("names the learnings file and forbids set_output", () => {
|
|
323
|
+
const prompt = buildLearningsReflectionPrompt("/tmp/run/learnings.md");
|
|
324
|
+
expect(prompt).toContain("REFLECTION");
|
|
325
|
+
expect(prompt).toContain("`/tmp/run/learnings.md`");
|
|
326
|
+
expect(prompt).toContain("do NOT call `set_output`");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("runPostRunRetryLoop", () => {
|
|
331
|
+
// pin `R` to plain AgentResult so per-test resume mocks can return success
|
|
332
|
+
// and failure shapes without fighting generic inference.
|
|
333
|
+
type ResumeFn = (c: { prompt: string; previousResult: AgentResult }) => Promise<AgentResult>;
|
|
334
|
+
|
|
335
|
+
it("returns the initial result with usage attached when the gates are clean", async () => {
|
|
336
|
+
const resume = vi.fn<ResumeFn>(async () => ({ success: true, output: "resumed" }));
|
|
337
|
+
const result = await runPostRunRetryLoop({
|
|
338
|
+
ctx: makeCtx({ selectedMode: "Build" }),
|
|
339
|
+
initialResult: { success: true, output: "task output" },
|
|
340
|
+
initialUsage: makeUsage(1),
|
|
341
|
+
resume,
|
|
342
|
+
});
|
|
343
|
+
expect(resume).not.toHaveBeenCalled();
|
|
344
|
+
expect(result.success).toBe(true);
|
|
345
|
+
expect(result.output).toBe("task output");
|
|
346
|
+
expect(result.usage).toEqual(makeUsage(1));
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("short-circuits on a failed initial result without running the gates", async () => {
|
|
350
|
+
const resume = vi.fn<ResumeFn>(async () => ({ success: true, output: "resumed" }));
|
|
351
|
+
const result = await runPostRunRetryLoop({
|
|
352
|
+
ctx: makeCtx({ selectedMode: "Build" }),
|
|
353
|
+
initialResult: { success: false, error: "agent crashed" },
|
|
354
|
+
initialUsage: makeUsage(7),
|
|
355
|
+
resume,
|
|
356
|
+
});
|
|
357
|
+
expect(resume).not.toHaveBeenCalled();
|
|
358
|
+
expect(result.success).toBe(false);
|
|
359
|
+
expect(result.error).toBe("agent crashed");
|
|
360
|
+
expect(result.usage).toEqual(makeUsage(7));
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("resumes once for a dirty tree and succeeds when the tree comes back clean", async () => {
|
|
364
|
+
getGitStatusMock.mockReturnValueOnce("M src/a.ts");
|
|
365
|
+
const resume = vi.fn<ResumeFn>(async () => ({
|
|
366
|
+
success: true,
|
|
367
|
+
output: "committed",
|
|
368
|
+
usage: makeUsage(2),
|
|
369
|
+
}));
|
|
370
|
+
const result = await runPostRunRetryLoop({
|
|
371
|
+
ctx: makeCtx({ selectedMode: "Build" }),
|
|
372
|
+
initialResult: { success: true, output: "task output", usage: makeUsage(1) },
|
|
373
|
+
initialUsage: makeUsage(1),
|
|
374
|
+
resume,
|
|
375
|
+
});
|
|
376
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
377
|
+
const call = resume.mock.calls[0]?.[0] as unknown as { prompt: string };
|
|
378
|
+
expect(call.prompt).toContain("UNCOMMITTED CHANGES");
|
|
379
|
+
expect(call.prompt).toContain("M src/a.ts");
|
|
380
|
+
expect(result.success).toBe(true);
|
|
381
|
+
expect(result.usage).toEqual({ agent: "test", inputTokens: 3, outputTokens: 3 });
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("hard-fails after exhausting retries on a still-unsubmitted review", async () => {
|
|
385
|
+
const resume = vi.fn<ResumeFn>(async () => ({
|
|
386
|
+
success: true,
|
|
387
|
+
output: "still no review",
|
|
388
|
+
usage: makeUsage(1),
|
|
389
|
+
}));
|
|
390
|
+
const result = await runPostRunRetryLoop({
|
|
391
|
+
ctx: makeCtx({ selectedMode: "Review" }),
|
|
392
|
+
initialResult: { success: true, output: "task output", usage: makeUsage(1) },
|
|
393
|
+
initialUsage: makeUsage(1),
|
|
394
|
+
resume,
|
|
395
|
+
});
|
|
396
|
+
expect(resume).toHaveBeenCalledTimes(MAX_POST_RUN_RETRIES);
|
|
397
|
+
expect(result.success).toBe(false);
|
|
398
|
+
expect(result.error).toBe(
|
|
399
|
+
"Review mode finished without calling create_pull_request_review " +
|
|
400
|
+
`after ${MAX_POST_RUN_RETRIES} retry attempts`,
|
|
401
|
+
);
|
|
402
|
+
expect(result.usage).toEqual({
|
|
403
|
+
agent: "test",
|
|
404
|
+
inputTokens: 1 + MAX_POST_RUN_RETRIES,
|
|
405
|
+
outputTokens: 1 + MAX_POST_RUN_RETRIES,
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("hard-fails without a retry note when the session cannot resume at all", async () => {
|
|
410
|
+
const resume = vi.fn<ResumeFn>(async () => ({ success: true, output: "resumed" }));
|
|
411
|
+
const result = await runPostRunRetryLoop({
|
|
412
|
+
ctx: makeCtx({ selectedMode: "Review" }),
|
|
413
|
+
initialResult: { success: true, output: "task output" },
|
|
414
|
+
initialUsage: undefined,
|
|
415
|
+
resume,
|
|
416
|
+
canResume: () => false,
|
|
417
|
+
});
|
|
418
|
+
expect(resume).not.toHaveBeenCalled();
|
|
419
|
+
expect(result.success).toBe(false);
|
|
420
|
+
expect(result.error).toBe("Review mode finished without calling create_pull_request_review");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("uses the singular retry note when canResume flips false after one attempt", async () => {
|
|
424
|
+
const resume = vi.fn<ResumeFn>(async () => ({ success: true, output: "second" }));
|
|
425
|
+
const result = await runPostRunRetryLoop({
|
|
426
|
+
ctx: makeCtx({ selectedMode: "Review" }),
|
|
427
|
+
initialResult: { success: true, output: "first" },
|
|
428
|
+
initialUsage: undefined,
|
|
429
|
+
resume,
|
|
430
|
+
canResume: (r: AgentResult) => r.output !== "second",
|
|
431
|
+
});
|
|
432
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
433
|
+
expect(result.success).toBe(false);
|
|
434
|
+
expect(result.error).toBe(
|
|
435
|
+
"Review mode finished without calling create_pull_request_review after 1 retry attempt",
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("delivers the reflection prompt once and keeps the task output and set_output", async () => {
|
|
440
|
+
const ctx = makeCtx({ selectedMode: "Build" });
|
|
441
|
+
ctx.toolState.output = "task set_output value";
|
|
442
|
+
const resume = vi.fn(
|
|
443
|
+
async (_c: { prompt: string; previousResult: AgentResult }): Promise<AgentResult> => {
|
|
444
|
+
// simulate a model clobbering set_output during the reflection turn
|
|
445
|
+
ctx.toolState.output = "done";
|
|
446
|
+
return { success: true, output: "updated learnings", usage: makeUsage(2) };
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
const result = await runPostRunRetryLoop({
|
|
450
|
+
ctx,
|
|
451
|
+
initialResult: { success: true, output: "task output", usage: makeUsage(1) },
|
|
452
|
+
initialUsage: makeUsage(1),
|
|
453
|
+
resume,
|
|
454
|
+
reflectionPrompt: "REFLECT NOW",
|
|
455
|
+
});
|
|
456
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
457
|
+
expect(resume.mock.calls[0]?.[0]?.prompt).toBe("REFLECT NOW");
|
|
458
|
+
expect(result.success).toBe(true);
|
|
459
|
+
expect(result.output).toBe("task output");
|
|
460
|
+
expect(ctx.toolState.output).toBe("task set_output value");
|
|
461
|
+
expect(result.usage).toEqual({ agent: "test", inputTokens: 3, outputTokens: 3 });
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("falls through to the reflection reply when the task turn produced no output", async () => {
|
|
465
|
+
const resume = vi.fn<ResumeFn>(async () => ({ success: true, output: "reflection reply" }));
|
|
466
|
+
const result = await runPostRunRetryLoop({
|
|
467
|
+
ctx: makeCtx({ selectedMode: "Build" }),
|
|
468
|
+
initialResult: { success: true, output: "" },
|
|
469
|
+
initialUsage: undefined,
|
|
470
|
+
resume,
|
|
471
|
+
reflectionPrompt: "REFLECT NOW",
|
|
472
|
+
});
|
|
473
|
+
expect(result.success).toBe(true);
|
|
474
|
+
expect(result.output).toBe("reflection reply");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("preserves the prior successful result when the reflection turn fails", async () => {
|
|
478
|
+
const resume = vi.fn<ResumeFn>(async () => ({
|
|
479
|
+
success: false,
|
|
480
|
+
error: "provider blew up",
|
|
481
|
+
usage: makeUsage(5),
|
|
482
|
+
}));
|
|
483
|
+
const result = await runPostRunRetryLoop({
|
|
484
|
+
ctx: makeCtx({ selectedMode: "Build" }),
|
|
485
|
+
initialResult: { success: true, output: "task output", usage: makeUsage(1) },
|
|
486
|
+
initialUsage: makeUsage(1),
|
|
487
|
+
resume,
|
|
488
|
+
reflectionPrompt: "REFLECT NOW",
|
|
489
|
+
});
|
|
490
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
491
|
+
expect(result.success).toBe(true);
|
|
492
|
+
expect(result.output).toBe("task output");
|
|
493
|
+
expect(result.usage).toEqual({ agent: "test", inputTokens: 6, outputTokens: 6 });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("skips the reflection turn when the session cannot resume", async () => {
|
|
497
|
+
const resume = vi.fn<ResumeFn>(async () => ({ success: true, output: "resumed" }));
|
|
498
|
+
const result = await runPostRunRetryLoop({
|
|
499
|
+
ctx: makeCtx({ selectedMode: "Build" }),
|
|
500
|
+
initialResult: { success: true, output: "task output" },
|
|
501
|
+
initialUsage: undefined,
|
|
502
|
+
resume,
|
|
503
|
+
canResume: () => false,
|
|
504
|
+
reflectionPrompt: "REFLECT NOW",
|
|
505
|
+
});
|
|
506
|
+
expect(resume).not.toHaveBeenCalled();
|
|
507
|
+
expect(result.success).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("preserves the successful result when a summary-stale-only resume fails", async () => {
|
|
511
|
+
const filePath = join(tempDir, "summary-loop-fail.md");
|
|
512
|
+
writeFileSync(filePath, "seed content");
|
|
513
|
+
const resume = vi.fn<ResumeFn>(async () => ({ success: false, error: "resume exploded" }));
|
|
514
|
+
const result = await runPostRunRetryLoop({
|
|
515
|
+
ctx: makeCtx({
|
|
516
|
+
selectedMode: "Plan",
|
|
517
|
+
hadProgressComment: false,
|
|
518
|
+
summaryFilePath: filePath,
|
|
519
|
+
summarySeed: "seed content",
|
|
520
|
+
}),
|
|
521
|
+
initialResult: { success: true, output: "task output" },
|
|
522
|
+
initialUsage: undefined,
|
|
523
|
+
resume,
|
|
524
|
+
});
|
|
525
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
526
|
+
expect(resume.mock.calls[0]?.[0]?.prompt).toContain("PR SUMMARY UNTOUCHED");
|
|
527
|
+
expect(result.success).toBe(true);
|
|
528
|
+
expect(result.output).toBe("task output");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("nudges a stale summary at most once per run", async () => {
|
|
532
|
+
const filePath = join(tempDir, "summary-loop-once.md");
|
|
533
|
+
writeFileSync(filePath, "seed content");
|
|
534
|
+
const resume = vi.fn<ResumeFn>(async () => ({ success: true, output: "considered it" }));
|
|
535
|
+
const result = await runPostRunRetryLoop({
|
|
536
|
+
ctx: makeCtx({
|
|
537
|
+
selectedMode: "Plan",
|
|
538
|
+
hadProgressComment: false,
|
|
539
|
+
summaryFilePath: filePath,
|
|
540
|
+
summarySeed: "seed content",
|
|
541
|
+
}),
|
|
542
|
+
initialResult: { success: true, output: "task output" },
|
|
543
|
+
initialUsage: undefined,
|
|
544
|
+
resume,
|
|
545
|
+
});
|
|
546
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
547
|
+
expect(result.success).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
});
|