- Add comprehensive settings-import tests for global/project scope handling and error paths - Add settings-export tests covering output targets, filtering, and serialization behavior - Expand project-resolver test matrix for flag/default/CWD resolution and fallback cases - Extend bin entrypoint tests for command routing, argument forwarding, and failure handling
195 lines
6.7 KiB
TypeScript
195 lines
6.7 KiB
TypeScript
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
|
import { existsSync } from "node:fs";
|
|
import { TaskStore, importSettings, readExportFile, validateImportData } from "@fusion/core";
|
|
import { resolveProject } from "../../project-context.js";
|
|
|
|
const mockStoreInit = vi.fn().mockResolvedValue(undefined);
|
|
|
|
vi.mock("node:fs", () => ({
|
|
existsSync: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@fusion/core", () => ({
|
|
TaskStore: vi.fn().mockImplementation(() => ({
|
|
init: mockStoreInit,
|
|
})),
|
|
importSettings: vi.fn(),
|
|
readExportFile: vi.fn(),
|
|
validateImportData: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../project-context.js", () => ({
|
|
resolveProject: vi.fn(),
|
|
}));
|
|
|
|
import { runSettingsImport } from "../settings-import.js";
|
|
|
|
describe("runSettingsImport", () => {
|
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
const exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: string | number | null) => {
|
|
if ((code ?? 0) === 0) {
|
|
return undefined as never;
|
|
}
|
|
throw new Error(`process.exit:${code ?? 0}`);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
vi.mocked(resolveProject).mockResolvedValue({
|
|
projectId: "proj-1",
|
|
projectName: "demo",
|
|
projectPath: "/tmp/demo",
|
|
isRegistered: true,
|
|
store: {} as any,
|
|
});
|
|
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readExportFile).mockResolvedValue({
|
|
version: 1,
|
|
exportedAt: "2026-04-08T00:00:00.000Z",
|
|
global: { ntfyEnabled: true, defaultProvider: "anthropic" },
|
|
project: { maxConcurrent: 3, autoResolveConflicts: true, maxWorktrees: 4 },
|
|
} as any);
|
|
vi.mocked(validateImportData).mockReturnValue([]);
|
|
vi.mocked(importSettings).mockResolvedValue({
|
|
success: true,
|
|
globalCount: 2,
|
|
projectCount: 3,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("exits when import file does not exist", async () => {
|
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
|
|
await expect(runSettingsImport("./missing.json", { yes: true })).rejects.toThrow("process.exit:1");
|
|
|
|
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("File not found"));
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("exits when reading the import file fails", async () => {
|
|
vi.mocked(readExportFile).mockRejectedValue(new Error("read denied"));
|
|
|
|
await expect(runSettingsImport("./settings.json", { yes: true })).rejects.toThrow("process.exit:1");
|
|
|
|
expect(errorSpy).toHaveBeenCalledWith("Error: Failed to read import file: read denied");
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("prints validation errors and exits", async () => {
|
|
vi.mocked(validateImportData).mockReturnValue(["bad version", "missing global"]);
|
|
|
|
await expect(runSettingsImport("./settings.json", { yes: true })).rejects.toThrow("process.exit:1");
|
|
|
|
expect(errorSpy).toHaveBeenCalledWith("Error: Invalid import file:");
|
|
expect(errorSpy).toHaveBeenCalledWith(" - bad version");
|
|
expect(errorSpy).toHaveBeenCalledWith(" - missing global");
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("fails when selected scope has no settings to import", async () => {
|
|
vi.mocked(readExportFile).mockResolvedValue({
|
|
version: 1,
|
|
exportedAt: "2026-04-08T00:00:00.000Z",
|
|
global: {},
|
|
project: {},
|
|
} as any);
|
|
|
|
await expect(runSettingsImport("./settings.json", { yes: true })).rejects.toThrow("process.exit:1");
|
|
|
|
expect(errorSpy).toHaveBeenCalledWith("Error: No settings to import in the specified scope");
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("requires --yes confirmation before importing", async () => {
|
|
await expect(runSettingsImport("./settings.json")).rejects.toThrow("process.exit:1");
|
|
|
|
expect(logSpy).toHaveBeenCalledWith(" Use --yes to confirm this import operation");
|
|
expect(importSettings).not.toHaveBeenCalled();
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("imports successfully with --yes", async () => {
|
|
await runSettingsImport("./settings.json", { yes: true });
|
|
|
|
expect(importSettings).toHaveBeenCalledWith(
|
|
expect.any(Object),
|
|
expect.any(Object),
|
|
{ scope: "both", merge: true },
|
|
);
|
|
expect(logSpy).toHaveBeenCalledWith(" ✓ Settings imported successfully");
|
|
expect(logSpy).toHaveBeenCalledWith(" Imported 2 global setting(s)");
|
|
expect(logSpy).toHaveBeenCalledWith(" Imported 3 project setting(s)");
|
|
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
});
|
|
|
|
it("imports only global settings when --scope global is used", async () => {
|
|
vi.mocked(importSettings).mockResolvedValue({
|
|
success: true,
|
|
globalCount: 2,
|
|
projectCount: 0,
|
|
});
|
|
|
|
await runSettingsImport("./settings.json", { scope: "global", yes: true });
|
|
|
|
expect(importSettings).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), {
|
|
scope: "global",
|
|
merge: true,
|
|
});
|
|
expect(logSpy).toHaveBeenCalledWith(" Imported 2 global setting(s)");
|
|
expect(logSpy).not.toHaveBeenCalledWith(" Imported 3 project setting(s)");
|
|
});
|
|
|
|
it("imports only project settings when --scope project is used", async () => {
|
|
vi.mocked(importSettings).mockResolvedValue({
|
|
success: true,
|
|
globalCount: 0,
|
|
projectCount: 3,
|
|
});
|
|
|
|
await runSettingsImport("./settings.json", { scope: "project", yes: true });
|
|
|
|
expect(importSettings).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), {
|
|
scope: "project",
|
|
merge: true,
|
|
});
|
|
expect(logSpy).toHaveBeenCalledWith(" Imported 3 project setting(s)");
|
|
expect(logSpy).not.toHaveBeenCalledWith(" Imported 2 global setting(s)");
|
|
});
|
|
|
|
it("prints core import failure and exits", async () => {
|
|
vi.mocked(importSettings).mockResolvedValue({
|
|
success: false,
|
|
globalCount: 0,
|
|
projectCount: 0,
|
|
error: "DB error",
|
|
});
|
|
|
|
await expect(runSettingsImport("./settings.json", { yes: true })).rejects.toThrow("process.exit:1");
|
|
|
|
expect(errorSpy).toHaveBeenCalledWith("Error: Import failed: DB error");
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("shows replace mode when merge is false", async () => {
|
|
await expect(runSettingsImport("./settings.json", { merge: false })).rejects.toThrow("process.exit:1");
|
|
|
|
expect(logSpy).toHaveBeenCalledWith(" Mode: replace");
|
|
expect(importSettings).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves project name and initializes store at resolved path", async () => {
|
|
await runSettingsImport("./settings.json", { projectName: "alpha", yes: true });
|
|
|
|
expect(resolveProject).toHaveBeenCalledWith("alpha");
|
|
expect(TaskStore).toHaveBeenCalledWith("/tmp/demo");
|
|
expect(mockStoreInit).toHaveBeenCalledOnce();
|
|
});
|
|
});
|