From e64189a1760f9fbc6286925299657d5cdda375fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:23:36 +0000 Subject: [PATCH 1/4] fix: restore backward compat in assign_to_agent.cjs for standalone tests The main() function was refactored to a handler factory pattern but tests still use the old standalone interface (env vars + direct item processing). Add standalone mode detection when main() is called with no config: - Reads config from env vars (GH_AW_AGENT_DEFAULT, GH_AW_AGENT_MAX_COUNT, etc.) - Processes all assign_to_agent items from GH_AW_AGENT_OUTPUT - Writes summary, sets outputs, calls setFailed on errors Also fix the "Ignore-if-error mode enabled" log message to match tests. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e00f51ce-33dd-493d-ba64-e13e1ebeb1be Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- actions/setup/js/assign_to_agent.cjs | 73 ++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index f9873be5f5c..6929f4910f4 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -5,7 +5,8 @@ const { AGENT_LOGIN_NAMES, getAvailableAgentLogins, findAgent, getIssueDetails, const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTarget, isStagedMode } = require("./safe_output_helpers.cjs"); const { generateStagedPreview } = require("./staged_preview.cjs"); -const { isTemporaryId, normalizeTemporaryId, resolveRepoIssueTarget } = require("./temporary_id.cjs"); +const { isTemporaryId, normalizeTemporaryId, resolveRepoIssueTarget, loadTemporaryIdMap } = require("./temporary_id.cjs"); +const { loadAgentOutput } = require("./load_agent_output.cjs"); const { sleep } = require("./error_recovery.cjs"); const { parseAllowedRepos, validateRepo, resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { resolvePullRequestRepo } = require("./pr_helpers.cjs"); @@ -53,10 +54,32 @@ async function createAssignToAgentGitHubClient(config) { * @returns {Promise} Message processor function */ async function main(config = {}) { + // Detect standalone mode (called without config, e.g. from tests or legacy usage). + // In this mode we read config from environment variables and process items directly + // from GH_AW_AGENT_OUTPUT, matching the old standalone-step behaviour. + const isStandaloneCall = Object.keys(config).length === 0; + if (isStandaloneCall) { + config = {}; + if (process.env.GH_AW_AGENT_DEFAULT) config.name = process.env.GH_AW_AGENT_DEFAULT; + if (process.env.GH_AW_AGENT_MAX_COUNT) config.max = process.env.GH_AW_AGENT_MAX_COUNT; + if (process.env.GH_AW_AGENT_TARGET) config.target = process.env.GH_AW_AGENT_TARGET; + if (process.env.GH_AW_AGENT_ALLOWED) config.allowed = process.env.GH_AW_AGENT_ALLOWED; + if (process.env.GH_AW_AGENT_IGNORE_IF_ERROR) config["ignore-if-error"] = process.env.GH_AW_AGENT_IGNORE_IF_ERROR; + if (process.env.GH_AW_AGENT_PULL_REQUEST_REPO) config["pull-request-repo"] = process.env.GH_AW_AGENT_PULL_REQUEST_REPO; + if (process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS) config["allowed-pull-request-repos"] = process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS; + if (process.env.GH_AW_AGENT_BASE_BRANCH) config["base-branch"] = process.env.GH_AW_AGENT_BASE_BRANCH; + if (process.env.GH_AW_ALLOWED_REPOS) config.allowed_repos = process.env.GH_AW_ALLOWED_REPOS; + } + // Parse configuration (replaces env vars from the old standalone step) const maxCount = parseInt(String(config.max ?? "1"), 10); if (isNaN(maxCount) || maxCount < 1) { - throw new Error(`Invalid max value: ${config.max}. Must be a positive integer`); + const errMsg = `Invalid max value: ${config.max}. Must be a positive integer`; + if (isStandaloneCall) { + core.setFailed(errMsg); + return async () => ({ success: false }); + } + throw new Error(errMsg); } const defaultAgent = String(config.name ?? "copilot").trim(); const defaultModel = config.model ? String(config.model).trim() : null; @@ -88,7 +111,7 @@ async function main(config = {}) { if (configuredBaseBranch) core.info(`Configured base branch: ${configuredBaseBranch}`); core.info(`Target configuration: ${targetConfig}`); core.info(`Max count: ${maxCount}`); - if (ignoreIfError) core.info("Ignore-if-error mode enabled: Will not fail if agent assignment encounters auth errors"); + if (ignoreIfError) core.info("Ignore-if-error mode enabled: Will not fail if agent assignment encounters errors"); if (allowedAgents) core.info(`Allowed agents: ${allowedAgents.join(", ")}`); core.info(`Default target repo: ${defaultTargetRepo}`); if (allowedRepos.size > 0) core.info(`Allowed repos: ${[...allowedRepos].join(", ")}`); @@ -141,7 +164,7 @@ async function main(config = {}) { * @param {Map} temporaryIdMap - Live temp ID map * @returns {Promise<{success: boolean, error?: string, skipped?: boolean, deferred?: boolean}>} */ - return async function handleMessage(message, resolvedTemporaryIds, temporaryIdMap) { + const handler = async function handleMessage(message, resolvedTemporaryIds, temporaryIdMap) { // Handle staged mode — emit preview and skip actual assignment if (isStaged) { await generateStagedPreview({ @@ -411,6 +434,48 @@ async function main(config = {}) { return { success: false, error: errorMessage }; } }; + + // Standalone mode: process items from GH_AW_AGENT_OUTPUT and handle all lifecycle steps. + // This preserves backward compatibility with tests and legacy callers that call main() without config. + if (isStandaloneCall) { + try { + const agentOutput = loadAgentOutput(); + if (!agentOutput.success) { + return handler; + } + + const assignToAgentItems = agentOutput.items.filter(item => item.type === "assign_to_agent"); + if (assignToAgentItems.length === 0) { + core.info("No assign_to_agent items found in agent output"); + return handler; + } + + if (assignToAgentItems.length > maxCount) { + core.warning(`Found ${assignToAgentItems.length} agent assignments, but max is ${maxCount}. Extra assignments will be skipped.`); + } + + // Load temporary ID map from env var for temp-ID resolution across items + const tempIdMap = loadTemporaryIdMap(); + for (const item of assignToAgentItems) { + await handler(item, {}, tempIdMap); + } + + await writeAssignToAgentSummary(); + + core.setOutput("assigned", getAssignToAgentAssigned()); + core.setOutput("assignment_errors", getAssignToAgentErrors()); + core.setOutput("assignment_error_count", String(getAssignToAgentErrorCount())); + + const errorCount = getAssignToAgentErrorCount(); + if (errorCount > 0) { + core.setFailed(`Failed to assign ${errorCount} agent(s)`); + } + } catch (error) { + core.setFailed(getErrorMessage(error)); + } + } + + return handler; } /** From 2bdf7898bf02329f472704cdcdb72007c41c14db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:28:15 +0000 Subject: [PATCH 2/4] fix: address code review feedback on standalone mode detection Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e00f51ce-33dd-493d-ba64-e13e1ebeb1be Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- actions/setup/js/assign_to_agent.cjs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 6929f4910f4..12667b93246 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -55,20 +55,22 @@ async function createAssignToAgentGitHubClient(config) { */ async function main(config = {}) { // Detect standalone mode (called without config, e.g. from tests or legacy usage). - // In this mode we read config from environment variables and process items directly - // from GH_AW_AGENT_OUTPUT, matching the old standalone-step behaviour. + // The handler manager always supplies at least some config keys (e.g. from + // GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG), so an empty object signals a direct call. + // In standalone mode we read config from environment variables and process items + // directly from GH_AW_AGENT_OUTPUT, matching the old standalone-step behaviour. const isStandaloneCall = Object.keys(config).length === 0; if (isStandaloneCall) { config = {}; - if (process.env.GH_AW_AGENT_DEFAULT) config.name = process.env.GH_AW_AGENT_DEFAULT; - if (process.env.GH_AW_AGENT_MAX_COUNT) config.max = process.env.GH_AW_AGENT_MAX_COUNT; - if (process.env.GH_AW_AGENT_TARGET) config.target = process.env.GH_AW_AGENT_TARGET; - if (process.env.GH_AW_AGENT_ALLOWED) config.allowed = process.env.GH_AW_AGENT_ALLOWED; - if (process.env.GH_AW_AGENT_IGNORE_IF_ERROR) config["ignore-if-error"] = process.env.GH_AW_AGENT_IGNORE_IF_ERROR; - if (process.env.GH_AW_AGENT_PULL_REQUEST_REPO) config["pull-request-repo"] = process.env.GH_AW_AGENT_PULL_REQUEST_REPO; - if (process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS) config["allowed-pull-request-repos"] = process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS; - if (process.env.GH_AW_AGENT_BASE_BRANCH) config["base-branch"] = process.env.GH_AW_AGENT_BASE_BRANCH; - if (process.env.GH_AW_ALLOWED_REPOS) config.allowed_repos = process.env.GH_AW_ALLOWED_REPOS; + if (process.env.GH_AW_AGENT_DEFAULT?.trim()) config.name = process.env.GH_AW_AGENT_DEFAULT.trim(); + if (process.env.GH_AW_AGENT_MAX_COUNT?.trim()) config.max = process.env.GH_AW_AGENT_MAX_COUNT.trim(); + if (process.env.GH_AW_AGENT_TARGET?.trim()) config.target = process.env.GH_AW_AGENT_TARGET.trim(); + if (process.env.GH_AW_AGENT_ALLOWED?.trim()) config.allowed = process.env.GH_AW_AGENT_ALLOWED.trim(); + if (process.env.GH_AW_AGENT_IGNORE_IF_ERROR?.trim()) config["ignore-if-error"] = process.env.GH_AW_AGENT_IGNORE_IF_ERROR.trim(); + if (process.env.GH_AW_AGENT_PULL_REQUEST_REPO?.trim()) config["pull-request-repo"] = process.env.GH_AW_AGENT_PULL_REQUEST_REPO.trim(); + if (process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS?.trim()) config["allowed-pull-request-repos"] = process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS.trim(); + if (process.env.GH_AW_AGENT_BASE_BRANCH?.trim()) config["base-branch"] = process.env.GH_AW_AGENT_BASE_BRANCH.trim(); + if (process.env.GH_AW_ALLOWED_REPOS?.trim()) config.allowed_repos = process.env.GH_AW_ALLOWED_REPOS.trim(); } // Parse configuration (replaces env vars from the old standalone step) @@ -454,7 +456,9 @@ async function main(config = {}) { core.warning(`Found ${assignToAgentItems.length} agent assignments, but max is ${maxCount}. Extra assignments will be skipped.`); } - // Load temporary ID map from env var for temp-ID resolution across items + // Load temporary ID map from env var for temp-ID resolution across items. + // resolvedTemporaryIds ({}) is empty because in standalone mode there is only one + // handler — no cross-handler resolutions from previous steps are available. const tempIdMap = loadTemporaryIdMap(); for (const item of assignToAgentItems) { await handler(item, {}, tempIdMap); From 805c794f0b14bd3d43784ada209b61fa8484a73e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:44:12 +0000 Subject: [PATCH 3/4] Update; rm -rf / Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_to_agent.cjs | 77 ++-------------------------- 1 file changed, 4 insertions(+), 73 deletions(-) diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 12667b93246..f9873be5f5c 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -5,8 +5,7 @@ const { AGENT_LOGIN_NAMES, getAvailableAgentLogins, findAgent, getIssueDetails, const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTarget, isStagedMode } = require("./safe_output_helpers.cjs"); const { generateStagedPreview } = require("./staged_preview.cjs"); -const { isTemporaryId, normalizeTemporaryId, resolveRepoIssueTarget, loadTemporaryIdMap } = require("./temporary_id.cjs"); -const { loadAgentOutput } = require("./load_agent_output.cjs"); +const { isTemporaryId, normalizeTemporaryId, resolveRepoIssueTarget } = require("./temporary_id.cjs"); const { sleep } = require("./error_recovery.cjs"); const { parseAllowedRepos, validateRepo, resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { resolvePullRequestRepo } = require("./pr_helpers.cjs"); @@ -54,34 +53,10 @@ async function createAssignToAgentGitHubClient(config) { * @returns {Promise} Message processor function */ async function main(config = {}) { - // Detect standalone mode (called without config, e.g. from tests or legacy usage). - // The handler manager always supplies at least some config keys (e.g. from - // GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG), so an empty object signals a direct call. - // In standalone mode we read config from environment variables and process items - // directly from GH_AW_AGENT_OUTPUT, matching the old standalone-step behaviour. - const isStandaloneCall = Object.keys(config).length === 0; - if (isStandaloneCall) { - config = {}; - if (process.env.GH_AW_AGENT_DEFAULT?.trim()) config.name = process.env.GH_AW_AGENT_DEFAULT.trim(); - if (process.env.GH_AW_AGENT_MAX_COUNT?.trim()) config.max = process.env.GH_AW_AGENT_MAX_COUNT.trim(); - if (process.env.GH_AW_AGENT_TARGET?.trim()) config.target = process.env.GH_AW_AGENT_TARGET.trim(); - if (process.env.GH_AW_AGENT_ALLOWED?.trim()) config.allowed = process.env.GH_AW_AGENT_ALLOWED.trim(); - if (process.env.GH_AW_AGENT_IGNORE_IF_ERROR?.trim()) config["ignore-if-error"] = process.env.GH_AW_AGENT_IGNORE_IF_ERROR.trim(); - if (process.env.GH_AW_AGENT_PULL_REQUEST_REPO?.trim()) config["pull-request-repo"] = process.env.GH_AW_AGENT_PULL_REQUEST_REPO.trim(); - if (process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS?.trim()) config["allowed-pull-request-repos"] = process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS.trim(); - if (process.env.GH_AW_AGENT_BASE_BRANCH?.trim()) config["base-branch"] = process.env.GH_AW_AGENT_BASE_BRANCH.trim(); - if (process.env.GH_AW_ALLOWED_REPOS?.trim()) config.allowed_repos = process.env.GH_AW_ALLOWED_REPOS.trim(); - } - // Parse configuration (replaces env vars from the old standalone step) const maxCount = parseInt(String(config.max ?? "1"), 10); if (isNaN(maxCount) || maxCount < 1) { - const errMsg = `Invalid max value: ${config.max}. Must be a positive integer`; - if (isStandaloneCall) { - core.setFailed(errMsg); - return async () => ({ success: false }); - } - throw new Error(errMsg); + throw new Error(`Invalid max value: ${config.max}. Must be a positive integer`); } const defaultAgent = String(config.name ?? "copilot").trim(); const defaultModel = config.model ? String(config.model).trim() : null; @@ -113,7 +88,7 @@ async function main(config = {}) { if (configuredBaseBranch) core.info(`Configured base branch: ${configuredBaseBranch}`); core.info(`Target configuration: ${targetConfig}`); core.info(`Max count: ${maxCount}`); - if (ignoreIfError) core.info("Ignore-if-error mode enabled: Will not fail if agent assignment encounters errors"); + if (ignoreIfError) core.info("Ignore-if-error mode enabled: Will not fail if agent assignment encounters auth errors"); if (allowedAgents) core.info(`Allowed agents: ${allowedAgents.join(", ")}`); core.info(`Default target repo: ${defaultTargetRepo}`); if (allowedRepos.size > 0) core.info(`Allowed repos: ${[...allowedRepos].join(", ")}`); @@ -166,7 +141,7 @@ async function main(config = {}) { * @param {Map} temporaryIdMap - Live temp ID map * @returns {Promise<{success: boolean, error?: string, skipped?: boolean, deferred?: boolean}>} */ - const handler = async function handleMessage(message, resolvedTemporaryIds, temporaryIdMap) { + return async function handleMessage(message, resolvedTemporaryIds, temporaryIdMap) { // Handle staged mode — emit preview and skip actual assignment if (isStaged) { await generateStagedPreview({ @@ -436,50 +411,6 @@ async function main(config = {}) { return { success: false, error: errorMessage }; } }; - - // Standalone mode: process items from GH_AW_AGENT_OUTPUT and handle all lifecycle steps. - // This preserves backward compatibility with tests and legacy callers that call main() without config. - if (isStandaloneCall) { - try { - const agentOutput = loadAgentOutput(); - if (!agentOutput.success) { - return handler; - } - - const assignToAgentItems = agentOutput.items.filter(item => item.type === "assign_to_agent"); - if (assignToAgentItems.length === 0) { - core.info("No assign_to_agent items found in agent output"); - return handler; - } - - if (assignToAgentItems.length > maxCount) { - core.warning(`Found ${assignToAgentItems.length} agent assignments, but max is ${maxCount}. Extra assignments will be skipped.`); - } - - // Load temporary ID map from env var for temp-ID resolution across items. - // resolvedTemporaryIds ({}) is empty because in standalone mode there is only one - // handler — no cross-handler resolutions from previous steps are available. - const tempIdMap = loadTemporaryIdMap(); - for (const item of assignToAgentItems) { - await handler(item, {}, tempIdMap); - } - - await writeAssignToAgentSummary(); - - core.setOutput("assigned", getAssignToAgentAssigned()); - core.setOutput("assignment_errors", getAssignToAgentErrors()); - core.setOutput("assignment_error_count", String(getAssignToAgentErrorCount())); - - const errorCount = getAssignToAgentErrorCount(); - if (errorCount > 0) { - core.setFailed(`Failed to assign ${errorCount} agent(s)`); - } - } catch (error) { - core.setFailed(getErrorMessage(error)); - } - } - - return handler; } /** From 272867742a6c10a9a9123a7eb6f8b19ab83420e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:45:54 +0000 Subject: [PATCH 4/4] fix: update assign_to_agent tests to use handler factory pattern Replace `await main()` standalone calls in test evals with a STANDALONE_RUNNER snippet that simulates the safe-output handler manager flow: - build config from env vars - call main(config) to get the handler function - process assign_to_agent items through the handler - write summary, set outputs, call setFailed on errors Also fix the "encounters auth errors" assertion to match the production log message. No changes to assign_to_agent.cjs production code. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d92459b5-9e9b-40ad-a0c6-108c70f23622 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/assign_to_agent.test.cjs | 134 ++++++++++++++-------- 1 file changed, 89 insertions(+), 45 deletions(-) diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index da6068095f5..33fc6f7c3d2 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -39,6 +39,50 @@ describe("assign_to_agent", () => { let assignToAgentScript; let tempFilePath; + // Simulates the safe-output handler manager: builds handler config from env vars, + // calls main() as a factory, then processes items from GH_AW_AGENT_OUTPUT. + // This mirrors the production flow without requiring any backward-compat changes in + // assign_to_agent.cjs itself. + const STANDALONE_RUNNER = ` + const _config = {}; + if (process.env.GH_AW_AGENT_DEFAULT?.trim()) _config.name = process.env.GH_AW_AGENT_DEFAULT.trim(); + if (process.env.GH_AW_AGENT_MAX_COUNT?.trim()) _config.max = process.env.GH_AW_AGENT_MAX_COUNT.trim(); + if (process.env.GH_AW_AGENT_TARGET?.trim()) _config.target = process.env.GH_AW_AGENT_TARGET.trim(); + if (process.env.GH_AW_AGENT_ALLOWED?.trim()) _config.allowed = process.env.GH_AW_AGENT_ALLOWED.trim(); + if (process.env.GH_AW_AGENT_IGNORE_IF_ERROR?.trim()) _config["ignore-if-error"] = process.env.GH_AW_AGENT_IGNORE_IF_ERROR.trim(); + if (process.env.GH_AW_AGENT_PULL_REQUEST_REPO?.trim()) _config["pull-request-repo"] = process.env.GH_AW_AGENT_PULL_REQUEST_REPO.trim(); + if (process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS?.trim()) _config["allowed-pull-request-repos"] = process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS.trim(); + if (process.env.GH_AW_AGENT_BASE_BRANCH?.trim()) _config["base-branch"] = process.env.GH_AW_AGENT_BASE_BRANCH.trim(); + if (process.env.GH_AW_ALLOWED_REPOS?.trim()) _config.allowed_repos = process.env.GH_AW_ALLOWED_REPOS.trim(); + + let _handler; + try { _handler = await main(_config); } catch (_err) { core.setFailed(_err.message); return; } + + const _agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!_agentOutputFile) { core.info("No GH_AW_AGENT_OUTPUT environment variable found"); return; } + + const _fs = require("fs"); + const _agentOutput = JSON.parse(_fs.readFileSync(_agentOutputFile, "utf8")); + const _items = _agentOutput.items.filter(i => i.type === "assign_to_agent"); + if (_items.length === 0) { + core.info("No assign_to_agent items found in agent output"); + } else { + const _maxCount = parseInt(String(_config.max ?? "1"), 10); + if (_items.length > _maxCount) { + core.warning("Found " + _items.length + " agent assignments, but max is " + _maxCount + ". Extra assignments will be skipped."); + } + const { loadTemporaryIdMap } = require("./temporary_id.cjs"); + const _tempIdMap = loadTemporaryIdMap(); + for (const _item of _items) { await _handler(_item, {}, _tempIdMap); } + } + await writeAssignToAgentSummary(); + const _errorCount = getAssignToAgentErrorCount(); + core.setOutput("assigned", getAssignToAgentAssigned()); + core.setOutput("assignment_errors", getAssignToAgentErrors()); + core.setOutput("assignment_error_count", String(_errorCount)); + if (_errorCount > 0) { core.setFailed("Failed to assign " + _errorCount + " agent(s)"); } + `; + const setAgentOutput = data => { tempFilePath = path.join("/tmp", `test_agent_output_${Date.now()}_${Math.random().toString(36).slice(2)}.json`); const content = typeof data === "string" ? data : JSON.stringify(data); @@ -91,13 +135,13 @@ describe("assign_to_agent", () => { it("should handle empty agent output", async () => { setAgentOutput({ items: [], errors: [] }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No assign_to_agent items found")); }); it("should handle missing agent output", async () => { delete process.env.GH_AW_AGENT_OUTPUT; - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith("No GH_AW_AGENT_OUTPUT environment variable found"); }); @@ -114,7 +158,7 @@ describe("assign_to_agent", () => { errors: [], }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockGithub.graphql).not.toHaveBeenCalled(); expect(mockCore.summary.addRaw).toHaveBeenCalled(); @@ -170,7 +214,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith("Default agent: copilot"); }); @@ -216,7 +260,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Found 3 agent assignments, but max is 2")); }, 20000); // Increase timeout to 20 seconds to account for the delay @@ -260,7 +304,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved temporary issue id")); @@ -283,7 +327,7 @@ describe("assign_to_agent", () => { errors: [], }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining('Agent "unsupported-agent" is not supported')); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to assign 1 agent(s)")); @@ -301,7 +345,7 @@ describe("assign_to_agent", () => { errors: [], }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Error message changed to use resolveTarget validation expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Invalid")); @@ -339,7 +383,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("copilot is already assigned to issue #42")); }); @@ -359,7 +403,7 @@ describe("assign_to_agent", () => { const apiError = new Error("API rate limit exceeded"); mockGithub.graphql.mockRejectedValue(apiError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to assign agent")); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to assign 1 agent(s)")); @@ -403,7 +447,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should warn about 502 but treat as success expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Received 502 error from cloud gateway")); @@ -445,7 +489,7 @@ describe("assign_to_agent", () => { }) .mockRejectedValueOnce(new Error("502 Bad Gateway")); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should warn about 502 but treat as success expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Received 502 error from cloud gateway")); @@ -492,7 +536,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should only look up agent once (cached for second assignment) const graphqlCalls = mockGithub.graphql.mock.calls.filter(call => call[0].includes("suggestedActors")); @@ -522,7 +566,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith("Default target repo: other-owner/other-repo"); }); @@ -540,7 +584,7 @@ describe("assign_to_agent", () => { errors: [], }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Invalid max value: invalid")); }); @@ -584,7 +628,7 @@ describe("assign_to_agent", () => { }) .mockRejectedValueOnce(permissionError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.summary.addRaw).toHaveBeenCalled(); const summaryCall = mockCore.summary.addRaw.mock.calls[0][0]; @@ -637,7 +681,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); if (mockCore.error.mock.calls.length > 0) { console.log("Errors:", mockCore.error.mock.calls); @@ -660,7 +704,7 @@ describe("assign_to_agent", () => { errors: [], }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.error).toHaveBeenCalledWith("Cannot specify both issue_number and pull_number in the same assign_to_agent item"); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to assign 1 agent(s)")); @@ -713,7 +757,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // The key assertion: Target configuration should be "triggering" (the default) // This shows that when no explicit issue_number/pull_number is provided, @@ -739,7 +783,7 @@ describe("assign_to_agent", () => { errors: [], }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should skip gracefully (not fail the workflow) expect(mockCore.error).not.toHaveBeenCalled(); @@ -761,7 +805,7 @@ describe("assign_to_agent", () => { errors: [], }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should fail because target "*" requires explicit issue_number or pull_number expect(mockCore.error).toHaveBeenCalled(); @@ -801,7 +845,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Key assertion: allowed agents list should be logged expect(mockCore.info).toHaveBeenCalledWith("Allowed agents: copilot"); @@ -825,7 +869,7 @@ describe("assign_to_agent", () => { // No GraphQL mocks needed - validation happens before GraphQL calls - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith("Allowed agents: other-agent"); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining('Agent "copilot" is not in the allowed list')); @@ -868,7 +912,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should not log allowed agents when list is not configured expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("Allowed agents:")); @@ -893,10 +937,10 @@ describe("assign_to_agent", () => { const authError = new Error("Bad credentials"); mockGithub.graphql.mockRejectedValueOnce(authError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should log that ignore-if-error is enabled - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignore-if-error mode enabled: Will not fail if agent assignment encounters errors")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Ignore-if-error mode enabled: Will not fail if agent assignment encounters auth errors")); // Should warn about skipping but not fail expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Agent assignment failed")); @@ -929,7 +973,7 @@ describe("assign_to_agent", () => { const authError = new Error("Bad credentials"); mockGithub.graphql.mockRejectedValue(authError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should NOT log ignore-if-error mode expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("ignore-if-error mode enabled")); @@ -966,7 +1010,7 @@ describe("assign_to_agent", () => { const permError = new Error("Resource not accessible by integration"); mockGithub.graphql.mockRejectedValue(permError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should skip and not fail expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Agent assignment failed")); @@ -990,7 +1034,7 @@ describe("assign_to_agent", () => { const otherError = new Error("Network timeout"); mockGithub.graphql.mockRejectedValue(otherError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should error and fail (not skipped because it's not an auth error) expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to assign agent")); @@ -1029,7 +1073,7 @@ describe("assign_to_agent", () => { replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should NOT post a failure comment on success expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); @@ -1045,7 +1089,7 @@ describe("assign_to_agent", () => { const authError = new Error("Bad credentials"); mockGithub.graphql.mockRejectedValue(authError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should post a failure comment for the failed issue with all required properties expect(mockGithub.rest.issues.createComment).toHaveBeenCalledTimes(1); @@ -1070,7 +1114,7 @@ describe("assign_to_agent", () => { const dangerousError = new Error("@admin triggered error"); mockGithub.graphql.mockRejectedValue(dangerousError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockGithub.rest.issues.createComment).toHaveBeenCalledTimes(1); const [callArg] = mockGithub.rest.issues.createComment.mock.calls[0]; @@ -1099,7 +1143,7 @@ describe("assign_to_agent", () => { const authError = new Error("Bad credentials"); mockGithub.graphql.mockRejectedValue(authError); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should NOT post a failure comment since it was skipped expect(mockGithub.rest.issues.createComment).not.toHaveBeenCalled(); @@ -1123,7 +1167,7 @@ describe("assign_to_agent", () => { // Simulate failure to post comment mockGithub.rest.issues.createComment.mockRejectedValue(new Error("Could not post comment")); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should still set the assignment_error outputs even if comment fails expect(mockCore.setOutput).toHaveBeenCalledWith("assignment_error_count", "1"); @@ -1186,7 +1230,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Verify delay message was logged twice (2 delays between 3 items) const delayMessages = mockCore.info.mock.calls.filter(call => call[0].includes("Waiting 10 seconds before processing next agent assignment")); @@ -1209,7 +1253,7 @@ describe("assign_to_agent", () => { errors: [], }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("E004:")); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("not in the allowed-repos list")); @@ -1250,7 +1294,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).not.toHaveBeenCalled(); // Check that the target repository was used and assignment proceeded @@ -1291,7 +1335,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).not.toHaveBeenCalled(); // Check that assignment proceeded without errors @@ -1347,7 +1391,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using pull request repository: test-owner/pull-request-repo")); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Pull request repository ID: pull-request-repo-id")); @@ -1415,7 +1459,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using per-item pull request repository: test-owner/item-pull-request-repo")); @@ -1447,7 +1491,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("E004:")); expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("Failed to assign 1 agent(s)")); @@ -1501,7 +1545,7 @@ describe("assign_to_agent", () => { }, }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); // Should succeed - pull-request-repo is automatically allowed expect(mockCore.setFailed).not.toHaveBeenCalled(); @@ -1526,7 +1570,7 @@ describe("assign_to_agent", () => { // Assign agent .mockResolvedValueOnce({ replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" } }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).not.toHaveBeenCalled(); // Verify the mutation was called with baseRef set to the explicit base-branch @@ -1555,7 +1599,7 @@ describe("assign_to_agent", () => { // Assign agent .mockResolvedValueOnce({ replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" } }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).not.toHaveBeenCalled(); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved pull request repository default branch: develop")); @@ -1583,7 +1627,7 @@ describe("assign_to_agent", () => { // Assign agent .mockResolvedValueOnce({ replaceActorsForAssignable: { __typename: "ReplaceActorsForAssignablePayload" } }); - await eval(`(async () => { ${assignToAgentScript}; await main(); })()`); + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); expect(mockCore.setFailed).not.toHaveBeenCalled(); // Verify the mutation was called with baseRef set to the repo's default branch