test: speed up slow dashboard and tui tests

This commit is contained in:
gsxdsm
2026-04-12 12:28:29 -07:00
parent 31815e0fba
commit d570ad5d8c
5 changed files with 148 additions and 50 deletions

View File

@@ -5119,14 +5119,11 @@ describe("POST /github/issues/batch-import", () => {
}, 10000); // Increase timeout for retry delay
it("returns error after max retries exceeded on 429", async () => {
// Always return 429
fetchSpy.mockResolvedValue({
ok: false,
status: 429,
statusText: "Too Many Requests",
headers: new Headers({ "Retry-After": "1" }), // 1 second for test speed
json: () => Promise.resolve({ message: "Rate limited" }),
} as Response);
const throttledSpy = vi.spyOn(GitHubClient.prototype, "fetchThrottled").mockResolvedValueOnce({
success: false,
error: "GitHub API rate limit exceeded. Retry after 1 seconds.",
retryAfter: 1,
} as Awaited<ReturnType<GitHubClient["fetchThrottled"]>>);
const res = await REQUEST(
buildApp(),
@@ -5141,8 +5138,8 @@ describe("POST /github/issues/batch-import", () => {
expect(res.body.results[0].success).toBe(false);
expect(res.body.results[0].error).toContain("rate limit");
expect(res.body.results[0].retryAfter).toBe(1);
expect(fetchSpy.mock.calls.length).toBeGreaterThanOrEqual(4);
}, 60000); // Retry path can exceed 30s in CI/load-constrained environments
expect(throttledSpy).toHaveBeenCalledTimes(1);
});
it("processes issues sequentially (not parallel)", async () => {
const startedIssues: number[] = [];
@@ -6197,14 +6194,17 @@ describe("Git Management endpoints", () => {
execFileSync("git", ["-C", gitRepoDir, "push", "-u", "origin", "HEAD"], { stdio: "pipe" });
}
beforeEach(() => {
beforeAll(() => {
createGitTestRepo();
});
beforeEach(() => {
store = createMockStore({
getRootDir: vi.fn().mockReturnValue(gitRepoDir),
});
});
afterEach(() => {
afterAll(() => {
if (gitTestRoot) {
rmSync(gitTestRoot, { recursive: true, force: true });
gitTestRoot = "";
@@ -6833,9 +6833,10 @@ describe("Git Management endpoints", () => {
expect(res.body.error).toContain("does not exist");
});
});
});
// ── File API tests ────────────────────────────────────────────────────
describe("File API endpoints", () => {
// ── File API tests ────────────────────────────────────────────────────
describe("File API endpoints", () => {
let store: TaskStore;
beforeEach(() => {
@@ -7001,9 +7002,18 @@ describe("Git Management endpoints", () => {
expect([400, 404, 500]).toContain(res.status);
});
});
});
});
describe("Planning Mode Routes", () => {
let store: TaskStore;
function buildApp() {
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store));
return app;
}
describe("Planning Mode Routes", () => {
/** Mock agent for planning session tests */
function setupPlanningMockAgent() {
const questionResponses = [
@@ -7069,6 +7079,7 @@ describe("Git Management endpoints", () => {
beforeEach(() => {
// Reset planning state before each test to avoid cross-test contamination
store = createMockStore();
__resetPlanningState();
setupPlanningMockAgent();
});
@@ -7668,7 +7679,6 @@ describe("Git Management endpoints", () => {
expect(res.body.error).toContain("sessionId is required");
});
});
});
});
describe("DELETE /api/ai-sessions/cleanup", () => {

View File

@@ -5,6 +5,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import React from "react";
import { render } from "ink";
import { Writable } from "node:stream";
import { detectProjectDir } from "../project-detect";
import { FusionProvider, useFusion, FusionContext } from "../fusion-context";
import { TaskStore } from "@fusion/core";
@@ -14,6 +15,27 @@ import { join } from "node:path";
// Track temp directories for cleanup
const tempDirs: string[] = [];
function createSinkStream(): NodeJS.WriteStream {
const stream = new Writable({
write(_chunk, _encoding, callback) {
callback();
},
}) as NodeJS.WriteStream;
stream.columns = 80;
stream.rows = 24;
return stream;
}
function renderTest(node: React.ReactNode) {
return render(node, {
stdout: createSinkStream(),
stderr: createSinkStream(),
patchConsole: false,
exitOnCtrlC: false,
maxFps: 1000,
});
}
afterEach(async () => {
// Clean up temp directories
for (const dir of tempDirs) {
@@ -132,7 +154,7 @@ describe("FusionProvider", () => {
return null;
}
const instance = render(
const instance = renderTest(
<FusionProvider projectDir={projectDir}>
<TestComponent />
</FusionProvider>
@@ -156,7 +178,7 @@ describe("FusionProvider", () => {
return null;
}
const instance = render(
const instance = renderTest(
<FusionProvider projectDir={nonExistentDir}>
<TestComponent />
</FusionProvider>
@@ -196,7 +218,7 @@ describe("FusionProvider", () => {
return null;
}
const instance = render(
const instance = renderTest(
<FusionProvider projectDir={projectDir}>
<TestComponent />
</FusionProvider>
@@ -228,7 +250,7 @@ describe("FusionProvider", () => {
return null;
}
const instance = render(
const instance = renderTest(
<FusionProvider projectDir={explicitDir}>
<TestComponent />
</FusionProvider>
@@ -281,7 +303,7 @@ describe("useFusion hook", () => {
return null;
}
const instance = render(
const instance = renderTest(
<FusionProvider projectDir={projectDir}>
<GoodComponent />
</FusionProvider>

View File

@@ -5,6 +5,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import React from "react";
import { render, Box, Text } from "ink";
import { Writable } from "node:stream";
import { useGlobalShortcuts, HelpOverlay, FocusGuardRef, type ScreenId } from "../hooks/use-global-shortcuts";
import type { Key } from "ink";
@@ -12,6 +13,27 @@ import type { Key } from "ink";
let capturedUseInputHandlers: ((input: string, key: Key) => void)[] = [];
let capturedExitFn: (() => void) | undefined;
function createSinkStream(): NodeJS.WriteStream {
const stream = new Writable({
write(_chunk, _encoding, callback) {
callback();
},
}) as NodeJS.WriteStream;
stream.columns = 80;
stream.rows = 24;
return stream;
}
function renderTest(node: React.ReactNode) {
return render(node, {
stdout: createSinkStream(),
stderr: createSinkStream(),
patchConsole: false,
exitOnCtrlC: false,
maxFps: 1000,
});
}
// Mock ink hooks to avoid raw mode errors in tests
vi.mock("ink", async (importOriginal) => {
const actual = await importOriginal<typeof import("ink")>();
@@ -51,7 +73,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(capturedHelpVisible).toBe(false);
@@ -67,7 +89,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(toggleHelpFn).toBeDefined();
@@ -84,7 +106,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(hideHelpFn).toBeDefined();
@@ -105,7 +127,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
// Initial state should be false
@@ -133,7 +155,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
// First toggle
@@ -160,7 +182,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(onScreenChange).toBeDefined();
@@ -175,7 +197,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
// Get the registered input handler
@@ -199,7 +221,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const expectedScreens: ScreenId[] = ["board", "detail", "activity", "agents", "settings"];
@@ -230,7 +252,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[0];
@@ -256,7 +278,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[0];
@@ -285,7 +307,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[0];
@@ -313,7 +335,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[0];
@@ -339,7 +361,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[0];
@@ -364,7 +386,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[0];
@@ -390,7 +412,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[0];
@@ -417,7 +439,7 @@ describe("useGlobalShortcuts", () => {
return <Text>Test</Text>;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[0];
@@ -439,7 +461,7 @@ describe("HelpOverlay", () => {
it("renders without crashing", async () => {
const onClose = vi.fn();
const instance = render(<HelpOverlay onClose={onClose} />);
const instance = renderTest(<HelpOverlay onClose={onClose} />);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(() => instance.unmount()).not.toThrow();
@@ -452,7 +474,7 @@ describe("HelpOverlay", () => {
return <HelpOverlay onClose={onClose} />;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[capturedUseInputHandlers.length - 1];
@@ -472,7 +494,7 @@ describe("HelpOverlay", () => {
return <HelpOverlay onClose={onClose} />;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
await new Promise((resolve) => setTimeout(resolve, 50));
const handler = capturedUseInputHandlers[capturedUseInputHandlers.length - 1];
@@ -488,7 +510,7 @@ describe("HelpOverlay", () => {
it("displays keyboard shortcuts", async () => {
const onClose = vi.fn();
const instance = render(<HelpOverlay onClose={onClose} />);
const instance = renderTest(<HelpOverlay onClose={onClose} />);
await new Promise((resolve) => setTimeout(resolve, 50));
// The component should render without error

View File

@@ -4,6 +4,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import React from "react";
import { Writable } from "node:stream";
import { computeColumnLayout, MIN_TERMINAL_COLUMNS, MIN_TERMINAL_ROWS, type ColumnDefinition } from "../utils/terminal";
import { truncateText, truncateWithOptions, padText, fitText } from "../utils/truncate";
@@ -14,6 +15,27 @@ const mockStdout = {
write: vi.fn(),
};
function createSinkStream(): NodeJS.WriteStream {
const stream = new Writable({
write(_chunk, _encoding, callback) {
callback();
},
}) as NodeJS.WriteStream;
stream.columns = 80;
stream.rows = 24;
return stream;
}
function renderTest(node: React.ReactNode) {
return render(node, {
stdout: createSinkStream(),
stderr: createSinkStream(),
patchConsole: false,
exitOnCtrlC: false,
maxFps: 1000,
});
}
// Mock Ink's useStdout for terminal dimension tests
// The mock returns a function that reads from the mutable mockStdout object
vi.mock("ink", async (importOriginal) => {
@@ -52,7 +74,7 @@ describe("terminal.ts", () => {
return null;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
instance.unmount();
expect(dimensions).not.toBeNull();
@@ -69,7 +91,7 @@ describe("terminal.ts", () => {
return null;
}
const instance = render(<TestComponent />);
const instance = renderTest(<TestComponent />);
expect(() => instance.unmount()).not.toThrow();
});
});

View File

@@ -5,6 +5,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import React, { useState } from "react";
import { render, Box, Text } from "ink";
import { Writable } from "node:stream";
import { mkdir, writeFile, remove } from "fs/promises";
import { join } from "node:path";
import { ScreenRouter, SCREENS, type ScreenId } from "../components/screen-router";
@@ -12,6 +13,27 @@ import { ScreenRouter, SCREENS, type ScreenId } from "../components/screen-route
// Track temp directories for cleanup
const tempDirs: string[] = [];
function createSinkStream(): NodeJS.WriteStream {
const stream = new Writable({
write(_chunk, _encoding, callback) {
callback();
},
}) as NodeJS.WriteStream;
stream.columns = 80;
stream.rows = 24;
return stream;
}
function renderTest(node: React.ReactNode) {
return render(node, {
stdout: createSinkStream(),
stderr: createSinkStream(),
patchConsole: false,
exitOnCtrlC: false,
maxFps: 1000,
});
}
afterEach(async () => {
// Clean up temp directories
for (const dir of tempDirs) {
@@ -68,7 +90,7 @@ describe("SCREENS constant", () => {
describe("ScreenRouter", () => {
describe("rendering", () => {
it("renders without crashing", async () => {
const { unmount } = render(
const { unmount } = renderTest(
<ScreenRouter>
{({ activeScreen }) => (
<Box>
@@ -85,7 +107,7 @@ describe("ScreenRouter", () => {
});
it("renders all five tab markers with shortcut numbers", async () => {
const { unmount } = render(
const { unmount } = renderTest(
<ScreenRouter>
{({ activeScreen }) => (
<Box>
@@ -106,7 +128,7 @@ describe("ScreenRouter", () => {
it("passes activeScreen prop to children function", async () => {
let capturedActiveScreen: ScreenId | undefined;
const { unmount } = render(
const { unmount } = renderTest(
<ScreenRouter>
{({ activeScreen }) => {
capturedActiveScreen = activeScreen;
@@ -126,7 +148,7 @@ describe("ScreenRouter", () => {
});
it("renders screen content below tab bar", async () => {
const { unmount } = render(
const { unmount } = renderTest(
<ScreenRouter>
{({ activeScreen }) => (
<Box>
@@ -147,7 +169,7 @@ describe("ScreenRouter", () => {
it("defaults to board screen", async () => {
let activeScreen: ScreenId = "detail"; // Start with non-default
const { unmount } = render(
const { unmount } = renderTest(
<ScreenRouter>
{({ activeScreen: screen }) => {
activeScreen = screen;
@@ -188,7 +210,7 @@ describe("ScreenRouter", () => {
);
};
const { unmount } = render(<TestApp />);
const { unmount } = renderTest(<TestApp />);
await new Promise((resolve) => setTimeout(resolve, 50));
// Active screen is board