From 214dd3be35afaa17ee69641e3a95e40cc2c101bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:39:57 +0000 Subject: [PATCH 1/2] jsweep: clean dispatch_repository.cjs - Use error_codes.cjs constants (SAFE_OUTPUT_E001, SAFE_OUTPUT_E099) instead of raw string literals - Replace manual for-of loop with spread operator when building clientPayload from message.inputs - Add comprehensive test file with 18 tests covering all handler paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- actions/setup/js/dispatch_repository.cjs | 18 +- actions/setup/js/dispatch_repository.test.cjs | 251 ++++++++++++++++++ 2 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 actions/setup/js/dispatch_repository.test.cjs diff --git a/actions/setup/js/dispatch_repository.cjs b/actions/setup/js/dispatch_repository.cjs index a59e9132af2..6dfc1034384 100644 --- a/actions/setup/js/dispatch_repository.cjs +++ b/actions/setup/js/dispatch_repository.cjs @@ -14,6 +14,7 @@ const { parseRepoSlug, validateTargetRepo, parseAllowedRepos } = require("./repo const { logStagedPreviewInfo } = require("./staged_preview.cjs"); const { isStagedMode } = require("./safe_output_helpers.cjs"); const { buildAwContext } = require("./aw_context.cjs"); +const { SAFE_OUTPUT_E001, SAFE_OUTPUT_E099 } = require("./error_codes.cjs"); /** * Main handler factory for dispatch_repository @@ -45,7 +46,7 @@ async function main(config = {}) { core.warning("dispatch_repository: tool_name is empty, skipping"); return { success: false, - error: "E001: tool_name is required", + error: `${SAFE_OUTPUT_E001}: tool_name is required`, }; } @@ -55,7 +56,7 @@ async function main(config = {}) { core.warning(`dispatch_repository: unknown tool "${toolName}", skipping`); return { success: false, - error: `E001: tool "${toolName}" is not configured in dispatch_repository`, + error: `${SAFE_OUTPUT_E001}: tool "${toolName}" is not configured in dispatch_repository`, }; } @@ -88,7 +89,7 @@ async function main(config = {}) { core.warning(`dispatch_repository: no target repository for tool "${toolName}"`); return { success: false, - error: `E001: No target repository configured for tool "${toolName}"`, + error: `${SAFE_OUTPUT_E001}: No target repository configured for tool "${toolName}"`, }; } @@ -110,7 +111,7 @@ async function main(config = {}) { core.warning(`dispatch_repository: invalid repository slug "${targetRepoSlug}"`); return { success: false, - error: `E001: Invalid repository slug "${targetRepoSlug}" (expected "owner/repo")`, + error: `${SAFE_OUTPUT_E001}: Invalid repository slug "${targetRepoSlug}" (expected "owner/repo")`, }; } @@ -118,14 +119,9 @@ async function main(config = {}) { /** @type {Record} */ const clientPayload = { workflow: toolConfig.workflow || "", + ...(message.inputs && typeof message.inputs === "object" ? message.inputs : {}), }; - if (message.inputs && typeof message.inputs === "object") { - for (const [key, value] of Object.entries(message.inputs)) { - clientPayload[key] = value; - } - } - // Inject aw_context so the receiving repository can trace the dispatch back to its caller. clientPayload["aw_context"] = buildAwContext(); @@ -178,7 +174,7 @@ async function main(config = {}) { return { success: false, - error: `E099: Failed to dispatch repository_dispatch event "${eventType}" to ${targetRepoSlug}: ${errorMessage}`, + error: `${SAFE_OUTPUT_E099}: Failed to dispatch repository_dispatch event "${eventType}" to ${targetRepoSlug}: ${errorMessage}`, }; } }; diff --git a/actions/setup/js/dispatch_repository.test.cjs b/actions/setup/js/dispatch_repository.test.cjs new file mode 100644 index 00000000000..abb6b1da01a --- /dev/null +++ b/actions/setup/js/dispatch_repository.test.cjs @@ -0,0 +1,251 @@ +// @ts-check +import { describe, it, expect, beforeEach, vi } from "vitest"; + +const { main } = require("./dispatch_repository.cjs"); + +describe("dispatch_repository", () => { + /** @type {any} */ + let mockCore; + /** @type {any} */ + let mockGithub; + /** @type {any} */ + let mockContext; + + /** @type {any} */ + let dispatchEventCalls; + + beforeEach(() => { + dispatchEventCalls = []; + + mockCore = { + infos: /** @type {string[]} */ [], + warnings: /** @type {string[]} */ [], + errors: /** @type {string[]} */ [], + info: /** @param {string} msg */ msg => mockCore.infos.push(msg), + warning: /** @param {string} msg */ msg => mockCore.warnings.push(msg), + error: /** @param {string} msg */ msg => mockCore.errors.push(msg), + }; + + mockGithub = { + rest: { + repos: { + createDispatchEvent: /** @param {any} params */ async params => { + dispatchEventCalls.push(params); + return { data: {} }; + }, + }, + }, + }; + + mockContext = { + runId: 9999, + repo: { owner: "test-owner", repo: "test-repo" }, + payload: {}, + }; + + global.core = mockCore; + global.github = mockGithub; + global.context = mockContext; + + // Stub handler_auth to return the mock github client + const handlerAuth = require("./handler_auth.cjs"); + vi.spyOn(handlerAuth, "createAuthenticatedGitHubClient").mockResolvedValue(mockGithub); + }); + + describe("main factory", () => { + it("should return a handler function", async () => { + const handler = await main({ tools: {} }); + expect(typeof handler).toBe("function"); + }); + + it("should return a handler function with no config", async () => { + const handler = await main(); + expect(typeof handler).toBe("function"); + }); + + it("should log initialization info", async () => { + await main({ tools: { deploy: { event_type: "deploy", max: 2 } } }); + expect(mockCore.infos.some(/** @param {string} m */ m => m.includes("dispatch_repository handler initialized"))).toBe(true); + }); + }); + + describe("handleDispatchRepository", () => { + it("should return error when tool_name is missing", async () => { + const handler = await main({ tools: {} }); + const result = await handler({ tool_name: "" }, {}); + expect(result.success).toBe(false); + expect(result.error).toMatch(/tool_name is required/); + }); + + it("should return error when tool_name is whitespace-only", async () => { + const handler = await main({ tools: {} }); + const result = await handler({ tool_name: " " }, {}); + expect(result.success).toBe(false); + expect(result.error).toMatch(/tool_name is required/); + }); + + it("should return error when tool is not configured", async () => { + const handler = await main({ tools: {} }); + const result = await handler({ tool_name: "unknown_tool" }, {}); + expect(result.success).toBe(false); + expect(result.error).toMatch(/not configured/); + }); + + it("should return error when max count is reached", async () => { + const handler = await main({ + tools: { + deploy: { event_type: "deploy", repository: "test-owner/test-repo", max: 1 }, + }, + }); + + // First dispatch succeeds + const first = await handler({ tool_name: "deploy" }, {}); + expect(first.success).toBe(true); + + // Second dispatch hits the limit + const second = await handler({ tool_name: "deploy" }, {}); + expect(second.success).toBe(false); + expect(second.error).toMatch(/Max count/); + }); + + it("should return error when no target repository is configured", async () => { + const handler = await main({ + tools: { deploy: { event_type: "deploy" } }, + }); + const result = await handler({ tool_name: "deploy" }, {}); + expect(result.success).toBe(false); + expect(result.error).toMatch(/No target repository/); + }); + + it("should return error when repository slug is invalid", async () => { + const handler = await main({ + tools: { deploy: { event_type: "deploy", repository: "not-valid-slug" } }, + }); + const result = await handler({ tool_name: "deploy" }, {}); + expect(result.success).toBe(false); + expect(result.error).toMatch(/Invalid repository slug/); + }); + + it("should return error when event_type is not configured", async () => { + const handler = await main({ + tools: { deploy: { repository: "test-owner/test-repo" } }, + }); + const result = await handler({ tool_name: "deploy" }, {}); + expect(result.success).toBe(false); + expect(result.error).toMatch(/event_type is required/); + }); + + it("should dispatch successfully with valid config", async () => { + const handler = await main({ + tools: { + deploy: { event_type: "deploy", repository: "test-owner/other-repo", max: 2 }, + }, + }); + + const result = await handler({ tool_name: "deploy" }, {}); + expect(result.success).toBe(true); + expect(result.tool_name).toBe("deploy"); + expect(result.repository).toBe("test-owner/other-repo"); + expect(result.event_type).toBe("deploy"); + expect(dispatchEventCalls.length).toBe(1); + expect(dispatchEventCalls[0].event_type).toBe("deploy"); + }); + + it("should include message inputs in client_payload", async () => { + const handler = await main({ + tools: { + deploy: { event_type: "deploy", repository: "test-owner/test-repo", max: 5 }, + }, + }); + + await handler({ tool_name: "deploy", inputs: { env: "production", version: "1.2.3" } }, {}); + + expect(dispatchEventCalls.length).toBe(1); + expect(dispatchEventCalls[0].client_payload.env).toBe("production"); + expect(dispatchEventCalls[0].client_payload.version).toBe("1.2.3"); + }); + + it("should inject aw_context into client_payload", async () => { + const handler = await main({ + tools: { + deploy: { event_type: "deploy", repository: "test-owner/test-repo", max: 5 }, + }, + }); + + await handler({ tool_name: "deploy" }, {}); + + expect(dispatchEventCalls.length).toBe(1); + expect(dispatchEventCalls[0].client_payload).toHaveProperty("aw_context"); + }); + + it("should use message.repository over toolConfig.repository when both set", async () => { + const handler = await main({ + tools: { + deploy: { event_type: "deploy", repository: "test-owner/default-repo", max: 5 }, + }, + }); + + const result = await handler({ tool_name: "deploy", repository: "test-owner/override-repo" }, {}); + + expect(result.success).toBe(true); + expect(result.repository).toBe("test-owner/override-repo"); + expect(dispatchEventCalls[0].owner).toBe("test-owner"); + expect(dispatchEventCalls[0].repo).toBe("override-repo"); + }); + + it("should default to first allowed_repository when no target given", async () => { + const handler = await main({ + tools: { + deploy: { event_type: "deploy", allowed_repositories: ["test-owner/allowed-repo"], max: 5 }, + }, + }); + + const result = await handler({ tool_name: "deploy" }, {}); + expect(result.success).toBe(true); + expect(result.repository).toBe("test-owner/allowed-repo"); + }); + + it("should return error on API failure", async () => { + mockGithub.rest.repos.createDispatchEvent = async () => { + throw new Error("API rate limit exceeded"); + }; + + const handler = await main({ + tools: { deploy: { event_type: "deploy", repository: "test-owner/test-repo", max: 5 } }, + }); + + const result = await handler({ tool_name: "deploy" }, {}); + expect(result.success).toBe(false); + expect(result.error).toMatch(/Failed to dispatch/); + expect(result.error).toMatch(/API rate limit exceeded/); + }); + + it("should return staged result when isStaged is true", async () => { + const handler = await main({ + tools: { deploy: { event_type: "deploy", repository: "test-owner/test-repo", max: 5 } }, + staged: true, + }); + + const result = await handler({ tool_name: "deploy" }, {}); + expect(result.success).toBe(true); + expect(result.staged).toBe(true); + expect(dispatchEventCalls.length).toBe(0); + }); + + it("should parse numeric max from string config", async () => { + const handler = await main({ + tools: { deploy: { event_type: "deploy", repository: "test-owner/test-repo", max: "3" } }, + }); + + // Should allow up to 3 dispatches + for (let i = 0; i < 3; i++) { + const result = await handler({ tool_name: "deploy" }, {}); + expect(result.success).toBe(true); + } + + const overflow = await handler({ tool_name: "deploy" }, {}); + expect(overflow.success).toBe(false); + expect(overflow.error).toMatch(/Max count of 3/); + }); + }); +}); From 7e6c10e7a2318aa52e6ef0405f276c87adf4e084 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:38:51 +0000 Subject: [PATCH 2/2] fix(tests): restore globals in afterEach, remove broken handler_auth spy Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b554d0eb-cf0b-441e-bc19-9b58bae2cee3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/dispatch_repository.test.cjs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/dispatch_repository.test.cjs b/actions/setup/js/dispatch_repository.test.cjs index abb6b1da01a..77eac4136cb 100644 --- a/actions/setup/js/dispatch_repository.test.cjs +++ b/actions/setup/js/dispatch_repository.test.cjs @@ -1,5 +1,5 @@ // @ts-check -import { describe, it, expect, beforeEach, vi } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; const { main } = require("./dispatch_repository.cjs"); @@ -14,7 +14,16 @@ describe("dispatch_repository", () => { /** @type {any} */ let dispatchEventCalls; + /** @type {{ core: any, github: any, context: any }} */ + let savedGlobals; + beforeEach(() => { + savedGlobals = { + core: global.core, + github: global.github, + context: global.context, + }; + dispatchEventCalls = []; mockCore = { @@ -46,10 +55,13 @@ describe("dispatch_repository", () => { global.core = mockCore; global.github = mockGithub; global.context = mockContext; + }); - // Stub handler_auth to return the mock github client - const handlerAuth = require("./handler_auth.cjs"); - vi.spyOn(handlerAuth, "createAuthenticatedGitHubClient").mockResolvedValue(mockGithub); + afterEach(() => { + global.core = savedGlobals.core; + global.github = savedGlobals.github; + global.context = savedGlobals.context; + vi.restoreAllMocks(); }); describe("main factory", () => {