Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion actions/setup/js/create_issue.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,13 @@ async function main(config = {}) {
const bodyLines = processedBody.split("\n");

if (!title) {
title = message.body ?? "Agent Output";
// Use the first non-empty line of the body as the title fallback rather than
// the entire body, so the title stays concise and the body remains intact.
const firstBodyLine = (message.body ?? "")
.split("\n")
.map(l => l.replace(/^#+\s*/, "").trim())
.find(l => l.length > 0);
Comment on lines +783 to +786
title = firstBodyLine || "Agent Output";
}

// Sanitize title for Unicode security and remove any duplicate prefixes
Expand Down
14 changes: 14 additions & 0 deletions actions/setup/js/create_issue.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ describe("create_issue", () => {
);
});

it("should use the first meaningful body line as title when title is missing", async () => {
const handler = await main({});
const body = "\n\n## Incident Summary\n\nFull details line 1\nFull details line 2";
const result = await handler({ body });

expect(result.success).toBe(true);
expect(mockGithub.rest.issues.create).toHaveBeenCalledWith(
expect.objectContaining({
title: "Incident Summary",
body: expect.stringContaining("Full details line 1\nFull details line 2"),
})
);
});

it("should use 'Agent Output' as title when both title and body are missing", async () => {
const handler = await main({});
const result = await handler({});
Expand Down
7 changes: 6 additions & 1 deletion actions/setup/js/intent_probe.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ function resolveIssueTitleForValidation(entry) {
return entry.title;
}
if (typeof entry.body === "string" && entry.body.trim()) {
return entry.body;
// Mirror create_issue's fallback: use the first non-empty line of the body.
const firstBodyLine = entry.body
.split("\n")
.map(l => l.replace(/^#+\s*/, "").trim())
.find(l => l.length > 0);
if (firstBodyLine) return firstBodyLine;
}
// Mirror create_issue's own fallback so probe validation sees the same
// effective title the handler would ultimately use when both fields are absent.
Expand Down
16 changes: 15 additions & 1 deletion actions/setup/js/mcp_scripts_mcp_server_http.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ describe("mcp_scripts_mcp_server_http.cjs integration", () => {
serverName: "http-integration-test-server",
version: "1.0.0",
tools: [
{ name: "echo_tool", description: "Echoes the input message", inputSchema: { type: "object", properties: { message: { type: "string", description: "Message to echo" } }, required: ["message"] }, handler: "echo-handler.cjs" },
{
name: "echo_tool",
description: "Echoes the input message",
inputSchema: { type: "object", properties: { message: { type: "string", description: "Message to echo", minLength: 20 } }, required: ["message"] },
handler: "echo-handler.cjs",
},
],
})
),
Expand Down Expand Up @@ -146,6 +151,15 @@ describe("mcp_scripts_mcp_server_http.cjs integration", () => {
response = await makeRequest({ jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "echo_tool", arguments: {} } }, headers);
(expect(response.status).toBe(200), expect(response.data.error).toBeDefined(), expect(response.data.error.message).toContain("not allowed"));
}),
it("should enforce minLength from tool schema on HTTP calls", async () => {
const headers = sessionId ? { "Mcp-Session-Id": sessionId } : {},
response = await makeRequest({ jsonrpc: "2.0", id: 5, method: "tools/call", params: { name: "echo_tool", arguments: { message: "too short" } } }, headers);
(expect(response.status).toBe(200),
expect(response.data.error).toBeDefined(),
expect(response.data.error.code).toBe(-32602),
expect(response.data.error.message).toContain("Invalid arguments"),
expect(response.data.error.message).toContain("minimum 20 characters"));
}),
afterAll(async () => {
(serverProcess &&
(serverProcess.kill("SIGTERM"),
Expand Down
8 changes: 7 additions & 1 deletion actions/setup/js/safe_outputs_handlers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1697,7 +1697,13 @@ function createHandlers(server, appendSafeOutput, config = {}) {

let resolvedTitle = entry.title?.trim() || "";
if (!resolvedTitle) {
resolvedTitle = entry.body?.trim() || "Agent Output";
// Use the first non-empty line of the body as the title fallback rather than
// the entire body, so the title stays concise and the body remains intact.
const firstBodyLine = (entry.body || "")
.split("\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Same test gap in safe_outputs_handlers.test.cjs — the create_issue title fallback is tested only with a single-line body. The multiline/heading scenario that triggered this bug has no coverage here either.

💡 Suggested regression cases

Add to the create_issue handler tests:

it('uses first non-empty line of body as title when title is absent', async () => {
  // multiline with markdown heading
  const entry = { body: '# My Report\n\nFull content here.' };
  // ... call handler and assert resolved title === 'My Report'
});
it('falls back to Agent Output when body is blank', async () => {
  const entry = { body: '   \n\n  ' };
  // ... assert resolved title === 'Agent Output'
});

@copilot please address this.

.map(l => l.replace(/^#+\s*/, "").trim())
.find(l => l.length > 0);
resolvedTitle = firstBodyLine || "Agent Output";
}
resolvedTitle = applyTitlePrefix(sanitizeTitle(resolvedTitle, createIssueTitlePrefix), createIssueTitlePrefix);

Expand Down
36 changes: 36 additions & 0 deletions actions/setup/js/safe_outputs_handlers.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ describe("safe_outputs_handlers", () => {
it("resolves issue title using the create_issue fallback order", () => {
expect(resolveIssueTitleForValidation({ title: "Real title", body: "Ignored body" })).toBe("Real title");
expect(resolveIssueTitleForValidation({ body: "Body title" })).toBe("Body title");
expect(resolveIssueTitleForValidation({ body: "\n\n## Incident Summary\n\nBody details" })).toBe("Incident Summary");
expect(resolveIssueTitleForValidation({ body: " \n\n " })).toBe("Agent Output");
expect(resolveIssueTitleForValidation({})).toBe("Agent Output");
});

Expand Down Expand Up @@ -2305,6 +2307,40 @@ describe("safe_outputs_handlers", () => {
});

describe("createIssueHandler", () => {
it("should deduplicate against fallback title derived from first meaningful body line", () => {
const h = createHandlers(mockServer, mockAppendSafeOutput, {
create_issue: {
deduplicate_by_title: true,
},
});

const first = h.createIssueHandler({ body: "\n\n## Incident Summary\n\nBody A details" });
const second = h.createIssueHandler({ title: "Incident Summary", body: "Body B details" });

const firstResponse = JSON.parse(first.content[0].text);
const secondResponse = JSON.parse(second.content[0].text);
expect(firstResponse.result).toBe("success");
expect(secondResponse.result).toBe("duplicate_dropped");
const droppedEntry = mockAppendSafeOutput.mock.calls[1][0];
expect(droppedEntry._dropped_duplicate_by_title).toBe(true);
});

it("should fall back to Agent Output when title and body are blank", () => {
const h = createHandlers(mockServer, mockAppendSafeOutput, {
create_issue: {
deduplicate_by_title: true,
},
});

const first = h.createIssueHandler({ body: " \n\n " });
const second = h.createIssueHandler({ title: "Agent Output", body: "Real details for duplicate check" });

const firstResponse = JSON.parse(first.content[0].text);
const secondResponse = JSON.parse(second.content[0].text);
expect(firstResponse.result).toBe("success");
expect(secondResponse.result).toBe("duplicate_dropped");
});

it("should append create_issue entry when dedup is disabled", () => {
handlers.createIssueHandler({ title: "Issue A", body: "Body A" });
handlers.createIssueHandler({ title: "Issue A", body: "Body A again" });
Expand Down
6 changes: 6 additions & 0 deletions actions/setup/md/safe_outputs_auto_create_issue.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@

**IMPORTANT**: Report your findings or results by creating a GitHub issue using the create_issue tool. If you have no meaningful results to report, call the noop tool instead.

When calling create_issue:
- `title`: a short, descriptive summary (aim for one sentence, ≤ 100 characters)
- `body`: the full content — details, findings, context, and any supporting information

Do **not** put all the content in the title; the title is a headline, and the body is where the substance goes.
Loading