diff --git a/src/gateway/server.request-lifetime.test.ts b/src/gateway/server.request-lifetime.test.ts index c5ecb8d549b5..22f95c4ed73d 100644 --- a/src/gateway/server.request-lifetime.test.ts +++ b/src/gateway/server.request-lifetime.test.ts @@ -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>( 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; diff --git a/src/plugins/runtime.retention.test-support.ts b/src/plugins/runtime.retention.test-support.ts index 1f1c78cbb915..ae661419bb88 100644 --- a/src/plugins/runtime.retention.test-support.ts +++ b/src/plugins/runtime.retention.test-support.ts @@ -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((resolve) => { + scheduleImmediate(() => { + gc(); + resolve(); + }); + }); } assert.equal(control.deref(), undefined, "Unowned control must collect"); }