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:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { CronRunner, createAiPromptExecutor, isInProcessBackupCommand, isInProcessScheduledEvalCommand } from "../cron-runner.js";
|
||||
import { CronRunner, createAiPromptExecutor, isInProcessBackupCommand, isInProcessMemoryBackupCommand, isInProcessScheduledEvalCommand } from "../cron-runner.js";
|
||||
import type { AiPromptExecutor } from "../cron-runner.js";
|
||||
import type { TaskStore, AutomationStore, ScheduledTask, AutomationRunResult, AutomationStep, Settings } from "@fusion/core";
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -15,6 +15,10 @@ const piModuleMocks = vi.hoisted(() => ({
|
||||
promptWithFallback: vi.fn(),
|
||||
}));
|
||||
|
||||
const coreModuleMocks = vi.hoisted(() => ({
|
||||
runMemoryBackupCommand: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../logger.js", () => ({
|
||||
createLogger: () => ({
|
||||
log: cronLoggerSpies.log,
|
||||
@@ -28,6 +32,14 @@ vi.mock("../pi.js", () => ({
|
||||
promptWithFallback: piModuleMocks.promptWithFallback,
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/core", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@fusion/core")>();
|
||||
return {
|
||||
...actual,
|
||||
runMemoryBackupCommand: coreModuleMocks.runMemoryBackupCommand,
|
||||
};
|
||||
});
|
||||
|
||||
// Default settings inline to avoid @fusion/core build dependency during tests
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
maxConcurrent: 2,
|
||||
@@ -104,6 +116,10 @@ describe("CronRunner", () => {
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
});
|
||||
coreModuleMocks.runMemoryBackupCommand.mockResolvedValue({
|
||||
success: true,
|
||||
output: "memory backup ok",
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1843,6 +1859,51 @@ describe("CronRunner", () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe("isInProcessMemoryBackupCommand", () => {
|
||||
const positives = [
|
||||
"fn memory-backup --create",
|
||||
"npx runfusion.ai memory-backup --create",
|
||||
" fn memory-backup --create ",
|
||||
"FN MEMORY-BACKUP --CREATE",
|
||||
];
|
||||
|
||||
const negatives: Array<string | undefined> = [
|
||||
"fn memory-backup --list",
|
||||
"fn memory-backup",
|
||||
"fn backup --create",
|
||||
"",
|
||||
undefined,
|
||||
];
|
||||
|
||||
for (const cmd of positives) {
|
||||
it(`intercepts: ${cmd}`, () => {
|
||||
expect(isInProcessMemoryBackupCommand(cmd)).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
for (const cmd of negatives) {
|
||||
it(`does NOT intercept: ${JSON.stringify(cmd)}`, () => {
|
||||
expect(isInProcessMemoryBackupCommand(cmd)).toBe(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("memory backup in-process dispatch", () => {
|
||||
it("routes fn memory-backup --create through runMemoryBackupCommand", async () => {
|
||||
const store = createMockStore() as unknown as TaskStore & { getFusionDir: () => string };
|
||||
store.getFusionDir = vi.fn().mockReturnValue("/tmp/.fusion");
|
||||
const schedule = createMockSchedule({ id: "mem-bk", command: "fn memory-backup --create" });
|
||||
runner = new CronRunner(store, createMockAutomationStore());
|
||||
|
||||
const runResult = await (runner as unknown as { executeLegacyCommand: (s: ScheduledTask, startedAt: string) => Promise<AutomationRunResult> })
|
||||
.executeLegacyCommand(schedule, new Date().toISOString());
|
||||
|
||||
expect(coreModuleMocks.runMemoryBackupCommand).toHaveBeenCalledWith("/tmp/.fusion", expect.any(Object));
|
||||
expect(runResult.success).toBe(true);
|
||||
expect(runResult.output).toContain("memory backup ok");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isInProcessScheduledEvalCommand", () => {
|
||||
it("matches canonical scheduled eval command", () => {
|
||||
expect(isInProcessScheduledEvalCommand("fn eval --scheduled-batch")).toBe(true);
|
||||
|
||||
@@ -133,6 +133,46 @@ export function isInProcessBackupCommand(command: string | undefined): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isInProcessMemoryBackupCommand(command: string | undefined): boolean {
|
||||
if (!command) return false;
|
||||
const trimmed = command.trim();
|
||||
if (!trimmed) return false;
|
||||
if (SHELL_METACHARACTERS_REGEX.test(trimmed)) return false;
|
||||
|
||||
const tokens = trimmed.split(/\s+/).map((tok) => tok.toLowerCase());
|
||||
let cursor = 0;
|
||||
|
||||
if (tokens[cursor] === "npx") {
|
||||
cursor += 1;
|
||||
while (cursor < tokens.length) {
|
||||
const tok = tokens[cursor];
|
||||
if (tok === undefined || !tok.startsWith("-")) break;
|
||||
const takesValue = (tok === "-p" || tok === "--package")
|
||||
&& cursor + 1 < tokens.length
|
||||
&& tokens[cursor + 1] !== undefined
|
||||
&& !tokens[cursor + 1]!.startsWith("-");
|
||||
cursor += takesValue ? 2 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
const binary = tokens[cursor];
|
||||
if (!binary || !FUSION_BINARY_TOKENS.has(binary)) return false;
|
||||
cursor += 1;
|
||||
|
||||
if (tokens[cursor] !== "memory-backup") return false;
|
||||
cursor += 1;
|
||||
if (tokens[cursor] !== "--create") return false;
|
||||
cursor += 1;
|
||||
|
||||
for (; cursor < tokens.length; cursor += 1) {
|
||||
const tok = tokens[cursor];
|
||||
if (!tok) continue;
|
||||
if (!tok.startsWith("-")) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isInProcessScheduledEvalCommand(command: string | undefined): boolean {
|
||||
if (!command) return false;
|
||||
const trimmed = command.trim();
|
||||
@@ -412,6 +452,10 @@ export class CronRunner {
|
||||
return this.executeBackupInProcess(schedule, startedAt);
|
||||
}
|
||||
|
||||
if (isInProcessMemoryBackupCommand(schedule.command)) {
|
||||
return this.executeMemoryBackupInProcess(schedule, startedAt);
|
||||
}
|
||||
|
||||
if (isInProcessScheduledEvalCommand(schedule.command)) {
|
||||
return this.executeScheduledEvalInProcess(schedule, startedAt);
|
||||
}
|
||||
@@ -478,6 +522,25 @@ export class CronRunner {
|
||||
};
|
||||
}
|
||||
|
||||
private async executeMemoryBackupInProcess(
|
||||
schedule: ScheduledTask,
|
||||
startedAt: string,
|
||||
): Promise<AutomationRunResult> {
|
||||
const action = await this.runMemoryBackupActionInProcess();
|
||||
if (action.success) {
|
||||
log.log(`✓ ${schedule.name} completed in-process`);
|
||||
} else {
|
||||
log.warn(`✗ ${schedule.name} in-process memory backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
|
||||
}
|
||||
return {
|
||||
success: action.success,
|
||||
output: action.output,
|
||||
error: action.error,
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared in-process backup execution used by both the legacy-command path
|
||||
* and the command-step path. Returns the success/output/error tuple in
|
||||
@@ -546,6 +609,27 @@ export class CronRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private async runMemoryBackupActionInProcess(): Promise<{
|
||||
success: boolean;
|
||||
output: string;
|
||||
error: string | undefined;
|
||||
}> {
|
||||
try {
|
||||
const { runMemoryBackupCommand } = await import("@fusion/core");
|
||||
const fusionDir = this.store.getFusionDir();
|
||||
const settings = await this.store.getSettings();
|
||||
const result = await runMemoryBackupCommand(fusionDir, settings);
|
||||
return {
|
||||
success: result.success,
|
||||
output: truncateOutput(result.output ?? "", ""),
|
||||
error: result.success ? undefined : result.output,
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { success: false, output: "", error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute multiple steps sequentially.
|
||||
* Aggregates per-step results into an overall AutomationRunResult.
|
||||
@@ -681,6 +765,20 @@ export class CronRunner {
|
||||
};
|
||||
}
|
||||
|
||||
if (isInProcessMemoryBackupCommand(step.command)) {
|
||||
const action = await this.runMemoryBackupActionInProcess();
|
||||
return {
|
||||
stepId: step.id,
|
||||
stepName: step.name,
|
||||
stepIndex,
|
||||
success: action.success,
|
||||
output: action.output,
|
||||
error: action.error,
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execCommand(step.command, {
|
||||
timeout: timeoutMs,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { CronExpressionParser } from "cron-parser";
|
||||
import { exec } from "node:child_process";
|
||||
import { isInProcessBackupCommand } from "./cron-runner.js";
|
||||
import { isInProcessBackupCommand, isInProcessMemoryBackupCommand } from "./cron-runner.js";
|
||||
import { promisify } from "node:util";
|
||||
import type {
|
||||
RoutineStore,
|
||||
@@ -291,6 +291,31 @@ export class RoutineRunner {
|
||||
}
|
||||
}
|
||||
|
||||
if (isInProcessMemoryBackupCommand(command) && this.options.taskStore) {
|
||||
try {
|
||||
const { runMemoryBackupCommand } = await import("@fusion/core");
|
||||
const fusionDir = this.options.taskStore.getFusionDir();
|
||||
const settings = await this.options.taskStore.getSettings();
|
||||
const result = await runMemoryBackupCommand(fusionDir, settings);
|
||||
return {
|
||||
success: result.success,
|
||||
output: truncateOutput(result.output ?? "", ""),
|
||||
error: result.success ? undefined : result.output,
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
success: false,
|
||||
output: "",
|
||||
error: message,
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
|
||||
Reference in New Issue
Block a user