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,268 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { installPythonDependencies } from "#app/prep/installPythonDependencies";
|
|
4
|
+
import type { PrepOptions } from "#app/prep/types";
|
|
5
|
+
|
|
6
|
+
vi.mock("node:fs", () => ({
|
|
7
|
+
existsSync: vi.fn(() => false),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("#app/utils/cli", () => ({
|
|
11
|
+
log: {
|
|
12
|
+
info: vi.fn(),
|
|
13
|
+
debug: vi.fn(),
|
|
14
|
+
warning: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
success: vi.fn(),
|
|
17
|
+
startGroup: vi.fn(),
|
|
18
|
+
endGroup: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("#app/utils/subprocess", () => ({
|
|
23
|
+
spawn: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import type { PythonPrepResult } from "#app/prep/types";
|
|
28
|
+
import { spawn } from "#app/utils/subprocess";
|
|
29
|
+
|
|
30
|
+
const existsSyncMock = vi.mocked(existsSync);
|
|
31
|
+
const spawnMock = vi.mocked(spawn);
|
|
32
|
+
|
|
33
|
+
const options: PrepOptions = { ignoreScripts: false, binDir: "/tmp/run/pm-bin" };
|
|
34
|
+
|
|
35
|
+
type SpawnArg = Parameters<typeof spawn>[0];
|
|
36
|
+
|
|
37
|
+
function spawnResult(init: { exitCode?: number; stdout?: string; stderr?: string } = {}) {
|
|
38
|
+
return {
|
|
39
|
+
exitCode: init.exitCode ?? 0,
|
|
40
|
+
stdout: init.stdout ?? "",
|
|
41
|
+
stderr: init.stderr ?? "",
|
|
42
|
+
durationMs: 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** make only the given files exist in cwd */
|
|
47
|
+
function filesPresent(...names: string[]) {
|
|
48
|
+
const paths = new Set(names.map((name) => join(process.cwd(), name)));
|
|
49
|
+
existsSyncMock.mockImplementation((path) => paths.has(String(path)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** answer `which <name>` probes by availability; everything else by handler */
|
|
53
|
+
function routeSpawn(params: {
|
|
54
|
+
available?: string[];
|
|
55
|
+
handlers?: Record<string, (call: SpawnArg) => ReturnType<typeof spawnResult>>;
|
|
56
|
+
}) {
|
|
57
|
+
const available = new Set(params.available ?? []);
|
|
58
|
+
spawnMock.mockImplementation(async (call) => {
|
|
59
|
+
if (call.cmd === "which") {
|
|
60
|
+
return spawnResult({ exitCode: available.has(call.args[0] ?? "") ? 0 : 1 });
|
|
61
|
+
}
|
|
62
|
+
const handler = params.handlers?.[call.cmd];
|
|
63
|
+
if (!handler) throw new Error(`unexpected spawn: ${call.cmd} ${call.args.join(" ")}`);
|
|
64
|
+
return handler(call);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.clearAllMocks();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** run() is typed as the PrepResult union; narrow to the python shape */
|
|
73
|
+
async function runPython(opts: PrepOptions): Promise<PythonPrepResult> {
|
|
74
|
+
const result = await installPythonDependencies.run(opts);
|
|
75
|
+
if (result.language !== "python") {
|
|
76
|
+
throw new Error(`expected a python prep result, got ${result.language}`);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("installPythonDependencies.shouldRun", () => {
|
|
82
|
+
it("is false when no python interpreter is available", async () => {
|
|
83
|
+
routeSpawn({ available: [] });
|
|
84
|
+
await expect(installPythonDependencies.shouldRun()).resolves.toBe(false);
|
|
85
|
+
expect(existsSyncMock).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("is false with python but no config file", async () => {
|
|
89
|
+
routeSpawn({ available: ["python3"] });
|
|
90
|
+
filesPresent();
|
|
91
|
+
await expect(installPythonDependencies.shouldRun()).resolves.toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("is true with a fallback `python` binary and a requirements.txt", async () => {
|
|
95
|
+
routeSpawn({ available: ["python"] });
|
|
96
|
+
filesPresent("requirements.txt");
|
|
97
|
+
await expect(installPythonDependencies.shouldRun()).resolves.toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("installPythonDependencies.run", () => {
|
|
102
|
+
it("reports an issue when no config file is found", async () => {
|
|
103
|
+
filesPresent();
|
|
104
|
+
const result = await runPython(options);
|
|
105
|
+
expect(result).toEqual({
|
|
106
|
+
language: "python",
|
|
107
|
+
packageManager: "pip",
|
|
108
|
+
configFile: "unknown",
|
|
109
|
+
dependenciesInstalled: false,
|
|
110
|
+
issues: ["no python config file found"],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("skips installation entirely when shell is disabled", async () => {
|
|
115
|
+
filesPresent("requirements.txt");
|
|
116
|
+
const result = await runPython({ ...options, ignoreScripts: true });
|
|
117
|
+
expect(result.dependenciesInstalled).toBe(false);
|
|
118
|
+
expect(result.configFile).toBe("requirements.txt");
|
|
119
|
+
expect(result.issues[0]).toContain("can execute arbitrary code");
|
|
120
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("installs requirements.txt via pip when pip is available", async () => {
|
|
124
|
+
filesPresent("requirements.txt");
|
|
125
|
+
const pipCalls: SpawnArg[] = [];
|
|
126
|
+
routeSpawn({
|
|
127
|
+
available: ["pip"],
|
|
128
|
+
handlers: {
|
|
129
|
+
pip: (call) => {
|
|
130
|
+
pipCalls.push(call);
|
|
131
|
+
return spawnResult({ stdout: "installed" });
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await runPython(options);
|
|
137
|
+
|
|
138
|
+
expect(pipCalls[0]?.args).toEqual(["install", "-r", "requirements.txt"]);
|
|
139
|
+
expect(result).toEqual({
|
|
140
|
+
language: "python",
|
|
141
|
+
packageManager: "pip",
|
|
142
|
+
configFile: "requirements.txt",
|
|
143
|
+
dependenciesInstalled: true,
|
|
144
|
+
issues: [],
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("prefers requirements.txt over later configs in priority order", async () => {
|
|
149
|
+
filesPresent("requirements.txt", "pyproject.toml", "Pipfile");
|
|
150
|
+
routeSpawn({ available: ["pip"], handlers: { pip: () => spawnResult() } });
|
|
151
|
+
const result = await runPython(options);
|
|
152
|
+
expect(result.configFile).toBe("requirements.txt");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("installs a missing tool (pipenv) before running its install command", async () => {
|
|
156
|
+
filesPresent("Pipfile");
|
|
157
|
+
const pipCalls: SpawnArg[] = [];
|
|
158
|
+
const pipenvCalls: SpawnArg[] = [];
|
|
159
|
+
routeSpawn({
|
|
160
|
+
available: [],
|
|
161
|
+
handlers: {
|
|
162
|
+
pip: (call) => {
|
|
163
|
+
pipCalls.push(call);
|
|
164
|
+
return spawnResult();
|
|
165
|
+
},
|
|
166
|
+
pipenv: (call) => {
|
|
167
|
+
pipenvCalls.push(call);
|
|
168
|
+
return spawnResult();
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const result = await runPython(options);
|
|
174
|
+
|
|
175
|
+
expect(pipCalls[0]?.args).toEqual(["install", "pipenv"]);
|
|
176
|
+
expect(pipenvCalls[0]?.args).toEqual(["install"]);
|
|
177
|
+
expect(result.packageManager).toBe("pipenv");
|
|
178
|
+
expect(result.dependenciesInstalled).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("does not attempt to install pip itself when missing", async () => {
|
|
182
|
+
filesPresent("setup.py");
|
|
183
|
+
const pipCalls: SpawnArg[] = [];
|
|
184
|
+
routeSpawn({
|
|
185
|
+
available: [],
|
|
186
|
+
handlers: {
|
|
187
|
+
pip: (call) => {
|
|
188
|
+
pipCalls.push(call);
|
|
189
|
+
return spawnResult();
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await runPython(options);
|
|
195
|
+
|
|
196
|
+
// only the editable install ran — no `pip install pip` bootstrap
|
|
197
|
+
expect(pipCalls).toHaveLength(1);
|
|
198
|
+
expect(pipCalls[0]?.args).toEqual(["install", "-e", "."]);
|
|
199
|
+
expect(result.dependenciesInstalled).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("surfaces tool installation failures as issues", async () => {
|
|
203
|
+
filesPresent("poetry.lock");
|
|
204
|
+
routeSpawn({
|
|
205
|
+
available: [],
|
|
206
|
+
handlers: {
|
|
207
|
+
pip: (call) => {
|
|
208
|
+
// exercise the stderr-forwarding callback
|
|
209
|
+
call.onStderr?.("");
|
|
210
|
+
return spawnResult({ exitCode: 1, stderr: "no network" });
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result = await runPython(options);
|
|
216
|
+
|
|
217
|
+
expect(result).toEqual({
|
|
218
|
+
language: "python",
|
|
219
|
+
packageManager: "poetry",
|
|
220
|
+
configFile: "poetry.lock",
|
|
221
|
+
dependenciesInstalled: false,
|
|
222
|
+
issues: ["no network"],
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("falls back to a generic message when tool install fails silently", async () => {
|
|
227
|
+
filesPresent("Pipfile.lock");
|
|
228
|
+
routeSpawn({
|
|
229
|
+
available: [],
|
|
230
|
+
handlers: {
|
|
231
|
+
pip: () => spawnResult({ exitCode: 1 }),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const result = await runPython(options);
|
|
236
|
+
|
|
237
|
+
expect(result.issues).toEqual(["failed to install pipenv"]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("reports install command failures with combined output", async () => {
|
|
241
|
+
filesPresent("pyproject.toml");
|
|
242
|
+
routeSpawn({
|
|
243
|
+
available: ["pip"],
|
|
244
|
+
handlers: {
|
|
245
|
+
pip: () => spawnResult({ exitCode: 2, stdout: "resolving...", stderr: "conflict" }),
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const result = await runPython(options);
|
|
250
|
+
|
|
251
|
+
expect(result.dependenciesInstalled).toBe(false);
|
|
252
|
+
expect(result.issues).toEqual(["resolving...\nconflict"]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("reports the exit code when a failing install produces no output", async () => {
|
|
256
|
+
filesPresent("pyproject.toml");
|
|
257
|
+
routeSpawn({
|
|
258
|
+
available: ["pip"],
|
|
259
|
+
handlers: {
|
|
260
|
+
pip: () => spawnResult({ exitCode: 3 }),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result = await runPython(options);
|
|
265
|
+
|
|
266
|
+
expect(result.issues).toEqual(["pip exited with code 3"]);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
PrepDefinition,
|
|
5
|
+
PrepOptions,
|
|
6
|
+
PythonPackageManager,
|
|
7
|
+
PythonPrepResult,
|
|
8
|
+
} from "#app/prep/types";
|
|
9
|
+
import { log } from "#app/utils/cli";
|
|
10
|
+
import { spawn } from "#app/utils/subprocess";
|
|
11
|
+
|
|
12
|
+
interface PythonConfig {
|
|
13
|
+
file: string;
|
|
14
|
+
tool: PythonPackageManager;
|
|
15
|
+
// non-empty: the first element is the executable, the rest are args
|
|
16
|
+
installCmd: [string, ...string[]];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// python dependency file patterns in priority order
|
|
20
|
+
const PYTHON_CONFIGS: PythonConfig[] = [
|
|
21
|
+
{
|
|
22
|
+
file: "requirements.txt",
|
|
23
|
+
tool: "pip",
|
|
24
|
+
installCmd: ["pip", "install", "-r", "requirements.txt"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
file: "pyproject.toml",
|
|
28
|
+
tool: "pip",
|
|
29
|
+
installCmd: ["pip", "install", "."],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
file: "Pipfile",
|
|
33
|
+
tool: "pipenv",
|
|
34
|
+
installCmd: ["pipenv", "install"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
file: "Pipfile.lock",
|
|
38
|
+
tool: "pipenv",
|
|
39
|
+
installCmd: ["pipenv", "sync"],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
file: "poetry.lock",
|
|
43
|
+
tool: "poetry",
|
|
44
|
+
installCmd: ["poetry", "install", "--no-interaction"],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
file: "setup.py",
|
|
48
|
+
tool: "pip",
|
|
49
|
+
installCmd: ["pip", "install", "-e", "."],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// tool install commands (via pip). Each command is non-empty (executable + args).
|
|
54
|
+
const TOOL_INSTALL_COMMANDS: Record<string, [string, ...string[]]> = {
|
|
55
|
+
pipenv: ["pip", "install", "pipenv"],
|
|
56
|
+
poetry: ["pip", "install", "poetry"],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
async function isCommandAvailable(command: string): Promise<boolean> {
|
|
60
|
+
const result = await spawn({
|
|
61
|
+
cmd: "which",
|
|
62
|
+
args: [command],
|
|
63
|
+
env: { PATH: process.env.PATH || "" },
|
|
64
|
+
});
|
|
65
|
+
return result.exitCode === 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function installTool(name: string): Promise<string | null> {
|
|
69
|
+
const installCmd = TOOL_INSTALL_COMMANDS[name];
|
|
70
|
+
if (!installCmd) {
|
|
71
|
+
// tool doesn't need installation (e.g., pip)
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
log.info(`» installing ${name}...`);
|
|
76
|
+
const [cmd, ...args] = installCmd;
|
|
77
|
+
const result = await spawn({
|
|
78
|
+
cmd,
|
|
79
|
+
args,
|
|
80
|
+
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
|
81
|
+
onStderr: (chunk) => process.stderr.write(chunk),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.exitCode !== 0) {
|
|
85
|
+
return result.stderr || `failed to install ${name}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
log.info(`» installed ${name}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const installPythonDependencies: PrepDefinition = {
|
|
93
|
+
name: "installPythonDependencies",
|
|
94
|
+
|
|
95
|
+
shouldRun: async () => {
|
|
96
|
+
// check if python is available
|
|
97
|
+
const hasPython = (await isCommandAvailable("python3")) || (await isCommandAvailable("python"));
|
|
98
|
+
if (!hasPython) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// check if any python config file exists
|
|
103
|
+
const cwd = process.cwd();
|
|
104
|
+
return PYTHON_CONFIGS.some((config) => existsSync(join(cwd, config.file)));
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
run: async (options: PrepOptions): Promise<PythonPrepResult> => {
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
|
|
110
|
+
// find the first matching config
|
|
111
|
+
const config = PYTHON_CONFIGS.find((c) => existsSync(join(cwd, c.file)));
|
|
112
|
+
if (!config) {
|
|
113
|
+
return {
|
|
114
|
+
language: "python",
|
|
115
|
+
packageManager: "pip",
|
|
116
|
+
configFile: "unknown",
|
|
117
|
+
dependenciesInstalled: false,
|
|
118
|
+
issues: ["no python config file found"],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
log.info(`» detected python config: ${config.file} (using ${config.tool})`);
|
|
123
|
+
|
|
124
|
+
// SECURITY: when shell is disabled, skip ALL python dependency installation.
|
|
125
|
+
// every python install path can potentially execute arbitrary code:
|
|
126
|
+
// - setup.py / pyproject.toml: directly execute build backends
|
|
127
|
+
// - requirements.txt: can contain "-e ." or local path references that
|
|
128
|
+
// trigger setup.py execution
|
|
129
|
+
// - Pipfile/poetry.lock: can contain path dependencies pointing to local
|
|
130
|
+
// directories with malicious setup.py
|
|
131
|
+
// - source distributions from PyPI also execute setup.py
|
|
132
|
+
// there is no equivalent of npm's --ignore-scripts for pip.
|
|
133
|
+
if (options.ignoreScripts) {
|
|
134
|
+
log.info(
|
|
135
|
+
`» skipping python install (shell disabled, python packages can execute arbitrary code)`,
|
|
136
|
+
);
|
|
137
|
+
return {
|
|
138
|
+
language: "python",
|
|
139
|
+
packageManager: config.tool,
|
|
140
|
+
configFile: config.file,
|
|
141
|
+
dependenciesInstalled: false,
|
|
142
|
+
issues: [
|
|
143
|
+
`skipped: python dependency installation can execute arbitrary code (setup.py, build backends, local path references), which is blocked when shell is disabled`,
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// check if the tool is available, install if needed
|
|
149
|
+
const isAvailable = await isCommandAvailable(config.tool);
|
|
150
|
+
if (!isAvailable) {
|
|
151
|
+
log.info(`» ${config.tool} not found, attempting to install...`);
|
|
152
|
+
const installError = await installTool(config.tool);
|
|
153
|
+
if (installError) {
|
|
154
|
+
return {
|
|
155
|
+
language: "python",
|
|
156
|
+
packageManager: config.tool,
|
|
157
|
+
configFile: config.file,
|
|
158
|
+
dependenciesInstalled: false,
|
|
159
|
+
issues: [installError],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// run the install command
|
|
165
|
+
const [cmd, ...args] = config.installCmd;
|
|
166
|
+
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
167
|
+
log.info(`» running: ${fullCommand}`);
|
|
168
|
+
const result = await spawn({
|
|
169
|
+
cmd,
|
|
170
|
+
args,
|
|
171
|
+
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
175
|
+
if (output) {
|
|
176
|
+
log.startGroup(`${fullCommand} output`);
|
|
177
|
+
log.info(output);
|
|
178
|
+
log.endGroup();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (result.exitCode !== 0) {
|
|
182
|
+
return {
|
|
183
|
+
language: "python",
|
|
184
|
+
packageManager: config.tool,
|
|
185
|
+
configFile: config.file,
|
|
186
|
+
dependenciesInstalled: false,
|
|
187
|
+
issues: [output || `${cmd} exited with code ${result.exitCode}`],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
language: "python",
|
|
193
|
+
packageManager: config.tool,
|
|
194
|
+
configFile: config.file,
|
|
195
|
+
dependenciesInstalled: true,
|
|
196
|
+
issues: [],
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
interface PrepResultBase {
|
|
2
|
+
dependenciesInstalled: boolean;
|
|
3
|
+
issues: string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type NodePackageManager = "npm" | "pnpm" | "yarn" | "bun" | "deno";
|
|
7
|
+
|
|
8
|
+
export interface NodePrepResult extends PrepResultBase {
|
|
9
|
+
language: "node";
|
|
10
|
+
packageManager: NodePackageManager;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PythonPackageManager = "pip" | "pipenv" | "poetry";
|
|
14
|
+
|
|
15
|
+
export interface PythonPrepResult extends PrepResultBase {
|
|
16
|
+
language: "python";
|
|
17
|
+
packageManager: PythonPackageManager;
|
|
18
|
+
configFile: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UnknownLanguagePrepResult extends PrepResultBase {
|
|
22
|
+
language: "unknown";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PrepResult = NodePrepResult | PythonPrepResult | UnknownLanguagePrepResult;
|
|
26
|
+
|
|
27
|
+
export type PrepOptions = {
|
|
28
|
+
/** when true, lifecycle scripts (postinstall, etc.) are suppressed */
|
|
29
|
+
ignoreScripts: boolean;
|
|
30
|
+
/** directory the corepack shim is installed into (see `packageManagerBinDir`) */
|
|
31
|
+
binDir: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface PrepDefinition {
|
|
35
|
+
name: string;
|
|
36
|
+
shouldRun: () => Promise<boolean> | boolean;
|
|
37
|
+
run: (options: PrepOptions) => Promise<PrepResult>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
FINDING_VERIFICATION_PASS,
|
|
4
|
+
FP_PRECEDENTS_ADDENDUM_HEADING,
|
|
5
|
+
mergeReviewModeInstructions,
|
|
6
|
+
} from "#app/reviewQuality";
|
|
7
|
+
|
|
8
|
+
describe("mergeReviewModeInstructions", () => {
|
|
9
|
+
it("returns the base untouched when neither input is set", () => {
|
|
10
|
+
const base = { Review: "backend rules", Build: "build rules" };
|
|
11
|
+
expect(mergeReviewModeInstructions(base, {})).toBe(base);
|
|
12
|
+
expect(mergeReviewModeInstructions(base, { reviewInstructions: " " })).toBe(base);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("appends review instructions after backend-provided ones", () => {
|
|
16
|
+
const merged = mergeReviewModeInstructions(
|
|
17
|
+
{ Review: "backend rules" },
|
|
18
|
+
{ reviewInstructions: "org policy" },
|
|
19
|
+
);
|
|
20
|
+
expect(merged.Review).toBe("backend rules\n\norg policy");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("wraps FP instructions under the addendum heading the verification pass names", () => {
|
|
24
|
+
const merged = mergeReviewModeInstructions(
|
|
25
|
+
{},
|
|
26
|
+
{ fpFilteringInstructions: "TLS terminates at the ALB" },
|
|
27
|
+
);
|
|
28
|
+
expect(merged.Review).toContain(FP_PRECEDENTS_ADDENDUM_HEADING);
|
|
29
|
+
expect(merged.Review).toContain("TLS terminates at the ALB");
|
|
30
|
+
expect(merged.Review).toContain("verbatim in every verification dispatch");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps order: backend, review policy, then the FP addendum last", () => {
|
|
34
|
+
const merged = mergeReviewModeInstructions(
|
|
35
|
+
{ Review: "backend" },
|
|
36
|
+
{ reviewInstructions: "policy", fpFilteringInstructions: "fp" },
|
|
37
|
+
);
|
|
38
|
+
const text = merged.Review ?? "";
|
|
39
|
+
expect(text.indexOf("backend")).toBeLessThan(text.indexOf("policy"));
|
|
40
|
+
expect(text.indexOf("policy")).toBeLessThan(text.indexOf(FP_PRECEDENTS_ADDENDUM_HEADING));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not touch other modes", () => {
|
|
44
|
+
const merged = mergeReviewModeInstructions(
|
|
45
|
+
{ Build: "build rules" },
|
|
46
|
+
{ reviewInstructions: "org policy" },
|
|
47
|
+
);
|
|
48
|
+
expect(merged.Build).toBe("build rules");
|
|
49
|
+
expect(merged.Review).toBe("org policy");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("addendum heading contract", () => {
|
|
54
|
+
it("the verification pass names the exact heading the merge produces", () => {
|
|
55
|
+
// FINDING_VERIFICATION_PASS instructs the orchestrator to include any
|
|
56
|
+
// "Finding precedents — org addendum" section verbatim in dispatches; the
|
|
57
|
+
// heading the merge emits must match that pointer or org FP rules never
|
|
58
|
+
// reach the verification subagents.
|
|
59
|
+
expect(FINDING_VERIFICATION_PASS).toContain(
|
|
60
|
+
FP_PRECEDENTS_ADDENDUM_HEADING.replace(/^### /, ""),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|