Skip to content

Commit 2a151fd

Browse files
Fix flaky React and chat concurrency tests (#1426)
Made-with: Cursor
1 parent 58ca2fc commit 2a151fd

3 files changed

Lines changed: 64 additions & 11 deletions

File tree

packages/agents/src/react-tests/setup.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ function isPortAvailable(port: number): Promise<boolean> {
3232
});
3333
}
3434

35+
async function isWorkerReachable(port: number): Promise<boolean> {
36+
try {
37+
const response = await fetch(`http://127.0.0.1:${port}`, {
38+
signal: AbortSignal.timeout(500)
39+
});
40+
// Any HTTP response means a worker is already serving the test port.
41+
return response.status >= 100;
42+
} catch {
43+
return false;
44+
}
45+
}
46+
47+
async function waitForReachableWorker(
48+
port: number,
49+
timeoutMs: number
50+
): Promise<boolean> {
51+
const deadline = Date.now() + timeoutMs;
52+
while (Date.now() < deadline) {
53+
if (await isWorkerReachable(port)) return true;
54+
await new Promise((r) => setTimeout(r, 100));
55+
}
56+
return false;
57+
}
58+
3559
/**
3660
* Kill any process listening on the given port.
3761
* Handles stale processes left behind by previous test runs that were
@@ -77,6 +101,13 @@ export async function setup() {
77101
// that was forcefully terminated before teardown could run.
78102
const portAvailable = await isPortAvailable(TEST_WORKER_PORT);
79103
if (!portAvailable) {
104+
if (await waitForReachableWorker(TEST_WORKER_PORT, 2000)) {
105+
console.log(
106+
`[setup] Reusing test worker at http://127.0.0.1:${TEST_WORKER_PORT}`
107+
);
108+
return;
109+
}
110+
80111
console.log(
81112
`[setup] Port ${TEST_WORKER_PORT} in use — killing stale process...`
82113
);

packages/agents/src/react-tests/useAgent.test.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -377,27 +377,41 @@ describe("useAgent hook", () => {
377377

378378
it("should update state property on server broadcast", async () => {
379379
const { host, protocol } = getTestWorkerHost();
380-
let capturedAgent: TestAgent | null = null;
380+
let observerAgent: TestAgent | null = null;
381+
let senderAgent: TestAgent | null = null;
382+
const room = `hook-test-state-prop-server-${crypto.randomUUID()}`;
381383

382384
const { container } = await render(
383385
<SuspenseWrapper>
384386
<StateTrackingComponent
385387
options={{
386388
agent: "TestStateAgent",
387-
name: "hook-test-state-prop-server",
389+
name: room,
388390
host,
389391
protocol
390392
}}
391393
onAgent={(agent) => {
392-
capturedAgent = agent;
394+
observerAgent = agent;
395+
}}
396+
/>
397+
<TestAgentComponent
398+
options={{
399+
agent: "TestStateAgent",
400+
name: room,
401+
host,
402+
protocol
403+
}}
404+
onAgent={(agent) => {
405+
senderAgent = agent;
393406
}}
394407
/>
395408
</SuspenseWrapper>
396409
);
397410

398411
await vi.waitFor(
399412
() => {
400-
expect(capturedAgent?.identified).toBe(true);
413+
expect(observerAgent?.identified).toBe(true);
414+
expect(senderAgent?.identified).toBe(true);
401415
const stateEl = container.querySelector(
402416
'[data-testid="agent-state"]'
403417
);
@@ -406,15 +420,16 @@ describe("useAgent hook", () => {
406420
{ timeout: 10000 }
407421
);
408422

409-
// Send state — server will broadcast back, which updates agent.state
423+
// Trigger a server-side setState. Unlike client setState, this exercises
424+
// the server broadcast path back to the same connection.
410425
const newState = {
411426
count: 999,
412427
items: ["server-state"],
413428
lastUpdated: 2000
414429
};
415-
capturedAgent!.setState(newState);
430+
senderAgent!.setState(newState);
416431

417-
// Wait for the server broadcast to update state (second render)
432+
// Wait for the server broadcast to update state (second render).
418433
await vi.waitFor(
419434
() => {
420435
const stateEl = container.querySelector(

packages/ai-chat/src/tests/message-concurrency.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,10 +380,14 @@ describe("AIChatAgent messageConcurrency", () => {
380380
room
381381
);
382382

383+
// Keep the first turn in-flight long enough for both overlapping submits
384+
// to reach the concurrency controller before the queued debounce turn can
385+
// evaluate whether it was superseded. Otherwise slow CI WebSocket dispatch
386+
// can let req-debounce-2 run before req-debounce-3 has been admitted.
383387
sendChatRequest(ws, "req-debounce-1", [firstUserMessage], {
384388
format: "plaintext",
385-
chunkCount: 8,
386-
chunkDelayMs: 80
389+
chunkCount: 15,
390+
chunkDelayMs: 150
387391
});
388392
await delay(50);
389393

@@ -846,10 +850,13 @@ describe("AIChatAgent messageConcurrency", () => {
846850
room
847851
);
848852

853+
// Keep req-1 open long enough for both overlapping merge submits to be
854+
// admitted before req-2 can acquire the turn lock and check supersession.
855+
// This mirrors the primary merge strategy test above.
849856
sendChatRequest(ws, "req-resp-merge-1", [firstUserMessage], {
850857
format: "plaintext",
851-
chunkCount: 8,
852-
chunkDelayMs: 80
858+
chunkCount: 15,
859+
chunkDelayMs: 150
853860
});
854861
await delay(50);
855862

0 commit comments

Comments
 (0)