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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user