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:
gsxdsm
2026-03-31 22:52:55 -07:00
parent 3dec4b4497
commit d42ee113f8
8 changed files with 95 additions and 37 deletions

View 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

View File

@@ -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:

View File

@@ -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"],

View File

@@ -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);

View File

@@ -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"],

View File

@@ -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() {

View File

@@ -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"],

View File

@@ -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"],