From 25925a0661d9468fa4304d671da058e0be8cf818 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sun, 9 Aug 2026 01:00:51 +0900 Subject: [PATCH] test(ci): fully drain deferred PR target work --- tests/ci-workflows.test.ts | 17 ++++++++++++ tests/helpers/enforce-pr-target-harness.ts | 31 +++++++++++++--------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/tests/ci-workflows.test.ts b/tests/ci-workflows.test.ts index 67be862ea..91ea87137 100644 --- a/tests/ci-workflows.test.ts +++ b/tests/ci-workflows.test.ts @@ -4161,6 +4161,23 @@ describe("GitHub Actions hardening", () => { expect(probe.bun).toBe("undefined"); }); + test("the harness drains nested deferred work and preserves timer arguments", async () => { + for (const body of [ + `setTimeout(() => queueMicrotask(() => github.request("POST /repos/attacker/other/issues", {})), 0);`, + `setTimeout((enabled) => { + if (enabled) github.request("POST /repos/attacker/other/issues", {}); + }, 0, true);`, + `setImmediate((enabled) => { + if (enabled) github.request("POST /repos/attacker/other/issues", {}); + }, true);`, + ]) { + const result = await runEnforcePrTarget(body, { + pr: { base: { ref: "dev" } }, + }); + expect(methodsOf(result)).toEqual(["request"]); + } + }); + test("a draft GraphQL failure is soft-failed with accurate state and a hard check failure", async () => { // Observed on PR #626: convertPullRequestToDraft failed with // "Resource not accessible by integration", the job crashed before diff --git a/tests/helpers/enforce-pr-target-harness.ts b/tests/helpers/enforce-pr-target-harness.ts index 9f4710a21..713d0aeac 100644 --- a/tests/helpers/enforce-pr-target-harness.ts +++ b/tests/helpers/enforce-pr-target-harness.ts @@ -421,13 +421,18 @@ function nodeLikeRuntime(deferred: (() => unknown)[]): Record { const deny = (name: string) => () => { throw new Error(`the script must not use ${name}`); }; - /** Record the callback so the run can drain it, and hand back a timer id. */ - const capture = (callback: unknown) => { + /** Record a callback and its arguments so the run can drain it. */ + const capture = (callback: unknown, args: unknown[] = []) => { if (typeof callback === "function") { - deferred.push(callback as () => unknown); + deferred.push(() => callback(...args)); } return { unref: () => {}, ref: () => {} }; }; + const captureTimer = (callback: unknown, _delay?: unknown, ...args: unknown[]) => + capture(callback, args); + const captureImmediate = (callback: unknown, ...args: unknown[]) => + capture(callback, args); + const captureMicrotask = (callback: unknown) => capture(callback); const fakeGlobal: Record = { process: nodeProcess, @@ -452,10 +457,10 @@ function nodeLikeRuntime(deferred: (() => unknown)[]): Record { fakeGlobal.globalThis = fakeGlobal; fakeGlobal.global = fakeGlobal; - fakeGlobal.setTimeout = capture; - fakeGlobal.setInterval = capture; - fakeGlobal.setImmediate = capture; - fakeGlobal.queueMicrotask = capture; + fakeGlobal.setTimeout = captureTimer; + fakeGlobal.setInterval = captureTimer; + fakeGlobal.setImmediate = captureImmediate; + fakeGlobal.queueMicrotask = captureMicrotask; return { process: nodeProcess, @@ -467,10 +472,10 @@ function nodeLikeRuntime(deferred: (() => unknown)[]): Record { eval: deny("eval"), module: undefined, import_meta: undefined, - setTimeout: capture, - setInterval: capture, - setImmediate: capture, - queueMicrotask: capture, + setTimeout: captureTimer, + setInterval: captureTimer, + setImmediate: captureImmediate, + queueMicrotask: captureMicrotask, }; } @@ -1039,8 +1044,8 @@ export async function runEnforcePrTarget( // Run whatever the script deferred. Node would run these too, with the write // token still live, so their calls belong in the recording — a scenario that // asserts on the exact call list then sees them. - for (const callback of deferred.splice(0)) { - await callback(); + while (deferred.length > 0) { + await deferred.shift()!(); } return {