Files
claude-code-action/test/actor.test.ts
T
Vlad MocanuandClaude Opus 4.6 c97c9765c3 fix: allow Forgejo bot actor on non-comment events
The self-trigger guard blocked the claude bot on all events. Loop risk
only exists for comment/review events (tag mode). PR, workflow_dispatch,
and schedule events are safe — refined the guard to only block on
issue_comment, issues, pull_request_review, and
pull_request_review_comment events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:57:59 +02:00

246 lines
8.2 KiB
TypeScript

#!/usr/bin/env bun
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { checkHumanActor } from "../src/github/validation/actor";
import { _resetPlatformCache } from "../src/platform/detector";
import type { Octokit } from "@octokit/rest";
import { createMockContext } from "./mockContext";
function createMockOctokit(userType: string): Octokit {
return {
users: {
getByUsername: async () => ({
data: {
type: userType,
},
}),
},
} as unknown as Octokit;
}
describe("checkHumanActor", () => {
test("should pass for human actor", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.actor = "human-user";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should throw error for bot actor when not allowed", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "test-bot[bot]";
context.inputs.allowedBots = "";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: test-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
test("should pass for bot actor when all bots allowed", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "test-bot[bot]";
context.inputs.allowedBots = "*";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should pass for specific bot when in allowed list", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "dependabot[bot]";
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should pass for specific bot when in allowed list (without [bot])", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "dependabot[bot]";
context.inputs.allowedBots = "dependabot,renovate";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should throw error for bot not in allowed list", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "other-bot[bot]";
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
test("should throw error for bot not in allowed list (without [bot])", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "other-bot[bot]";
context.inputs.allowedBots = "dependabot,renovate";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
test("should treat undefined actor type as human (GitHub compatibility)", async () => {
// Edge case: GitHub API returns no `type` field
const mockOctokit = {
users: {
getByUsername: async () => ({
data: {},
}),
},
} as unknown as Octokit;
const context = createMockContext();
context.actor = "unknown-user";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
describe("Forgejo platform", () => {
let savedPlatform: string | undefined;
let savedServerUrl: string | undefined;
beforeEach(() => {
_resetPlatformCache();
savedPlatform = process.env.PLATFORM;
savedServerUrl = process.env.GITHUB_SERVER_URL;
process.env.PLATFORM = "forgejo";
// Ensure cross-validation doesn't reject PLATFORM=forgejo
process.env.GITHUB_SERVER_URL = "https://git.example.com";
});
afterEach(() => {
if (savedPlatform !== undefined) process.env.PLATFORM = savedPlatform;
else delete process.env.PLATFORM;
if (savedServerUrl !== undefined)
process.env.GITHUB_SERVER_URL = savedServerUrl;
else delete process.env.GITHUB_SERVER_URL;
_resetPlatformCache();
});
test("should skip GitHub API actor check on Forgejo for regular users", async () => {
let apiCalled = false;
const mockOctokit = {
users: {
getByUsername: async () => {
apiCalled = true;
return { data: {} };
},
},
} as unknown as Octokit;
const context = createMockContext();
context.actor = "forgejo-user";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
expect(apiCalled).toBe(false);
});
test("should skip actor check for bot-like names that don't match bot_name", async () => {
let apiCalled = false;
const mockOctokit = {
users: {
getByUsername: async () => {
apiCalled = true;
return { data: { type: "Bot" } };
},
},
} as unknown as Octokit;
const context = createMockContext();
context.actor = "some-bot[bot]";
context.inputs.allowedBots = "";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
expect(apiCalled).toBe(false);
});
test("should block self-trigger when actor matches bot_name (with [bot] suffix)", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
// Default botName is "claude[bot]" from CLAUDE_BOT_LOGIN
context.actor = "claude[bot]";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
test("should block self-trigger when actor matches bot_name without [bot] suffix", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.inputs.botName = "claude[bot]";
context.actor = "claude";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
test("should block self-trigger with custom bot_name", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.inputs.botName = "my-assistant";
context.actor = "my-assistant";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
test("should allow bot actor on pull_request events (no loop risk)", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.actor = "claude";
context.inputs.botName = "claude[bot]";
(context as { eventName: string }).eventName = "pull_request";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should allow bot actor on workflow_dispatch events (no loop risk)", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.actor = "claude";
context.inputs.botName = "claude[bot]";
(context as { eventName: string }).eventName = "workflow_dispatch";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should block bot actor on issue_comment events (loop risk)", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.actor = "claude";
context.inputs.botName = "claude[bot]";
// eventName defaults to "issue_comment" from createMockContext
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
});
});