Public: anyone can read this; the crew can change it.
MCP `app_deploy` refuses in the deploys page's words, not `refused: <code>` #7607
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7597-mcp-app-deploy-refuses-in-the-deploys-pa → main 1 commit fork
2 files
changed +18
−2
src/http/mcp/tools/app_deploy.ts +3 −2
| @@ -1,3 +1,4 @@ | ||
| 1 | import { envRefusalText } from "../../../actions/env_words.ts"; | |
| 1 | 2 | import { redeployEnv, stopEnv } from "../../../actions/apps.ts"; |
| 2 | 3 | import { good, bad, type Tool, str, REPO_ARG, repoArg, isAnswer } from "../shared.ts"; |
| 3 | 4 | |
| @@ -27,11 +28,11 @@ export const tool: Tool = { | ||
| 27 | 28 | const stopped = await stopEnv(ctx.app, user, repo, env); |
| 28 | 29 | return stopped.ok |
| 29 | 30 | ? good({ stopped: true }) |
| 30 | : bad(stopped.error, `refused: ${stopped.error}`); | |
| 31 | : bad(stopped.error, envRefusalText(stopped.error)); | |
| 31 | 32 | } |
| 32 | 33 | const queued = await redeployEnv(ctx.app, user, repo, env); |
| 33 | 34 | return queued.ok |
| 34 | 35 | ? good({ queued: true, run: queued.value?.id ?? null }) |
| 35 | : bad(queued.error, `refused: ${queued.error}`); | |
| 36 | : bad(queued.error, envRefusalText(queued.error)); | |
| 36 | 37 | }, |
| 37 | 38 | }; |
test/mcp-app-deploy.test.ts +15
| @@ -9,6 +9,7 @@ import { tmpdir } from "node:os"; | ||
| 9 | 9 | import { join } from "node:path"; |
| 10 | 10 | import { createApp, type App } from "../src/app.ts"; |
| 11 | 11 | import { AppsRuntime } from "../src/apps/runtime.ts"; |
| 12 | import { envRefusalText } from "../src/actions/env_words.ts"; | |
| 12 | 13 | import { newApp } from "../src/actions/apps.ts"; |
| 13 | 14 | import { openPull } from "../src/actions/pulls.ts"; |
| 14 | 15 | import { commitToBranch } from "../src/actions/repos.ts"; |
| @@ -141,4 +142,18 @@ describe("MCP app_deploy", () => { | ||
| 141 | 142 | } |
| 142 | 143 | expect(runCount()).toBe(before); |
| 143 | 144 | }); |
| 145 | ||
| 146 | test("a refusal is said in the deploys page's words, the code unchanged", async () => { | |
| 147 | for (const action of ["redeploy", "stop"]) { | |
| 148 | const gone = await tool(aliceRw, { repo: "alice/nothing", env: "production", action }); | |
| 149 | expect(gone.error).toContain("no such app or repository"); | |
| 150 | const reader = await tool(bobRw, { repo: "alice/hello", env: "production", action }); | |
| 151 | expect(reader.error).toContain("forbidden"); | |
| 152 | expect(reader.error).toContain(envRefusalText("forbidden")); | |
| 153 | const env = await tool(aliceRw, { repo: "alice/hello", env: "staging", action }); | |
| 154 | expect(env.error).toContain("no_such_env"); | |
| 155 | expect(env.error).toContain(envRefusalText("no_such_env")); | |
| 156 | } | |
| 157 | expect(envRefusalText("not_found")).toBe("This repository has no app."); | |
| 158 | }); | |
| 144 | 159 | }); |
new above
offline