feat(KB-503): merge kb/kb-503

- feat(KB-503): complete Step 5 other command project support
- fix(KB-503): align task resolution with shared project context
- fix(KB-503): preserve local task command fallback
- test(KB-503): complete Step 4 task command coverage
- test(KB-503): harden Step 3 CLI parser coverage
- fix(KB-503): keep settings set scope explicit
- fix(KB-503): allow implicit project resolution for project settings
- fix(KB-503): enforce explicit settings scope rules
- fix(KB-503): restore shared settings resolution behavior
- fix(KB-503): align settings scope and project flag behavior
- feat(KB-503): complete Step 5 — add project-aware settings, git, and backup behavior
- fix(KB-503): enforce settings scope for global and project modes
- fix(KB-503): preserve project-scoped settings and cwd-aware git helpers
- fix(KB-503): add scoped settings behavior and git project tests
- feat(KB-503): complete Step 5 — add project-aware git and backup coverage
- test(KB-503): cover remaining project-aware task command paths
- fix(KB-503): use shared resolution flow for all task commands
- fix(KB-503): restore legacy task fallback and correct task mocks
- fix(KB-503): align task resolution order and kb storage paths
- fix(KB-503): restore local task store fallback without project flag
- test(KB-503): cover remaining project-aware task handlers
- fix(KB-503): preserve branch naming and avoid duplicate log resolution
- test(KB-503): expand project-aware task command coverage
- fix(KB-503): restore cwd fallback for path-based task commands
- fix(KB-503): add project-aware task output and tests
- fix(KB-503): harden project command output and coverage
- fix(KB-503): preserve legacy task resolution without project flag
- test(KB-503): add bin routing coverage for project flag parsing
- feat(KB-503): complete Step 3 — CLI argument parsing updates
- fix(KB-503): add defaultProjectId to GlobalSettings type and fix TypeScript errors
- test(KB-503): fix resolveProject mock in task tests for runTaskLogs follow mode
- docs(KB-503): add multi-project CLI documentation and changeset
- test(KB-503): add project-context mock to task tests
- test(KB-503): update git tests for new cwd parameter
- feat(KB-503): Step 3 — CLI argument parsing updates with --project flag support
- feat(KB-503): Steps 1-2 — project context utilities and project subcommands
This commit is contained in:
gsxdsm
2026-04-01 15:06:58 -07:00
parent c6acedb432
commit 07adb42d64
37 changed files with 2500 additions and 5763 deletions

View File

@@ -0,0 +1,110 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
const mockListBackups = vi.fn();
const mockRestoreBackup = vi.fn();
const mockCleanupOldBackups = vi.fn();
const mockGetSettings = vi.fn();
const mockRunBackupCommand = vi.fn();
const mockResolveProject = vi.fn();
vi.mock("@fusion/core", () => ({
BackupManager: vi.fn(),
TaskStore: vi.fn().mockImplementation(() => ({
init: vi.fn().mockResolvedValue(undefined),
getSettings: mockGetSettings,
kbDir: "/cwd/.kb",
})),
createBackupManager: vi.fn(() => ({
listBackups: mockListBackups,
restoreBackup: mockRestoreBackup,
cleanupOldBackups: mockCleanupOldBackups,
})),
runBackupCommand: mockRunBackupCommand,
}));
vi.mock("../project-context.js", () => ({
resolveProject: mockResolveProject,
}));
import { TaskStore } from "@fusion/core";
import { runBackupCreate, runBackupList, runBackupRestore, runBackupCleanup } from "./backup.js";
describe("backup commands", () => {
let logSpy: ReturnType<typeof vi.spyOn>;
let errorSpy: ReturnType<typeof vi.spyOn>;
let exitSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: string | number | null) => {
throw new Error(`process.exit:${code ?? 0}`);
});
mockGetSettings.mockResolvedValue({ autoBackupDir: ".kb/backups" });
mockRunBackupCommand.mockResolvedValue({ success: true, output: "backup created" });
mockListBackups.mockResolvedValue([]);
mockRestoreBackup.mockResolvedValue(undefined);
mockCleanupOldBackups.mockResolvedValue(0);
mockResolveProject.mockResolvedValue({
projectId: "proj-1",
projectName: "demo-project",
projectPath: "/projects/demo",
isRegistered: true,
store: { getSettings: mockGetSettings, kbDir: "/projects/demo/.kb" },
});
});
afterEach(() => {
logSpy.mockRestore();
errorSpy.mockRestore();
exitSpy.mockRestore();
});
it("runBackupCreate uses resolved project store with --project", async () => {
await expect(runBackupCreate("demo-project")).rejects.toThrow("process.exit:0");
expect(mockResolveProject).toHaveBeenCalledWith("demo-project");
expect(mockRunBackupCommand).toHaveBeenCalledWith("/projects/demo/.kb", expect.anything());
});
it("runBackupList uses resolved project store with --project", async () => {
mockListBackups.mockResolvedValue([{ filename: "kb.db.bak", size: 1024, createdAt: new Date().toISOString() }]);
await runBackupList("demo-project");
expect(mockResolveProject).toHaveBeenCalledWith("demo-project");
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Found 1 backup"));
});
it("runBackupRestore uses resolved project store with --project", async () => {
await runBackupRestore("kb.db.bak", "demo-project");
expect(mockResolveProject).toHaveBeenCalledWith("demo-project");
expect(mockRestoreBackup).toHaveBeenCalledWith("kb.db.bak", { createPreRestoreBackup: true });
});
it("runBackupCleanup uses resolved project store with --project", async () => {
mockCleanupOldBackups.mockResolvedValue(2);
await runBackupCleanup("demo-project");
expect(mockResolveProject).toHaveBeenCalledWith("demo-project");
expect(logSpy).toHaveBeenCalledWith("Removed 2 old backup(s).");
});
it("runBackupList without project uses shared resolution flow", async () => {
await runBackupList();
expect(mockResolveProject).toHaveBeenCalledWith(undefined);
expect(TaskStore).not.toHaveBeenCalled();
});
it("runBackupList without project falls back to current cwd task store when resolution fails", async () => {
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/local/project");
mockResolveProject.mockRejectedValueOnce(new Error("No kb project found"));
await runBackupList();
expect(mockResolveProject).toHaveBeenCalledWith(undefined);
expect(TaskStore).toHaveBeenCalledWith("/local/project");
cwdSpy.mockRestore();
});
it("propagates project resolution errors for project-targeted backup commands", async () => {
mockResolveProject.mockRejectedValue(new Error("Project 'missing' not found. Run 'kb project list' to see registered projects."));
await expect(runBackupList("missing")).rejects.toThrow("Project 'missing' not found");
});
});