* fix: fall back to repo default_branch instead of hardcoded "main" When no explicit base_branch input is provided, the action previously fell back to a hardcoded "main", which fails on repositories whose default branch is named differently (e.g. "master", "develop"). This reads repository.default_branch from the GitHub event payload (populated once in parseGitHubContext) and uses it as the fallback in all three callsites: agent/index.ts, run.ts, and update-comment-link.ts. Explicit env/input precedence is preserved; "main" remains only as a last-resort defensive fallback if the payload somehow lacks the field. * test: drop unused BASE_BRANCH env handling from default_branch test agent/index.ts no longer reads process.env.BASE_BRANCH directly (it now goes through context.inputs.baseBranch which is set on the mock context), so saving/clearing/restoring that env var in the regression test is dead code.
261 lines
7.7 KiB
TypeScript
261 lines
7.7 KiB
TypeScript
import {
|
|
describe,
|
|
test,
|
|
expect,
|
|
beforeEach,
|
|
afterEach,
|
|
spyOn,
|
|
mock,
|
|
} from "bun:test";
|
|
import { prepareAgentMode } from "../../src/modes/agent";
|
|
import { createMockAutomationContext } from "../mockContext";
|
|
import * as core from "@actions/core";
|
|
import * as gitConfig from "../../src/github/operations/git-config";
|
|
|
|
describe("Agent Mode", () => {
|
|
let exportVariableSpy: any;
|
|
let setOutputSpy: any;
|
|
let configureGitAuthSpy: any;
|
|
|
|
beforeEach(() => {
|
|
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
|
|
() => {},
|
|
);
|
|
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
|
// Mock configureGitAuth to prevent actual git commands from running
|
|
configureGitAuthSpy = spyOn(
|
|
gitConfig,
|
|
"configureGitAuth",
|
|
).mockImplementation(async () => {
|
|
// Do nothing - prevent actual git config modifications
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
exportVariableSpy?.mockClear();
|
|
setOutputSpy?.mockClear();
|
|
configureGitAuthSpy?.mockClear();
|
|
exportVariableSpy?.mockRestore();
|
|
setOutputSpy?.mockRestore();
|
|
configureGitAuthSpy?.mockRestore();
|
|
});
|
|
|
|
test("prepareAgentMode is exported as a function", () => {
|
|
expect(typeof prepareAgentMode).toBe("function");
|
|
});
|
|
|
|
test("prepare passes through claude_args", async () => {
|
|
// Clear any previous calls before this test
|
|
exportVariableSpy.mockClear();
|
|
setOutputSpy.mockClear();
|
|
|
|
const contextWithCustomArgs = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
});
|
|
|
|
// Save original env vars and set test values
|
|
const originalHeadRef = process.env.GITHUB_HEAD_REF;
|
|
const originalRefName = process.env.GITHUB_REF_NAME;
|
|
delete process.env.GITHUB_HEAD_REF;
|
|
delete process.env.GITHUB_REF_NAME;
|
|
|
|
// Set CLAUDE_ARGS environment variable
|
|
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10";
|
|
|
|
const mockOctokit = {
|
|
rest: {
|
|
users: {
|
|
getAuthenticated: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345, type: "User" },
|
|
}),
|
|
),
|
|
getByUsername: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345, type: "User" },
|
|
}),
|
|
),
|
|
},
|
|
},
|
|
} as any;
|
|
const result = await prepareAgentMode({
|
|
context: contextWithCustomArgs,
|
|
octokit: mockOctokit,
|
|
githubToken: "test-token",
|
|
});
|
|
|
|
// Verify claude_args includes user args (no MCP config in agent mode without allowed tools)
|
|
expect(result.claudeArgs).toBe("--model claude-sonnet-4 --max-turns 10");
|
|
expect(result.claudeArgs).not.toContain("--mcp-config");
|
|
|
|
// Verify return structure - should fall back to repository.default_branch when no env vars set
|
|
expect(result).toEqual({
|
|
commentId: undefined,
|
|
branchInfo: {
|
|
baseBranch: "main",
|
|
currentBranch: "main",
|
|
claudeBranch: undefined,
|
|
},
|
|
mcpConfig: expect.any(String),
|
|
claudeArgs: "--model claude-sonnet-4 --max-turns 10",
|
|
});
|
|
|
|
// Clean up
|
|
delete process.env.CLAUDE_ARGS;
|
|
if (originalHeadRef !== undefined)
|
|
process.env.GITHUB_HEAD_REF = originalHeadRef;
|
|
if (originalRefName !== undefined)
|
|
process.env.GITHUB_REF_NAME = originalRefName;
|
|
});
|
|
|
|
test("prepare falls back to repository.default_branch when not 'main'", async () => {
|
|
const contextWithDevelop = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
repository: {
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
full_name: "test-owner/test-repo",
|
|
default_branch: "develop",
|
|
},
|
|
});
|
|
|
|
// Save and clear env vars that would otherwise override the fallback
|
|
const originalClaudeBranch = process.env.CLAUDE_BRANCH;
|
|
const originalHeadRef = process.env.GITHUB_HEAD_REF;
|
|
const originalRefName = process.env.GITHUB_REF_NAME;
|
|
delete process.env.CLAUDE_BRANCH;
|
|
delete process.env.GITHUB_HEAD_REF;
|
|
delete process.env.GITHUB_REF_NAME;
|
|
|
|
const mockOctokit = {
|
|
rest: {
|
|
users: {
|
|
getAuthenticated: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345, type: "User" },
|
|
}),
|
|
),
|
|
getByUsername: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345, type: "User" },
|
|
}),
|
|
),
|
|
},
|
|
},
|
|
} as any;
|
|
|
|
const result = await prepareAgentMode({
|
|
context: contextWithDevelop,
|
|
octokit: mockOctokit,
|
|
githubToken: "test-token",
|
|
});
|
|
|
|
expect(result.branchInfo.baseBranch).toBe("develop");
|
|
expect(result.branchInfo.currentBranch).toBe("develop");
|
|
|
|
// Restore env vars
|
|
if (originalClaudeBranch !== undefined)
|
|
process.env.CLAUDE_BRANCH = originalClaudeBranch;
|
|
if (originalHeadRef !== undefined)
|
|
process.env.GITHUB_HEAD_REF = originalHeadRef;
|
|
if (originalRefName !== undefined)
|
|
process.env.GITHUB_REF_NAME = originalRefName;
|
|
});
|
|
|
|
test("prepare rejects bot actors without allowed_bots", async () => {
|
|
const contextWithPrompts = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
});
|
|
contextWithPrompts.actor = "claude[bot]";
|
|
contextWithPrompts.inputs.allowedBots = "";
|
|
|
|
const mockOctokit = {
|
|
rest: {
|
|
users: {
|
|
getByUsername: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "claude[bot]", id: 12345, type: "Bot" },
|
|
}),
|
|
),
|
|
},
|
|
},
|
|
} as any;
|
|
|
|
await expect(
|
|
prepareAgentMode({
|
|
context: contextWithPrompts,
|
|
octokit: mockOctokit,
|
|
githubToken: "test-token",
|
|
}),
|
|
).rejects.toThrow(
|
|
"Workflow initiated by non-human actor: claude (type: Bot)",
|
|
);
|
|
});
|
|
|
|
test("prepare allows bot actors when in allowed_bots list", async () => {
|
|
const contextWithPrompts = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
});
|
|
contextWithPrompts.actor = "dependabot[bot]";
|
|
contextWithPrompts.inputs.allowedBots = "dependabot";
|
|
|
|
const mockOctokit = {
|
|
rest: {
|
|
users: {
|
|
getByUsername: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "dependabot[bot]", id: 12345, type: "Bot" },
|
|
}),
|
|
),
|
|
},
|
|
},
|
|
} as any;
|
|
|
|
// Should not throw - bot is in allowed list
|
|
await expect(
|
|
prepareAgentMode({
|
|
context: contextWithPrompts,
|
|
octokit: mockOctokit,
|
|
githubToken: "test-token",
|
|
}),
|
|
).resolves.toBeDefined();
|
|
});
|
|
|
|
test("prepare creates prompt file with correct content", async () => {
|
|
const contextWithPrompts = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
});
|
|
// In v1-dev, we only have the unified prompt field
|
|
contextWithPrompts.inputs.prompt = "Custom prompt content";
|
|
|
|
const mockOctokit = {
|
|
rest: {
|
|
users: {
|
|
getAuthenticated: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345, type: "User" },
|
|
}),
|
|
),
|
|
getByUsername: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345, type: "User" },
|
|
}),
|
|
),
|
|
},
|
|
},
|
|
} as any;
|
|
const result = await prepareAgentMode({
|
|
context: contextWithPrompts,
|
|
octokit: mockOctokit,
|
|
githubToken: "test-token",
|
|
});
|
|
|
|
// Note: We can't easily test file creation in this unit test,
|
|
// but we can verify the method completes without errors
|
|
// With our conditional MCP logic, agent mode with no allowed tools
|
|
// should not include any MCP config
|
|
// Should be empty or just whitespace when no MCP servers are included
|
|
expect(result.claudeArgs).not.toContain("--mcp-config");
|
|
});
|
|
});
|