feat(FN-2123): merge fusion/fn-2123

This commit is contained in:
gsxdsm
2026-04-19 04:52:39 -07:00
parent 9884f87208
commit 3472c122d4
24 changed files with 224 additions and 105 deletions

View File

@@ -748,7 +748,7 @@ describe("fn pi extension", () => {
makeCtx(tmpDir),
);
const store = new TaskStore(tmpDir);
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
await store.init();
const persisted = store.getMissionStore().getMilestone(result.details.milestoneId);
@@ -780,7 +780,7 @@ describe("fn pi extension", () => {
makeCtx(tmpDir),
);
const store = new TaskStore(tmpDir);
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
await store.init();
const persisted = store.getMissionStore().getSlice(result.details.sliceId);
@@ -820,7 +820,7 @@ describe("fn pi extension", () => {
makeCtx(tmpDir),
);
const store = new TaskStore(tmpDir);
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
await store.init();
const persisted = store.getMissionStore().getFeature(result.details.featureId);
@@ -890,7 +890,7 @@ describe("fn pi extension", () => {
makeCtx(tmpDir),
);
const store = new TaskStore(tmpDir);
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
await store.init();
const persisted = store.getMissionStore().getSlice(slice.details.sliceId);
@@ -989,7 +989,7 @@ describe("fn pi extension", () => {
makeCtx(tmpDir),
);
const store = new TaskStore(tmpDir);
const store = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"));
await store.init();
const missionStore = store.getMissionStore();
const persisted = missionStore.getFeature(feature.details.featureId);

View File

@@ -21,14 +21,12 @@ describe("project-context", () => {
let tempDir: string;
let homeDir: string;
let central: CentralCore;
const originalHome = process.env.HOME;
const createdProjectIds: string[] = [];
beforeEach(async () => {
tempDir = mkdtempSync(join(tmpdir(), "kb-test-"));
homeDir = mkdtempSync(join(tmpdir(), "kb-home-"));
process.env.HOME = homeDir;
central = new CentralCore();
central = new CentralCore(homeDir);
await central.init();
});
@@ -59,11 +57,6 @@ describe("project-context", () => {
} catch {
// Ignore cleanup errors
}
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
});
function createMockProject(name: string, parentDir: string = tempDir): string {
@@ -167,7 +160,7 @@ describe("project-context", () => {
describe("resolveProject", () => {
it("should throw for unknown project name", async () => {
await expect(resolveProject("unknown-project", tempDir)).rejects.toThrow(
await expect(resolveProject("unknown-project", tempDir, homeDir)).rejects.toThrow(
"not found"
);
});
@@ -175,7 +168,7 @@ describe("project-context", () => {
it("should resolve unregistered local project from cwd", async () => {
const projectPath = createMockProject("legacy-project");
const context = await resolveProject(undefined, projectPath);
const context = await resolveProject(undefined, projectPath, homeDir);
expect(context.projectPath).toBe(resolve(projectPath));
expect(context.projectName).toBe("legacy-project");
@@ -186,7 +179,7 @@ describe("project-context", () => {
const randomDir = join(tempDir, "no-project-here");
mkdirSync(randomDir, { recursive: true });
await expect(resolveProject(undefined, randomDir)).rejects.toThrow(
await expect(resolveProject(undefined, randomDir, homeDir)).rejects.toThrow(
"No fusion project found"
);
});

View File

@@ -41,9 +41,10 @@ const storeCache = new Map<string, TaskStore>();
*/
export async function resolveProject(
projectNameFlag?: string,
cwd: string = process.cwd()
cwd: string = process.cwd(),
globalDir?: string,
): Promise<ProjectContext> {
const central = new CentralCore();
const central = new CentralCore(globalDir);
await central.init();
try {
@@ -61,12 +62,12 @@ export async function resolveProject(
// 2. Default project from global settings
if (!project) {
const defaultProject = await getDefaultProject();
const defaultProject = await getDefaultProject(globalDir);
if (defaultProject) {
project = await central.getProject(defaultProject.id);
// If default project was deleted from registry, clear it
if (!project) {
await clearDefaultProject();
await clearDefaultProject(globalDir);
}
}
}
@@ -82,8 +83,8 @@ export async function resolveProject(
const isRegistered = Boolean(detected.id);
const store = isRegistered
? await getStoreForProject(detected.id, detected.path)
: await createLocalStore(detected.path);
? await getStoreForProject(detected.id, detected.path, globalDir)
: await createLocalStore(detected.path, globalDir);
// For unregistered projects, use the path as the project ID
const projectId = isRegistered ? detected.id : detected.path;
@@ -97,7 +98,7 @@ export async function resolveProject(
};
}
const store = await getStoreForProject(project.id, project.path);
const store = await getStoreForProject(project.id, project.path, globalDir);
return {
projectId: project.id,
@@ -115,8 +116,8 @@ export async function resolveProject(
* Get the default project from global settings.
* Returns undefined if no default is set or if the project no longer exists.
*/
export async function getDefaultProject(): Promise<RegisteredProject | undefined> {
const globalStore = new GlobalSettingsStore();
export async function getDefaultProject(globalDir?: string): Promise<RegisteredProject | undefined> {
const globalStore = new GlobalSettingsStore(globalDir);
await globalStore.init();
const settings = await globalStore.getSettings();
@@ -124,7 +125,7 @@ export async function getDefaultProject(): Promise<RegisteredProject | undefined
return undefined;
}
const central = new CentralCore();
const central = new CentralCore(globalDir);
await central.init();
try {
return await central.getProject(settings.defaultProjectId);
@@ -138,9 +139,9 @@ export async function getDefaultProject(): Promise<RegisteredProject | undefined
* @param projectId - Project ID to set as default
* @throws Error if project not found
*/
export async function setDefaultProject(projectId: string): Promise<void> {
export async function setDefaultProject(projectId: string, globalDir?: string): Promise<void> {
// Verify project exists
const central = new CentralCore();
const central = new CentralCore(globalDir);
await central.init();
try {
const project = await central.getProject(projectId);
@@ -151,7 +152,7 @@ export async function setDefaultProject(projectId: string): Promise<void> {
await central.close();
}
const globalStore = new GlobalSettingsStore();
const globalStore = new GlobalSettingsStore(globalDir);
await globalStore.init();
await globalStore.updateSettings({ defaultProjectId: projectId });
}
@@ -159,8 +160,8 @@ export async function setDefaultProject(projectId: string): Promise<void> {
/**
* Clear the default project setting.
*/
export async function clearDefaultProject(): Promise<void> {
const globalStore = new GlobalSettingsStore();
export async function clearDefaultProject(globalDir?: string): Promise<void> {
const globalStore = new GlobalSettingsStore(globalDir);
await globalStore.init();
const current = await globalStore.getSettings();
@@ -243,7 +244,8 @@ async function findProjectByNameOrId(
*/
export async function getStoreForProject(
projectId: string,
projectPath: string
projectPath: string,
globalSettingsDir?: string,
): Promise<TaskStore> {
// Check cache first
const cached = storeCache.get(projectId);
@@ -252,7 +254,7 @@ export async function getStoreForProject(
}
// Create new store
const store = new TaskStore(projectPath);
const store = new TaskStore(projectPath, globalSettingsDir);
await store.init();
// Cache it
@@ -267,8 +269,11 @@ export function clearStoreCache(): void {
storeCache.clear();
}
async function createLocalStore(projectPath: string): Promise<TaskStore> {
const store = new TaskStore(projectPath);
async function createLocalStore(
projectPath: string,
globalSettingsDir?: string,
): Promise<TaskStore> {
const store = new TaskStore(projectPath, globalSettingsDir);
await store.init();
return store;
}
@@ -298,9 +303,10 @@ export function formatProjectLine(project: RegisteredProject, isDefault: boolean
*/
export async function getStore(
projectName?: string,
cwd: string = process.cwd()
cwd: string = process.cwd(),
globalDir?: string,
): Promise<TaskStore> {
const context = await resolveProject(projectName, cwd);
const context = await resolveProject(projectName, cwd, globalDir);
return context.store;
}

View File

@@ -72,7 +72,9 @@ describe("FirstRunExperience", () => {
it("should return true when central core is not initialized", async () => {
const uninitializedCore = new CentralCore(tempDir);
const uninitializedFirstRun = new FirstRunExperience(uninitializedCore);
const { GlobalSettingsStore } = await import("../global-settings.js");
const globalSettingsStore = new GlobalSettingsStore(tempDir);
const uninitializedFirstRun = new FirstRunExperience(uninitializedCore, globalSettingsStore);
const result = await uninitializedFirstRun.isFirstRun();
expect(result).toBe(true);
@@ -298,8 +300,10 @@ describe("FirstRunExperience", () => {
});
describe("createFirstRunExperience", () => {
it("should create a FirstRunExperience instance", () => {
const instance = createFirstRunExperience(centralCore);
it("should create a FirstRunExperience instance", async () => {
const { GlobalSettingsStore } = await import("../global-settings.js");
const globalSettingsStore = new GlobalSettingsStore(tempDir);
const instance = createFirstRunExperience(centralCore, globalSettingsStore);
expect(instance).toBeInstanceOf(FirstRunExperience);
});
});

View File

@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { resolveGlobalDir } from "../global-settings.js";
function withTempHome<T>(fn: (homeDir: string) => T): T {
const originalHome = process.env.HOME;
const homeDir = mkdtempSync(join(tmpdir(), "kb-global-dir-guard-"));
process.env.HOME = homeDir;
try {
return fn(homeDir);
} finally {
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
rmSync(homeDir, { recursive: true, force: true });
}
}
function withVitestEnv<T>(value: string | undefined, fn: () => T): T {
const originalVitest = process.env.VITEST;
if (value === undefined) {
delete process.env.VITEST;
} else {
process.env.VITEST = value;
}
try {
return fn();
} finally {
if (originalVitest === undefined) {
delete process.env.VITEST;
} else {
process.env.VITEST = originalVitest;
}
}
}
describe("resolveGlobalDir() VITEST guard", () => {
it("throws without explicit dir during test execution", () => {
withVitestEnv("true", () => {
withTempHome(() => {
expect(() => resolveGlobalDir()).toThrow(
"resolveGlobalDir() called without explicit dir during test execution. Pass a temp directory to avoid writing to real ~/.fusion/",
);
});
});
});
it("allows explicit dir during test execution", () => {
withVitestEnv("true", () => {
const explicitPath = "/some/explicit/path";
expect(resolveGlobalDir(explicitPath)).toBe(explicitPath);
});
});
it("preserves production behavior when VITEST is not set", () => {
withVitestEnv(undefined, () => {
withTempHome((homeDir) => {
expect(resolveGlobalDir()).toBe(join(homeDir, ".fusion"));
});
});
});
});

View File

@@ -177,7 +177,7 @@ describe("TaskStore Backward Compatibility", () => {
mkdirSync(projectDir, { recursive: true });
// Direct construction should still work
const store = new TaskStore(projectDir);
const store = new TaskStore(projectDir, join(projectDir, ".fusion-global-settings"));
await store.init();
expect(store).toBeInstanceOf(TaskStore);
@@ -198,7 +198,7 @@ describe("TaskStore Backward Compatibility", () => {
const projectDir = join(tempDir, "event-test");
mkdirSync(projectDir, { recursive: true });
const store = new TaskStore(projectDir);
const store = new TaskStore(projectDir, join(projectDir, ".fusion-global-settings"));
await store.init();
const taskCreatedListener = vi.fn();

View File

@@ -15,7 +15,7 @@ describe("TaskStore.listTasks() sort order", () => {
beforeEach(async () => {
rootDir = makeTmpDir();
store = new TaskStore(rootDir);
store = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await store.init();
});

View File

@@ -25,7 +25,7 @@ describe("TaskStore task documents", () => {
kbDir = join(rootDir, ".fusion");
db = new Database(kbDir);
db.init();
store = new TaskStore(rootDir);
store = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await store.init();
});

View File

@@ -60,6 +60,9 @@ function splitSettings(settings?: Partial<Settings>): {
* This file provides per-fixture isolation on top of that safety net.
*
* Create an isolated temporary test project with a real TaskStore + SQLite DB.
*
* Note: we always pass an explicit `globalDir` into TaskStore so tests never
* hit `resolveGlobalDir()`'s VITEST guard or write to a real ~/.fusion path.
*/
export async function createTestProject(
options: CreateTestProjectOptions = {},

View File

@@ -1492,7 +1492,7 @@ describe("AgentStore", () => {
let taskId: string;
beforeEach(async () => {
taskStore = new TaskStore(rootDir);
taskStore = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore.init();
store = new AgentStore({ rootDir, taskStore });

View File

@@ -10,6 +10,25 @@ function makeTmpDir(): string {
return mkdtempSync(join(tmpdir(), "kb-global-settings-test-"));
}
/** Temporarily clear VITEST to test default GlobalSettingsStore resolution. */
async function withDefaultGlobalSettingsStore<T>(
fn: (store: GlobalSettingsStore) => Promise<T>,
): Promise<T> {
const savedVitest = process.env.VITEST;
delete process.env.VITEST;
try {
const store = new GlobalSettingsStore();
return await fn(store);
} finally {
if (savedVitest === undefined) {
delete process.env.VITEST;
} else {
process.env.VITEST = savedVitest;
}
}
}
describe("GlobalSettingsStore", () => {
let dir: string;
let store: GlobalSettingsStore;
@@ -78,17 +97,20 @@ describe("GlobalSettingsStore", () => {
JSON.stringify({ themeMode: "light" }),
);
const defaultStore = new GlobalSettingsStore();
await defaultStore.init();
try {
await withDefaultGlobalSettingsStore(async (defaultStore) => {
await defaultStore.init();
expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json"));
expect(existsSync(join(homeDir, ".fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".pi", "kb"))).toBe(false);
expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json"));
expect(existsSync(join(homeDir, ".fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".pi", "kb"))).toBe(false);
const settings = await defaultStore.getSettings();
expect(settings.themeMode).toBe("light");
await rm(homeDir, { recursive: true, force: true });
const settings = await defaultStore.getSettings();
expect(settings.themeMode).toBe("light");
});
} finally {
await rm(homeDir, { recursive: true, force: true });
}
});
it("adopts the legacy ~/.pi/fusion directory when ~/.fusion does not exist", async () => {
@@ -107,20 +129,22 @@ describe("GlobalSettingsStore", () => {
expect(existsSync(join(homeDir, ".pi", "fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".fusion"))).toBe(false);
// Instantiate GlobalSettingsStore with no argument (uses default resolution)
const defaultStore = new GlobalSettingsStore();
await defaultStore.init();
try {
await withDefaultGlobalSettingsStore(async (defaultStore) => {
await defaultStore.init();
// Verify migration happened
expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json"));
expect(existsSync(join(homeDir, ".fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".pi", "fusion"))).toBe(false);
// Verify migration happened
expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json"));
expect(existsSync(join(homeDir, ".fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".pi", "fusion"))).toBe(false);
// Verify settings were preserved
const settings = await defaultStore.getSettings();
expect(settings.themeMode).toBe("light");
await rm(homeDir, { recursive: true, force: true });
// Verify settings were preserved
const settings = await defaultStore.getSettings();
expect(settings.themeMode).toBe("light");
});
} finally {
await rm(homeDir, { recursive: true, force: true });
}
});
});

View File

@@ -45,7 +45,15 @@ export function defaultGlobalDir(): string {
* 4. Else → return `~/.fusion` (will be created on first use)
*/
export function resolveGlobalDir(dir?: string): string {
if (dir) return dir;
const hasExplicitDir = typeof dir === "string" && dir.length > 0;
if (!hasExplicitDir && process.env.VITEST === "true") {
throw new Error(
"resolveGlobalDir() called without explicit dir during test execution. Pass a temp directory to avoid writing to real ~/.fusion/",
);
}
if (hasExplicitDir) return dir;
const preferredDir = defaultGlobalDir();

View File

@@ -46,7 +46,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
vi.setSystemTime(new Date("2026-04-11T00:00:00.000Z"));
rootDir = makeTmpDir();
taskStore = new TaskStore(rootDir);
taskStore = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore.init();
});
@@ -75,7 +75,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Simulate restart by creating new store instance
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -99,7 +99,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Restart
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -122,7 +122,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Restart
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -145,7 +145,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Restart
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -239,7 +239,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Restart and verify link persists
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -391,7 +391,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Restart
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -434,7 +434,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Restart
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -456,7 +456,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Restart
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -533,7 +533,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Restart
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();

View File

@@ -88,7 +88,7 @@ describe("MissionStore integration with TaskStore", () => {
vi.setSystemTime(new Date("2026-04-01T00:00:00.000Z"));
rootDir = makeTmpDir();
taskStore = new TaskStore(rootDir);
taskStore = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore.init();
});
@@ -410,7 +410,7 @@ describe("MissionStore integration with TaskStore", () => {
// Restart store
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -436,7 +436,7 @@ describe("MissionStore integration with TaskStore", () => {
// Restart store
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -461,7 +461,7 @@ describe("MissionStore integration with TaskStore", () => {
// Restart store
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -484,7 +484,7 @@ describe("MissionStore integration with TaskStore", () => {
// Restart store
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -506,7 +506,7 @@ describe("MissionStore integration with TaskStore", () => {
// Restart store
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -530,7 +530,7 @@ describe("MissionStore integration with TaskStore", () => {
// Restart store
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();
@@ -556,7 +556,7 @@ describe("MissionStore integration with TaskStore", () => {
// Restart store
taskStore.close();
const taskStore2 = new TaskStore(rootDir);
const taskStore2 = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore2.init();
const missionStore2 = taskStore2.getMissionStore();

View File

@@ -28,7 +28,7 @@ describe("MissionStore planning context integration", () => {
vi.setSystemTime(new Date("2026-04-01T00:00:00.000Z"));
rootDir = makeTmpDir();
taskStore = new TaskStore(rootDir);
taskStore = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await taskStore.init();
});

View File

@@ -1663,7 +1663,7 @@ describe("MissionStore", () => {
it("throws if feature not found", async () => {
// Need a TaskStore reference for this test
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
await expect(msWithTs.triageFeature("F-NONEXISTENT")).rejects.toThrow(
@@ -1673,7 +1673,7 @@ describe("MissionStore", () => {
it("throws if feature is already triaged", async () => {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
const mission = msWithTs.createMission({ title: "Mission" });
@@ -1693,7 +1693,7 @@ describe("MissionStore", () => {
it("creates a task and links it to the feature", async () => {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
const mission = msWithTs.createMission({ title: "Mission" });
@@ -1725,7 +1725,7 @@ describe("MissionStore", () => {
it("uses provided title and description overrides", async () => {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
const mission = msWithTs.createMission({ title: "Mission" });
@@ -1746,7 +1746,7 @@ describe("MissionStore", () => {
it("emits feature:linked event", async () => {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
const linkedHandler = vi.fn();
@@ -1781,7 +1781,7 @@ describe("MissionStore", () => {
it("throws if slice not found", async () => {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
await expect(msWithTs.triageSlice("SL-NONEXISTENT")).rejects.toThrow(
@@ -1791,7 +1791,7 @@ describe("MissionStore", () => {
it("triages all defined features in a slice", async () => {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
const mission = msWithTs.createMission({ title: "Mission" });
@@ -1818,7 +1818,7 @@ describe("MissionStore", () => {
it("skips already triaged features", async () => {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
const mission = msWithTs.createMission({ title: "Mission" });
@@ -1840,7 +1840,7 @@ describe("MissionStore", () => {
it("returns empty array if no defined features", async () => {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const msWithTs = ts.getMissionStore();
const mission = msWithTs.createMission({ title: "Mission" });
@@ -1861,7 +1861,7 @@ describe("MissionStore", () => {
ms: MissionStore;
}> {
const { TaskStore } = await import("./store.js");
const ts = new TaskStore(kbDir);
const ts = new TaskStore(kbDir, join(kbDir, ".fusion-global-settings"));
const ms = ts.getMissionStore();
return { ts, ms };
}

View File

@@ -35,7 +35,7 @@ describe("Run Audit Integration", () => {
kbDir = join(rootDir, ".fusion");
db = new Database(kbDir);
db.init();
store = new TaskStore(rootDir);
store = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await store.init();
});

View File

@@ -22,7 +22,7 @@ describe("Run Audit", () => {
kbDir = join(rootDir, ".fusion");
db = new Database(kbDir);
db.init();
store = new TaskStore(rootDir);
store = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await store.init();
});

View File

@@ -86,7 +86,11 @@ export interface TaskStoreEvents {
}
export class TaskStore extends EventEmitter<TaskStoreEvents> {
static async getOrCreateForProject(projectId?: string, centralCore?: CentralCore): Promise<TaskStore> {
static async getOrCreateForProject(
projectId?: string,
centralCore?: CentralCore,
globalSettingsDir?: string,
): Promise<TaskStore> {
const central = centralCore ?? new CentralCore();
let initializedHere = false;
@@ -98,7 +102,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
try {
const compat = new BackwardCompat(central);
const context = await compat.resolveProjectContext(process.cwd(), projectId);
const store = new TaskStore(context.workingDirectory);
const resolvedGlobalSettingsDir = globalSettingsDir
?? (process.env.VITEST === "true"
? join(context.workingDirectory, ".fusion-global-settings")
: undefined);
const store = new TaskStore(context.workingDirectory, resolvedGlobalSettingsDir);
await store.init();
return store;
} catch (error) {
@@ -176,7 +184,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
this.kbDir = join(rootDir, ".fusion");
this.tasksDir = join(this.kbDir, "tasks");
this.configPath = join(this.kbDir, "config.json");
this.globalSettingsStore = new GlobalSettingsStore(globalSettingsDir);
const resolvedGlobalSettingsDir = globalSettingsDir
?? (process.env.VITEST === "true" ? join(rootDir, ".fusion-global-settings") : undefined);
this.globalSettingsStore = new GlobalSettingsStore(resolvedGlobalSettingsDir);
}
/**

View File

@@ -46,7 +46,7 @@ describe("cross-tab session locking", () => {
beforeEach(async () => {
tmpRoot = mkdtempSync(join(tmpdir(), "kb-session-cross-tab-"));
taskStore = new TaskStore(tmpRoot);
taskStore = new TaskStore(tmpRoot, join(tmpRoot, ".fusion-global-settings"));
await taskStore.init();
aiSessionStore = new AiSessionStore(taskStore.getDatabase());

View File

@@ -91,7 +91,7 @@ describe("session reconnect + replay", () => {
__resetMissionInterviewState();
tmpRoot = mkdtempSync(join(tmpdir(), "kb-session-reconnect-"));
store = new TaskStore(tmpRoot);
store = new TaskStore(tmpRoot, join(tmpRoot, ".fusion-global-settings"));
await store.init();
aiSessionStore = new AiSessionStore(store.getDatabase());

View File

@@ -1719,7 +1719,7 @@ describe("planning routes lock enforcement", () => {
setupMockAgent();
tmpRoot = mkdtempSync(join(tmpdir(), "kb-planning-lock-routes-"));
taskStore = new TaskStore(tmpRoot);
taskStore = new TaskStore(tmpRoot, join(tmpRoot, ".fusion-global-settings"));
await taskStore.init();
db = new Database(join(tmpRoot, ".fusion-locks"));

View File

@@ -15566,7 +15566,7 @@ describe("Messaging Routes", () => {
rootDir = mkdtempSync(join(tmpdir(), "kb-message-routes-"));
const { TaskStore, MessageStore } = await import("@fusion/core");
store = new TaskStore(rootDir);
store = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await store.init();
messageStore = new MessageStore(store.getDatabase());

View File

@@ -6,6 +6,7 @@ import { join } from "node:path";
import { createFusionAuthStorage, getFusionAuthPath } from "./auth-storage.js";
describe("createFusionAuthStorage", () => {
// HOME override required — createFusionAuthStorage() has no dir parameter
const originalHome = process.env.HOME;
let homeDir: string;