diff --git a/packages/mcp-gateway/src/server.test.ts b/packages/mcp-gateway/src/server.test.ts
index 31d929926dc3..dbe549a1a662 100644
--- a/packages/mcp-gateway/src/server.test.ts
+++ b/packages/mcp-gateway/src/server.test.ts
@@ -1417,12 +1417,25 @@ describe("mcp gateway lifecycle compatibility", () => {
it("keeps aggregate initialize available when one upstream is unhealthy", async () => {
const alpha = await createStrictMcpUpstream([{ name: "search", description: "Alpha search" }]);
const hanging = await createHangingUpstream();
+ // `GatewayState.upstreamTimeoutMs` is ONE global budget, not per-upstream,
+ // so this deadline applies to the healthy `alpha` as much as to `stuck`.
+ // With `failureThreshold: 1`, a single `alpha` response slower than the
+ // budget opens its breaker for the rest of the test and `tools/list`
+ // returns `[]`. At the previous 150 ms that left a local in-process
+ // upstream almost no margin: on a CPU-starved runner it lost the race and
+ // this test failed `expected [] to deeply equal [ 'alpha__search' ]`,
+ // ejecting the merge group for #1952 on 2026-09-21.
+ //
+ // `hanging` never answers at all, so raising the budget does not weaken
+ // what this test checks — it only costs wall-clock. Reproduced directly:
+ // squeezing this to 1 ms reproduces that exact assertion locally.
+ const UPSTREAM_TIMEOUT_MS = 1_000;
const gateway = await createAggregateGateway(
{
alpha: { url: alpha.url, credentialHeaders: [] },
stuck: { url: hanging.url, credentialHeaders: [] },
},
- { timeoutMs: 150, failureThreshold: 1 },
+ { timeoutMs: UPSTREAM_TIMEOUT_MS, failureThreshold: 1 },
);
const start = Date.now();
@@ -1432,7 +1445,11 @@ describe("mcp gateway lifecycle compatibility", () => {
expect(initialize.status).toBe(200);
expect(clientSessionId).toBeTruthy();
- expect(elapsed).toBeLessThan(1000);
+ // Proportional to the budget above, not an absolute wall-clock number:
+ // what this asserts is "initialize returns near the timeout rather than
+ // hanging on `stuck`", and an absolute bound re-introduces exactly the
+ // steal sensitivity this change removes.
+ expect(elapsed).toBeLessThan(UPSTREAM_TIMEOUT_MS * 5);
expect(gateway.state.breaker.stateOf("stuck")).toBe("open");
const list = await postJson(
diff --git a/ui/src/components/AgentActionButtons.test.tsx b/ui/src/components/AgentActionButtons.test.tsx
index 6a156c0ceed0..1d8553f39505 100644
--- a/ui/src/components/AgentActionButtons.test.tsx
+++ b/ui/src/components/AgentActionButtons.test.tsx
@@ -124,17 +124,28 @@ describe("AgentActionButtons", () => {
vi.clearAllMocks();
});
- function render(agent: Agent) {
- root = createRoot(container);
- root.render(
-
-
- ,
- );
+ // `root.render` must run INSIDE `act`, which is what every other UI suite
+ // here does (Inbox, Routines, CompanyInvites, ProjectDetail). Rendering
+ // outside it and relying on a later `flushReact()` to catch up makes the
+ // first paint depend on the scheduler getting a turn within that helper's
+ // single microtask + one `setTimeout(0)`. On a CPU-starved runner it does
+ // not, and the assertion reads `container.textContent === ""` — the
+ // `expected '' to contain 'Pause'` failure that ejected the merge group for
+ // #1787 on 2026-09-21. Inside `act`, the render is flushed before this
+ // returns, so the assertion no longer races the scheduler.
+ async function render(agent: Agent) {
+ await act(async () => {
+ root = createRoot(container);
+ root.render(
+
+
+ ,
+ );
+ });
}
it("replaces the pause slot with Clear error for error agents", async () => {
- render(makeAgent({ status: "error" }));
+ await render(makeAgent({ status: "error" }));
await flushReact();
expect(container.textContent).toContain("Clear error");
@@ -150,7 +161,7 @@ describe("AgentActionButtons", () => {
});
it("calls clearError and refreshes agent-related queries", async () => {
- render(makeAgent({ status: "error" }));
+ await render(makeAgent({ status: "error" }));
await flushReact();
await act(async () => {
@@ -169,7 +180,7 @@ describe("AgentActionButtons", () => {
});
it("keeps the normal pause action for non-error agents", async () => {
- render(makeAgent({ status: "active" }));
+ await render(makeAgent({ status: "active" }));
await flushReact();
expect(container.textContent).toContain("Pause");