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:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user