-
Notifications
You must be signed in to change notification settings - Fork 391
Analyze: update_pull_request with empty args silently drops safe outputs #33134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
41ed207
6dafb93
d00aae7
d58eb03
8c4c500
815d328
783555d
dd679de
79c65cc
0db0368
c25ece2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,23 @@ function buildIntentErrorResponse(error) { | |
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Returns true if `args` contains at least one meaningful field for update_pull_request: | ||
| * a string `title`, a string `body`, or `update_branch === true`. | ||
| * Mirrors the downstream requiresOneOf:title,body,update_branch validation in | ||
| * safe_output_type_validator.cjs (which also excludes field === false from the count). | ||
| * @param {Record<string, any> | null | undefined} args | ||
| * @returns {boolean} | ||
| */ | ||
| function hasUpdatePullRequestFields(args) { | ||
| const safeArgs = args || {}; | ||
| return ( | ||
| typeof safeArgs.title === "string" || | ||
| typeof safeArgs.body === "string" || | ||
| safeArgs.update_branch === true | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Create handlers for safe output tools | ||
| * @param {Object} server - The MCP server instance for logging | ||
|
|
@@ -1354,6 +1371,25 @@ function createHandlers(server, appendSafeOutput, config = {}) { | |
| }; | ||
| }; | ||
|
|
||
| /** | ||
| * Handler for update_pull_request tool | ||
| * Spec cross-reference: Safe Output Outcome Evaluation §update_pull_request. | ||
| * Per Safe Outputs Specification MCE1: Enforces constraints during tool invocation | ||
| * to provide immediate feedback to the LLM before recording to NDJSON. | ||
| * Uses hasUpdatePullRequestFields to validate that at least one of 'title', 'body', | ||
| * or 'update_branch' is provided before recording to NDJSON. | ||
| */ | ||
| const updatePullRequestHandler = args => { | ||
| if (!hasUpdatePullRequestFields(args)) { | ||
| throw { | ||
| code: -32602, | ||
| message: `${ERR_VALIDATION}: update_pull_request requires at least one of: 'title', 'body', 'update_branch' fields`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] The Consider extracting a tiny shared predicate, or at minimum adding a cross-reference comment pointing to the exact line in |
||
| }; | ||
| } | ||
|
|
||
| return defaultHandler("update_pull_request")(args || {}); | ||
| }; | ||
|
|
||
| return { | ||
| defaultHandler, | ||
| uploadAssetHandler, | ||
|
|
@@ -1366,10 +1402,12 @@ function createHandlers(server, appendSafeOutput, config = {}) { | |
| addCommentHandler, | ||
| createPullRequestReviewCommentHandler, | ||
| submitPullRequestReviewHandler, | ||
| updatePullRequestHandler, | ||
| }; | ||
| } | ||
|
|
||
| module.exports = { | ||
| buildIntentErrorResponse, | ||
| createHandlers, | ||
| hasUpdatePullRequestFields, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; | |
| import fs from "fs"; | ||
| import path from "path"; | ||
| import { execSync } from "child_process"; | ||
| import { createHandlers } from "./safe_outputs_handlers.cjs"; | ||
| import { createHandlers, hasUpdatePullRequestFields } from "./safe_outputs_handlers.cjs"; | ||
| import { | ||
| looksLikeExploratoryBranch, | ||
| normalizeProbeValue, | ||
|
|
@@ -1764,4 +1764,149 @@ describe("safe_outputs_handlers", () => { | |
| expect(() => handlers.submitPullRequestReviewHandler({ event: "COMMENT" })).toThrow(expect.objectContaining({ code: -32602, message: expect.stringContaining("review body is empty") })); | ||
| }); | ||
| }); | ||
|
|
||
| describe("updatePullRequestHandler", () => { | ||
| it("should throw MCP error when no fields are provided", () => { | ||
| expect(() => handlers.updatePullRequestHandler({})).toThrow( | ||
| expect.objectContaining({ | ||
| code: -32602, | ||
| message: expect.stringContaining("requires at least one of"), | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should throw MCP error when called with null/undefined args", () => { | ||
| expect(() => handlers.updatePullRequestHandler(null)).toThrow( | ||
| expect.objectContaining({ code: -32602 }) | ||
| ); | ||
| expect(() => handlers.updatePullRequestHandler(undefined)).toThrow( | ||
| expect.objectContaining({ code: -32602 }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should throw MCP error when update_branch is explicitly false and no other fields", () => { | ||
| expect(() => handlers.updatePullRequestHandler({ update_branch: false })).toThrow( | ||
| expect.objectContaining({ code: -32602 }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should throw MCP error when title is null", () => { | ||
| expect(() => handlers.updatePullRequestHandler({ title: null })).toThrow( | ||
| expect.objectContaining({ code: -32602 }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should throw MCP error when body is null", () => { | ||
| expect(() => handlers.updatePullRequestHandler({ body: null })).toThrow( | ||
| expect.objectContaining({ code: -32602 }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should throw MCP error when update_branch is null", () => { | ||
| expect(() => handlers.updatePullRequestHandler({ update_branch: null })).toThrow( | ||
| expect.objectContaining({ code: -32602 }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should write entry and return success when title is provided", () => { | ||
| const result = handlers.updatePullRequestHandler({ title: "New Title" }); | ||
| expect(result).toHaveProperty("content"); | ||
| const data = JSON.parse(result.content[0].text); | ||
| expect(data.result).toBe("success"); | ||
| expect(mockAppendSafeOutput).toHaveBeenCalledWith( | ||
| expect.objectContaining({ type: "update_pull_request", title: "New Title" }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should write entry and return success when body is provided", () => { | ||
| const result = handlers.updatePullRequestHandler({ body: "Updated body" }); | ||
| expect(result).toHaveProperty("content"); | ||
| const data = JSON.parse(result.content[0].text); | ||
| expect(data.result).toBe("success"); | ||
| expect(mockAppendSafeOutput).toHaveBeenCalledWith( | ||
| expect.objectContaining({ type: "update_pull_request", body: "Updated body" }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should write entry and return success when update_branch is true", () => { | ||
| const result = handlers.updatePullRequestHandler({ update_branch: true }); | ||
| expect(result).toHaveProperty("content"); | ||
| const data = JSON.parse(result.content[0].text); | ||
| expect(data.result).toBe("success"); | ||
| expect(mockAppendSafeOutput).toHaveBeenCalledWith( | ||
| expect.objectContaining({ type: "update_pull_request", update_branch: true }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should write entry and return success when both title and body are provided", () => { | ||
| const result = handlers.updatePullRequestHandler({ title: "New Title", body: "New body" }); | ||
| expect(result).toHaveProperty("content"); | ||
| const data = JSON.parse(result.content[0].text); | ||
| expect(data.result).toBe("success"); | ||
| expect(mockAppendSafeOutput).toHaveBeenCalledWith( | ||
| expect.objectContaining({ type: "update_pull_request", title: "New Title", body: "New body" }) | ||
| ); | ||
| }); | ||
|
|
||
| it("error message should mention all required fields", () => { | ||
| try { | ||
| handlers.updatePullRequestHandler({}); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] This test uses it('error message should mention all required fields', () => {
expect(() => handlers.updatePullRequestHandler({})).toThrow(
expect.objectContaining({
message: expect.stringMatching(/title.*body.*update_branch/s),
})
);
});Consistent style makes the test suite easier to scan and keeps coverage reporters accurate. |
||
| expect.fail("Should have thrown"); | ||
| } catch (err) { | ||
| expect(err.message).toContain("'title'"); | ||
| expect(err.message).toContain("'body'"); | ||
| expect(err.message).toContain("'update_branch'"); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("hasUpdatePullRequestFields", () => { | ||
| it("returns false for empty object", () => { | ||
| expect(hasUpdatePullRequestFields({})).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false for null", () => { | ||
| expect(hasUpdatePullRequestFields(null)).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false for undefined", () => { | ||
| expect(hasUpdatePullRequestFields(undefined)).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when update_branch is false", () => { | ||
| expect(hasUpdatePullRequestFields({ update_branch: false })).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when title is null", () => { | ||
| expect(hasUpdatePullRequestFields({ title: null })).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when body is null", () => { | ||
| expect(hasUpdatePullRequestFields({ body: null })).toBe(false); | ||
| }); | ||
|
|
||
| it("returns false when update_branch is null", () => { | ||
| expect(hasUpdatePullRequestFields({ update_branch: null })).toBe(false); | ||
| }); | ||
|
|
||
| it("returns true when title is a string", () => { | ||
| expect(hasUpdatePullRequestFields({ title: "New Title" })).toBe(true); | ||
| }); | ||
|
|
||
| it("returns true when body is a string", () => { | ||
| expect(hasUpdatePullRequestFields({ body: "Updated body" })).toBe(true); | ||
| }); | ||
|
|
||
| it("returns true when update_branch is exactly true", () => { | ||
| expect(hasUpdatePullRequestFields({ update_branch: true })).toBe(true); | ||
| }); | ||
|
|
||
| it("returns true when both title and body are provided", () => { | ||
| expect(hasUpdatePullRequestFields({ title: "t", body: "b" })).toBe(true); | ||
| }); | ||
|
|
||
| it("returns true for empty string title (typeof === 'string')", () => { | ||
| expect(hasUpdatePullRequestFields({ title: "" })).toBe(true); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot move inline helper to top level function and add tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in the latest commit. Extracted the validation logic into a top-level
hasUpdatePullRequestFields(args)function (placed betweenbuildIntentErrorResponseandcreateHandlers), exported it frommodule.exports, and added 12 direct unit tests for it in a new top-leveldescribe("hasUpdatePullRequestFields", ...)block — coveringnull,undefined,{},false/nullfield values, and all valid field combinations.