diff --git a/actions/setup/js/remove_trigger_label.cjs b/actions/setup/js/remove_trigger_label.cjs
index 102342778a8..b9dd6df6de5 100644
--- a/actions/setup/js/remove_trigger_label.cjs
+++ b/actions/setup/js/remove_trigger_label.cjs
@@ -2,6 +2,7 @@
///
const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs");
+const { getErrorMessage } = require("./error_helpers.cjs");
/**
* Remove the label that triggered this workflow from the issue, pull request, or discussion.
@@ -13,8 +14,6 @@ const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs");
async function main() {
const labelNamesJSON = process.env.GH_AW_LABEL_NAMES;
- const { getErrorMessage } = require("./error_helpers.cjs");
-
if (!labelNamesJSON) {
core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_LABEL_NAMES not specified.`);
return;
diff --git a/actions/setup/js/remove_trigger_label.test.cjs b/actions/setup/js/remove_trigger_label.test.cjs
new file mode 100644
index 00000000000..59d8ed413f5
--- /dev/null
+++ b/actions/setup/js/remove_trigger_label.test.cjs
@@ -0,0 +1,220 @@
+// @ts-check
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+const { ERR_CONFIG, ERR_API } = require("./error_codes.cjs");
+
+const mockCore = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ setFailed: vi.fn(),
+ setOutput: vi.fn(),
+};
+
+const mockGithub = {
+ rest: {
+ issues: {
+ removeLabel: vi.fn(),
+ },
+ },
+ graphql: vi.fn(),
+};
+
+/** @type {any} */
+let mockContext = {
+ eventName: "issues",
+ repo: { owner: "testowner", repo: "testrepo" },
+ payload: {
+ label: { name: "ai-label", node_id: "LA_label1" },
+ issue: { number: 42 },
+ },
+};
+
+global.core = mockCore;
+global.github = mockGithub;
+global.context = mockContext;
+
+/** @returns {Promise} */
+async function runScript() {
+ const { main } = await import("./remove_trigger_label.cjs?" + Date.now());
+ await main();
+}
+
+describe("remove_trigger_label", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.resetModules();
+
+ process.env.GH_AW_LABEL_NAMES = JSON.stringify(["ai-label", "bot-run"]);
+
+ mockContext = {
+ eventName: "issues",
+ repo: { owner: "testowner", repo: "testrepo" },
+ payload: {
+ label: { name: "ai-label", node_id: "LA_label1" },
+ issue: { number: 42 },
+ },
+ };
+ global.context = mockContext;
+
+ mockGithub.rest.issues.removeLabel.mockResolvedValue({});
+ mockGithub.graphql.mockResolvedValue({});
+ });
+
+ afterEach(() => {
+ delete process.env.GH_AW_LABEL_NAMES;
+ });
+
+ describe("missing configuration", () => {
+ it("should fail when GH_AW_LABEL_NAMES is not set", async () => {
+ delete process.env.GH_AW_LABEL_NAMES;
+ await runScript();
+ expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(ERR_CONFIG));
+ });
+
+ it("should fail when GH_AW_LABEL_NAMES is invalid JSON", async () => {
+ process.env.GH_AW_LABEL_NAMES = "not-valid-json";
+ await runScript();
+ expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(ERR_CONFIG));
+ });
+
+ it("should fail when GH_AW_LABEL_NAMES is not an array", async () => {
+ process.env.GH_AW_LABEL_NAMES = JSON.stringify({ label: "ai-label" });
+ await runScript();
+ expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(ERR_CONFIG));
+ });
+ });
+
+ describe("workflow_dispatch event", () => {
+ it("should skip label removal for workflow_dispatch", async () => {
+ global.context = { ...mockContext, eventName: "workflow_dispatch", payload: {} };
+ await runScript();
+ expect(mockGithub.rest.issues.removeLabel).not.toHaveBeenCalled();
+ expect(mockCore.setOutput).toHaveBeenCalledWith("label_name", "");
+ });
+ });
+
+ describe("no trigger label in payload", () => {
+ it("should skip removal when payload has no label", async () => {
+ global.context = { ...mockContext, payload: { issue: { number: 42 } } };
+ await runScript();
+ expect(mockGithub.rest.issues.removeLabel).not.toHaveBeenCalled();
+ expect(mockCore.setOutput).toHaveBeenCalledWith("label_name", "");
+ });
+ });
+
+ describe("label not in configured list", () => {
+ it("should skip removal when label is not configured", async () => {
+ global.context = {
+ ...mockContext,
+ payload: { label: { name: "random-label" }, issue: { number: 42 } },
+ };
+ await runScript();
+ expect(mockGithub.rest.issues.removeLabel).not.toHaveBeenCalled();
+ expect(mockCore.setOutput).toHaveBeenCalledWith("label_name", "random-label");
+ });
+ });
+
+ describe("issues event", () => {
+ it("should remove label from issue", async () => {
+ await runScript();
+ expect(mockGithub.rest.issues.removeLabel).toHaveBeenCalledWith({
+ owner: "testowner",
+ repo: "testrepo",
+ issue_number: 42,
+ name: "ai-label",
+ });
+ expect(mockCore.setOutput).toHaveBeenCalledWith("label_name", "ai-label");
+ });
+
+ it("should skip when issue number is missing", async () => {
+ global.context = {
+ ...mockContext,
+ payload: { label: { name: "ai-label" } },
+ };
+ await runScript();
+ expect(mockGithub.rest.issues.removeLabel).not.toHaveBeenCalled();
+ expect(mockCore.setOutput).toHaveBeenCalledWith("label_name", "ai-label");
+ });
+ });
+
+ describe("pull_request event", () => {
+ it("should remove label from pull request", async () => {
+ global.context = {
+ ...mockContext,
+ eventName: "pull_request",
+ payload: {
+ label: { name: "ai-label" },
+ pull_request: { number: 99 },
+ },
+ };
+ await runScript();
+ expect(mockGithub.rest.issues.removeLabel).toHaveBeenCalledWith({
+ owner: "testowner",
+ repo: "testrepo",
+ issue_number: 99,
+ name: "ai-label",
+ });
+ expect(mockCore.setOutput).toHaveBeenCalledWith("label_name", "ai-label");
+ });
+
+ it("should skip when PR number is missing", async () => {
+ global.context = {
+ ...mockContext,
+ eventName: "pull_request",
+ payload: { label: { name: "ai-label" } },
+ };
+ await runScript();
+ expect(mockGithub.rest.issues.removeLabel).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("discussion event", () => {
+ it("should remove label from discussion via graphql", async () => {
+ global.context = {
+ ...mockContext,
+ eventName: "discussion",
+ payload: {
+ label: { name: "ai-label", node_id: "LA_label1" },
+ discussion: { node_id: "D_disc1" },
+ },
+ };
+ await runScript();
+ expect(mockGithub.graphql).toHaveBeenCalledWith(
+ expect.stringContaining("removeLabelsFromLabelable"),
+ expect.objectContaining({
+ labelableId: "D_disc1",
+ labelIds: ["LA_label1"],
+ })
+ );
+ expect(mockCore.setOutput).toHaveBeenCalledWith("label_name", "ai-label");
+ });
+
+ it("should skip when discussion node_id is missing", async () => {
+ global.context = {
+ ...mockContext,
+ eventName: "discussion",
+ payload: { label: { name: "ai-label" } },
+ };
+ await runScript();
+ expect(mockGithub.graphql).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("error handling", () => {
+ it("should treat 404 error as already-removed (non-fatal)", async () => {
+ const err = Object.assign(new Error("Not Found"), { status: 404 });
+ mockGithub.rest.issues.removeLabel.mockRejectedValue(err);
+ await runScript();
+ expect(mockCore.setFailed).not.toHaveBeenCalled();
+ expect(mockCore.setOutput).toHaveBeenCalledWith("label_name", "ai-label");
+ });
+
+ it("should warn on non-404 API errors", async () => {
+ const err = Object.assign(new Error("Server error"), { status: 500 });
+ mockGithub.rest.issues.removeLabel.mockRejectedValue(err);
+ await runScript();
+ expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining(ERR_API));
+ expect(mockCore.setFailed).not.toHaveBeenCalled();
+ });
+ });
+});