mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
35a38abf34
A double-triggered "AI Optimize" affordance created two concurrent design-system enrichment runs 383 ms apart in one conversation and both were billed (2026-07-28 incident). Reject the newcomer on the daemon with HTTP 409 DESIGN_SYSTEM_ENRICHMENT_IN_PROGRESS (details name the owning run), keep ordinary chat turns ungated, and make the web trigger a synchronous single-flight call so the duplicate never leaves the client.
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act, cleanup, renderHook } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { useSingleFlightCallback } from '../../src/runtime/useSingleFlightCallback';
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
describe('useSingleFlightCallback', () => {
|
|
it('admits only the first of two triggers fired inside the same tick', async () => {
|
|
let settle: () => void = () => {};
|
|
const action = vi.fn(() => new Promise<void>((resolve) => { settle = resolve; }));
|
|
const { result } = renderHook(() => useSingleFlightCallback(action));
|
|
|
|
// Incident shape: the ready-toast nudge and the chat "Continue" affordance
|
|
// both fire "AI Optimize" before React has re-rendered any state flag.
|
|
let admitted: boolean[] = [];
|
|
act(() => {
|
|
admitted = [result.current(), result.current()];
|
|
});
|
|
expect(admitted).toEqual([true, false]);
|
|
expect(action).toHaveBeenCalledTimes(1);
|
|
|
|
await act(async () => {
|
|
settle();
|
|
await Promise.resolve();
|
|
});
|
|
// The slot reopens once the in-flight action settles.
|
|
let again = false;
|
|
act(() => {
|
|
again = result.current();
|
|
});
|
|
expect(again).toBe(true);
|
|
expect(action).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('reopens the slot when the action rejects or throws synchronously', async () => {
|
|
const rejecting = vi.fn(() => Promise.reject(new Error('boom')));
|
|
const { result } = renderHook(() => useSingleFlightCallback(rejecting));
|
|
await act(async () => {
|
|
expect(result.current()).toBe(true);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
expect(result.current()).toBe(true);
|
|
expect(rejecting).toHaveBeenCalledTimes(2);
|
|
|
|
const throwing = vi.fn(() => { throw new Error('sync'); });
|
|
const thrown = renderHook(() => useSingleFlightCallback(throwing));
|
|
expect(() => thrown.result.current()).toThrow('sync');
|
|
expect(() => thrown.result.current()).toThrow('sync');
|
|
expect(throwing).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('passes arguments through and returns false while a call is in flight', () => {
|
|
const action = vi.fn((value: string) => new Promise<void>(() => { void value; }));
|
|
const { result } = renderHook(() => useSingleFlightCallback(action));
|
|
expect(result.current('first')).toBe(true);
|
|
expect(result.current('second')).toBe(false);
|
|
expect(action).toHaveBeenCalledWith('first');
|
|
expect(action).not.toHaveBeenCalledWith('second');
|
|
});
|
|
});
|