feat(KB-648): optimize test execution speed across all packages
- Enable parallel file execution in core, engine, CLI, and dashboard packages via vitest.config.ts - Optimize backup tests using fake timers instead of real setTimeout delays - Add test optimization patterns to AGENTS.md documentation (fake timers, parallel execution) - Add changeset documenting the test speed improvements - Include test fixes for SettingsModal, engine tests, and git worktree mocks from KB-637
This commit is contained in:
10
.changeset/increase-test-speed-kb-648.md
Normal file
10
.changeset/increase-test-speed-kb-648.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
"@gsxdsm/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Increase test speed by optimizing backup tests with fake timers and enabling parallel file execution across all packages.
|
||||||
|
|
||||||
|
- Refactored backup tests to use `vi.useFakeTimers()` and `vi.setSystemTime()` instead of real 1100ms delays
|
||||||
|
- Core package tests now run with `fileParallelism: true` (reduced from ~30s to ~3s)
|
||||||
|
- Enabled parallel execution in engine, CLI, and dashboard packages
|
||||||
|
- All backup tests now complete in <200ms instead of ~24s
|
||||||
@@ -94,6 +94,15 @@ pnpm build # build all packages
|
|||||||
|
|
||||||
Tests are required. Typechecks and manual verification are not substitutes for real tests with assertions.
|
Tests are required. Typechecks and manual verification are not substitutes for real tests with assertions.
|
||||||
|
|
||||||
|
### Test Optimization Patterns
|
||||||
|
|
||||||
|
When writing tests, follow these patterns to keep the test suite fast:
|
||||||
|
|
||||||
|
- **Use fake timers** (`vi.useFakeTimers()`, `vi.setSystemTime()`) instead of real `setTimeout` for timestamp-dependent tests. See `packages/core/src/backup.test.ts` for an example.
|
||||||
|
- **Default to `fileParallelism: true`** in vitest configs; use `test.sequential()` for specific tests that truly need isolation
|
||||||
|
- **Avoid real delays** — Never use `setTimeout`, `sleep`, or waiting for actual time to pass in tests. Use Vitest's timer mocks instead.
|
||||||
|
- **Use unique temp directories** — Each test should use isolated temp directories (e.g., `mkdtempSync`) to avoid conflicts in parallel execution
|
||||||
|
|
||||||
## Multi-Project Architecture / Central Core
|
## Multi-Project Architecture / Central Core
|
||||||
|
|
||||||
kb supports multi-project coordination through a central infrastructure that provides:
|
kb supports multi-project coordination through a central infrastructure that provides:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
include: ["src/**/*.test.ts"],
|
include: ["src/**/*.test.ts"],
|
||||||
maxWorkers,
|
maxWorkers,
|
||||||
fileParallelism: false,
|
fileParallelism: true,
|
||||||
coverage: {
|
coverage: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
reporter: ["text", "html", "json"],
|
reporter: ["text", "html", "json"],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
import { mkdtempSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
import { mkdtempSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -14,17 +14,16 @@ import {
|
|||||||
} from "./backup.js";
|
} from "./backup.js";
|
||||||
import type { ProjectSettings } from "./types.js";
|
import type { ProjectSettings } from "./types.js";
|
||||||
|
|
||||||
// Helper to wait with a delay that ensures different timestamps
|
|
||||||
async function waitForNextSecond(): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, 1100));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("BackupManager", () => {
|
describe("BackupManager", () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
let kbDir: string;
|
let kbDir: string;
|
||||||
let backupManager: BackupManager;
|
let backupManager: BackupManager;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
// Use fake timers for deterministic timestamp control
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||||
|
|
||||||
tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
||||||
kbDir = join(tempDir, ".fusion");
|
kbDir = join(tempDir, ".fusion");
|
||||||
await mkdir(kbDir, { recursive: true });
|
await mkdir(kbDir, { recursive: true });
|
||||||
@@ -34,6 +33,7 @@ describe("BackupManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
vi.useRealTimers();
|
||||||
await rm(tempDir, { recursive: true, force: true });
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,20 +83,25 @@ describe("BackupManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return sorted array newest-first", async () => {
|
it("should return sorted array newest-first", async () => {
|
||||||
// Create multiple backups with delays to ensure different timestamps
|
// Create multiple backups by advancing system time deterministically
|
||||||
await backupManager.createBackup();
|
const backup1 = await backupManager.createBackup();
|
||||||
await waitForNextSecond();
|
|
||||||
await backupManager.createBackup();
|
vi.setSystemTime(new Date("2026-01-01T00:00:01.000Z"));
|
||||||
await waitForNextSecond();
|
const backup2 = await backupManager.createBackup();
|
||||||
await backupManager.createBackup();
|
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:02.000Z"));
|
||||||
|
const backup3 = await backupManager.createBackup();
|
||||||
|
|
||||||
const backups = await backupManager.listBackups();
|
const backups = await backupManager.listBackups();
|
||||||
|
|
||||||
expect(backups).toHaveLength(3);
|
expect(backups).toHaveLength(3);
|
||||||
// Verify sorted by createdAt descending
|
// Verify sorted by createdAt descending (newest first)
|
||||||
for (let i = 0; i < backups.length - 1; i++) {
|
expect(backups[0].createdAt >= backups[1].createdAt).toBe(true);
|
||||||
expect(backups[i].createdAt >= backups[i + 1].createdAt).toBe(true);
|
expect(backups[1].createdAt >= backups[2].createdAt).toBe(true);
|
||||||
}
|
// Verify correct ordering by filename
|
||||||
|
expect(backups[0].filename).toBe(backup3.filename);
|
||||||
|
expect(backups[1].filename).toBe(backup2.filename);
|
||||||
|
expect(backups[2].filename).toBe(backup1.filename);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should only list files matching backup pattern", async () => {
|
it("should only list files matching backup pattern", async () => {
|
||||||
@@ -123,10 +128,10 @@ describe("BackupManager", () => {
|
|||||||
|
|
||||||
describe("cleanupOldBackups", () => {
|
describe("cleanupOldBackups", () => {
|
||||||
it("should not delete when backup count is within retention", async () => {
|
it("should not delete when backup count is within retention", async () => {
|
||||||
// Create 3 backups with retention of 7
|
// Create 3 backups with retention of 7 by advancing time
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
|
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||||
await backupManager.createBackup();
|
await backupManager.createBackup();
|
||||||
await waitForNextSecond();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleted = await backupManager.cleanupOldBackups();
|
const deleted = await backupManager.cleanupOldBackups();
|
||||||
@@ -139,10 +144,10 @@ describe("BackupManager", () => {
|
|||||||
it("should delete oldest backups exceeding retention", async () => {
|
it("should delete oldest backups exceeding retention", async () => {
|
||||||
const manager = new BackupManager(kbDir, { retention: 2 });
|
const manager = new BackupManager(kbDir, { retention: 2 });
|
||||||
|
|
||||||
// Create 4 backups with 1-second delays to ensure different timestamps
|
// Create 4 backups by advancing time deterministically
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
|
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||||
await manager.createBackup();
|
await manager.createBackup();
|
||||||
await waitForNextSecond();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleted = await manager.cleanupOldBackups();
|
const deleted = await manager.cleanupOldBackups();
|
||||||
@@ -150,17 +155,17 @@ describe("BackupManager", () => {
|
|||||||
expect(deleted).toBe(2); // 4 - 2 = 2 deleted
|
expect(deleted).toBe(2); // 4 - 2 = 2 deleted
|
||||||
const backups = await manager.listBackups();
|
const backups = await manager.listBackups();
|
||||||
expect(backups).toHaveLength(2);
|
expect(backups).toHaveLength(2);
|
||||||
}, 10000);
|
});
|
||||||
|
|
||||||
it("should keep the newest backups after cleanup", async () => {
|
it("should keep the newest backups after cleanup", async () => {
|
||||||
const manager = new BackupManager(kbDir, { retention: 2 });
|
const manager = new BackupManager(kbDir, { retention: 2 });
|
||||||
|
|
||||||
// Create 4 backups and record their names
|
// Create 4 backups and record their names by advancing time
|
||||||
const backupNames: string[] = [];
|
const backupNames: string[] = [];
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
|
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||||
const backup = await manager.createBackup();
|
const backup = await manager.createBackup();
|
||||||
backupNames.push(backup.filename);
|
backupNames.push(backup.filename);
|
||||||
await waitForNextSecond();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await manager.cleanupOldBackups();
|
await manager.cleanupOldBackups();
|
||||||
@@ -200,8 +205,8 @@ describe("BackupManager", () => {
|
|||||||
it("should create pre-restore backup by default", async () => {
|
it("should create pre-restore backup by default", async () => {
|
||||||
const backup = await backupManager.createBackup();
|
const backup = await backupManager.createBackup();
|
||||||
|
|
||||||
// Wait to ensure different timestamp
|
// Advance time to ensure different timestamp for pre-restore backup
|
||||||
await waitForNextSecond();
|
vi.setSystemTime(new Date("2026-01-01T00:00:01.000Z"));
|
||||||
|
|
||||||
// Restore with default options (should create pre-restore backup)
|
// Restore with default options (should create pre-restore backup)
|
||||||
await backupManager.restoreBackup(backup.filename);
|
await backupManager.restoreBackup(backup.filename);
|
||||||
@@ -221,12 +226,19 @@ describe("generateBackupFilename", () => {
|
|||||||
expect(filename).toMatch(/^kb-\d{4}-\d{2}-\d{2}-\d{6}\.db$/);
|
expect(filename).toMatch(/^kb-\d{4}-\d{2}-\d{2}-\d{6}\.db$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should generate unique filenames for different timestamps", async () => {
|
it("should generate unique filenames for different timestamps", () => {
|
||||||
|
// Use fake timers for deterministic time control
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||||
|
|
||||||
const filename1 = generateBackupFilename();
|
const filename1 = generateBackupFilename();
|
||||||
await waitForNextSecond();
|
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:01.000Z"));
|
||||||
const filename2 = generateBackupFilename();
|
const filename2 = generateBackupFilename();
|
||||||
|
|
||||||
expect(filename1).not.toBe(filename2);
|
expect(filename1).not.toBe(filename2);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,6 +306,10 @@ describe("createBackupManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should use settings when provided", async () => {
|
it("should use settings when provided", async () => {
|
||||||
|
// Use fake timers for deterministic time control
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||||
|
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
const tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
||||||
const kbDir = join(tempDir, ".fusion");
|
const kbDir = join(tempDir, ".fusion");
|
||||||
await mkdir(kbDir, { recursive: true });
|
await mkdir(kbDir, { recursive: true });
|
||||||
@@ -306,18 +322,19 @@ describe("createBackupManager", () => {
|
|||||||
|
|
||||||
const manager = createBackupManager(kbDir, settings);
|
const manager = createBackupManager(kbDir, settings);
|
||||||
|
|
||||||
// Create 4 backups with 1-second delays
|
// Create 4 backups by advancing time
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
|
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||||
await manager.createBackup();
|
await manager.createBackup();
|
||||||
await waitForNextSecond();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup should leave only 2
|
// Cleanup should leave only 2
|
||||||
const deleted = await manager.cleanupOldBackups();
|
const deleted = await manager.cleanupOldBackups();
|
||||||
expect(deleted).toBe(2);
|
expect(deleted).toBe(2);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
await rm(tempDir, { recursive: true, force: true });
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
}, 10000);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("runBackupCommand", () => {
|
describe("runBackupCommand", () => {
|
||||||
@@ -325,6 +342,10 @@ describe("runBackupCommand", () => {
|
|||||||
let kbDir: string;
|
let kbDir: string;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
// Use fake timers for deterministic timestamp control
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||||
|
|
||||||
tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
||||||
kbDir = join(tempDir, ".fusion");
|
kbDir = join(tempDir, ".fusion");
|
||||||
await mkdir(kbDir, { recursive: true });
|
await mkdir(kbDir, { recursive: true });
|
||||||
@@ -332,6 +353,7 @@ describe("runBackupCommand", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
vi.useRealTimers();
|
||||||
await rm(tempDir, { recursive: true, force: true });
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -398,14 +420,15 @@ describe("runBackupCommand", () => {
|
|||||||
autoBackupRetention: 2,
|
autoBackupRetention: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create 3 backups first (manually to test cleanup) with delays
|
// Create 3 backups first (manually to test cleanup) by advancing time
|
||||||
const manager = createBackupManager(kbDir, settings);
|
const manager = createBackupManager(kbDir, settings);
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
|
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||||
await manager.createBackup();
|
await manager.createBackup();
|
||||||
await waitForNextSecond();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now run backup command
|
// Now run backup command
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:03.000Z"));
|
||||||
const result = await runBackupCommand(kbDir, settings);
|
const result = await runBackupCommand(kbDir, settings);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
include: ["src/**/*.test.ts"],
|
include: ["src/**/*.test.ts"],
|
||||||
maxWorkers,
|
maxWorkers,
|
||||||
fileParallelism: false,
|
fileParallelism: true,
|
||||||
coverage: {
|
coverage: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
reporter: ["text", "html", "json"],
|
reporter: ["text", "html", "json"],
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ function createMockGlobalSettingsStore() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createMockMissionStore() {
|
||||||
|
return {
|
||||||
|
createSession: vi.fn().mockResolvedValue({ id: "session-1", status: "active" }),
|
||||||
|
getSession: vi.fn().mockResolvedValue({ id: "session-1", status: "active", answers: [] }),
|
||||||
|
updateSession: vi.fn().mockResolvedValue(undefined),
|
||||||
|
addAnswer: vi.fn().mockResolvedValue(undefined),
|
||||||
|
deleteSession: vi.fn().mockResolvedValue(undefined),
|
||||||
|
listSessions: vi.fn().mockResolvedValue([]),
|
||||||
|
generatePlan: vi.fn().mockResolvedValue({ plan: "Test plan", steps: [] }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||||
return {
|
return {
|
||||||
getTask: vi.fn(),
|
getTask: vi.fn(),
|
||||||
@@ -63,6 +75,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
|||||||
getWorkflowStep: vi.fn(),
|
getWorkflowStep: vi.fn(),
|
||||||
updateWorkflowStep: vi.fn(),
|
updateWorkflowStep: vi.fn(),
|
||||||
deleteWorkflowStep: vi.fn(),
|
deleteWorkflowStep: vi.fn(),
|
||||||
|
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as TaskStore;
|
} as unknown as TaskStore;
|
||||||
}
|
}
|
||||||
@@ -3665,7 +3678,10 @@ describe("Git Management endpoints", () => {
|
|||||||
let store: TaskStore;
|
let store: TaskStore;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = createMockStore();
|
// Use the actual project root so git commands work
|
||||||
|
store = createMockStore({
|
||||||
|
getRootDir: vi.fn().mockReturnValue(process.cwd()),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildApp() {
|
function buildApp() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default defineConfig({
|
|||||||
include: ["app/**/*.test.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
|
include: ["app/**/*.test.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
|
||||||
setupFiles: ["./vitest.setup.ts"],
|
setupFiles: ["./vitest.setup.ts"],
|
||||||
maxWorkers,
|
maxWorkers,
|
||||||
fileParallelism: false,
|
fileParallelism: true,
|
||||||
coverage: {
|
coverage: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
reporter: ["text", "html", "json"],
|
reporter: ["text", "html", "json"],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
include: ["src/**/*.test.ts"],
|
include: ["src/**/*.test.ts"],
|
||||||
maxWorkers,
|
maxWorkers,
|
||||||
fileParallelism: false,
|
fileParallelism: true,
|
||||||
coverage: {
|
coverage: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
reporter: ["text", "html", "json"],
|
reporter: ["text", "html", "json"],
|
||||||
|
|||||||
Reference in New Issue
Block a user