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:
Fusion
2026-05-09 14:38:06 -07:00
committed by gsxdsm
parent 1d20c420c2
commit ff20e9f337
15 changed files with 1171 additions and 2 deletions

View File

@@ -4498,6 +4498,82 @@ export function SettingsModal({
<small className="field-error">Path cannot contain parent directory traversal (..)</small>
)}
</div>
<h4 className="settings-section-heading">Memory Backups</h4>
<div className="form-group">
<label htmlFor="memoryBackupEnabled" className="checkbox-label">
<input
id="memoryBackupEnabled"
type="checkbox"
checked={form.memoryBackupEnabled || false}
onChange={(e) => setForm((f) => ({ ...f, memoryBackupEnabled: e.target.checked }))}
/>
Enable automatic memory backups
</label>
<small>When enabled, project and agent memory files are backed up automatically on a schedule.</small>
</div>
<div className="form-group">
<label htmlFor="memoryBackupSchedule">Memory Backup Schedule (Cron)</label>
<input
id="memoryBackupSchedule"
type="text"
placeholder="0 3 * * *"
value={form.memoryBackupSchedule || "0 3 * * *"}
onChange={(e) => setForm((f) => ({ ...f, memoryBackupSchedule: e.target.value }))}
disabled={!form.memoryBackupEnabled}
/>
<small>Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM).</small>
{form.memoryBackupSchedule && !/^[\s\d*,/-]+$/.test(form.memoryBackupSchedule) && (
<small className="field-error">Invalid cron expression format</small>
)}
</div>
<div className="form-group">
<label htmlFor="memoryBackupRetention">Memory Retention Count</label>
<input
id="memoryBackupRetention"
type="number"
min={1}
max={100}
value={form.memoryBackupRetention ?? ""}
onChange={(e) => {
const val = e.target.value;
setForm((f) => ({ ...f, memoryBackupRetention: val === "" ? undefined : Number(val) }));
}}
disabled={!form.memoryBackupEnabled}
/>
<small>Number of memory backups to keep (oldest are deleted first). Range: 1-100.</small>
{form.memoryBackupRetention !== undefined && (form.memoryBackupRetention < 1 || form.memoryBackupRetention > 100) && (
<small className="field-error">Must be between 1 and 100</small>
)}
</div>
<div className="form-group">
<label htmlFor="memoryBackupDir">Memory Backup Directory</label>
<input
id="memoryBackupDir"
type="text"
placeholder=".fusion/backups/memory"
value={form.memoryBackupDir || ".fusion/backups/memory"}
onChange={(e) => setForm((f) => ({ ...f, memoryBackupDir: e.target.value }))}
disabled={!form.memoryBackupEnabled}
/>
<small>Directory for memory backups, relative to project root.</small>
{form.memoryBackupDir && form.memoryBackupDir.includes("..") && (
<small className="field-error">Path cannot contain parent directory traversal (..)</small>
)}
</div>
<div className="form-group">
<label htmlFor="memoryBackupScope">Memory Backup Scope</label>
<select
id="memoryBackupScope"
value={form.memoryBackupScope || "all"}
onChange={(e) => setForm((f) => ({ ...f, memoryBackupScope: e.target.value as "project" | "agents" | "all" }))}
disabled={!form.memoryBackupEnabled}
>
<option value="all">All (project + agents)</option>
<option value="project">Project only (.fusion/memory)</option>
<option value="agents">Agents only (.fusion/agent-memory)</option>
</select>
</div>
{backupLoading ? (
<div className="settings-empty-state">Loading backup info</div>
) : backupInfo ? (

View File

@@ -2988,6 +2988,39 @@ describe("SettingsModal", () => {
});
});
describe("memory backups settings", () => {
it("renders memory backup fields with defaults and saves changes", async () => {
renderModal({ initialSection: "backups" });
await waitForSettingsModalReady();
expect(screen.getByRole("heading", { name: "Memory Backups" })).toBeInTheDocument();
expect(screen.getByLabelText("Memory Backup Schedule (Cron)")).toHaveValue("0 3 * * *");
expect(screen.getByLabelText("Memory Retention Count")).toHaveValue(null);
expect(screen.getByLabelText("Memory Backup Directory")).toHaveValue(".fusion/backups/memory");
expect(screen.getByLabelText("Memory Backup Scope")).toHaveValue("all");
await userEvent.click(screen.getByLabelText("Enable automatic memory backups"));
fireEvent.change(screen.getByLabelText("Memory Backup Schedule (Cron)"), { target: { value: "0 5 * * *" } });
fireEvent.change(screen.getByLabelText("Memory Retention Count"), { target: { value: "21" } });
fireEvent.change(screen.getByLabelText("Memory Backup Directory"), { target: { value: ".fusion/backups/custom-memory" } });
await userEvent.selectOptions(screen.getByLabelText("Memory Backup Scope"), "agents");
await userEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(mockUpdateSettings).toHaveBeenCalledWith(
expect.objectContaining({
memoryBackupEnabled: true,
memoryBackupSchedule: "0 5 * * *",
memoryBackupRetention: 21,
memoryBackupDir: ".fusion/backups/custom-memory",
memoryBackupScope: "agents",
}),
undefined,
);
});
});
});
describe("research settings sections", () => {
beforeEach(() => {
mockFetchSettings.mockResolvedValue({