feat(FN-3891): document memory backup settings in settings reference
Documents the memory backup feature in the settings reference and adds a changeset to prepare for publishing. Fusion-Task-Id: FN-3891
This commit is contained in:
90
packages/cli/src/commands/__tests__/memory-backup.test.ts
Normal file
90
packages/cli/src/commands/__tests__/memory-backup.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const {
|
||||
mockListBackups,
|
||||
mockRestoreBackup,
|
||||
mockGetSettings,
|
||||
mockRunMemoryBackupCommand,
|
||||
mockResolveProject,
|
||||
} = vi.hoisted(() => ({
|
||||
mockListBackups: vi.fn(),
|
||||
mockRestoreBackup: vi.fn(),
|
||||
mockGetSettings: vi.fn(),
|
||||
mockRunMemoryBackupCommand: vi.fn(),
|
||||
mockResolveProject: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/core", () => ({
|
||||
TaskStore: vi.fn().mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
getSettings: mockGetSettings,
|
||||
fusionDir: "/cwd/.fusion",
|
||||
})),
|
||||
createMemoryBackupManager: vi.fn(() => ({
|
||||
listBackups: mockListBackups,
|
||||
restoreBackup: mockRestoreBackup,
|
||||
})),
|
||||
runMemoryBackupCommand: mockRunMemoryBackupCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../../project-context.js", () => ({
|
||||
resolveProject: mockResolveProject,
|
||||
}));
|
||||
|
||||
import { runMemoryBackupCreate, runMemoryBackupList, runMemoryBackupRestore } from "../memory-backup.js";
|
||||
|
||||
describe("memory-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({ memoryBackupSchedule: "0 3 * * *" });
|
||||
mockRunMemoryBackupCommand.mockResolvedValue({ success: true, output: "memory backup created" });
|
||||
mockListBackups.mockResolvedValue([]);
|
||||
mockRestoreBackup.mockResolvedValue(undefined);
|
||||
mockResolveProject.mockResolvedValue({
|
||||
store: { getSettings: mockGetSettings, fusionDir: "/projects/demo/.fusion" },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("create path succeeds", async () => {
|
||||
await expect(runMemoryBackupCreate({ projectName: "demo", scope: "agents" })).rejects.toThrow("process.exit:0");
|
||||
expect(mockRunMemoryBackupCommand).toHaveBeenCalledWith(
|
||||
"/projects/demo/.fusion",
|
||||
expect.objectContaining({ memoryBackupScope: "agents" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("list path renders entries", async () => {
|
||||
mockListBackups.mockResolvedValue([
|
||||
{ filename: "memory-2026-01-01-000000", createdAt: new Date().toISOString(), size: 100, scope: "all", entryCount: 3 },
|
||||
]);
|
||||
await runMemoryBackupList("demo");
|
||||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Found 1 memory backup"));
|
||||
});
|
||||
|
||||
it("restore path calls manager", async () => {
|
||||
await runMemoryBackupRestore("memory-2026-01-01-000000", "demo");
|
||||
expect(mockRestoreBackup).toHaveBeenCalledWith("memory-2026-01-01-000000", { overwrite: true });
|
||||
});
|
||||
|
||||
it("create path fails on invalid schedule", async () => {
|
||||
mockRunMemoryBackupCommand.mockResolvedValue({ success: false, output: "Invalid memory backup schedule: bad" });
|
||||
await expect(runMemoryBackupCreate({ projectName: "demo" })).rejects.toThrow("process.exit:1");
|
||||
expect(errorSpy).toHaveBeenCalledWith("Invalid memory backup schedule: bad");
|
||||
});
|
||||
});
|
||||
96
packages/cli/src/commands/memory-backup.ts
Normal file
96
packages/cli/src/commands/memory-backup.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
createMemoryBackupManager,
|
||||
runMemoryBackupCommand,
|
||||
TaskStore,
|
||||
type ProjectSettings,
|
||||
} from "@fusion/core";
|
||||
import { resolveProject } from "../project-context.js";
|
||||
|
||||
type MemoryBackupScope = "project" | "agents" | "all";
|
||||
|
||||
async function resolveBackupStore(projectName?: string): Promise<TaskStore> {
|
||||
try {
|
||||
return (await resolveProject(projectName)).store;
|
||||
} catch {
|
||||
const store = new TaskStore(process.cwd());
|
||||
await store.init();
|
||||
return store;
|
||||
}
|
||||
}
|
||||
|
||||
async function getMemoryBackupContext(projectName?: string): Promise<{
|
||||
store: TaskStore;
|
||||
fusionDir: string;
|
||||
settings: ProjectSettings;
|
||||
}> {
|
||||
const store = await resolveBackupStore(projectName);
|
||||
const fusionDir = (store as unknown as { fusionDir: string }).fusionDir;
|
||||
const settings = await store.getSettings();
|
||||
return { store, fusionDir, settings };
|
||||
}
|
||||
|
||||
export async function runMemoryBackupCreate(options?: { projectName?: string; scope?: MemoryBackupScope }): Promise<void> {
|
||||
const { fusionDir, settings } = await getMemoryBackupContext(options?.projectName);
|
||||
const effectiveSettings = options?.scope ? { ...settings, memoryBackupScope: options.scope } : settings;
|
||||
|
||||
console.log("Creating memory backup...");
|
||||
const result = await runMemoryBackupCommand(fusionDir, effectiveSettings);
|
||||
if (result.success) {
|
||||
console.log(result.output);
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(result.output);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export async function runMemoryBackupList(projectName?: string): Promise<void> {
|
||||
const { fusionDir, settings } = await getMemoryBackupContext(projectName);
|
||||
const manager = createMemoryBackupManager(fusionDir, settings);
|
||||
const backups = await manager.listBackups();
|
||||
|
||||
if (backups.length === 0) {
|
||||
console.log("No memory backups found.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${backups.length} memory backup(s):\n`);
|
||||
console.log("Date Scope Entries Size Filename");
|
||||
console.log("-".repeat(80));
|
||||
|
||||
let totalSize = 0;
|
||||
for (const backup of backups) {
|
||||
totalSize += backup.size;
|
||||
const date = new Date(backup.createdAt).toLocaleString();
|
||||
const scope = backup.scope.padEnd(7);
|
||||
const entries = String(backup.entryCount).padEnd(7);
|
||||
const size = formatBytes(backup.size).padEnd(9);
|
||||
console.log(`${date} ${scope} ${entries} ${size} ${backup.filename}`);
|
||||
}
|
||||
|
||||
console.log("-".repeat(80));
|
||||
console.log(`Total: ${formatBytes(totalSize)}`);
|
||||
}
|
||||
|
||||
export async function runMemoryBackupRestore(filename: string, projectName?: string): Promise<void> {
|
||||
const { fusionDir, settings } = await getMemoryBackupContext(projectName);
|
||||
const manager = createMemoryBackupManager(fusionDir, settings);
|
||||
|
||||
console.log(`Restoring memory backup: ${filename}`);
|
||||
console.log("This may overwrite project and/or agent memory files.\n");
|
||||
|
||||
try {
|
||||
await manager.restoreBackup(filename, { overwrite: true });
|
||||
console.log(`Successfully restored memory from ${filename}`);
|
||||
} catch (err) {
|
||||
console.error(`Memory restore failed: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
}
|
||||
Reference in New Issue
Block a user