refactor: remove legacy kb compatibility

Drops the .kb/kb.db migration path, legacy backup filename handling, and
backward-compat test suites. Renames internal kbDir identifiers to
fusionDir and hasKbProject/isValidKbProject to their fusion equivalents.

- Remove needsCentralMigration, autoMigrateToCentral, and the
  "needs-migration" FirstRunState; checkAndMigrate and KB_SKIP_MIGRATION
  env var are gone
- Remove LEGACY_BACKUP_DIR and canonicalizeBackupDir; listBackups no
  longer matches kb-* filenames
- Delete backward-compat.test.ts and store-backward-compat.test.ts;
  update remaining tests to new 3-state first-run model

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-23 16:55:46 -07:00
parent 32da0aedac
commit a269f4829f
36 changed files with 365 additions and 1194 deletions

View File

@@ -12,16 +12,16 @@ function makeTmpDir(): string {
describe("ChatStore", () => {
let tmpDir: string;
let kbDir: string;
let fusionDir: string;
let db: Database;
let store: ChatStore;
beforeEach(() => {
tmpDir = makeTmpDir();
kbDir = join(tmpDir, ".fusion");
db = new Database(kbDir);
fusionDir = join(tmpDir, ".fusion");
db = new Database(fusionDir);
db.init();
store = new ChatStore(kbDir, db);
store = new ChatStore(fusionDir, db);
});
afterEach(async () => {

View File

@@ -1,215 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdirSync, rmSync, existsSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";
import { join } from "node:path";
import { tempWorkspace } from "@fusion/test-utils";
import { TaskStore } from "../store.js";
import { CentralCore } from "../central-core.js";
// Helper to create a fake fusion project structure for the current store implementation
function createFakeFusionProject(dir: string): void {
const fusionDir = join(dir, ".fusion");
mkdirSync(fusionDir, { recursive: true });
const db = new DatabaseSync(join(fusionDir, "kb.db"));
db.exec("CREATE TABLE IF NOT EXISTS sanity (id INTEGER PRIMARY KEY)");
db.close();
}
describe("TaskStore Backward Compatibility", () => {
let tempDir: string;
let centralCore: CentralCore;
let originalCwd: string;
beforeEach(async () => {
tempDir = tempWorkspace("kb-compat-test-");
centralCore = new CentralCore(tempDir);
await centralCore.init();
originalCwd = process.cwd();
});
afterEach(async () => {
try {
process.chdir(originalCwd);
await centralCore.close();
} catch {
// Ignore cleanup errors
}
});
describe("getOrCreateForProject", () => {
it("should create store for specified project ID", async () => {
const projectDir = join(tempDir, "my-project");
mkdirSync(projectDir, { recursive: true });
// Register the project first
const project = await centralCore.registerProject({
name: "my-project",
path: projectDir,
isolationMode: "in-process",
});
const store = await TaskStore.getOrCreateForProject(project.id, centralCore);
expect(store).toBeInstanceOf(TaskStore);
// Verify it's using the correct path
const settings = await store.getSettings();
expect(settings).toBeDefined();
});
it("should fall back to exact project name lookup when ID lookup misses", async () => {
const projectDir = join(tempDir, "my-project");
mkdirSync(projectDir, { recursive: true });
// Register the project
await centralCore.registerProject({
name: "my-project",
path: projectDir,
isolationMode: "in-process",
});
// Look up by name instead of ID
const store = await TaskStore.getOrCreateForProject("my-project", centralCore);
expect(store).toBeInstanceOf(TaskStore);
});
it("should use single registered project when no ID provided", async () => {
const projectDir = join(tempDir, "single-project");
mkdirSync(projectDir, { recursive: true });
// Register exactly one project
await centralCore.registerProject({
name: "single-project",
path: projectDir,
isolationMode: "in-process",
});
const store = await TaskStore.getOrCreateForProject(undefined, centralCore);
expect(store).toBeInstanceOf(TaskStore);
});
it("should throw when multiple projects and no ID specified", async () => {
const project1 = join(tempDir, "project-1");
const project2 = join(tempDir, "project-2");
mkdirSync(project1, { recursive: true });
mkdirSync(project2, { recursive: true });
// Register two projects
await centralCore.registerProject({
name: "project-1",
path: project1,
isolationMode: "in-process",
});
await centralCore.registerProject({
name: "project-2",
path: project2,
isolationMode: "in-process",
});
await expect(
TaskStore.getOrCreateForProject(undefined, centralCore)
).rejects.toThrow("Multiple projects registered");
});
it("should fall back to process.cwd() legacy mode against the current .fusion path", async () => {
const projectDir = join(tempDir, "legacy-project");
mkdirSync(projectDir, { recursive: true });
createFakeFusionProject(projectDir);
process.chdir(projectDir);
const centralDb = join(tempDir, "fusion-central.db");
await centralCore.close();
rmSync(centralDb, { force: true });
centralCore = new CentralCore(tempDir);
const store = await TaskStore.getOrCreateForProject(undefined, centralCore);
expect(store).toBeInstanceOf(TaskStore);
const task = await store.createTask({ description: "legacy task" });
expect(task.id).toBe("FN-001");
expect(existsSync(join(projectDir, ".fusion", "kb.db"))).toBe(true);
expect(existsSync(join(projectDir, ".fusion", "tasks", task.id, "task.json"))).toBe(true);
});
it("should throw when project ID not found", async () => {
await expect(
TaskStore.getOrCreateForProject("non-existent-project", centralCore)
).rejects.toThrow('Project "non-existent-project" not found');
});
it("should find project by exact registered name", async () => {
const projectDir = join(tempDir, "Casey");
mkdirSync(projectDir, { recursive: true });
await centralCore.registerProject({
name: "Casey",
path: projectDir,
isolationMode: "in-process",
});
const store = await TaskStore.getOrCreateForProject("Casey", centralCore);
expect(store).toBeInstanceOf(TaskStore);
});
it("should auto-initialize central core if not provided", async () => {
const projectDir = join(tempDir, "my-project");
mkdirSync(projectDir, { recursive: true });
// Register a project
const { id: projectId } = await centralCore.registerProject({
name: "my-project",
path: projectDir,
isolationMode: "in-process",
});
// Pass the central core explicitly to ensure it uses the right database
const store = await TaskStore.getOrCreateForProject(projectId, centralCore);
expect(store).toBeInstanceOf(TaskStore);
});
});
describe("existing constructor", () => {
it("should still support direct TaskStore construction", async () => {
const projectDir = join(tempDir, "direct-project");
mkdirSync(projectDir, { recursive: true });
// Direct construction should still work
const store = new TaskStore(projectDir, join(projectDir, ".fusion-global-settings"));
await store.init();
expect(store).toBeInstanceOf(TaskStore);
// Should be able to create tasks
const task = await store.createTask({
description: "Test task",
column: "triage",
});
expect(task.id).toBeDefined();
expect(task.description).toBe("Test task");
});
});
describe("events without central core", () => {
it("should emit events in single-project mode", async () => {
const projectDir = join(tempDir, "event-test");
mkdirSync(projectDir, { recursive: true });
const store = new TaskStore(projectDir, join(projectDir, ".fusion-global-settings"));
await store.init();
const taskCreatedListener = vi.fn();
store.on("task:created", taskCreatedListener);
await store.createTask({
description: "Event test task",
column: "triage",
});
expect(taskCreatedListener).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -16,14 +16,14 @@ function sleep(ms: number): Promise<void> {
describe("TaskStore task documents", () => {
let rootDir: string;
let kbDir: string;
let fusionDir: string;
let db: Database;
let store: TaskStore;
beforeEach(async () => {
rootDir = makeTmpDir();
kbDir = join(rootDir, ".fusion");
db = new Database(kbDir);
fusionDir = join(rootDir, ".fusion");
db = new Database(fusionDir);
db.init();
store = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings"));
await store.init();