FN-7005: close cached plugin store with TaskStore
TaskStore shutdown now tears down its cached plugin store to avoid leaking plugin database handles. - Dispose the cached PluginStore during TaskStore.close(), clear the cache, and remove listeners before closing. - Keep close idempotent when no plugin store was created or after a prior teardown. - Add regression coverage for direct TaskStore.close() and disk-backed harness reopen lifecycle. Files changed: .../src/__tests__/store-plugin-store-close.test.ts | 63 ++++++++++++++++++++++ packages/core/src/store.ts | 15 ++++++ 2 files changed, 78 insertions(+) Fusion-Task-Id: FN-7005 Fusion-Task-Lineage: fe2242d8-7004-4d29-904d-58ee77861d99
This commit is contained in:
63
packages/core/src/__tests__/store-plugin-store-close.test.ts
Normal file
63
packages/core/src/__tests__/store-plugin-store-close.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { PluginStore } from "../plugin-store.js";
|
||||
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
|
||||
|
||||
async function withTaskStoreHarness<T>(
|
||||
fn: (harness: ReturnType<typeof createTaskStoreTestHarness>) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const harness = createTaskStoreTestHarness();
|
||||
await harness.beforeEach();
|
||||
try {
|
||||
return await fn(harness);
|
||||
} finally {
|
||||
await harness.afterEach();
|
||||
}
|
||||
}
|
||||
|
||||
describe("TaskStore pluginStore close lifecycle", () => {
|
||||
it("closes and nulls the cached plugin store when TaskStore.close runs", async () => {
|
||||
await withTaskStoreHarness(async (harness) => {
|
||||
const store = harness.store();
|
||||
const pluginStore = store.getPluginStore();
|
||||
await pluginStore.listPlugins();
|
||||
const closeSpy = vi.spyOn(pluginStore, "close");
|
||||
|
||||
await store.close();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
expect((store as any).pluginStore).toBeNull();
|
||||
|
||||
await expect(store.close()).resolves.toBeUndefined();
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not attempt plugin-store teardown when the cached store was never created", async () => {
|
||||
await withTaskStoreHarness(async (harness) => {
|
||||
const store = harness.store();
|
||||
const prototypeCloseSpy = vi.spyOn(PluginStore.prototype, "close");
|
||||
|
||||
await expect(store.close()).resolves.toBeUndefined();
|
||||
|
||||
expect(prototypeCloseSpy).not.toHaveBeenCalled();
|
||||
expect((store as any).pluginStore).toBeNull();
|
||||
prototypeCloseSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it("closes disk-backed plugin stores during reopenDiskBackedStore", async () => {
|
||||
await withTaskStoreHarness(async (harness) => {
|
||||
await harness.reopenDiskBackedStore();
|
||||
const originalStore = harness.store();
|
||||
const pluginStore = originalStore.getPluginStore();
|
||||
await pluginStore.listPlugins();
|
||||
const closeSpy = vi.spyOn(pluginStore, "close");
|
||||
|
||||
await expect(harness.reopenDiskBackedStore()).resolves.toBeUndefined();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
expect((originalStore as any).pluginStore).toBeNull();
|
||||
expect((harness.store() as any).pluginStore).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16534,6 +16534,21 @@ ${stepsSection}`;
|
||||
this.secretsCentralCore = null;
|
||||
}
|
||||
this.secretsStore = null;
|
||||
if (this.pluginStore) {
|
||||
/**
|
||||
* FNXC:Plugins 2026-06-25-00:00:
|
||||
* FN-7005 requires TaskStore.close() to own the cached PluginStore lifecycle because PluginStore has separate local and central SQLite connections.
|
||||
* Dispose it here so long-running processes and tests outside shared reset helpers do not leak handles after TaskStore shutdown; PluginStore.close() follows FN-7003's null-safe handle teardown.
|
||||
*/
|
||||
const pluginStore = this.pluginStore;
|
||||
this.pluginStore = null;
|
||||
pluginStore.removeAllListeners();
|
||||
try {
|
||||
pluginStore.close();
|
||||
} catch (err) {
|
||||
console.warn(`[fusion] Could not close plugin store on TaskStore close:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get fts5Available(): boolean {
|
||||
|
||||
Reference in New Issue
Block a user