fix(FN-1029): fix flaky test failures and keyboard event forwarding
- Add onKeyDown handler to CustomModelDropdown portal container for keyboard event forwarding - Wrap SettingsModal empty state assertions in waitFor for async test stability - Extract rate limiter state for proper test isolation in routes tests - Add reconcileProjectStatuses to CentralCore mock for consistent test setup - Improve /api/ai/summarize-title rate limiting with per-test isolation
This commit is contained in:
@@ -440,6 +440,7 @@ export function CustomModelDropdown({
|
||||
className="model-combobox-dropdown model-combobox-dropdown--portal"
|
||||
role="listbox"
|
||||
data-testid="model-combobox-portal"
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
top: `${dropdownPosition.top}px`,
|
||||
left: `${dropdownPosition.left}px`,
|
||||
|
||||
@@ -657,7 +657,9 @@ describe("SettingsModal", () => {
|
||||
fireEvent.click(screen.getByText("Models"));
|
||||
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
|
||||
|
||||
expect(screen.getAllByText("No models available. Configure authentication first.").length).toBeGreaterThanOrEqual(1);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("No models available. Configure authentication first.").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Planning & Validation model tests ---
|
||||
@@ -727,7 +729,9 @@ describe("SettingsModal", () => {
|
||||
fireEvent.click(screen.getByText("Models"));
|
||||
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
|
||||
|
||||
expect(screen.getAllByText("No models available. Configure authentication first.").length).toBeGreaterThanOrEqual(1);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("No models available. Configure authentication first.").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Authentication in sidebar", async () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { githubRateLimiter } from "./github-poll.js";
|
||||
import type { TaskStore, TaskAttachment } from "@fusion/core";
|
||||
import type { TaskDetail } from "@fusion/core";
|
||||
import type { AuthStorageLike, ModelRegistryLike } from "./routes.js";
|
||||
import { __resetBatchImportRateLimiter } from "./routes.js";
|
||||
import { __resetPlanningState } from "./planning.js";
|
||||
import { __resetSubtaskBreakdownState } from "./subtask-breakdown.js";
|
||||
import * as terminalServiceModule from "./terminal-service.js";
|
||||
@@ -23,6 +24,7 @@ import { get as performGet, request as performRequest } from "./test-request.js"
|
||||
const mockCentralListProjects = vi.fn().mockResolvedValue([]);
|
||||
const mockCentralInit = vi.fn().mockResolvedValue(undefined);
|
||||
const mockCentralClose = vi.fn().mockResolvedValue(undefined);
|
||||
const mockCentralReconcileProjectStatuses = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mock("@fusion/core", async () => {
|
||||
const actual = await vi.importActual<typeof import("@fusion/core")>("@fusion/core");
|
||||
return {
|
||||
@@ -32,6 +34,7 @@ vi.mock("@fusion/core", async () => {
|
||||
init: mockCentralInit,
|
||||
close: mockCentralClose,
|
||||
listProjects: mockCentralListProjects,
|
||||
reconcileProjectStatuses: mockCentralReconcileProjectStatuses,
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -3325,6 +3328,8 @@ describe("POST /github/issues/batch-import", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
__resetBatchImportRateLimiter();
|
||||
|
||||
fetchSpy = vi.fn();
|
||||
globalThis.fetch = fetchSpy as any;
|
||||
|
||||
|
||||
@@ -1213,6 +1213,23 @@ function discardGitChanges(files: string[], cwd?: string): string[] {
|
||||
return files;
|
||||
}
|
||||
|
||||
// ── Module-level batch-import rate limiter state (resettable for testing) ──
|
||||
const batchImportWindowMs = 10_000; // 10 seconds
|
||||
const batchImportInstances: Map<string, number>[] = [];
|
||||
let batchImportCleanupInterval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
/** @internal Reset batch-import rate limiter state (for test isolation) */
|
||||
export function __resetBatchImportRateLimiter(): void {
|
||||
for (const clients of batchImportInstances) {
|
||||
clients.clear();
|
||||
}
|
||||
batchImportInstances.length = 0;
|
||||
if (batchImportCleanupInterval) {
|
||||
clearInterval(batchImportCleanupInterval);
|
||||
batchImportCleanupInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiRoutes(store: TaskStore, options?: ServerOptions): Router {
|
||||
const router = Router();
|
||||
|
||||
@@ -3433,16 +3450,20 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Batch import rate limiter: max 1 request per 10 seconds per IP
|
||||
const batchImportRateLimiter = (() => {
|
||||
const clients = new Map<string, number>();
|
||||
const windowMs = 10_000; // 10 seconds
|
||||
batchImportInstances.push(clients);
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [ip, resetTime] of clients) {
|
||||
if (now >= resetTime) {
|
||||
clients.delete(ip);
|
||||
if (!batchImportCleanupInterval) {
|
||||
batchImportCleanupInterval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const instanceClients of batchImportInstances) {
|
||||
for (const [ip, resetTime] of instanceClients) {
|
||||
if (now >= resetTime) {
|
||||
instanceClients.delete(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, windowMs);
|
||||
}, batchImportWindowMs);
|
||||
}
|
||||
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
const ip = req.ip ?? req.socket.remoteAddress ?? "unknown";
|
||||
@@ -3456,7 +3477,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
return;
|
||||
}
|
||||
|
||||
clients.set(ip, now + windowMs);
|
||||
clients.set(ip, now + batchImportWindowMs);
|
||||
next();
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user