test: deflake the Gateway waiter lifetime and Bun plugin retention fixtures (#160200)

- server.request-lifetime: the default non-ephemeral test client starts a
  session-history prewarm (`ws:preauth`) alongside `ws:agent.wait`, so the
  "exactly one active root work" assertion raced it (seen as `2 !== 1` in
  CI). The waiter cases now connect as an ephemeral CLI client; failure
  messages name the active holders.
- runtime.retention: under Bun, forced collections inside Promise
  continuations can leave JavaScriptCore's microtask stack holding completed
  async values, so WeakRef checks intermittently saw retained instances.
  Collect inside native setImmediate callbacks instead; assertions unchanged.
This commit is contained in:
Peter Steinberger 2026-09-28 00:15:29 -07:00 • committed by GitHub
parent 7fac9b18dc
commit c136ec6314
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 26 additions and 4 deletions

View file

@ -6,6 +6,10 @@ import path from "node:path";
import { setImmediate as nextTurn } from "node:timers/promises";
import { describe, expect, it, vi } from "vitest";
import { WebSocket } from "ws";
import {
GATEWAY_CLIENT_IDS,
GATEWAY_CLIENT_MODES,
} from "../../packages/gateway-protocol/src/client-info.js";
import type { ResponseFrame } from "../../packages/gateway-protocol/src/schema/frames.js";
import { isAgentRunRestartAbortReason } from "../agents/run-termination.js";
import { resolveStateDir } from "../config/paths.js";
@ -14,6 +18,7 @@ import { initializeGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
import {
getActiveGatewayRootWorkCount,
getActiveGatewayRootWorkHolders,
markGatewayRestartDraining,
} from "../process/gateway-work-admission.js";
import { getAsyncWorkSignal } from "../shared/async-work-scope.js";
@ -93,7 +98,16 @@ describe("public Gateway close request lifetime", () => {
await gateway.server.startupSettled;
phase = "WebSocket connection";
ws = await gateway.openWs();
await connectOk(ws, { scopes: ["operator.admin"] });
// An ephemeral CLI avoids admitting history prewarming alongside the waiter.
await connectOk(ws, {
scopes: ["operator.admin"],
client: {
id: GATEWAY_CLIENT_IDS.CLI,
mode: GATEWAY_CLIENT_MODES.CLI,
version: "1.0.0",
platform: "test",
},
});
const response = onceMessage<Pick<ResponseFrame, "type" | "id" | "ok" | "error">>(
ws,
(frame) => frame.type === "res" && frame.id === "wait-for-shutdown",
@ -114,7 +128,9 @@ describe("public Gateway close request lifetime", () => {
requestSent = true;
phase = "waiter admission";
await Promise.race([entered.promise, prematureReply]);
expect(getActiveGatewayRootWorkCount()).toBe(1);
expect(getActiveGatewayRootWorkCount(), getActiveGatewayRootWorkHolders().join(", ")).toBe(
1,
);
// A failing owner is released through the real terminal registry, not an abandoned wait.
releaseTimer = setTimeout(() => {
emergencyRelease = true;

View file

@ -3,6 +3,7 @@ import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { setImmediate as scheduleImmediate } from "node:timers";
import { setImmediate } from "node:timers/promises";
import { withEnvAsync } from "../test-utils/env.js";
import {
@ -24,8 +25,13 @@ async function collect() {
assert.ok(gc, "The retention child requires --expose-gc");
const control = new WeakRef({ unowned: true });
for (let pass = 0; pass < 8; pass += 1) {
await setImmediate();
gc();
// JavaScriptCore's promise-microtask stack can retain completed async values.
await new Promise<void>((resolve) => {
scheduleImmediate(() => {
gc();
resolve();
});
});
}
assert.equal(control.deref(), undefined, "Unowned control must collect");
}