mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
1 Commits
demo/flawe
...
demo/flawe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6325e51611 |
@@ -1,37 +1,14 @@
|
||||
import {
|
||||
describe,
|
||||
test,
|
||||
expect,
|
||||
beforeEach,
|
||||
spyOn,
|
||||
afterEach,
|
||||
mock,
|
||||
} from "bun:test";
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||
import { mock } from "bun:test";
|
||||
import { setupBranch, type BranchInfo } from "../branch";
|
||||
import type { Octokits } from "../../api/client";
|
||||
import type { FetchDataResult } from "../../data/fetcher";
|
||||
import type { ParsedGitHubContext } from "../../context";
|
||||
import type { GitHubPullRequest, GitHubIssue } from "../../types";
|
||||
|
||||
// Mock the entire branch module to avoid executing shell commands
|
||||
const mockSetupBranch = mock();
|
||||
|
||||
// Mock bun shell to prevent actual git commands
|
||||
mock.module("bun", () => ({
|
||||
$: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: () => async () => ({ text: async () => "" }),
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock @actions/core
|
||||
mock.module("@actions/core", () => ({
|
||||
setOutput: mock(),
|
||||
info: mock(),
|
||||
warning: mock(),
|
||||
error: mock(),
|
||||
}));
|
||||
// Mock process.exit to prevent tests from actually exiting
|
||||
const mockExit = mock(() => {});
|
||||
const originalExit = process.exit;
|
||||
|
||||
describe("setupBranch", () => {
|
||||
let mockOctokits: Octokits;
|
||||
@@ -39,196 +16,163 @@ describe("setupBranch", () => {
|
||||
let mockGithubData: FetchDataResult;
|
||||
|
||||
beforeEach(() => {
|
||||
mock.restore();
|
||||
// Replace process.exit temporarily
|
||||
(process as any).exit = mockExit;
|
||||
mockExit.mockClear();
|
||||
|
||||
// Mock the Octokits object with both rest and graphql
|
||||
// Simple mock objects
|
||||
mockOctokits = {
|
||||
rest: {
|
||||
repos: {
|
||||
get: mock(() =>
|
||||
Promise.resolve({
|
||||
data: { default_branch: "main" },
|
||||
}),
|
||||
),
|
||||
get: mock(() => Promise.resolve({ data: { default_branch: "main" } })),
|
||||
},
|
||||
git: {
|
||||
getRef: mock(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
object: { sha: "abc123def456" },
|
||||
},
|
||||
}),
|
||||
),
|
||||
getRef: mock(() => Promise.resolve({
|
||||
data: { object: { sha: "abc123def456" } }
|
||||
})),
|
||||
},
|
||||
},
|
||||
graphql: mock(),
|
||||
graphql: mock(() => Promise.resolve({})),
|
||||
} as any;
|
||||
|
||||
// Create a base context
|
||||
mockContext = {
|
||||
runId: "12345",
|
||||
eventName: "pull_request",
|
||||
repository: {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
full_name: "test-owner/test-repo",
|
||||
},
|
||||
actor: "test-user",
|
||||
entityNumber: 42,
|
||||
isPR: true,
|
||||
isPR: false,
|
||||
entityNumber: 123,
|
||||
inputs: {
|
||||
prompt: "",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
baseBranch: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
useCommitSigning: false,
|
||||
allowedBots: "",
|
||||
trackProgress: true,
|
||||
},
|
||||
payload: {} as any,
|
||||
};
|
||||
} as ParsedGitHubContext;
|
||||
|
||||
// Create mock GitHub data for a PR
|
||||
// Default mock data for issues
|
||||
mockGithubData = {
|
||||
contextData: {
|
||||
headRefName: "feature/test-branch",
|
||||
baseRefName: "main",
|
||||
title: "Test Issue",
|
||||
body: "Test issue body",
|
||||
state: "OPEN",
|
||||
commits: {
|
||||
totalCount: 5,
|
||||
},
|
||||
} as GitHubPullRequest,
|
||||
} as GitHubIssue,
|
||||
comments: [],
|
||||
changedFiles: [],
|
||||
changedFilesWithSHA: [],
|
||||
reviewData: null,
|
||||
imageUrlMap: new Map(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("Branch operation test structure", () => {
|
||||
test("should handle PR context correctly", () => {
|
||||
// Verify PR context structure
|
||||
expect(mockContext.isPR).toBe(true);
|
||||
expect(mockContext.entityNumber).toBe(42);
|
||||
expect(mockGithubData.contextData).toHaveProperty("headRefName");
|
||||
expect(mockGithubData.contextData).toHaveProperty("baseRefName");
|
||||
afterEach(() => {
|
||||
// Restore original process.exit
|
||||
process.exit = originalExit;
|
||||
});
|
||||
|
||||
describe("Issue branch creation", () => {
|
||||
test("should create new branch for issue using default branch as source", async () => {
|
||||
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
expect(result.baseBranch).toBe("main");
|
||||
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||
expect(result.currentBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||
});
|
||||
|
||||
test("should handle issue context correctly", () => {
|
||||
// Convert to issue context
|
||||
mockContext.isPR = false;
|
||||
mockContext.eventName = "issues";
|
||||
mockGithubData.contextData = {
|
||||
title: "Test Issue",
|
||||
body: "Issue description",
|
||||
} as GitHubIssue;
|
||||
|
||||
// Verify issue context structure
|
||||
expect(mockContext.isPR).toBe(false);
|
||||
expect(mockContext.eventName).toBe("issues");
|
||||
expect(mockGithubData.contextData).toHaveProperty("title");
|
||||
expect(mockGithubData.contextData).toHaveProperty("body");
|
||||
});
|
||||
|
||||
test("should verify branch naming conventions", () => {
|
||||
const timestamp = new Date();
|
||||
const formattedTimestamp = `${timestamp.getFullYear()}${String(timestamp.getMonth() + 1).padStart(2, "0")}${String(timestamp.getDate()).padStart(2, "0")}-${String(timestamp.getHours()).padStart(2, "0")}${String(timestamp.getMinutes()).padStart(2, "0")}`;
|
||||
|
||||
// Test PR branch name
|
||||
const prBranchName = `${mockContext.inputs.branchPrefix}pr-${mockContext.entityNumber}-${formattedTimestamp}`;
|
||||
expect(prBranchName).toMatch(/^claude\/pr-42-\d{8}-\d{4}$/);
|
||||
|
||||
// Test issue branch name
|
||||
const issueBranchName = `${mockContext.inputs.branchPrefix}issue-${mockContext.entityNumber}-${formattedTimestamp}`;
|
||||
expect(issueBranchName).toMatch(/^claude\/issue-42-\d{8}-\d{4}$/);
|
||||
|
||||
// Verify Kubernetes compatibility (lowercase, max 50 chars)
|
||||
const kubeName = prBranchName.toLowerCase().substring(0, 50);
|
||||
expect(kubeName).toMatch(/^[a-z0-9\/-]+$/);
|
||||
expect(kubeName.length).toBeLessThanOrEqual(50);
|
||||
});
|
||||
|
||||
test("should handle different PR states", () => {
|
||||
const prData = mockGithubData.contextData as GitHubPullRequest;
|
||||
|
||||
// Test open PR
|
||||
prData.state = "OPEN";
|
||||
expect(prData.state).toBe("OPEN");
|
||||
|
||||
// Test closed PR
|
||||
prData.state = "CLOSED";
|
||||
expect(prData.state).toBe("CLOSED");
|
||||
|
||||
// Test merged PR
|
||||
prData.state = "MERGED";
|
||||
expect(prData.state).toBe("MERGED");
|
||||
});
|
||||
|
||||
test("should handle commit signing configuration", () => {
|
||||
// Without commit signing
|
||||
expect(mockContext.inputs.useCommitSigning).toBe(false);
|
||||
|
||||
// With commit signing
|
||||
mockContext.inputs.useCommitSigning = true;
|
||||
expect(mockContext.inputs.useCommitSigning).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle custom base branch", () => {
|
||||
// Default (no base branch)
|
||||
expect(mockContext.inputs.baseBranch).toBe("");
|
||||
|
||||
// Custom base branch
|
||||
test("should use provided base branch as source", async () => {
|
||||
mockContext.inputs.baseBranch = "develop";
|
||||
expect(mockContext.inputs.baseBranch).toBe("develop");
|
||||
|
||||
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
expect(result.baseBranch).toBe("develop");
|
||||
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||
});
|
||||
|
||||
test("should verify Octokits structure", () => {
|
||||
expect(mockOctokits).toHaveProperty("rest");
|
||||
expect(mockOctokits).toHaveProperty("graphql");
|
||||
expect(mockOctokits.rest).toHaveProperty("repos");
|
||||
expect(mockOctokits.rest).toHaveProperty("git");
|
||||
expect(mockOctokits.rest.repos).toHaveProperty("get");
|
||||
expect(mockOctokits.rest.git).toHaveProperty("getRef");
|
||||
});
|
||||
test("should handle commit signing mode", async () => {
|
||||
mockContext.inputs.useCommitSigning = true;
|
||||
|
||||
test("should verify FetchDataResult structure", () => {
|
||||
expect(mockGithubData).toHaveProperty("contextData");
|
||||
expect(mockGithubData).toHaveProperty("comments");
|
||||
expect(mockGithubData).toHaveProperty("changedFiles");
|
||||
expect(mockGithubData).toHaveProperty("changedFilesWithSHA");
|
||||
expect(mockGithubData).toHaveProperty("reviewData");
|
||||
expect(mockGithubData).toHaveProperty("imageUrlMap");
|
||||
});
|
||||
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
test("should handle PR with varying commit counts", () => {
|
||||
const prData = mockGithubData.contextData as GitHubPullRequest;
|
||||
|
||||
// Few commits
|
||||
prData.commits.totalCount = 5;
|
||||
const fetchDepthSmall = Math.max(prData.commits.totalCount, 20);
|
||||
expect(fetchDepthSmall).toBe(20);
|
||||
|
||||
// Many commits
|
||||
prData.commits.totalCount = 150;
|
||||
const fetchDepthLarge = Math.max(prData.commits.totalCount, 20);
|
||||
expect(fetchDepthLarge).toBe(150);
|
||||
});
|
||||
|
||||
test("should verify branch prefix customization", () => {
|
||||
// Default prefix
|
||||
expect(mockContext.inputs.branchPrefix).toBe("claude/");
|
||||
|
||||
// Custom prefix
|
||||
mockContext.inputs.branchPrefix = "bot/";
|
||||
expect(mockContext.inputs.branchPrefix).toBe("bot/");
|
||||
|
||||
// Another custom prefix
|
||||
mockContext.inputs.branchPrefix = "ai-assistant/";
|
||||
expect(mockContext.inputs.branchPrefix).toBe("ai-assistant/");
|
||||
expect(result.baseBranch).toBe("main");
|
||||
expect(result.currentBranch).toBe("main"); // Should stay on source branch
|
||||
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("PR branch handling", () => {
|
||||
beforeEach(() => {
|
||||
mockContext.isPR = true;
|
||||
mockGithubData.contextData = {
|
||||
title: "Test PR",
|
||||
body: "Test PR body",
|
||||
state: "OPEN",
|
||||
baseRefName: "main",
|
||||
headRefName: "feature/test",
|
||||
commits: { totalCount: 5 },
|
||||
} as GitHubPullRequest;
|
||||
});
|
||||
|
||||
test("should checkout existing PR branch for open PR", async () => {
|
||||
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
expect(result.baseBranch).toBe("main");
|
||||
expect(result.currentBranch).toBe("feature/test");
|
||||
expect(result.claudeBranch).toBeUndefined(); // No claude branch for open PRs
|
||||
});
|
||||
|
||||
test("should create new branch for closed PR", async () => {
|
||||
const closedPR = mockGithubData.contextData as GitHubPullRequest;
|
||||
closedPR.state = "CLOSED";
|
||||
|
||||
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
expect(result.baseBranch).toBe("main");
|
||||
expect(result.claudeBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
|
||||
expect(result.currentBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
|
||||
});
|
||||
|
||||
test("should create new branch for merged PR", async () => {
|
||||
const mergedPR = mockGithubData.contextData as GitHubPullRequest;
|
||||
mergedPR.state = "MERGED";
|
||||
|
||||
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
expect(result.baseBranch).toBe("main");
|
||||
expect(result.claudeBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error handling", () => {
|
||||
test("should exit with code 1 when source branch doesn't exist", async () => {
|
||||
mockOctokits.rest.git.getRef = mock(() => Promise.reject(new Error("Branch not found")));
|
||||
|
||||
await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
test("should exit with code 1 when repository fetch fails", async () => {
|
||||
mockOctokits.rest.repos.get = mock(() => Promise.reject(new Error("Repository not found")));
|
||||
|
||||
await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Branch naming", () => {
|
||||
test("should generate kubernetes-compatible branch names", async () => {
|
||||
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
// Branch name should be lowercase, use hyphens, and include timestamp
|
||||
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||
expect(result.claudeBranch?.length).toBeLessThanOrEqual(50);
|
||||
});
|
||||
|
||||
test("should use custom branch prefix", async () => {
|
||||
mockContext.inputs.branchPrefix = "ai/";
|
||||
|
||||
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||
|
||||
expect(result.claudeBranch).toMatch(/^ai\/issue-123-\d{8}-\d{4}$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user