diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index ed5797d727..50ad555e41 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -608,16 +608,9 @@ function buildTimeoutContext(isTimedOut, timeoutMinutes) { const currentMinutes = parseInt(timeoutMinutes || "20", 10); const suggestedMinutes = currentMinutes + 10; - let ctx = "\n**⏱️ Agent Timed Out**: The agent job exceeded the maximum allowed execution time"; - ctx += ` (${currentMinutes} minutes).`; - ctx += "\n\nTo increase the timeout, add or update the `timeout-minutes` setting in your workflow's frontmatter:\n\n"; - ctx += "```yaml\n"; - ctx += "---\n"; - ctx += `timeout-minutes: ${suggestedMinutes}\n`; - ctx += "---\n"; - ctx += "```\n\n"; - - return ctx; + const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/agent_timeout.md`; + const template = fs.readFileSync(templatePath, "utf8"); + return "\n" + renderTemplate(template, { current_minutes: currentMinutes, suggested_minutes: suggestedMinutes }); } /** @@ -1169,4 +1162,4 @@ async function main() { } } -module.exports = { main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildAppTokenMintingFailedContext, buildLockdownCheckFailedContext }; +module.exports = { main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildAppTokenMintingFailedContext, buildLockdownCheckFailedContext, buildTimeoutContext }; diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index e6a253bad8..86d56fdf21 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -389,4 +389,57 @@ describe("handle_agent_failure", () => { expect(result).toContain("gh aw compile --strict"); }); }); + + // ────────────────────────────────────────────────────── + // buildTimeoutContext + // ────────────────────────────────────────────────────── + + describe("buildTimeoutContext", () => { + let buildTimeoutContext; + const fs = require("fs"); + const path = require("path"); + const templateContent = fs.readFileSync(path.join(__dirname, "../md/agent_timeout.md"), "utf8"); + const originalReadFileSync = fs.readFileSync.bind(fs); + + beforeEach(() => { + vi.resetModules(); + // Stub readFileSync so the runtime path resolves to the source-tree template + fs.readFileSync = (filePath, encoding) => { + if (typeof filePath === "string" && filePath.includes("agent_timeout.md")) { + return templateContent; + } + return originalReadFileSync(filePath, encoding); + }; + ({ buildTimeoutContext } = require("./handle_agent_failure.cjs")); + }); + + afterEach(() => { + fs.readFileSync = originalReadFileSync; + }); + + it("returns empty string when not timed out", () => { + expect(buildTimeoutContext(false, "20")).toBe(""); + expect(buildTimeoutContext(false, "")).toBe(""); + }); + + it("returns formatted error message when timed out", () => { + const result = buildTimeoutContext(true, "20"); + expect(result).toContain("Agent Timed Out"); + expect(result).toContain("20"); + expect(result).toContain("30"); + expect(result).toContain("timeout-minutes"); + }); + + it("uses default of 20 minutes when timeoutMinutes is empty", () => { + const result = buildTimeoutContext(true, ""); + expect(result).toContain("20"); + expect(result).toContain("30"); + }); + + it("suggests current + 10 minutes", () => { + const result = buildTimeoutContext(true, "45"); + expect(result).toContain("45"); + expect(result).toContain("55"); + }); + }); }); diff --git a/actions/setup/md/agent_timeout.md b/actions/setup/md/agent_timeout.md new file mode 100644 index 0000000000..c8f1860bf0 --- /dev/null +++ b/actions/setup/md/agent_timeout.md @@ -0,0 +1,9 @@ +**⏱️ Agent Timed Out**: The agent job exceeded the maximum allowed execution time ({current_minutes} minutes). + +To increase the timeout, add or update the `timeout-minutes` setting in your workflow's frontmatter: + +```yaml +--- +timeout-minutes: {suggested_minutes} +--- +```