diff --git a/actions/setup/js/effective_tokens_context.cjs b/actions/setup/js/effective_tokens_context.cjs index 7f3a637517..f03202e7ec 100644 --- a/actions/setup/js/effective_tokens_context.cjs +++ b/actions/setup/js/effective_tokens_context.cjs @@ -32,6 +32,51 @@ function parsePositiveIntegerString(value) { return ""; } +/** + * Compare two integer strings using BigInt. + * Returns false when either value is missing or cannot be parsed as an integer. + * + * @param {string} left + * @param {string} right + * @returns {boolean} + */ +function isIntegerStringGreaterThanOrEqual(left, right) { + if (!left || !right) { + return false; + } + + try { + return BigInt(left) >= BigInt(right); + } catch { + return false; + } +} + +/** + * Decide whether an ET rate-limit signal should be surfaced as budget exhaustion. + * A missing signal always means "no". When the signal is present but one of the + * token counts is unavailable, keep reporting the condition; otherwise require the + * effective-token count to meet or exceed the configured max. + * + * @param {boolean} hasRateLimitSignal + * @param {string} effectiveTokens + * @param {string} maxEffectiveTokens + * @returns {boolean} + */ +function shouldReportEffectiveTokensRateLimitError(hasRateLimitSignal, effectiveTokens, maxEffectiveTokens) { + if (!hasRateLimitSignal) { + return false; + } + + if (!effectiveTokens || !maxEffectiveTokens) { + // Conservative fallback: when a rate-limit signal exists but the numeric budget + // values are unavailable, keep surfacing the ET failure instead of suppressing it. + return true; + } + + return isIntegerStringGreaterThanOrEqual(effectiveTokens, maxEffectiveTokens); +} + /** * @param {unknown} value * @returns {boolean} @@ -223,10 +268,18 @@ function parseEffectiveTokensErrorInfoFromAuditLog(auditJsonlPathOverride) { */ function resolveEffectiveTokensFailureState() { const parsedEffectiveTokensErrorInfo = parseEffectiveTokensErrorInfoFromAuditLog(); + // Treat invalid env fallbacks as missing so they do not produce misleading ET math. + const envEffectiveTokens = parsePositiveIntegerString(process.env.GH_AW_EFFECTIVE_TOKENS); + const envMaxEffectiveTokens = parsePositiveIntegerString(process.env.GH_AW_MAX_EFFECTIVE_TOKENS); + const effectiveTokens = parsedEffectiveTokensErrorInfo.effectiveTokens || envEffectiveTokens || ""; + const maxEffectiveTokens = parseMaxEffectiveTokensFromAuditLog() || envMaxEffectiveTokens || ""; + const rawEffectiveTokensRateLimitError = parsedEffectiveTokensErrorInfo.rateLimitError || process.env.GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR === "true"; + const effectiveTokensRateLimitError = shouldReportEffectiveTokensRateLimitError(rawEffectiveTokensRateLimitError, effectiveTokens, maxEffectiveTokens); + return { - effectiveTokens: parsedEffectiveTokensErrorInfo.effectiveTokens || process.env.GH_AW_EFFECTIVE_TOKENS || "", - maxEffectiveTokens: parseMaxEffectiveTokensFromAuditLog() || process.env.GH_AW_MAX_EFFECTIVE_TOKENS || "", - effectiveTokensRateLimitError: parsedEffectiveTokensErrorInfo.rateLimitError || process.env.GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR === "true", + effectiveTokens, + maxEffectiveTokens, + effectiveTokensRateLimitError, }; } diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index b3221db1cf..72acf242d4 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -1879,6 +1879,109 @@ describe("handle_agent_failure", () => { }); }); + describe("resolveEffectiveTokensFailureState", () => { + const fs = require("fs"); + const os = require("os"); + const path = require("path"); + + let tmpDir; + let resolveEffectiveTokensFailureState; + + beforeEach(() => { + vi.resetModules(); + ({ resolveEffectiveTokensFailureState } = require("./effective_tokens_context.cjs")); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "aw-resolve-et-")); + }); + + afterEach(() => { + delete process.env.GH_AW_AGENT_OUTPUT; + delete process.env.GH_AW_EFFECTIVE_TOKENS; + delete process.env.GH_AW_MAX_EFFECTIVE_TOKENS; + delete process.env.GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR; + if (tmpDir && fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it("suppresses ET budget exhaustion when usage is below the configured maximum", () => { + const auditDir = path.join(tmpDir, "sandbox", "firewall", "audit"); + fs.mkdirSync(auditDir, { recursive: true }); + fs.writeFileSync( + path.join(auditDir, "log.jsonl"), + JSON.stringify({ + _schema: "audit/v0.26.0", + ts: 1, + effective_tokens: 2097968, + max_effective_tokens: 10000000, + effective_tokens_rate_limit_error: true, + }) + ); + process.env.GH_AW_AGENT_OUTPUT = path.join(tmpDir, "agent_output.json"); + + expect(resolveEffectiveTokensFailureState()).toEqual({ + effectiveTokens: "2097968", + maxEffectiveTokens: "10000000", + effectiveTokensRateLimitError: false, + }); + }); + + it("keeps ET budget exhaustion when usage meets the configured maximum", () => { + const auditDir = path.join(tmpDir, "sandbox", "firewall", "audit"); + fs.mkdirSync(auditDir, { recursive: true }); + fs.writeFileSync( + path.join(auditDir, "log.jsonl"), + JSON.stringify({ + _schema: "audit/v0.26.0", + ts: 1, + effective_tokens: 10000000, + max_effective_tokens: 10000000, + effective_tokens_rate_limit_error: true, + }) + ); + process.env.GH_AW_AGENT_OUTPUT = path.join(tmpDir, "agent_output.json"); + + expect(resolveEffectiveTokensFailureState()).toEqual({ + effectiveTokens: "10000000", + maxEffectiveTokens: "10000000", + effectiveTokensRateLimitError: true, + }); + }); + + it("keeps ET budget exhaustion when the rate-limit signal is present but no max is available", () => { + process.env.GH_AW_EFFECTIVE_TOKENS = "2097968"; + process.env.GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR = "true"; + + expect(resolveEffectiveTokensFailureState()).toEqual({ + effectiveTokens: "2097968", + maxEffectiveTokens: "", + effectiveTokensRateLimitError: true, + }); + }); + + it("ignores invalid env token counts when reconciling ET budget exhaustion", () => { + process.env.GH_AW_EFFECTIVE_TOKENS = "2097968"; + process.env.GH_AW_MAX_EFFECTIVE_TOKENS = "not-a-number"; + process.env.GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR = "true"; + + expect(resolveEffectiveTokensFailureState()).toEqual({ + effectiveTokens: "2097968", + maxEffectiveTokens: "", + effectiveTokensRateLimitError: true, + }); + }); + + it("does not report ET budget exhaustion without a rate-limit signal", () => { + process.env.GH_AW_EFFECTIVE_TOKENS = "10000000"; + process.env.GH_AW_MAX_EFFECTIVE_TOKENS = "10000000"; + + expect(resolveEffectiveTokensFailureState()).toEqual({ + effectiveTokens: "10000000", + maxEffectiveTokens: "10000000", + effectiveTokensRateLimitError: false, + }); + }); + }); + describe("buildEffectiveTokensRateLimitErrorContext", () => { let buildEffectiveTokensRateLimitErrorContext;