feat(FN-2123): merge fusion/fn-2123
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
70
packages/core/src/__tests__/global-settings-guard.test.ts
Normal file
70
packages/core/src/__tests__/global-settings-guard.test.ts
Normal 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"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user