Compare commits

..

2 Commits

Author SHA1 Message Date
claude[bot]
1d100f7832 feat: add workflow cancellation detection and update comment
When a workflow is cancelled, the comment now shows "Claude's task was cancelled" 
instead of the generic "Claude encountered an error" message. This provides clearer 
feedback to users about why the workflow stopped.

Changes:
- Add CLAUDE_CANCELLED environment variable using cancelled() function in action.yml
- Implement cancellation detection logic in update-comment-link.ts
- Update comment-logic.ts to show specific cancellation message
- Add comprehensive tests for cancellation handling

Fixes #123

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
2025-06-04 01:03:26 +00:00
claude[bot]
e0dcb85d34 fix: improve workflow status detection in comment updates
- Changed CLAUDE_SUCCESS check from !== "false" to === "true" for more explicit handling
- This ensures undefined or empty values are treated as failures
- Moved status check before output file parsing to ensure consistent behavior
- Added comprehensive tests for various workflow status scenarios

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-04 00:56:28 +00:00
17 changed files with 313 additions and 181 deletions

View File

@@ -83,14 +83,14 @@ runs:
- name: Install Dependencies
shell: bash
run: |
cd ${GITHUB_ACTION_PATH}
cd ${{ github.action_path }}
bun install
- name: Prepare action
id: prepare
shell: bash
run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts
bun run ${{ github.action_path }}/src/entrypoints/prepare.ts
env:
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
@@ -105,9 +105,9 @@ runs:
- name: Run Claude Code
id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true'
uses: anthropics/claude-code-base-action@79b8cfc932eb13806c23905842145e6f05c89e2e # v0.0.13
uses: anthropics/claude-code-base-action@1370ac97fbba9bddec20ea2924b5726bf10d8b94 # v0.0.9
with:
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
prompt_file: /tmp/claude-prompts/claude-prompt.txt
allowed_tools: ${{ env.ALLOWED_TOOLS }}
disallowed_tools: ${{ env.DISALLOWED_TOOLS }}
timeout_minutes: ${{ inputs.timeout_minutes }}
@@ -147,7 +147,7 @@ runs:
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
shell: bash
run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts
bun run ${{ github.action_path }}/src/entrypoints/update-comment-link.ts
env:
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
@@ -160,6 +160,7 @@ runs:
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
CLAUDE_CANCELLED: ${{ cancelled() }}
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}

View File

@@ -2,7 +2,7 @@
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "@anthropic-ai/claude-code-action",
"name": "claude-pr-action",
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.1",

View File

@@ -1,5 +1,5 @@
{
"name": "@anthropic-ai/claude-code-action",
"name": "claude-pr-action",
"version": "1.0.0",
"private": true,
"scripts": {

View File

@@ -35,35 +35,38 @@ const BASE_ALLOWED_TOOLS = [
];
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
export function buildAllowedToolsString(customAllowedTools?: string[]): string {
export function buildAllowedToolsString(customAllowedTools?: string): string {
let baseTools = [...BASE_ALLOWED_TOOLS];
let allAllowedTools = baseTools.join(",");
if (customAllowedTools && customAllowedTools.length > 0) {
allAllowedTools = `${allAllowedTools},${customAllowedTools.join(",")}`;
if (customAllowedTools) {
allAllowedTools = `${allAllowedTools},${customAllowedTools}`;
}
return allAllowedTools;
}
export function buildDisallowedToolsString(
customDisallowedTools?: string[],
allowedTools?: string[],
customDisallowedTools?: string,
allowedTools?: string,
): string {
let disallowedTools = [...DISALLOWED_TOOLS];
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
if (allowedTools && allowedTools.length > 0) {
if (allowedTools) {
const allowedToolsArray = allowedTools
.split(",")
.map((tool) => tool.trim());
disallowedTools = disallowedTools.filter(
(tool) => !allowedTools.includes(tool),
(tool) => !allowedToolsArray.includes(tool),
);
}
let allDisallowedTools = disallowedTools.join(",");
if (customDisallowedTools && customDisallowedTools.length > 0) {
if (customDisallowedTools) {
if (allDisallowedTools) {
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools.join(",")}`;
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
} else {
allDisallowedTools = customDisallowedTools.join(",");
allDisallowedTools = customDisallowedTools;
}
}
return allDisallowedTools;
@@ -117,10 +120,8 @@ export function prepareContext(
triggerPhrase,
...(triggerUsername && { triggerUsername }),
...(customInstructions && { customInstructions }),
...(allowedTools.length > 0 && { allowedTools: allowedTools.join(",") }),
...(disallowedTools.length > 0 && {
disallowedTools: disallowedTools.join(","),
}),
...(allowedTools && { allowedTools }),
...(disallowedTools && { disallowedTools }),
...(directPrompt && { directPrompt }),
...(claudeBranch && { claudeBranch }),
};
@@ -620,9 +621,7 @@ export async function createPrompt(
claudeBranch,
);
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
recursive: true,
});
await mkdir("/tmp/claude-prompts", { recursive: true });
// Generate the prompt
const promptContent = generatePrompt(preparedContext, githubData);
@@ -633,18 +632,15 @@ export async function createPrompt(
console.log("=======================");
// Write the prompt file
await writeFile(
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
promptContent,
);
await writeFile("/tmp/claude-prompts/claude-prompt.txt", promptContent);
// Set allowed tools
const allAllowedTools = buildAllowedToolsString(
context.inputs.allowedTools,
preparedContext.allowedTools,
);
const allDisallowedTools = buildDisallowedToolsString(
context.inputs.disallowedTools,
context.inputs.allowedTools,
preparedContext.disallowedTools,
preparedContext.allowedTools,
);
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);

View File

@@ -92,7 +92,6 @@ async function run() {
branch: branchInfo.currentBranch,
additionalMcpConfig,
claudeCommentId: commentId.toString(),
allowedTools: context.inputs.allowedTools,
});
core.setOutput("mcp_config", mcpConfig);
} catch (error) {

View File

@@ -146,8 +146,12 @@ async function run() {
duration_api_ms?: number;
} | null = null;
let actionFailed = false;
let actionCancelled = false;
let errorDetails: string | undefined;
// Check if the workflow was cancelled
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
// First check if prepare step failed
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
@@ -155,8 +159,18 @@ async function run() {
if (!prepareSuccess && prepareError) {
actionFailed = true;
errorDetails = prepareError;
} else if (isCancelled) {
// If the workflow was cancelled, set the cancelled flag
actionCancelled = true;
} else {
// Check for existence of output file and parse it if available
// Check if the Claude action failed
// CLAUDE_SUCCESS is set to the result of: steps.claude-code.outputs.conclusion == 'success'
// If the step didn't run or didn't set outputs.conclusion, CLAUDE_SUCCESS will be "false"
// If the step succeeded, CLAUDE_SUCCESS will be "true"
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
// Try to read execution details from output file
try {
const outputFile = process.env.OUTPUT_FILE;
if (outputFile) {
@@ -179,14 +193,10 @@ async function run() {
}
}
}
// Check if the Claude action failed
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
actionFailed = !claudeSuccess;
} catch (error) {
console.error("Error reading output file:", error);
// If we can't read the file, check for any failure markers
actionFailed = process.env.CLAUDE_SUCCESS === "false";
// Error reading output file doesn't change the action status
// We already determined actionFailed based on CLAUDE_SUCCESS
}
}
@@ -194,6 +204,7 @@ async function run() {
const commentInput: CommentUpdateInput = {
currentBody,
actionFailed,
actionCancelled,
executionDetails,
jobUrl,
branchLink,

View File

@@ -28,8 +28,8 @@ export type ParsedGitHubContext = {
inputs: {
triggerPhrase: string;
assigneeTrigger: string;
allowedTools: string[];
disallowedTools: string[];
allowedTools: string;
disallowedTools: string;
customInstructions: string;
directPrompt: string;
baseBranch?: string;
@@ -52,14 +52,8 @@ export function parseGitHubContext(): ParsedGitHubContext {
inputs: {
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
allowedTools: (process.env.ALLOWED_TOOLS ?? "")
.split(",")
.map((tool) => tool.trim())
.filter((tool) => tool.length > 0),
disallowedTools: (process.env.DISALLOWED_TOOLS ?? "")
.split(",")
.map((tool) => tool.trim())
.filter((tool) => tool.length > 0),
allowedTools: process.env.ALLOWED_TOOLS ?? "",
disallowedTools: process.env.DISALLOWED_TOOLS ?? "",
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
directPrompt: process.env.DIRECT_PROMPT ?? "",
baseBranch: process.env.BASE_BRANCH,

View File

@@ -9,6 +9,7 @@ export type ExecutionDetails = {
export type CommentUpdateInput = {
currentBody: string;
actionFailed: boolean;
actionCancelled?: boolean;
executionDetails: ExecutionDetails | null;
jobUrl: string;
branchLink?: string;
@@ -74,6 +75,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
branchLink,
prLink,
actionFailed,
actionCancelled,
branchName,
triggerUsername,
errorDetails,
@@ -112,7 +114,13 @@ export function updateCommentBody(input: CommentUpdateInput): string {
// Build the header
let header = "";
if (actionFailed) {
if (actionCancelled) {
header = "**Claude's task was cancelled";
if (durationStr) {
header += ` after ${durationStr}`;
}
header += "**";
} else if (actionFailed) {
header = "**Claude encountered an error";
if (durationStr) {
header += ` after ${durationStr}`;

View File

@@ -7,7 +7,6 @@ type PrepareConfigParams = {
branch: string;
additionalMcpConfig?: string;
claudeCommentId?: string;
allowedTools: string[];
};
export async function prepareMcpConfig(
@@ -20,17 +19,24 @@ export async function prepareMcpConfig(
branch,
additionalMcpConfig,
claudeCommentId,
allowedTools,
} = params;
try {
const allowedToolsList = allowedTools || [];
const hasGitHubMcpTools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github__"),
);
const baseMcpConfig: { mcpServers: Record<string, unknown> } = {
const baseMcpConfig = {
mcpServers: {
github: {
command: "docker",
args: [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/anthropics/github-mcp-server:sha-7382253",
],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
},
},
github_file_ops: {
command: "bun",
args: [
@@ -51,23 +57,6 @@ export async function prepareMcpConfig(
},
};
if (hasGitHubMcpTools) {
baseMcpConfig.mcpServers.github = {
command: "docker",
args: [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-e9f748f", // https://github.com/github/github-mcp-server/releases/tag/v0.4.0
],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
},
};
}
// Merge with additional MCP config if provided
if (additionalMcpConfig && additionalMcpConfig.trim()) {
try {

View File

@@ -418,4 +418,48 @@ describe("updateCommentBody", () => {
);
});
});
describe("cancellation handling", () => {
it("shows cancellation message when actionCancelled is true", () => {
const input = {
...baseInput,
currentBody: "Claude Code is working…",
actionCancelled: true,
executionDetails: { duration_ms: 30000 }, // 30s
triggerUsername: "test-user",
};
const result = updateCommentBody(input);
expect(result).toContain("**Claude's task was cancelled after 30s**");
expect(result).not.toContain("Claude encountered an error");
expect(result).not.toContain("Claude finished");
});
it("shows cancellation message without duration when duration is missing", () => {
const input = {
...baseInput,
currentBody: "Claude Code is working…",
actionCancelled: true,
triggerUsername: "test-user",
};
const result = updateCommentBody(input);
expect(result).toContain("**Claude's task was cancelled**");
expect(result).not.toContain("after");
});
it("prioritizes cancellation over failure", () => {
const input = {
...baseInput,
currentBody: "Claude Code is working…",
actionCancelled: true,
actionFailed: true, // Both are true, cancellation should take precedence
executionDetails: { duration_ms: 45000 },
};
const result = updateCommentBody(input);
expect(result).toContain("**Claude's task was cancelled after 45s**");
expect(result).not.toContain("Claude encountered an error");
});
});
});

View File

@@ -652,7 +652,7 @@ describe("buildAllowedToolsString", () => {
});
test("should append custom tools when provided", () => {
const customTools = ["Tool1", "Tool2", "Tool3"];
const customTools = "Tool1,Tool2,Tool3";
const result = buildAllowedToolsString(customTools);
// Base tools should be present
@@ -683,7 +683,7 @@ describe("buildDisallowedToolsString", () => {
});
test("should append custom disallowed tools when provided", () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const customDisallowedTools = "BadTool1,BadTool2";
const result = buildDisallowedToolsString(customDisallowedTools);
// Base disallowed tools should be present
@@ -701,8 +701,8 @@ describe("buildDisallowedToolsString", () => {
});
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const allowedTools = ["WebSearch", "SomeOtherTool"];
const customDisallowedTools = "BadTool1,BadTool2";
const allowedTools = "WebSearch,SomeOtherTool";
const result = buildDisallowedToolsString(
customDisallowedTools,
allowedTools,
@@ -720,7 +720,7 @@ describe("buildDisallowedToolsString", () => {
});
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
const allowedTools = ["WebSearch", "WebFetch", "SomeOtherTool"];
const allowedTools = "WebSearch,WebFetch,SomeOtherTool";
const result = buildDisallowedToolsString(undefined, allowedTools);
// Both hardcoded disallowed tools should be removed
@@ -732,8 +732,8 @@ describe("buildDisallowedToolsString", () => {
});
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const allowedTools = ["WebSearch", "WebFetch"];
const customDisallowedTools = "BadTool1,BadTool2";
const allowedTools = "WebSearch,WebFetch";
const result = buildDisallowedToolsString(
customDisallowedTools,
allowedTools,

View File

@@ -24,19 +24,21 @@ describe("prepareMcpConfig", () => {
processExitSpy.mockRestore();
});
test("should return base config when no additional config is provided and no allowed_tools", async () => {
test("should return base config when no additional config is provided", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
allowedTools: [],
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
"test-token",
);
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
"test-token",
);
@@ -47,60 +49,6 @@ describe("prepareMcpConfig", () => {
);
});
test("should include github MCP server when mcp__github__ tools are allowed", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
allowedTools: [
"mcp__github__create_issue",
"mcp__github_file_ops__commit_files",
],
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
"test-token",
);
});
test("should not include github MCP server when only file_ops tools are allowed", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
allowedTools: [
"mcp__github_file_ops__commit_files",
"mcp__github_file_ops__update_claude_comment",
],
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
test("should include file_ops server even when no GitHub tools are allowed", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
allowedTools: ["Edit", "Read", "Write"],
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
test("should return base config when additional config is empty string", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
@@ -108,12 +56,11 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: "",
allowedTools: [],
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
expect(consoleWarningSpy).not.toHaveBeenCalled();
});
@@ -125,12 +72,11 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: " \n\t ",
allowedTools: [],
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
expect(consoleWarningSpy).not.toHaveBeenCalled();
});
@@ -154,10 +100,6 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: additionalConfig,
allowedTools: [
"mcp__github__create_issue",
"mcp__github_file_ops__commit_files",
],
});
const parsed = JSON.parse(result);
@@ -191,10 +133,6 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: additionalConfig,
allowedTools: [
"mcp__github__create_issue",
"mcp__github_file_ops__commit_files",
],
});
const parsed = JSON.parse(result);
@@ -231,13 +169,12 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: additionalConfig,
allowedTools: [],
});
const parsed = JSON.parse(result);
expect(parsed.customProperty).toBe("custom-value");
expect(parsed.anotherProperty).toEqual({ nested: "value" });
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.custom_server).toBeDefined();
});
@@ -250,14 +187,13 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: invalidJson,
allowedTools: [],
});
const parsed = JSON.parse(result);
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("Failed to parse additional MCP config:"),
);
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
@@ -270,7 +206,6 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: nonObjectJson,
allowedTools: [],
});
const parsed = JSON.parse(result);
@@ -280,7 +215,7 @@ describe("prepareMcpConfig", () => {
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("MCP config must be a valid JSON object"),
);
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
@@ -293,7 +228,6 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: nullJson,
allowedTools: [],
});
const parsed = JSON.parse(result);
@@ -303,7 +237,7 @@ describe("prepareMcpConfig", () => {
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("MCP config must be a valid JSON object"),
);
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
@@ -316,7 +250,6 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: arrayJson,
allowedTools: [],
});
const parsed = JSON.parse(result);
@@ -325,7 +258,7 @@ describe("prepareMcpConfig", () => {
expect(consoleInfoSpy).toHaveBeenCalledWith(
"Merging additional MCP server configuration with built-in servers",
);
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
expect(parsed[0]).toBe(1);
@@ -362,13 +295,12 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: additionalConfig,
allowedTools: [],
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.server1).toBeDefined();
expect(parsed.mcpServers.server2).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
expect(parsed.otherConfig.nested.deeply).toBe("value");
@@ -383,7 +315,6 @@ describe("prepareMcpConfig", () => {
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
allowedTools: [],
});
const parsed = JSON.parse(result);
@@ -403,7 +334,6 @@ describe("prepareMcpConfig", () => {
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
allowedTools: [],
});
const parsed = JSON.parse(result);

View File

@@ -11,8 +11,8 @@ const defaultInputs = {
triggerPhrase: "/claude",
assigneeTrigger: "",
anthropicModel: "claude-3-7-sonnet-20250219",
allowedTools: [] as string[],
disallowedTools: [] as string[],
allowedTools: "",
disallowedTools: "",
customInstructions: "",
directPrompt: "",
useBedrock: false,

View File

@@ -62,8 +62,8 @@ describe("checkWritePermissions", () => {
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
allowedTools: [],
disallowedTools: [],
allowedTools: "",
disallowedTools: "",
customInstructions: "",
directPrompt: "",
},

View File

@@ -242,7 +242,7 @@ describe("parseEnvVarsWithContext", () => {
...mockPullRequestCommentContext,
inputs: {
...mockPullRequestCommentContext.inputs,
allowedTools: ["Tool1", "Tool2"],
allowedTools: "Tool1,Tool2",
},
});
const result = prepareContext(contextWithAllowedTools, "12345");

View File

@@ -30,8 +30,8 @@ describe("checkContainsTrigger", () => {
triggerPhrase: "/claude",
assigneeTrigger: "",
directPrompt: "Fix the bug in the login form",
allowedTools: [],
disallowedTools: [],
allowedTools: "",
disallowedTools: "",
customInstructions: "",
},
});
@@ -56,8 +56,8 @@ describe("checkContainsTrigger", () => {
triggerPhrase: "/claude",
assigneeTrigger: "",
directPrompt: "",
allowedTools: [],
disallowedTools: [],
allowedTools: "",
disallowedTools: "",
customInstructions: "",
},
});
@@ -228,8 +228,8 @@ describe("checkContainsTrigger", () => {
triggerPhrase: "@claude",
assigneeTrigger: "",
directPrompt: "",
allowedTools: [],
disallowedTools: [],
allowedTools: "",
disallowedTools: "",
customInstructions: "",
},
});
@@ -255,8 +255,8 @@ describe("checkContainsTrigger", () => {
triggerPhrase: "@claude",
assigneeTrigger: "",
directPrompt: "",
allowedTools: [],
disallowedTools: [],
allowedTools: "",
disallowedTools: "",
customInstructions: "",
},
});
@@ -282,8 +282,8 @@ describe("checkContainsTrigger", () => {
triggerPhrase: "@claude",
assigneeTrigger: "",
directPrompt: "",
allowedTools: [],
disallowedTools: [],
allowedTools: "",
disallowedTools: "",
customInstructions: "",
},
});

View File

@@ -0,0 +1,160 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
describe("update-comment-link workflow status detection", () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
originalEnv = { ...process.env };
});
afterEach(() => {
process.env = originalEnv;
});
test("should detect prepare step failure", () => {
process.env.PREPARE_SUCCESS = "false";
process.env.PREPARE_ERROR = "Failed to fetch issue data";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
let errorDetails: string | undefined;
if (!prepareSuccess && prepareError) {
actionFailed = true;
errorDetails = prepareError;
}
expect(actionFailed).toBe(true);
expect(errorDetails).toBe("Failed to fetch issue data");
});
test("should detect claude-code step failure when prepare succeeds", () => {
process.env.PREPARE_SUCCESS = "true";
process.env.CLAUDE_SUCCESS = "false";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(true);
});
test("should detect success when both steps succeed", () => {
process.env.PREPARE_SUCCESS = "true";
process.env.CLAUDE_SUCCESS = "true";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(false);
});
test("should treat missing CLAUDE_SUCCESS env var as failure", () => {
process.env.PREPARE_SUCCESS = "true";
delete process.env.CLAUDE_SUCCESS;
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else {
// When CLAUDE_SUCCESS is undefined, it's not === "true", so claudeSuccess = false
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(true);
});
test("should handle undefined PREPARE_SUCCESS as success", () => {
delete process.env.PREPARE_SUCCESS;
delete process.env.PREPARE_ERROR;
process.env.CLAUDE_SUCCESS = "true";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
let actionFailed = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(false);
});
test("should detect cancellation when CLAUDE_CANCELLED is true", () => {
process.env.PREPARE_SUCCESS = "true";
process.env.CLAUDE_SUCCESS = "false";
process.env.CLAUDE_CANCELLED = "true";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
let actionFailed = false;
let actionCancelled = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else if (isCancelled) {
actionCancelled = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(false);
expect(actionCancelled).toBe(true);
});
test("should not detect cancellation when CLAUDE_CANCELLED is false", () => {
process.env.PREPARE_SUCCESS = "true";
process.env.CLAUDE_SUCCESS = "false";
process.env.CLAUDE_CANCELLED = "false";
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
const prepareError = process.env.PREPARE_ERROR;
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
let actionFailed = false;
let actionCancelled = false;
if (!prepareSuccess && prepareError) {
actionFailed = true;
} else if (isCancelled) {
actionCancelled = true;
} else {
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
actionFailed = !claudeSuccess;
}
expect(actionFailed).toBe(true);
expect(actionCancelled).toBe(false);
});
});