perf(core): opt-in in-memory SQLite for *-store tests

Adds an opt-in `inMemory` flag to `Database`/`ArchiveDatabase` (and
`{ inMemoryDb }` to TaskStore, AgentStore, RoutineStore,
AutomationStore, PluginStore) that swaps the on-disk fusion.db /
archive.db for SQLite's `:memory:` connection. Production callers
never set the flag, so behavior is unchanged.

Test files for each store now flip the flag in `beforeEach`. The
handful of tests that exercise cross-instance persistence (open store
A, close, open store B on same dir, expect data) construct disk-backed
stores explicitly inside the test body, marked with a comment at each
site.

Wall-clock impact:
- core:      69.4s → 18.5s  (3.7× faster, 3038 tests)
- dashboard: 156.6s → 30.0s (5.2× faster — improvement ripples through
                              any test that constructs a TaskStore)

The refactor eliminates the per-test SQLite open + WAL fsync + tmp
dir cleanup loop that dominated setup cost: ~50ms/test → ~5ms/test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-26 18:45:14 -07:00
parent e2bc644fd5
commit 6e4797ff14
17 changed files with 179 additions and 34 deletions

View File

@@ -31,7 +31,10 @@ describe("AgentStore", () => {
beforeEach(async () => {
rootDir = makeTmpDir();
store = new AgentStore({ rootDir });
// In-memory SQLite — see store.test.ts beforeEach for rationale.
// Tests that exercise cross-instance persistence (search for `store2`)
// construct disk-backed stores explicitly inside the test body.
store = new AgentStore({ rootDir, inMemoryDb: true });
await store.init();
});
@@ -102,6 +105,13 @@ describe("AgentStore", () => {
});
it("normalizes legacy durable agents to heartbeat enabled once", async () => {
// Migration test: opens a raw Database on disk to seed a meta key,
// then re-opens the AgentStore to assert migration ran. Needs both
// the store and the raw DB to be disk-backed.
store.close();
store = new AgentStore({ rootDir });
await store.init();
const agent = await store.createAgent({
name: "Legacy Durable Agent",
role: "executor",
@@ -2459,6 +2469,12 @@ describe("AgentStore", () => {
});
it("API keys survive store reinitialization", async () => {
// Cross-instance persistence — swap in-memory beforeEach store for
// disk-backed so store2 (also disk-backed) can read what we wrote.
store.close();
store = new AgentStore({ rootDir });
await store.init();
const agent = await store.createAgent({ name: "KeyPersistence", role: "executor" });
const { key } = await store.createApiKey(agent.id, { label: "persist" });
@@ -2536,6 +2552,11 @@ describe("AgentStore", () => {
describe("SQLite persistence", () => {
it("agent data survives store reinitialization", async () => {
// Cross-instance persistence — see counterpart in API keys describe.
store.close();
store = new AgentStore({ rootDir });
await store.init();
const agent = await store.createAgent({
name: "Persistent",
role: "reviewer",

View File

@@ -28,7 +28,10 @@ describe("AutomationStore", () => {
beforeEach(async () => {
rootDir = makeTmpDir();
store = new AutomationStore(rootDir);
// In-memory SQLite for test speed; see store.test.ts beforeEach.
// Cross-instance persistence sub-test below opens a disk-backed
// secondStore explicitly.
store = new AutomationStore(rootDir, { inMemoryDb: true });
await store.init();
});
@@ -179,6 +182,12 @@ describe("AutomationStore", () => {
});
it("persists schedule to database", async () => {
// Cross-instance persistence — swap to disk-backed for both stores.
// AutomationStore has no close() method; the in-memory beforeEach
// store is dropped on reassignment and its DB connection is GC'd.
store = new AutomationStore(rootDir);
await store.init();
const schedule = await store.createSchedule({
name: "Persist test",
command: "echo persist",

View File

@@ -19,7 +19,8 @@ describe("ChatStore", () => {
beforeEach(() => {
tmpDir = makeTmpDir();
fusionDir = join(tmpDir, ".fusion");
db = new Database(fusionDir);
// In-memory SQLite for test speed; see store.test.ts beforeEach.
db = new Database(fusionDir, { inMemory: true });
db.init();
store = new ChatStore(fusionDir, db);
});

View File

@@ -47,7 +47,10 @@ function createProvenance(overrides: Partial<InsightProvenance> = {}): InsightPr
beforeEach(() => {
fusionDir = makeTmpDir();
db = createDatabase(fusionDir);
// In-memory SQLite for test speed; see store.test.ts beforeEach.
// Tests below that exercise migration on a real on-disk DB construct
// their own disk-backed Database explicitly.
db = createDatabase(fusionDir, { inMemory: true });
db.init();
store = new InsightStore(db);
});

View File

@@ -13,7 +13,8 @@ describe("MessageStore", () => {
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "kb-msg-test-"));
db = new Database(tempDir);
// In-memory SQLite for test speed; see store.test.ts beforeEach.
db = new Database(tempDir, { inMemory: true });
db.init();
store = new MessageStore(db);
});

View File

@@ -32,7 +32,10 @@ describe("MissionStore", () => {
beforeEach(() => {
tmpDir = makeTmpDir();
fusionDir = join(tmpDir, ".fusion");
db = new Database(fusionDir);
// In-memory SQLite for test speed — see store.test.ts beforeEach for
// the broader rationale. MissionStore tests don't exercise
// cross-instance persistence, so this is safe across the whole file.
db = new Database(fusionDir, { inMemory: true });
db.init();
store = new MissionStore(fusionDir, db);
});

View File

@@ -26,7 +26,8 @@ describe("PluginStore", () => {
beforeEach(async () => {
rootDir = makeTmpDir();
store = new PluginStore(rootDir);
// In-memory SQLite for test speed; see store.test.ts beforeEach.
store = new PluginStore(rootDir, { inMemoryDb: true });
await store.init();
});
@@ -38,6 +39,10 @@ describe("PluginStore", () => {
describe("init", () => {
it("creates the database file", async () => {
// Asserts a real file on disk exists, which the in-memory
// beforeEach store can't satisfy — open a disk-backed store.
const diskStore = new PluginStore(rootDir);
await diskStore.init();
const dbPath = join(rootDir, ".fusion", "fusion.db");
const { existsSync } = await import("node:fs");
expect(existsSync(dbPath)).toBe(true);

View File

@@ -28,7 +28,10 @@ describe("RoadmapStore", () => {
beforeEach(() => {
tmpDir = makeTmpDir();
db = new Database(join(tmpDir, ".fusion"));
// In-memory SQLite for test speed; see store.test.ts beforeEach.
// Cross-instance persistence sub-tests below construct disk-backed
// Database instances explicitly (search for `persistDb`).
db = new Database(join(tmpDir, ".fusion"), { inMemory: true });
db.init();
store = new RoadmapStore(db);
});

View File

@@ -21,7 +21,8 @@ describe("RoutineStore", () => {
beforeEach(async () => {
rootDir = makeTmpDir();
store = new RoutineStore(rootDir);
// In-memory SQLite for test speed; see store.test.ts beforeEach.
store = new RoutineStore(rootDir, { inMemoryDb: true });
await store.init();
});

View File

@@ -43,7 +43,12 @@ describe("TaskStore", () => {
beforeEach(async () => {
rootDir = makeTmpDir();
globalDir = makeTmpDir();
store = new TaskStore(rootDir, globalDir);
// In-memory SQLite cuts per-test setup from ~50ms to ~5ms by avoiding
// disk open + WAL fsync for both fusion.db and archive.db. The few
// tests below that exercise cross-instance persistence (open store A,
// close, open store B on same dir, expect data) construct disk-backed
// stores explicitly — they are flagged with a comment at each site.
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
await store.init();
});
@@ -292,6 +297,13 @@ describe("TaskStore", () => {
});
it("persists token usage across TaskStore reinitialization", async () => {
// Cross-instance persistence test — swap beforeEach's in-memory
// store for disk-backed so the second `new TaskStore` below can
// observe what this instance writes.
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
const tokenUsage = {
inputTokens: 300,
outputTokens: 120,
@@ -315,6 +327,11 @@ describe("TaskStore", () => {
});
it("clears token usage via null update and keeps it absent after reload", async () => {
// Cross-instance persistence test — see counterpart above.
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
const task = await store.createTask({
description: "Clear token usage",
tokenUsage: {
@@ -2663,6 +2680,13 @@ describe("TaskStore", () => {
};
it("round-trips nested remoteAccess settings with both providers, token strategy, and lifecycle", async () => {
// Cross-instance persistence test — beforeEach uses in-memory DB
// for speed, but this case reloads via a second TaskStore on the
// same dir, so we need disk-backed for both.
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
await store.updateSettings({ remoteAccess: baseRemoteAccess });
const settings = await store.getSettings();
@@ -6918,6 +6942,12 @@ Task with acceptance criteria
describe("cleanupArchivedTasks", () => {
it("writes compact entry to archive DB with compact agent log", async () => {
// This test asserts the archive.db file exists on disk, which the
// in-memory beforeEach store can't satisfy. Swap to disk-backed.
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
// Create and archive a task
const task = await store.createTask({ description: "Test cleanup", title: "Cleanup Task" });
await store.moveTask(task.id, "todo");
@@ -7335,6 +7365,14 @@ Task with acceptance criteria
describe("archive log persistence", () => {
it("archive log survives TaskStore reinitialization", async () => {
// Cross-instance persistence test — beforeEach creates an in-memory
// store, but this test verifies disk persistence. Swap to a
// disk-backed store before doing any work so newStore (also
// disk-backed) can read what the first instance wrote.
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
const task = await store.createTask({ description: "Survival test" });
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
@@ -7515,6 +7553,12 @@ Task with acceptance criteria
});
it("activity log survives TaskStore reinitialization", async () => {
// Cross-instance persistence test — see archive-log counterpart
// above for the in-memory carve-out rationale.
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
await store.recordActivity({ type: "task:created", taskId: "FN-001", details: "Test" });
// Create new store instance
@@ -9233,6 +9277,12 @@ Task with acceptance criteria
});
it("recovery metadata persists across store re-initialization", async () => {
// Cross-instance persistence test — see archive-log counterpart in
// this file for the in-memory carve-out rationale.
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
const task = await createTestTask();
const futureTime = new Date(Date.now() + 60_000).toISOString();
await store.updateTask(task.id, {

View File

@@ -82,6 +82,12 @@ export interface AgentStoreOptions {
rootDir?: string;
/** Optional TaskStore for checkout/release operations */
taskStore?: TaskStore;
/**
* Test-only: open the underlying SQLite DB as `:memory:` instead of a
* disk-backed file. Skips per-test fsync and WAL setup; mirrors the
* pattern in TaskStore. Production callers must leave this unset.
*/
inMemoryDb?: boolean;
}
/** Agent data as stored in SQLite JSON columns */
@@ -174,6 +180,7 @@ export class AgentStore extends EventEmitter {
private locks: Map<string, AgentLock> = new Map();
private _db: Database | null = null;
private taskStore?: TaskStore;
private readonly inMemoryDb: boolean;
constructor(options: AgentStoreOptions = {}) {
super();
@@ -187,11 +194,12 @@ export class AgentStore extends EventEmitter {
this.rootDir = options.rootDir ?? resolve(".fusion");
this.agentsDir = join(this.rootDir, "agents");
this.taskStore = options.taskStore;
this.inMemoryDb = options.inMemoryDb === true;
}
private get db(): Database {
if (!this._db) {
this._db = new Database(this.rootDir);
this._db = new Database(this.rootDir, { inMemory: this.inMemoryDb });
this._db.init();
}
return this._db;

View File

@@ -54,12 +54,18 @@ export class ArchiveDatabase {
private db: DatabaseSync;
private readonly _fts5Available: boolean;
constructor(fusionDir: string) {
if (!existsSync(fusionDir)) {
constructor(fusionDir: string, options?: { inMemory?: boolean }) {
// See Database constructor in db.ts for the in-memory rationale —
// mirrors the same pattern so TaskStore can flip both DBs in lockstep
// for tests that don't exercise cross-instance persistence.
const inMemory = options?.inMemory === true;
if (!inMemory && !existsSync(fusionDir)) {
mkdirSync(fusionDir, { recursive: true });
}
this.db = new DatabaseSync(join(fusionDir, "archive.db"));
this.db.exec("PRAGMA journal_mode = WAL");
this.db = new DatabaseSync(inMemory ? ":memory:" : join(fusionDir, "archive.db"));
if (!inMemory) {
this.db.exec("PRAGMA journal_mode = WAL");
}
this.db.exec("PRAGMA busy_timeout = 5000");
this._fts5Available = probeFts5(this.db);
}

View File

@@ -48,8 +48,11 @@ export class AutomationStore extends EventEmitter<AutomationStoreEvents> {
/** SQLite database instance */
private _db: Database | null = null;
constructor(private rootDir: string) {
private readonly inMemoryDb: boolean;
constructor(private rootDir: string, options?: { inMemoryDb?: boolean }) {
super();
this.inMemoryDb = options?.inMemoryDb === true;
}
/**
@@ -58,7 +61,7 @@ export class AutomationStore extends EventEmitter<AutomationStoreEvents> {
private get db(): Database {
if (!this._db) {
const fusionDir = join(this.rootDir, ".fusion");
this._db = new Database(fusionDir);
this._db = new Database(fusionDir, { inMemory: this.inMemoryDb });
this._db.init();
}
return this._db;

View File

@@ -657,22 +657,35 @@ export class Database {
private transactionDepth = 0;
private readonly _fts5Available: boolean;
constructor(fusionDir: string) {
this.dbPath = join(fusionDir, "fusion.db");
constructor(fusionDir: string, options?: { inMemory?: boolean }) {
// In-memory mode is a test-only fast path that swaps the on-disk
// SQLite file for SQLite's `:memory:` connection. Schema + data live
// entirely in process RAM, eliminating per-test disk open/sync cost
// (~30-50ms × hundreds of tests in store.test.ts). Production code
// never sets this — it's plumbed through TaskStore for tests that
// don't need cross-instance persistence.
const inMemory = options?.inMemory === true;
this.dbPath = inMemory ? ":memory:" : join(fusionDir, "fusion.db");
if (!isAbsolute(fusionDir)) {
if (!inMemory && !isAbsolute(fusionDir)) {
throw new Error(`[fusion] Database constructor requires an absolute fusionDir path, got: ${fusionDir}`);
}
// Ensure .fusion directory exists
if (!existsSync(fusionDir)) {
// Ensure .fusion directory exists (only meaningful for disk-backed mode;
// in-memory mode never touches the filesystem here).
if (!inMemory && !existsSync(fusionDir)) {
mkdirSync(fusionDir, { recursive: true });
}
this.db = new DatabaseSync(this.dbPath);
// Enable WAL mode for concurrent reader/writer access
this.db.exec("PRAGMA journal_mode = WAL");
// WAL is meaningless for `:memory:` connections — SQLite ignores it
// and there's no other writer to coordinate with — so we skip it. The
// remaining pragmas apply uniformly.
if (!inMemory) {
// Enable WAL mode for concurrent reader/writer access
this.db.exec("PRAGMA journal_mode = WAL");
}
// Wait up to 5s for locks to clear before returning SQLITE_BUSY
this.db.exec("PRAGMA busy_timeout = 5000");
// Enable foreign key enforcement
@@ -2022,8 +2035,8 @@ export class Database {
* @param fusionDir - Path to the `.fusion` directory (e.g., `/path/to/project/.fusion`)
* @returns Database instance (not yet initialized)
*/
export function createDatabase(fusionDir: string): Database {
return new Database(fusionDir);
export function createDatabase(fusionDir: string, options?: { inMemory?: boolean }): Database {
return new Database(fusionDir, options);
}
export { normalizeTaskComments };

View File

@@ -65,8 +65,11 @@ export class PluginStore extends EventEmitter<PluginStoreEvents> {
/** SQLite database instance */
private _db: Database | null = null;
constructor(private rootDir: string) {
private readonly inMemoryDb: boolean;
constructor(private rootDir: string, options?: { inMemoryDb?: boolean }) {
super();
this.inMemoryDb = options?.inMemoryDb === true;
}
/**
@@ -75,7 +78,7 @@ export class PluginStore extends EventEmitter<PluginStoreEvents> {
private get db(): Database {
if (!this._db) {
const fusionDir = join(this.rootDir, ".fusion");
this._db = new Database(fusionDir);
this._db = new Database(fusionDir, { inMemory: this.inMemoryDb });
this._db.init();
}
return this._db;

View File

@@ -67,8 +67,11 @@ export class RoutineStore extends EventEmitter<RoutineStoreEvents> {
/** Per-routine promise chain for serializing writes. */
private routineLocks: Map<string, Promise<void>> = new Map();
constructor(private rootDir: string) {
private readonly inMemoryDb: boolean;
constructor(private rootDir: string, options?: { inMemoryDb?: boolean }) {
super();
this.inMemoryDb = options?.inMemoryDb === true;
}
// ── Database Access ────────────────────────────────────────────────
@@ -79,7 +82,7 @@ export class RoutineStore extends EventEmitter<RoutineStoreEvents> {
private get db(): Database {
if (!this._db) {
const fusionDir = `${this.rootDir}/.fusion`;
this._db = new Database(fusionDir);
this._db = new Database(fusionDir, { inMemory: this.inMemoryDb });
this._db.init();
}
return this._db;

View File

@@ -395,12 +395,24 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/** Cached TodoStore instance */
private todoStore: TodoStore | null = null;
constructor(private rootDir: string, globalSettingsDir?: string) {
// Test-only: when true, both fusion.db and archive.db open as `:memory:`
// SQLite connections instead of disk-backed files. Production code never
// sets this; it's gated through an opt-in TaskStoreOptions field below.
// Tests that need cross-instance persistence (open store A, close,
// open store B on the same dir, expect data) must leave this false.
private readonly inMemoryDb: boolean;
constructor(
private rootDir: string,
globalSettingsDir?: string,
options?: { inMemoryDb?: boolean },
) {
super();
this.setMaxListeners(100);
this.fusionDir = join(rootDir, ".fusion");
this.tasksDir = join(this.fusionDir, "tasks");
this.configPath = join(this.fusionDir, "config.json");
this.inMemoryDb = options?.inMemoryDb === true;
const resolvedGlobalSettingsDir = globalSettingsDir
?? (process.env.VITEST === "true" ? join(rootDir, ".fusion-global-settings") : undefined);
this.globalSettingsStore = new GlobalSettingsStore(resolvedGlobalSettingsDir);
@@ -412,7 +424,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
*/
private get db(): Database {
if (!this._db) {
this._db = new Database(this.fusionDir);
this._db = new Database(this.fusionDir, { inMemory: this.inMemoryDb });
this._db.init();
// Auto-migrate legacy data if needed
if (detectLegacyData(this.fusionDir)) {
@@ -426,7 +438,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
private get archiveDb(): ArchiveDatabase {
if (!this._archiveDb) {
this._archiveDb = new ArchiveDatabase(this.fusionDir);
this._archiveDb = new ArchiveDatabase(this.fusionDir, { inMemory: this.inMemoryDb });
this._archiveDb.init();
this.migrateLegacyArchiveEntriesToArchiveDb();
}
@@ -438,7 +450,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Initialize SQLite database
if (!this._db) {
this._db = new Database(this.fusionDir);
this._db = new Database(this.fusionDir, { inMemory: this.inMemoryDb });
this._db.init();
}