Skip to content
Open
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
15 changes: 13 additions & 2 deletions src/modes/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
setupSshSigning,
} from "../../github/operations/git-config";
import { checkHumanActor } from "../../github/validation/actor";
import { createInitialComment } from "../../github/operations/comments/create-initial";
import { isEntityContext } from "../../github/context";
import type { GitHubContext } from "../../github/context";
import type { Octokits } from "../../github/api/client";

Expand Down Expand Up @@ -95,14 +97,23 @@ export async function prepareAgentMode({
process.env.GITHUB_REF_NAME ||
defaultBranch;

// Create a sticky comment when requested and we have an entity (PR or issue) to comment on.
// Without this, the MCP comment server starts without CLAUDE_COMMENT_ID and every
// update_claude_comment call fails with "CLAUDE_COMMENT_ID environment variable is required".
let commentId: number | undefined;
if (context.inputs.useStickyComment && isEntityContext(context)) {
const commentData = await createInitialComment(octokit.rest, context);
commentId = commentData.id;
}

// Get our GitHub MCP servers config
const ourMcpConfig = await prepareMcpConfig({
githubToken,
owner: context.repository.owner,
repo: context.repository.repo,
branch: currentBranch,
baseBranch: baseBranch,
claudeCommentId: undefined, // No tracking comment in agent mode
claudeCommentId: commentId?.toString(),
allowedTools,
mode: "agent",
context,
Expand All @@ -122,7 +133,7 @@ export async function prepareAgentMode({
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();

return {
commentId: undefined,
commentId,
branchInfo: {
baseBranch: baseBranch,
currentBranch: baseBranch, // Use base branch as current when creating new branch
Expand Down
127 changes: 126 additions & 1 deletion test/modes/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import {
mock,
} from "bun:test";
import { prepareAgentMode } from "../../src/modes/agent";
import { createMockAutomationContext } from "../mockContext";
import {
createMockAutomationContext,
createMockContext,
} from "../mockContext";
import * as core from "@actions/core";
import * as gitConfig from "../../src/github/operations/git-config";
import * as createInitialModule from "../../src/github/operations/comments/create-initial";

describe("Agent Mode", () => {
let exportVariableSpy: any;
Expand Down Expand Up @@ -257,4 +261,125 @@ describe("Agent Mode", () => {
// Should be empty or just whitespace when no MCP servers are included
expect(result.claudeArgs).not.toContain("--mcp-config");
});

test("prepare creates sticky comment when useStickyComment is true and context is entity", async () => {
// Use entity context (pull_request) so isEntityContext returns true
const entityContext = createMockContext({
eventName: "pull_request",
eventAction: "labeled",
isPR: true,
entityNumber: 42,
inputs: {
useStickyComment: true,
prompt: "Review this PR",
},
});

const createInitialCommentSpy = spyOn(
createInitialModule,
"createInitialComment",
).mockResolvedValue({ id: 99999 } as any);

const mockOctokit = {
rest: {
users: {
getByUsername: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345, type: "User" },
}),
),
},
},
} as any;

const result = await prepareAgentMode({
context: entityContext,
octokit: mockOctokit,
githubToken: "test-token",
});

expect(createInitialCommentSpy).toHaveBeenCalledTimes(1);
expect(result.commentId).toBe(99999);

createInitialCommentSpy.mockRestore();
});

test("prepare skips sticky comment when useStickyComment is false", async () => {
const entityContext = createMockContext({
eventName: "pull_request",
eventAction: "labeled",
isPR: true,
entityNumber: 42,
inputs: {
useStickyComment: false,
prompt: "Review this PR",
},
});

const createInitialCommentSpy = spyOn(
createInitialModule,
"createInitialComment",
).mockResolvedValue({ id: 99999 } as any);

const mockOctokit = {
rest: {
users: {
getByUsername: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345, type: "User" },
}),
),
},
},
} as any;

const result = await prepareAgentMode({
context: entityContext,
octokit: mockOctokit,
githubToken: "test-token",
});

expect(createInitialCommentSpy).not.toHaveBeenCalled();
expect(result.commentId).toBeUndefined();

createInitialCommentSpy.mockRestore();
});

test("prepare skips sticky comment for non-entity events even when useStickyComment is true", async () => {
const automationContext = createMockAutomationContext({
eventName: "workflow_dispatch",
inputs: {
useStickyComment: true,
prompt: "Run analysis",
},
});

const createInitialCommentSpy = spyOn(
createInitialModule,
"createInitialComment",
).mockResolvedValue({ id: 99999 } as any);

const mockOctokit = {
rest: {
users: {
getByUsername: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345, type: "User" },
}),
),
},
},
} as any;

const result = await prepareAgentMode({
context: automationContext,
octokit: mockOctokit,
githubToken: "test-token",
});

expect(createInitialCommentSpy).not.toHaveBeenCalled();
expect(result.commentId).toBeUndefined();

createInitialCommentSpy.mockRestore();
});
});
Loading